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.
Files changed (105) hide show
  1. package/AGENTS.md +11 -18
  2. package/CONTEXT.md +3 -3
  3. package/MANIFEST.SHA256 +104 -53
  4. package/R43-PARENT.json +20 -0
  5. package/R44-DONORS.json +99 -0
  6. package/R44-PARENT.json +18 -0
  7. package/README.txt +7 -2
  8. package/controller/README.TXT +4 -5
  9. package/controller/deadbyte-controller.ps1 +1 -1
  10. package/docs/ARCHITECTURE.md +8 -1
  11. package/docs/R44-ARCHITECTURE.md +37 -0
  12. package/package.json +13 -8
  13. package/scripts/deferred-slot-operation-r43.mjs +126 -0
  14. package/scripts/deferred-slot-operation-r44.mjs +126 -0
  15. package/scripts/final-closure-r43.mjs +115 -0
  16. package/scripts/final-closure-r44.mjs +121 -0
  17. package/scripts/final-closure-verify-r43.mjs +213 -0
  18. package/scripts/final-closure-verify-r44.mjs +220 -0
  19. package/scripts/finalize-r43.mjs +217 -0
  20. package/scripts/finalize-r44.mjs +217 -0
  21. package/scripts/full-regression-r43.mjs +20 -0
  22. package/scripts/full-regression-r44.mjs +20 -0
  23. package/scripts/gate-windows-r43.ps1 +36 -0
  24. package/scripts/gate-windows-r44.ps1 +36 -0
  25. package/scripts/release-parity-r43.mjs +218 -0
  26. package/scripts/release-parity-r44.mjs +242 -0
  27. package/scripts/release-parity-tests-r43.ps1 +8 -0
  28. package/scripts/release-parity-tests-r44.ps1 +8 -0
  29. package/scripts/verify-r43-parent.mjs +126 -0
  30. package/scripts/verify-r44-parent.mjs +142 -0
  31. package/scripts/windows-gate-evidence-r43.mjs +55 -0
  32. package/scripts/windows-gate-evidence-r44.mjs +55 -0
  33. package/src/agent-backend-r44.mjs +254 -0
  34. package/src/autonomous-output-contracts.mjs +1 -1
  35. package/src/autonomous-policy.mjs +41 -4
  36. package/src/autonomous-runtime.mjs +25 -12
  37. package/src/autonomous-supervisor-host.mjs +12 -2
  38. package/src/capability-ledger.mjs +2 -1
  39. package/src/codex-app-server-r44.mjs +154 -0
  40. package/src/coding-plane.mjs +66 -19
  41. package/src/durable-job-r44.mjs +145 -0
  42. package/src/external-job-provider-r44.mjs +126 -0
  43. package/src/host-plane.mjs +27 -6
  44. package/src/job-supervisor-host-r44.mjs +112 -0
  45. package/src/machine-fs-smoke-client.mjs +1 -1
  46. package/src/machine-fs.mjs +21 -10
  47. package/src/mcp-host-smoke-client.mjs +1 -1
  48. package/src/mcp-server.mjs +20 -3
  49. package/src/mcp-smoke-client.mjs +1 -1
  50. package/src/native-diff-v2.mjs +3 -3
  51. package/src/native-diff.mjs +3 -2
  52. package/src/observation-smoke-client.mjs +1 -1
  53. package/src/operator-surface.mjs +2 -2
  54. package/src/process-policy.mjs +19 -0
  55. package/src/process-runtime.mjs +5 -2
  56. package/src/process-smoke-client.mjs +2 -2
  57. package/src/provider-circuit.mjs +8 -2
  58. package/src/r44-compact-disarmed-probe.mjs +74 -0
  59. package/src/r44-full-surface-probe.mjs +34 -0
  60. package/src/remote-device-r44.mjs +205 -0
  61. package/src/remote-mcp-autonomous-smoke-client.mjs +4 -4
  62. package/src/remote-mcp-r44-parity-client.mjs +203 -0
  63. package/src/remote-mcp-smoke-client.mjs +7 -4
  64. package/src/runtime-update.mjs +64 -31
  65. package/src/skill-mcp-tools-r44.mjs +37 -0
  66. package/src/skill-plane-r44.mjs +203 -0
  67. package/src/tunnel-stdio-compat-smoke.mjs +1 -1
  68. package/src/version.mjs +1 -1
  69. package/test/agent-backend-r44.test.mjs +115 -0
  70. package/test/agent-runtime.test.mjs +3 -3
  71. package/test/autonomous-loop.test.mjs +97 -11
  72. package/test/autonomous-runtime.test.mjs +2 -2
  73. package/test/capability-ledger-r36.test.mjs +3 -3
  74. package/test/cli-entrypoint.test.mjs +1 -1
  75. package/test/codex-app-server-r44.test.mjs +115 -0
  76. package/test/codex-autonomous-r44.test.mjs +87 -0
  77. package/test/coding-plane.test.mjs +2 -2
  78. package/test/coding-runtime.test.mjs +36 -1
  79. package/test/compact-catalog-r36.test.mjs +3 -3
  80. package/test/compact-max-catalog-r40.test.mjs +4 -2
  81. package/test/compact-max-catalog-r44.test.mjs +97 -0
  82. package/test/core.test.mjs +1 -1
  83. package/test/durable-job-r44.test.mjs +75 -0
  84. package/test/external-job-provider-r44.test.mjs +46 -0
  85. package/test/full-max-catalog-r40.test.mjs +5 -3
  86. package/test/full-max-catalog-r44.test.mjs +91 -0
  87. package/test/host-plane.test.mjs +15 -1
  88. package/test/native-diff.test.mjs +28 -0
  89. package/test/output-schema.test.mjs +2 -2
  90. package/test/process-policy.test.mjs +22 -1
  91. package/test/prompts-resources.test.mjs +1 -1
  92. package/test/r33-closeout-regression.test.mjs +13 -30
  93. package/test/r33-finalization.test.mjs +7 -7
  94. package/test/r43-finalization.test.mjs +31 -0
  95. package/test/r43-finalizer.test.mjs +34 -0
  96. package/test/r43-parent.test.mjs +29 -0
  97. package/test/r44-finalization.test.mjs +58 -0
  98. package/test/r44-finalizer.test.mjs +48 -0
  99. package/test/r44-parent.test.mjs +25 -0
  100. package/test/release-version.test.mjs +53 -19
  101. package/test/remote-device-r44.test.mjs +104 -0
  102. package/test/runtime-self-update.test.mjs +42 -0
  103. package/test/skill-mcp-r44.test.mjs +38 -0
  104. package/test/skill-plane-r44.test.mjs +93 -0
  105. package/test/standing-grants.test.mjs +5 -1
