deadbyte-mcp 0.14.0 → 0.15.0
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/AGENTS.md +11 -18
- package/CONTEXT.md +3 -3
- package/MANIFEST.SHA256 +104 -53
- package/R43-PARENT.json +20 -0
- package/R44-DONORS.json +99 -0
- package/R44-PARENT.json +18 -0
- package/README.txt +7 -2
- package/controller/README.TXT +4 -5
- package/controller/deadbyte-controller.ps1 +1 -1
- package/docs/ARCHITECTURE.md +8 -1
- package/docs/R44-ARCHITECTURE.md +37 -0
- package/package.json +13 -8
- package/scripts/deferred-slot-operation-r43.mjs +126 -0
- package/scripts/deferred-slot-operation-r44.mjs +126 -0
- package/scripts/final-closure-r43.mjs +115 -0
- package/scripts/final-closure-r44.mjs +121 -0
- package/scripts/final-closure-verify-r43.mjs +213 -0
- package/scripts/final-closure-verify-r44.mjs +220 -0
- package/scripts/finalize-r43.mjs +217 -0
- package/scripts/finalize-r44.mjs +217 -0
- package/scripts/full-regression-r43.mjs +20 -0
- package/scripts/full-regression-r44.mjs +20 -0
- package/scripts/gate-windows-r43.ps1 +36 -0
- package/scripts/gate-windows-r44.ps1 +36 -0
- package/scripts/release-parity-r43.mjs +218 -0
- package/scripts/release-parity-r44.mjs +242 -0
- package/scripts/release-parity-tests-r43.ps1 +8 -0
- package/scripts/release-parity-tests-r44.ps1 +8 -0
- package/scripts/verify-r43-parent.mjs +126 -0
- package/scripts/verify-r44-parent.mjs +142 -0
- package/scripts/windows-gate-evidence-r43.mjs +55 -0
- package/scripts/windows-gate-evidence-r44.mjs +55 -0
- package/src/agent-backend-r44.mjs +254 -0
- package/src/autonomous-output-contracts.mjs +1 -1
- package/src/autonomous-policy.mjs +41 -4
- package/src/autonomous-runtime.mjs +25 -12
- package/src/autonomous-supervisor-host.mjs +12 -2
- package/src/capability-ledger.mjs +2 -1
- package/src/codex-app-server-r44.mjs +154 -0
- package/src/coding-plane.mjs +66 -19
- package/src/durable-job-r44.mjs +145 -0
- package/src/external-job-provider-r44.mjs +126 -0
- package/src/host-plane.mjs +27 -6
- package/src/job-supervisor-host-r44.mjs +112 -0
- package/src/machine-fs-smoke-client.mjs +1 -1
- package/src/machine-fs.mjs +21 -10
- package/src/mcp-host-smoke-client.mjs +1 -1
- package/src/mcp-server.mjs +20 -3
- package/src/mcp-smoke-client.mjs +1 -1
- package/src/native-diff-v2.mjs +3 -3
- package/src/native-diff.mjs +3 -2
- package/src/observation-smoke-client.mjs +1 -1
- package/src/operator-surface.mjs +2 -2
- package/src/process-policy.mjs +19 -0
- package/src/process-runtime.mjs +5 -2
- package/src/process-smoke-client.mjs +2 -2
- package/src/provider-circuit.mjs +8 -2
- package/src/r44-compact-disarmed-probe.mjs +74 -0
- package/src/r44-full-surface-probe.mjs +34 -0
- package/src/remote-device-r44.mjs +205 -0
- package/src/remote-mcp-autonomous-smoke-client.mjs +4 -4
- package/src/remote-mcp-r44-parity-client.mjs +203 -0
- package/src/remote-mcp-smoke-client.mjs +7 -4
- package/src/runtime-update.mjs +64 -31
- package/src/skill-mcp-tools-r44.mjs +37 -0
- package/src/skill-plane-r44.mjs +203 -0
- package/src/tunnel-stdio-compat-smoke.mjs +1 -1
- package/src/version.mjs +1 -1
- package/test/agent-backend-r44.test.mjs +115 -0
- package/test/agent-runtime.test.mjs +3 -3
- package/test/autonomous-loop.test.mjs +97 -11
- package/test/autonomous-runtime.test.mjs +2 -2
- package/test/capability-ledger-r36.test.mjs +3 -3
- package/test/cli-entrypoint.test.mjs +1 -1
- package/test/codex-app-server-r44.test.mjs +115 -0
- package/test/codex-autonomous-r44.test.mjs +87 -0
- package/test/coding-plane.test.mjs +2 -2
- package/test/coding-runtime.test.mjs +36 -1
- package/test/compact-catalog-r36.test.mjs +3 -3
- package/test/compact-max-catalog-r40.test.mjs +4 -2
- package/test/compact-max-catalog-r44.test.mjs +97 -0
- package/test/core.test.mjs +1 -1
- package/test/durable-job-r44.test.mjs +75 -0
- package/test/external-job-provider-r44.test.mjs +46 -0
- package/test/full-max-catalog-r40.test.mjs +5 -3
- package/test/full-max-catalog-r44.test.mjs +91 -0
- package/test/host-plane.test.mjs +15 -1
- package/test/native-diff.test.mjs +28 -0
- package/test/output-schema.test.mjs +2 -2
- package/test/process-policy.test.mjs +22 -1
- package/test/prompts-resources.test.mjs +1 -1
- package/test/r33-closeout-regression.test.mjs +13 -30
- package/test/r33-finalization.test.mjs +7 -7
- package/test/r43-finalization.test.mjs +31 -0
- package/test/r43-finalizer.test.mjs +34 -0
- package/test/r43-parent.test.mjs +29 -0
- package/test/r44-finalization.test.mjs +58 -0
- package/test/r44-finalizer.test.mjs +48 -0
- package/test/r44-parent.test.mjs +25 -0
- package/test/release-version.test.mjs +53 -19
- package/test/remote-device-r44.test.mjs +104 -0
- package/test/runtime-self-update.test.mjs +42 -0
- package/test/skill-mcp-r44.test.mjs +38 -0
- package/test/skill-plane-r44.test.mjs +93 -0
- package/test/standing-grants.test.mjs +5 -1
package/src/runtime-update.mjs
CHANGED
|
@@ -168,7 +168,7 @@ export function parseRollbackArgs(argv){
|
|
|
168
168
|
function powershellPath(){
|
|
169
169
|
return path.join(process.env.SystemRoot||'C:\\Windows','System32','WindowsPowerShell','v1.0','powershell.exe');
|
|
170
170
|
}
|
|
171
|
-
function windowsPowerShellEnvironment(base=process.env){
|
|
171
|
+
export function windowsPowerShellEnvironment(base=process.env){
|
|
172
172
|
const systemRoot=base.SystemRoot||base.SYSTEMROOT||'C:\\Windows';
|
|
173
173
|
const userProfile=base.USERPROFILE||os.homedir();
|
|
174
174
|
const programFiles=base.ProgramFiles||base.PROGRAMFILES||'C:\\Program Files';
|
|
@@ -485,6 +485,7 @@ export function parseControllerStatus(text){
|
|
|
485
485
|
bridge_pid:number(/Bridge PID\s*:\s*(\d+)\s*\[verified\]/),
|
|
486
486
|
cloud_pid:number(/Cloud PID\s*:\s*(\d+)\s*\[verified\]/),
|
|
487
487
|
local_ready:/Local ready\s*:\s*True/.test(text),
|
|
488
|
+
process_armed:/Process plane\s*:\s*ARMED \[standing-grant\]/.test(text),
|
|
488
489
|
coding_armed:/Coding plane\s*:\s*ARMED \[standing-grant\]/.test(text),
|
|
489
490
|
autonomous_armed:/Autonomous\s*:\s*ARMED \[standing-grant\]/.test(text),
|
|
490
491
|
desktop_armed:/Desktop plane\s*:\s*ARMED \[standing-grant\]/.test(text),
|
|
@@ -562,7 +563,7 @@ async function readLedger(ctx,name){
|
|
|
562
563
|
}
|
|
563
564
|
return {schema:value.schema,component:value.component,instance_nonce:String(value.instance_nonce),
|
|
564
565
|
pid:Number(value.pid),creation_time_utc:String(value.creation_time_utc??''),image_path:String(value.image_path??''),
|
|
565
|
-
image_sha256:String(value.image_sha256)};
|
|
566
|
+
image_sha256:String(value.image_sha256),script_path:String(value.script_path??''),script_sha256:String(value.script_sha256??'')};
|
|
566
567
|
}
|
|
567
568
|
function sameGeneration(a,b){
|
|
568
569
|
return a.pid===b.pid&&a.instance_nonce===b.instance_nonce&&a.creation_time_utc===b.creation_time_utc&&a.image_sha256===b.image_sha256;
|
|
@@ -573,19 +574,37 @@ function nodeProcesses(){
|
|
|
573
574
|
const text=assertRun(result,'node process query').trim(); if(!text) return [];
|
|
574
575
|
const parsed=JSON.parse(text); return Array.isArray(parsed)?parsed:[parsed];
|
|
575
576
|
}
|
|
576
|
-
function
|
|
577
|
+
export function selectCoreProcess(processes,coreRoot,bridgePid,verifiedBridge=null){
|
|
577
578
|
const needle=path.join(coreRoot,'src','mcp-server.mjs').replaceAll('/','\\').toLowerCase();
|
|
578
|
-
const
|
|
579
|
-
|
|
580
|
-
const
|
|
581
|
-
|
|
579
|
+
const children=processes.filter(item=>Number(item.ParentProcessId)===bridgePid);
|
|
580
|
+
const visible=children.filter(item=>String(item.CommandLine??'').length>0);
|
|
581
|
+
const matches=visible.filter(item=>String(item.CommandLine).replaceAll('/','\\').toLowerCase().includes(needle));
|
|
582
|
+
let item=null,identityBasis=null;
|
|
583
|
+
if(children.length===1&&visible.length===1&&matches.length===1){
|
|
584
|
+
item=matches[0]; identityBasis='command_line';
|
|
585
|
+
} else if(children.length===1&&visible.length===0){
|
|
586
|
+
if(verifiedBridge?.component!=='bridge'||verifiedBridge.pid!==bridgePid||!path.isAbsolute(String(verifiedBridge.script_path??''))||
|
|
587
|
+
!HEX64.test(String(verifiedBridge.script_sha256??''))) {
|
|
588
|
+
throw new Error(`opaque downstream core requires verified Bridge script binding for bridge ${bridgePid}`);
|
|
589
|
+
}
|
|
590
|
+
item=children[0]; identityBasis='verified_bridge_single_opaque_node_child';
|
|
591
|
+
} else {
|
|
592
|
+
throw new Error(`expected one unambiguous downstream core for bridge ${bridgePid}, found children=${children.length} visible_matches=${matches.length}`);
|
|
593
|
+
}
|
|
594
|
+
const pid=Number(item.ProcessId),creationDate=String(item.CreationDate??'');
|
|
595
|
+
if(!Number.isInteger(pid)||pid<1||creationDate.length<1) throw new Error(`downstream core identity incomplete for bridge ${bridgePid}`);
|
|
596
|
+
return {pid,parent_pid:Number(item.ParentProcessId),creation_date:creationDate,
|
|
597
|
+
executable_path:String(item.ExecutablePath??''),command_line:String(item.CommandLine??''),identity_basis:identityBasis};
|
|
598
|
+
}
|
|
599
|
+
function coreProcess(coreRoot,bridge){
|
|
600
|
+
return selectCoreProcess(nodeProcesses(),coreRoot,bridge.pid,bridge);
|
|
582
601
|
}
|
|
583
602
|
async function captureRuntime(ctx){
|
|
584
603
|
const statusText=controllerCaptured(ctx,'status',15000); const status=parseControllerStatus(statusText);
|
|
585
604
|
if(!status.online||!status.bridge_pid||!status.cloud_pid||!status.local_ready) throw new Error(`slot update requires ONLINE runtime: ${statusText}`);
|
|
586
605
|
if(status.host_armed) throw new Error('slot update requires host plane DISARMED');
|
|
587
606
|
if(status.autonomous_armed&&!status.coding_armed) throw new Error('invalid runtime authority: autonomous armed without coding');
|
|
588
|
-
const bridge=await readLedger(ctx,'bridge'); const cloud=await readLedger(ctx,'cloud'); const core=coreProcess(ctx.core_root,
|
|
607
|
+
const bridge=await readLedger(ctx,'bridge'); const cloud=await readLedger(ctx,'cloud'); const core=coreProcess(ctx.core_root,bridge);
|
|
589
608
|
return {status,status_text:statusText.trim(),bridge,cloud,core};
|
|
590
609
|
}
|
|
591
610
|
async function capturePredecessor(ctx,recoverOfflinePredecessor){
|
|
@@ -631,7 +650,7 @@ async function startAndVerify(ctx,oldRuntime){
|
|
|
631
650
|
catch {} await sleep(500);
|
|
632
651
|
}
|
|
633
652
|
if(!status?.online||!status.bridge_pid||!status.cloud_pid||!status.local_ready) throw new Error(`new runtime failed ONLINE gate: ${statusText}`);
|
|
634
|
-
const bridge=await readLedger(ctx,'bridge'); const cloud=await readLedger(ctx,'cloud'); const core=coreProcess(ctx.core_root,
|
|
653
|
+
const bridge=await readLedger(ctx,'bridge'); const cloud=await readLedger(ctx,'cloud'); const core=coreProcess(ctx.core_root,bridge);
|
|
635
654
|
if(oldRuntime?.bridge&&sameGeneration(oldRuntime.bridge,bridge)) throw new Error('fresh bridge generation gate failed');
|
|
636
655
|
if(oldRuntime?.cloud&&sameGeneration(oldRuntime.cloud,cloud)) throw new Error('fresh cloud generation gate failed');
|
|
637
656
|
if(oldRuntime?.core&&oldRuntime.core.pid===core.pid&&oldRuntime.core.creation_date===core.creation_date) throw new Error('fresh downstream core generation gate failed');
|
|
@@ -756,26 +775,42 @@ async function revokeAuthority(ctx,prior){
|
|
|
756
775
|
if(prior.desktop_armed){ controllerCaptured(ctx,'disarmdesktop',15000); revoked=true; }
|
|
757
776
|
if(prior.autonomous_armed){ controllerCaptured(ctx,'disarmautonomous',15000); revoked=true; }
|
|
758
777
|
if(prior.coding_armed){ controllerCaptured(ctx,'disarmcoding',15000); revoked=true; }
|
|
778
|
+
if(prior.process_armed){ controllerCaptured(ctx,'disarmprocess',15000); revoked=true; }
|
|
759
779
|
const status=parseControllerStatus(controllerCaptured(ctx,'status',15000));
|
|
760
|
-
if(status.coding_armed||status.autonomous_armed||status.desktop_armed) throw new Error('standing authority revoke gate failed');
|
|
780
|
+
if(status.process_armed||status.coding_armed||status.autonomous_armed||status.desktop_armed) throw new Error('standing authority revoke gate failed');
|
|
761
781
|
if(status.host_armed) throw new Error('host plane became armed during revoke');
|
|
762
782
|
return {revoked,status};
|
|
763
783
|
}
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
784
|
+
export function standingAuthorityRestorePlan(prior,enabled){
|
|
785
|
+
const before={
|
|
786
|
+
process_armed:prior?.process_armed===true,
|
|
787
|
+
coding_armed:prior?.coding_armed===true,
|
|
788
|
+
autonomous_armed:prior?.autonomous_armed===true,
|
|
789
|
+
desktop_armed:prior?.desktop_armed===true,
|
|
790
|
+
host_armed:prior?.host_armed===true
|
|
791
|
+
};
|
|
792
|
+
if(before.autonomous_armed&&!before.coding_armed) throw new Error('invalid prior authority: autonomous armed without coding');
|
|
793
|
+
const expected={process_armed:false,coding_armed:false,autonomous_armed:false,desktop_armed:false,host_armed:false};
|
|
794
|
+
const commands=[];
|
|
795
|
+
if(enabled){
|
|
796
|
+
expected.process_armed=before.process_armed;
|
|
797
|
+
expected.coding_armed=before.coding_armed;
|
|
798
|
+
expected.autonomous_armed=before.autonomous_armed;
|
|
799
|
+
if(before.coding_armed) commands.push('armcoding');
|
|
800
|
+
if(before.autonomous_armed) commands.push('armautonomous');
|
|
801
|
+
if(before.process_armed) commands.push('armprocess');
|
|
770
802
|
}
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
803
|
+
return {commands,expected};
|
|
804
|
+
}
|
|
805
|
+
async function restoreStandingAuthority(ctx,prior,enabled){
|
|
806
|
+
const plan=standingAuthorityRestorePlan(prior,enabled);
|
|
807
|
+
for(const command of plan.commands) controllerCaptured(ctx,command,15000);
|
|
774
808
|
const status=parseControllerStatus(controllerCaptured(ctx,'status',15000));
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
if(status.
|
|
809
|
+
for(const key of ['process_armed','coding_armed','autonomous_armed']){
|
|
810
|
+
if(status[key]!==plan.expected[key]) throw new Error(`${key.replace('_armed','')} standing-grant restore gate failed`);
|
|
811
|
+
}
|
|
812
|
+
if(status.desktop_armed!==plan.expected.desktop_armed) throw new Error('desktop plane must remain disarmed after slot switch');
|
|
813
|
+
if(status.host_armed!==plan.expected.host_armed) throw new Error('host plane became armed during slot switch');
|
|
779
814
|
return status;
|
|
780
815
|
}
|
|
781
816
|
async function stopAndVerify(ctx,runtime){
|
|
@@ -818,7 +853,8 @@ function generationTuple(runtime){
|
|
|
818
853
|
if(!runtime) return null;
|
|
819
854
|
return {bridge:{pid:runtime.bridge.pid,instance_nonce:runtime.bridge.instance_nonce,creation_time_utc:runtime.bridge.creation_time_utc,image_sha256:runtime.bridge.image_sha256},
|
|
820
855
|
cloud:{pid:runtime.cloud.pid,instance_nonce:runtime.cloud.instance_nonce,creation_time_utc:runtime.cloud.creation_time_utc,image_sha256:runtime.cloud.image_sha256},
|
|
821
|
-
core:{pid:runtime.core.pid,parent_pid:runtime.core.parent_pid,creation_date:runtime.core.creation_date,
|
|
856
|
+
core:{pid:runtime.core.pid,parent_pid:runtime.core.parent_pid,creation_date:runtime.core.creation_date,
|
|
857
|
+
command_line:runtime.core.command_line,identity_basis:runtime.core.identity_basis}};
|
|
822
858
|
}
|
|
823
859
|
function publicPointer(pointer){
|
|
824
860
|
if(!pointer) return null;
|
|
@@ -933,7 +969,7 @@ export async function performSlotUpdate({stage,developmentRoot,expectedVersion,e
|
|
|
933
969
|
const stabilization=await stabilizeRuntime(newCtx,newRuntime);
|
|
934
970
|
await writeState(statePath,journal,'fresh_runtime_verified',{new_runtime:generationTuple(newRuntime),stabilization});
|
|
935
971
|
const finalAuthority=await restoreStandingAuthority(newCtx,priorStatus,restoreStandingGrants);
|
|
936
|
-
await writeState(statePath,journal,'authority_verified',{final_authority:{coding_armed:finalAuthority.coding_armed,
|
|
972
|
+
await writeState(statePath,journal,'authority_verified',{final_authority:{process_armed:finalAuthority.process_armed,coding_armed:finalAuthority.coding_armed,
|
|
937
973
|
autonomous_armed:finalAuthority.autonomous_armed,desktop_armed:finalAuthority.desktop_armed,host_armed:finalAuthority.host_armed}});
|
|
938
974
|
const body={schema:UPDATE_SCHEMA,update_id:updateId,release_version:expectedVersion,candidate_manifest_sha256:expectedManifest,
|
|
939
975
|
completed_at_utc:new Date().toISOString(),activation_boundary:'active-release-pointer',mechanism:'immutable-release-slot-plus-atomic-pointer-rename',
|
|
@@ -944,10 +980,10 @@ export async function performSlotUpdate({stage,developmentRoot,expectedVersion,e
|
|
|
944
980
|
development_root:development,
|
|
945
981
|
host_repin:{requested:hostCommandPins.length>0,repinned:authorityPreparation.hostRepinned},
|
|
946
982
|
coding_repin:{requested:hostCommandPins.length>0,repinned:authorityPreparation.codingRepinned},old_runtime:generationTuple(oldRuntime),new_runtime:generationTuple(newRuntime),stabilization,
|
|
947
|
-
authority:{prior:{coding_armed:priorStatus.coding_armed,
|
|
983
|
+
authority:{prior:{process_armed:priorStatus.process_armed,coding_armed:priorStatus.coding_armed,
|
|
948
984
|
autonomous_armed:priorStatus.autonomous_armed,desktop_armed:priorStatus.desktop_armed,host_armed:priorStatus.host_armed},revoked_before_switch:revoked,restore_requested:restoreStandingGrants,
|
|
949
985
|
desktop_restore_policy:'explicit-rearm-required',
|
|
950
|
-
final:{coding_armed:finalAuthority.coding_armed,autonomous_armed:finalAuthority.autonomous_armed,desktop_armed:finalAuthority.desktop_armed,host_armed:finalAuthority.host_armed}},rollback_performed:false};
|
|
986
|
+
final:{process_armed:finalAuthority.process_armed,coding_armed:finalAuthority.coding_armed,autonomous_armed:finalAuthority.autonomous_armed,desktop_armed:finalAuthority.desktop_armed,host_armed:finalAuthority.host_armed}},rollback_performed:false};
|
|
951
987
|
const receipt=await writeEnvelope(receiptPath,body,key);
|
|
952
988
|
await writeState(statePath,journal,'completed',{receipt_path:receiptPath,receipt_sha256:receipt.receipt_sha256});
|
|
953
989
|
return {ok:true,update_id:updateId,state_path:statePath,receipt_path:receiptPath,receipt_sha256:receipt.receipt_sha256,
|
|
@@ -1027,10 +1063,7 @@ export async function performSlotRollback({activationReceipt,stateRoot=DEFAULT_S
|
|
|
1027
1063
|
if(!pointerInfo.pointer||canonicalJson(publicPointer(pointerInfo.pointer))!==canonicalJson(publicPointer(activePointer))) throw new Error('rollback current pointer does not match signed activation pointer');
|
|
1028
1064
|
await verifyActiveInstallation(state,priorPointer);
|
|
1029
1065
|
const currentCtx=slotContext(state,pointerInfo.pointer),currentRuntime=await captureRuntime(currentCtx);
|
|
1030
|
-
if(/Process plane\s*:\s*ARMED/.test(currentRuntime.status_text)) controllerCaptured(currentCtx,'disarmprocess',15000);
|
|
1031
1066
|
const revoked=await revokeAuthority(currentCtx,currentRuntime.status);
|
|
1032
|
-
const authorityText=controllerCaptured(currentCtx,'status',15000);
|
|
1033
|
-
if(/Process plane\s*:\s*ARMED/.test(authorityText)) throw new Error('process standing authority revoke gate failed');
|
|
1034
1067
|
const rollbackId=`r31-rollback-${activationEnvelope.receipt_sha256.slice(0,16)}-${Date.now().toString(36)}-${randomBytes(4).toString('hex')}`;
|
|
1035
1068
|
const evidenceDir=path.join(state,'evidence','updates',rollbackId),statePath=path.join(evidenceDir,'state.json'),receiptPath=path.join(evidenceDir,'receipt.json');
|
|
1036
1069
|
await mkdir(evidenceDir,{recursive:true});
|
|
@@ -1048,7 +1081,7 @@ export async function performSlotRollback({activationReceipt,stateRoot=DEFAULT_S
|
|
|
1048
1081
|
priorRuntime=await startAndVerify(priorCtx,currentRuntime);
|
|
1049
1082
|
const stabilization=await stabilizeRuntime(priorCtx,priorRuntime);
|
|
1050
1083
|
const finalStatus=parseControllerStatus(controllerCaptured(priorCtx,'status',15000));
|
|
1051
|
-
if(finalStatus.coding_armed||finalStatus.autonomous_armed||finalStatus.host_armed) throw new Error('rollback predecessor authority must remain disarmed');
|
|
1084
|
+
if(finalStatus.process_armed||finalStatus.coding_armed||finalStatus.autonomous_armed||finalStatus.desktop_armed||finalStatus.host_armed) throw new Error('rollback predecessor authority must remain disarmed');
|
|
1052
1085
|
const body={schema:ROLLBACK_SCHEMA,rollback_id:rollbackId,activation_receipt_sha256:activationEnvelope.receipt_sha256,
|
|
1053
1086
|
completed_at_utc:new Date().toISOString(),from_pointer:publicPointer(activePointer),active_pointer:publicPointer(priorPointer),
|
|
1054
1087
|
old_runtime:generationTuple(currentRuntime),new_runtime:generationTuple(priorRuntime),stabilization,authority_revoked:revoked.revoked,rollback_performed:true};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import * as z from 'zod/v4';
|
|
2
|
+
|
|
3
|
+
const name=z.string().regex(/^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/);
|
|
4
|
+
const rel=z.string().min(1).max(512);
|
|
5
|
+
export const skillCatalogInputSchema=z.discriminatedUnion('op',[
|
|
6
|
+
z.strictObject({op:z.literal('list')}),
|
|
7
|
+
z.strictObject({op:z.literal('load'),name}),
|
|
8
|
+
z.strictObject({op:z.literal('resource'),name,path:rel})
|
|
9
|
+
]);
|
|
10
|
+
export const skillCatalogOutputSchema=z.strictObject({
|
|
11
|
+
status:z.enum(['ok','denied']),
|
|
12
|
+
op:z.enum(['list','load','resource']),
|
|
13
|
+
result:z.record(z.string(),z.json()).optional(),
|
|
14
|
+
error:z.string().optional()
|
|
15
|
+
}).superRefine((value,ctx)=>{
|
|
16
|
+
if(value.status==='ok'&&value.result===undefined) ctx.addIssue({code:'custom',path:['result'],message:'result required for status=ok'});
|
|
17
|
+
if(value.status==='denied'&&value.error===undefined) ctx.addIssue({code:'custom',path:['error'],message:'error required for status=denied'});
|
|
18
|
+
if(value.status==='ok'&&value.error!==undefined) ctx.addIssue({code:'custom',path:['error'],message:'error invalid for status=ok'});
|
|
19
|
+
});
|
|
20
|
+
function result(value,isError=false){
|
|
21
|
+
const parsed=skillCatalogOutputSchema.safeParse(value);
|
|
22
|
+
if(!parsed.success) throw new Error(`R44 skill output contract violation: ${z.prettifyError(parsed.error)}`);
|
|
23
|
+
return {isError,structuredContent:parsed.data,content:[{type:'text',text:JSON.stringify(parsed.data)}]};
|
|
24
|
+
}
|
|
25
|
+
export function registerSkillToolsR44(server,{runtime}={}){
|
|
26
|
+
if(!server||!runtime) throw new Error('R44 skill tool registration requires runtime');
|
|
27
|
+
server.registerTool('skill_catalog',{
|
|
28
|
+
description:'Read-only content-addressed R44 skill catalog. list is metadata-only; load/resource verify signed bytes. This tool never arms or renews authority.',
|
|
29
|
+
inputSchema:skillCatalogInputSchema,outputSchema:skillCatalogOutputSchema
|
|
30
|
+
},async args=>{
|
|
31
|
+
try{
|
|
32
|
+
if(args.op==='list') return result({status:'ok',op:'list',result:await runtime.list()});
|
|
33
|
+
if(args.op==='load') return result({status:'ok',op:'load',result:await runtime.load({name:args.name})});
|
|
34
|
+
return result({status:'ok',op:'resource',result:await runtime.readResource({name:args.name,resourcePath:args.path})});
|
|
35
|
+
}catch(error){return result({status:'denied',op:args.op,error:String(error)},true);}
|
|
36
|
+
});
|
|
37
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { createHash, createPublicKey, verify as cryptoVerify } from 'node:crypto';
|
|
2
|
+
import { lstat, readFile, readdir, realpath } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
const DOMAIN=Buffer.from('DEADBYTE-SKILL-MANIFEST-V1\0','utf8');
|
|
6
|
+
const SKILL_NAME=/^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/;
|
|
7
|
+
const VERSION=/^[0-9]+\.[0-9]+\.[0-9]+(?:-[A-Za-z0-9.-]+)?$/;
|
|
8
|
+
const CAPABILITY=/^[A-Za-z0-9._-]{1,96}$/;
|
|
9
|
+
const AUTHORITY_PLANES=new Set(['process','coding','autonomous','desktop','host']);
|
|
10
|
+
const RESOURCE_KINDS=new Set(['reference','script','agent','asset']);
|
|
11
|
+
const sha=bytes=>createHash('sha256').update(bytes).digest('hex');
|
|
12
|
+
const canonical=value=>Array.isArray(value)?value.map(canonical):value&&typeof value==='object'
|
|
13
|
+
?Object.fromEntries(Object.keys(value).sort((a,b)=>a.localeCompare(b,'en')).map(key=>[key,canonical(value[key])])):value;
|
|
14
|
+
const canonicalJson=value=>JSON.stringify(canonical(value));
|
|
15
|
+
const strictUtf8=bytes=>new TextDecoder('utf-8',{fatal:true}).decode(bytes);
|
|
16
|
+
function insist(ok,message){if(!ok) throw new Error(message);}
|
|
17
|
+
function exactKeys(object,expected,label){
|
|
18
|
+
insist(object&&typeof object==='object'&&!Array.isArray(object),`${label} must be an object`);
|
|
19
|
+
const actual=Object.keys(object).sort();
|
|
20
|
+
const wanted=[...expected].sort();
|
|
21
|
+
insist(JSON.stringify(actual)===JSON.stringify(wanted),`${label} fields mismatch`);
|
|
22
|
+
}
|
|
23
|
+
function uniqueSorted(values,label,validator){
|
|
24
|
+
insist(Array.isArray(values),`${label} must be an array`);
|
|
25
|
+
const copy=values.map(value=>String(value));
|
|
26
|
+
insist(copy.every(validator),`${label} contains invalid value`);
|
|
27
|
+
insist(new Set(copy).size===copy.length,`${label} contains duplicate value`);
|
|
28
|
+
const sorted=[...copy].sort((a,b)=>a.localeCompare(b,'en'));
|
|
29
|
+
insist(JSON.stringify(copy)===JSON.stringify(sorted),`${label} must be sorted`);
|
|
30
|
+
return copy;
|
|
31
|
+
}
|
|
32
|
+
function resourcePath(value){
|
|
33
|
+
insist(typeof value==='string'&&value.length>0&&value.length<=512,'resource path invalid');
|
|
34
|
+
insist(!value.includes('\\'),'resource path must use forward slashes');
|
|
35
|
+
insist(!path.posix.isAbsolute(value),'resource path must be relative');
|
|
36
|
+
const parts=value.split('/');
|
|
37
|
+
insist(parts.every(part=>part&&part!=='.'&&part!=='..'),'resource path traversal denied');
|
|
38
|
+
insist(value!=='SKILL.md'&&value!=='skill.json','resource path is reserved');
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
function parseFrontmatter(text){
|
|
42
|
+
const lines=text.split(/\r?\n/);
|
|
43
|
+
insist(lines[0]==='---','SKILL.md frontmatter missing');
|
|
44
|
+
const end=lines.indexOf('---',1);
|
|
45
|
+
insist(end>1&&end<=32,'SKILL.md frontmatter boundary invalid');
|
|
46
|
+
const meta={};
|
|
47
|
+
for(const line of lines.slice(1,end)){
|
|
48
|
+
const m=/^([a-z_]+):\s*(.*)$/.exec(line);
|
|
49
|
+
insist(m,'SKILL.md frontmatter line malformed');
|
|
50
|
+
insist(!Object.hasOwn(meta,m[1]),`SKILL.md duplicate frontmatter key ${m[1]}`);
|
|
51
|
+
meta[m[1]]=m[2];
|
|
52
|
+
}
|
|
53
|
+
exactKeys(meta,['name','description','version','audience'],'SKILL.md frontmatter');
|
|
54
|
+
return meta;
|
|
55
|
+
}
|
|
56
|
+
async function walkFiles(root,rel=''){
|
|
57
|
+
const dir=path.join(root,...(rel?rel.split('/'):[]));
|
|
58
|
+
const names=(await readdir(dir)).sort((a,b)=>a.localeCompare(b,'en'));
|
|
59
|
+
const out=[];
|
|
60
|
+
for(const name of names){
|
|
61
|
+
const childRel=rel?`${rel}/${name}`:name;
|
|
62
|
+
const full=path.join(root,...childRel.split('/'));
|
|
63
|
+
const st=await lstat(full);
|
|
64
|
+
insist(!st.isSymbolicLink(),`skill symlink forbidden: ${childRel}`);
|
|
65
|
+
if(st.isDirectory()) out.push(...await walkFiles(root,childRel));
|
|
66
|
+
else { insist(st.isFile(),`skill non-file forbidden: ${childRel}`);out.push(childRel); }
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
async function readBounded(file,max,label){
|
|
71
|
+
const bytes=await readFile(file);
|
|
72
|
+
insist(bytes.length<=max,`${label} exceeds byte limit`);
|
|
73
|
+
return bytes;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function createSkillPlane({root,verifyKeyPath}={}){
|
|
77
|
+
insist(typeof root==='string'&&path.isAbsolute(root),'skill root must be absolute');
|
|
78
|
+
insist(typeof verifyKeyPath==='string'&&path.isAbsolute(verifyKeyPath),'skill verify key path must be absolute');
|
|
79
|
+
const rootStat=await lstat(root);
|
|
80
|
+
insist(rootStat.isDirectory()&&!rootStat.isSymbolicLink(),'skill root must be a real directory');
|
|
81
|
+
const rootReal=await realpath(root);
|
|
82
|
+
const publicKey=createPublicKey(await readFile(verifyKeyPath));
|
|
83
|
+
insist(publicKey.asymmetricKeyType==='ed25519','skill verify key must be Ed25519');
|
|
84
|
+
const keyId=sha(Buffer.from(publicKey.export({type:'spki',format:'der'})));
|
|
85
|
+
|
|
86
|
+
async function verified(name){
|
|
87
|
+
insist(typeof name==='string'&&SKILL_NAME.test(name),'skill name invalid');
|
|
88
|
+
const dir=path.join(rootReal,name);
|
|
89
|
+
const st=await lstat(dir);
|
|
90
|
+
insist(st.isDirectory()&&!st.isSymbolicLink(),'skill directory must be real');
|
|
91
|
+
const dirReal=await realpath(dir);
|
|
92
|
+
insist(path.dirname(dirReal).toLowerCase()===rootReal.toLowerCase(),'skill directory escape denied');
|
|
93
|
+
|
|
94
|
+
for(const reserved of ['skill.json','SKILL.md']){
|
|
95
|
+
const rst=await lstat(path.join(dirReal,reserved));
|
|
96
|
+
insist(rst.isFile()&&!rst.isSymbolicLink(),`${reserved} must be a real file`);
|
|
97
|
+
}
|
|
98
|
+
const envelopeBytes=await readBounded(path.join(dirReal,'skill.json'),256*1024,'skill.json');
|
|
99
|
+
let envelope;
|
|
100
|
+
try{envelope=JSON.parse(strictUtf8(envelopeBytes));}catch(error){throw new Error(`skill.json invalid JSON/UTF-8: ${error.message}`);}
|
|
101
|
+
exactKeys(envelope,['body','body_sha256','attestation','manifest_sha256'],'skill envelope');
|
|
102
|
+
insist(/^[0-9a-f]{64}$/.test(envelope.body_sha256??''),'skill body hash malformed');
|
|
103
|
+
insist(/^[0-9a-f]{64}$/.test(envelope.manifest_sha256??''),'skill manifest hash malformed');
|
|
104
|
+
exactKeys(envelope.attestation,['algorithm','domain','key_id','signature_base64'],'skill attestation');
|
|
105
|
+
insist(envelope.attestation.algorithm==='ed25519'&&envelope.attestation.domain==='DEADBYTE-SKILL-MANIFEST-V1','skill attestation contract mismatch');
|
|
106
|
+
insist(envelope.attestation.key_id===keyId,'skill signer mismatch');
|
|
107
|
+
|
|
108
|
+
const body=envelope.body;
|
|
109
|
+
exactKeys(body,['schema','name','version','description','audience','skill_md_sha256','resources','required_capabilities','authority_planes','provenance'],'skill manifest body');
|
|
110
|
+
insist(body.schema==='deadbyte.skill-manifest.v1','skill manifest schema mismatch');
|
|
111
|
+
insist(body.name===name&&SKILL_NAME.test(body.name),'skill manifest name mismatch');
|
|
112
|
+
insist(typeof body.version==='string'&&VERSION.test(body.version),'skill version invalid');
|
|
113
|
+
insist(typeof body.description==='string'&&body.description.length>=1&&body.description.length<=512,'skill description invalid');
|
|
114
|
+
insist(body.audience==='agent','skill audience invalid');
|
|
115
|
+
insist(/^[0-9a-f]{64}$/.test(body.skill_md_sha256??''),'SKILL.md hash malformed');
|
|
116
|
+
const requiredCapabilities=uniqueSorted(body.required_capabilities,'required capabilities',value=>CAPABILITY.test(value));
|
|
117
|
+
const authorityPlanes=uniqueSorted(body.authority_planes,'authority planes',value=>typeof value==='string'&&value.length>=1&&value.length<=32);
|
|
118
|
+
for(const plane of authorityPlanes) insist(AUTHORITY_PLANES.has(plane),`unknown authority plane: ${plane}`);
|
|
119
|
+
exactKeys(body.provenance,['source','origin_url','source_revision'],'skill provenance');
|
|
120
|
+
insist(typeof body.provenance.source==='string'&&body.provenance.source.length>=1&&body.provenance.source.length<=128,'skill provenance source invalid');
|
|
121
|
+
insist(body.provenance.origin_url===null||typeof body.provenance.origin_url==='string','skill provenance origin_url invalid');
|
|
122
|
+
insist(body.provenance.source_revision===null||typeof body.provenance.source_revision==='string','skill provenance source_revision invalid');
|
|
123
|
+
|
|
124
|
+
insist(Array.isArray(body.resources)&&body.resources.length<=128,'skill resources invalid');
|
|
125
|
+
const resources=body.resources.map(item=>{
|
|
126
|
+
exactKeys(item,['path','sha256','kind'],'skill resource');
|
|
127
|
+
const rel=resourcePath(item.path);
|
|
128
|
+
insist(/^[0-9a-f]{64}$/.test(item.sha256??''),'skill resource hash malformed');
|
|
129
|
+
insist(RESOURCE_KINDS.has(item.kind),'skill resource kind invalid');
|
|
130
|
+
return {path:rel,sha256:item.sha256,kind:item.kind};
|
|
131
|
+
});
|
|
132
|
+
const resourceNames=resources.map(item=>item.path);
|
|
133
|
+
insist(new Set(resourceNames).size===resourceNames.length,'skill resource paths duplicate');
|
|
134
|
+
insist(JSON.stringify(resourceNames)===JSON.stringify([...resourceNames].sort((a,b)=>a.localeCompare(b,'en'))),'skill resources must be sorted');
|
|
135
|
+
|
|
136
|
+
const bodyBytes=Buffer.from(canonicalJson(body),'utf8');
|
|
137
|
+
insist(sha(bodyBytes)===envelope.body_sha256,'skill body hash mismatch');
|
|
138
|
+
const {manifest_sha256,...unsignedEnvelope}=envelope;
|
|
139
|
+
insist(sha(Buffer.from(canonicalJson(unsignedEnvelope),'utf8'))===manifest_sha256,'skill manifest envelope hash mismatch');
|
|
140
|
+
insist(cryptoVerify(null,Buffer.concat([DOMAIN,bodyBytes]),publicKey,Buffer.from(envelope.attestation.signature_base64,'base64')),'skill signature invalid');
|
|
141
|
+
|
|
142
|
+
const skillBytes=await readBounded(path.join(dirReal,'SKILL.md'),128*1024,'SKILL.md');
|
|
143
|
+
insist(sha(skillBytes)===body.skill_md_sha256,'SKILL.md hash mismatch');
|
|
144
|
+
const skillText=strictUtf8(skillBytes);
|
|
145
|
+
const frontmatter=parseFrontmatter(skillText);
|
|
146
|
+
insist(frontmatter.name===body.name,'SKILL.md name mismatch');
|
|
147
|
+
insist(frontmatter.version===body.version,'SKILL.md version mismatch');
|
|
148
|
+
insist(frontmatter.description===body.description,'SKILL.md description mismatch');
|
|
149
|
+
insist(frontmatter.audience===body.audience,'SKILL.md audience mismatch');
|
|
150
|
+
|
|
151
|
+
for(const item of resources){
|
|
152
|
+
const full=path.join(dirReal,...item.path.split('/'));
|
|
153
|
+
const rst=await lstat(full);
|
|
154
|
+
insist(rst.isFile()&&!rst.isSymbolicLink(),`skill resource must be a real file: ${item.path}`);
|
|
155
|
+
const bytes=await readBounded(full,512*1024,`skill resource ${item.path}`);
|
|
156
|
+
insist(sha(bytes)===item.sha256,`resource hash mismatch: ${item.path}`);
|
|
157
|
+
}
|
|
158
|
+
const actual=await walkFiles(dirReal);
|
|
159
|
+
const allowed=['SKILL.md','skill.json',...resourceNames].sort((a,b)=>a.localeCompare(b,'en'));
|
|
160
|
+
const extra=actual.filter(file=>!allowed.includes(file));
|
|
161
|
+
insist(extra.length===0,`undeclared skill file: ${extra[0]}`);
|
|
162
|
+
const missing=allowed.filter(file=>!actual.includes(file));
|
|
163
|
+
insist(missing.length===0,`declared skill file missing: ${missing[0]}`);
|
|
164
|
+
|
|
165
|
+
return {dir:dirReal,envelope,body,skillText,resources,requiredCapabilities,authorityPlanes};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function list(){
|
|
169
|
+
const names=(await readdir(rootReal)).sort((a,b)=>a.localeCompare(b,'en'));
|
|
170
|
+
const skills=[];
|
|
171
|
+
for(const name of names){
|
|
172
|
+
const full=path.join(rootReal,name);
|
|
173
|
+
const st=await lstat(full);
|
|
174
|
+
insist(st.isDirectory()&&!st.isSymbolicLink(),`skill registry contains non-directory entry: ${name}`);
|
|
175
|
+
const item=await verified(name);
|
|
176
|
+
skills.push({
|
|
177
|
+
name:item.body.name,version:item.body.version,description:item.body.description,audience:item.body.audience,
|
|
178
|
+
manifest_sha256:item.envelope.manifest_sha256,required_capabilities:item.requiredCapabilities,
|
|
179
|
+
authority_planes:item.authorityPlanes,resource_count:item.resources.length,provenance:item.body.provenance,verified:true
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
const registrySha=sha(Buffer.from(canonicalJson(skills.map(item=>({name:item.name,manifest_sha256:item.manifest_sha256}))),'utf8'));
|
|
183
|
+
return {status:'ok',skills,registry_sha256:registrySha,verify_key_id:keyId};
|
|
184
|
+
}
|
|
185
|
+
async function load({name}={}){
|
|
186
|
+
const item=await verified(name);
|
|
187
|
+
return {
|
|
188
|
+
status:'ok',name:item.body.name,version:item.body.version,description:item.body.description,audience:item.body.audience,
|
|
189
|
+
content:item.skillText,manifest_sha256:item.envelope.manifest_sha256,required_capabilities:item.requiredCapabilities,
|
|
190
|
+
authority_planes:item.authorityPlanes,resources:item.resources,provenance:item.body.provenance,verified:true,verify_key_id:keyId
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
async function readResource({name,resourcePath:requested}={}){
|
|
194
|
+
const item=await verified(name);
|
|
195
|
+
const rel=resourcePath(requested);
|
|
196
|
+
const resource=item.resources.find(entry=>entry.path===rel);
|
|
197
|
+
insist(resource,`resource not declared: ${rel}`);
|
|
198
|
+
const bytes=await readBounded(path.join(item.dir,...rel.split('/')),512*1024,`skill resource ${rel}`);
|
|
199
|
+
insist(sha(bytes)===resource.sha256,`resource hash mismatch: ${rel}`);
|
|
200
|
+
return {status:'ok',name:item.body.name,path:rel,kind:resource.kind,sha256:resource.sha256,content:strictUtf8(bytes),verified:true};
|
|
201
|
+
}
|
|
202
|
+
return Object.freeze({list,load,readResource,root:rootReal,verify_key_id:keyId});
|
|
203
|
+
}
|
|
@@ -112,7 +112,7 @@ async function modernPassThrough() {
|
|
|
112
112
|
if (era !== 'modern') throw new Error(`expected modern era through compat prefilter, got ${String(era)}`);
|
|
113
113
|
const listed = await client.listTools();
|
|
114
114
|
const names = listed.tools.map(tool => tool.name).sort();
|
|
115
|
-
const baseTools = ['contained_transform', 'evidence_verify', 'file_read', 'proof_execute', 'workspace_open'];
|
|
115
|
+
const baseTools = ['contained_transform', 'evidence_verify', 'file_read', 'proof_execute', 'skill_catalog', 'workspace_open'];
|
|
116
116
|
const privilegedTools = ['host_exec', 'host_file_read', 'host_file_write'];
|
|
117
117
|
const allowedCatalogs = [
|
|
118
118
|
baseTools,
|
package/src/version.mjs
CHANGED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { EventEmitter } from 'node:events';
|
|
4
|
+
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { createAgentBackendR44 } from '../src/agent-backend-r44.mjs';
|
|
8
|
+
|
|
9
|
+
async function waitFor(predicate,timeoutMs=2000){
|
|
10
|
+
const deadline=Date.now()+timeoutMs;
|
|
11
|
+
while(Date.now()<deadline){if(predicate())return;await new Promise(resolve=>setTimeout(resolve,5));}
|
|
12
|
+
throw new Error('waitFor timeout');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
class FakeTransport extends EventEmitter {
|
|
16
|
+
constructor(){super();this.sent=[];this.started=false;this.stopped=false;}
|
|
17
|
+
async start(){this.started=true;}
|
|
18
|
+
async send(value){this.sent.push(structuredClone(value));}
|
|
19
|
+
async stop(){this.stopped=true;}
|
|
20
|
+
reply(id,result){this.emit('message',{jsonrpc:'2.0',id,result});}
|
|
21
|
+
serverRequest(id,method,params={}){this.emit('message',{jsonrpc:'2.0',id,method,params});}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
test('R44 agent backend persists thread identity and request lifecycle atomically',async()=>{
|
|
25
|
+
const root=await mkdtemp(path.join(os.tmpdir(),'deadbyte-r44-backend-'));
|
|
26
|
+
const transport=new FakeTransport();
|
|
27
|
+
try{
|
|
28
|
+
const backend=await createAgentBackendR44({backendId:'codex',stateDir:root,transportFactory:async()=>transport});
|
|
29
|
+
await backend.start();
|
|
30
|
+
await backend.bindThread({logicalId:'main',threadId:'thread-abc'});
|
|
31
|
+
assert.equal((await backend.status()).threads.main,'thread-abc');
|
|
32
|
+
const pending=backend.request({method:'thread/read',params:{threadId:'thread-abc'},timeoutMs:2000});
|
|
33
|
+
await waitFor(()=>transport.sent.length===1);
|
|
34
|
+
assert.equal(transport.sent.length,1);
|
|
35
|
+
const call=transport.sent[0];
|
|
36
|
+
assert.equal(call.jsonrpc,'2.0');assert.equal(call.method,'thread/read');assert.match(call.id,/^[0-9a-f]{64}$/);
|
|
37
|
+
transport.reply(call.id,{ok:true});
|
|
38
|
+
assert.deepEqual(await pending,{ok:true});
|
|
39
|
+
const state=JSON.parse(await readFile(path.join(root,'codex.json'),'utf8'));
|
|
40
|
+
assert.equal(state.schema,'deadbyte.agent-backend-state.v1');
|
|
41
|
+
assert.equal(state.threads.main,'thread-abc');
|
|
42
|
+
assert.equal(state.requests[call.id].state,'completed');
|
|
43
|
+
assert.deepEqual(state.requests[call.id].result,{ok:true});
|
|
44
|
+
assert.equal('token' in state,false);assert.equal('api_key' in state,false);
|
|
45
|
+
await backend.close();
|
|
46
|
+
}finally{await rm(root,{recursive:true,force:true});}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('R44 agent backend denies server approval by default and never auto-approves',async()=>{
|
|
50
|
+
const root=await mkdtemp(path.join(os.tmpdir(),'deadbyte-r44-backend-deny-'));
|
|
51
|
+
const transport=new FakeTransport();
|
|
52
|
+
try{
|
|
53
|
+
const backend=await createAgentBackendR44({backendId:'codex',stateDir:root,transportFactory:async()=>transport});
|
|
54
|
+
await backend.start();
|
|
55
|
+
transport.serverRequest('srv-1','approval/request',{kind:'shell'});
|
|
56
|
+
await waitFor(()=>transport.sent.some(x=>x.id==='srv-1'));
|
|
57
|
+
const response=transport.sent.find(x=>x.id==='srv-1');
|
|
58
|
+
assert.ok(response);assert.equal(response.error.code,-32001);assert.match(response.error.message,/denied/i);
|
|
59
|
+
assert.equal(response.result,undefined);
|
|
60
|
+
await backend.close();
|
|
61
|
+
}finally{await rm(root,{recursive:true,force:true});}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('R44 backend restart classifies unresolved requests interrupted and preserves threads',async()=>{
|
|
65
|
+
const root=await mkdtemp(path.join(os.tmpdir(),'deadbyte-r44-backend-restart-'));
|
|
66
|
+
const firstTransport=new FakeTransport();
|
|
67
|
+
try{
|
|
68
|
+
const first=await createAgentBackendR44({backendId:'codex',stateDir:root,transportFactory:async()=>firstTransport});
|
|
69
|
+
await first.start();await first.bindThread({logicalId:'main',threadId:'thread-persist'});
|
|
70
|
+
const promise=first.request({method:'turn/start',params:{threadId:'thread-persist'},timeoutMs:10000});
|
|
71
|
+
promise.catch(()=>{});
|
|
72
|
+
await waitFor(()=>firstTransport.sent.length===1);
|
|
73
|
+
const id=firstTransport.sent[0].id;
|
|
74
|
+
await first.simulateCrashForTest();
|
|
75
|
+
const secondTransport=new FakeTransport();
|
|
76
|
+
const second=await createAgentBackendR44({backendId:'codex',stateDir:root,transportFactory:async()=>secondTransport});
|
|
77
|
+
const status=await second.status();
|
|
78
|
+
assert.equal(status.threads.main,'thread-persist');
|
|
79
|
+
assert.equal(status.requests[id].state,'interrupted');
|
|
80
|
+
assert.equal(secondTransport.sent.length,0,'interrupted request must not auto-replay');
|
|
81
|
+
}finally{await rm(root,{recursive:true,force:true});}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('R44 backend fails closed on truncated state and binds production executable hash',async()=>{
|
|
85
|
+
const root=await mkdtemp(path.join(os.tmpdir(),'deadbyte-r44-backend-corrupt-'));
|
|
86
|
+
try{
|
|
87
|
+
await writeFile(path.join(root,'codex.json'),'{"schema":','utf8');
|
|
88
|
+
await assert.rejects(createAgentBackendR44({backendId:'codex',stateDir:root,transportFactory:async()=>new FakeTransport()}),/state JSON invalid/);
|
|
89
|
+
await rm(path.join(root,'codex.json'));
|
|
90
|
+
await assert.rejects(createAgentBackendR44({backendId:'codex',stateDir:root,executable:process.execPath,executableSha256:'0'.repeat(64),args:[]}),/executable hash mismatch/);
|
|
91
|
+
}finally{await rm(root,{recursive:true,force:true});}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
test('R44 agent backend supports Codex wire mode notifications without jsonrpc envelope',async()=>{
|
|
96
|
+
const root=await mkdtemp(path.join(os.tmpdir(),'deadbyte-r44-backend-codex-wire-'));
|
|
97
|
+
const transport=new FakeTransport();const seen=[];
|
|
98
|
+
try{
|
|
99
|
+
const backend=await createAgentBackendR44({backendId:'codex-wire',stateDir:root,transportFactory:async()=>transport,
|
|
100
|
+
wireMode:'codex-app-server',notificationHandler:async event=>seen.push(event)});
|
|
101
|
+
await backend.start();
|
|
102
|
+
const pending=backend.request({method:'initialize',params:{clientInfo:{name:'deadbyte',version:'0.15.0'}},timeoutMs:2000});
|
|
103
|
+
await waitFor(()=>transport.sent.length===1);
|
|
104
|
+
const request=transport.sent[0];
|
|
105
|
+
assert.equal('jsonrpc' in request,false);assert.equal(request.method,'initialize');
|
|
106
|
+
transport.reply(request.id,{userAgent:'codex-test'});
|
|
107
|
+
assert.deepEqual(await pending,{userAgent:'codex-test'});
|
|
108
|
+
await backend.notify({method:'initialized',params:{}});
|
|
109
|
+
assert.equal(transport.sent[1].method,'initialized');assert.equal('id' in transport.sent[1],false);assert.equal('jsonrpc' in transport.sent[1],false);
|
|
110
|
+
transport.emit('message',{method:'turn/completed',params:{threadId:'t',turn:{id:'turn-1',status:'completed'}}});
|
|
111
|
+
await waitFor(()=>seen.length===1);
|
|
112
|
+
assert.deepEqual(seen[0],{method:'turn/completed',params:{threadId:'t',turn:{id:'turn-1',status:'completed'}},backendId:'codex-wire'});
|
|
113
|
+
await backend.close();
|
|
114
|
+
}finally{await rm(root,{recursive:true,force:true});}
|
|
115
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
import test from 'node:test';
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
3
|
import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises';
|
|
4
4
|
import os from 'node:os';
|
|
@@ -20,7 +20,7 @@ const AGENT_TOOLS = [
|
|
|
20
20
|
'agent_task_submit',
|
|
21
21
|
'agent_task_verify'
|
|
22
22
|
];
|
|
23
|
-
const BASE_TOOLS = ['contained_transform', 'evidence_verify', 'file_read', 'proof_execute', 'workspace_open'];
|
|
23
|
+
const BASE_TOOLS = ['contained_transform', 'evidence_verify', 'file_read', 'proof_execute', 'skill_catalog', 'workspace_open'];
|
|
24
24
|
|
|
25
25
|
function structured(result, label) {
|
|
26
26
|
if (result.isError) throw new Error(`${label}: ${JSON.stringify(result.structuredContent ?? result.content)}`);
|
|
@@ -68,7 +68,7 @@ async function cleanup(f, client) {
|
|
|
68
68
|
await rm(f.trust, { recursive: true, force: true }).catch(() => {});
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
test('agent runtime
|
|
71
|
+
test('R44 agent runtime exposes base skill catalog plus durable task tools without host authority', async () => {
|
|
72
72
|
const f = await fixture();
|
|
73
73
|
let client;
|
|
74
74
|
try {
|