@@ -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 coreProcess(coreRoot,bridgePid){
577
+ export function selectCoreProcess(processes,coreRoot,bridgePid,verifiedBridge=null){
577
578
  const needle=path.join(coreRoot,'src','mcp-server.mjs').replaceAll('/','\\').toLowerCase();
578
- const matches=nodeProcesses().filter(item=>Number(item.ParentProcessId)===bridgePid&&String(item.CommandLine??'').toLowerCase().includes(needle));
579
- if(matches.length!==1) throw new Error(`expected one downstream core for bridge ${bridgePid}, found ${matches.length}`);
580
- const item=matches[0]; return {pid:Number(item.ProcessId),parent_pid:Number(item.ParentProcessId),creation_date:String(item.CreationDate??''),
581
- executable_path:String(item.ExecutablePath??''),command_line:String(item.CommandLine??'')};
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,status.bridge_pid);
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,status.bridge_pid);
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
- async function restoreStandingAuthority(ctx,prior,enabled){
765
- if(!enabled){
766
- const status=parseControllerStatus(controllerCaptured(ctx,'status',15000));
767
- if(status.coding_armed||status.autonomous_armed||status.desktop_armed) throw new Error('standing authority unexpectedly survived slot switch');
768
- if(status.host_armed) throw new Error('host plane became armed during slot switch');
769
- return status;
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
- if(prior.autonomous_armed&&!prior.coding_armed) throw new Error('invalid prior authority: autonomous armed without coding');
772
- if(prior.coding_armed) controllerCaptured(ctx,'armcoding',15000);
773
- if(prior.autonomous_armed) controllerCaptured(ctx,'armautonomous',15000);
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
- if(status.coding_armed!==prior.coding_armed) throw new Error('coding standing-grant restore gate failed');
776
- if(status.autonomous_armed!==prior.autonomous_armed) throw new Error('autonomous standing-grant restore gate failed');
777
- if(status.desktop_armed) throw new Error('desktop plane must remain disarmed after slot switch');
778
- if(status.host_armed) throw new Error('host plane became armed during slot switch');
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,command_line:runtime.core.command_line}};
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
@@ -1,4 +1,4 @@
1
- export const DEADBYTE_VERSION = '0.14.0';
1
+ export const DEADBYTE_VERSION = '0.15.0';
2
2
  export const MCP_PROTOCOL_VERSION = '2026-07-28';
3
3
  export const RECEIPT_SCHEMA_V1 = 'deadbyte.receipt.v1';
4
4
  export const RECEIPT_SCHEMA_V2 = 'deadbyte.receipt.v2';
@@ -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
- import test from 'node:test';
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 without an explicit host policy exposes only base plus durable task tools', async () => {
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 {