deadbyte-mcp 0.11.3 → 0.12.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 (93) hide show
  1. package/MANIFEST.SHA256 +92 -38
  2. package/README.txt +5 -4
  3. package/acceptance/r40/desktop-live-drift.mjs +106 -0
  4. package/acceptance/r40/desktop-live-ground.mjs +110 -0
  5. package/acceptance/r40/desktop-live-input.mjs +128 -0
  6. package/acceptance/r40/desktop-live-observe.mjs +100 -0
  7. package/acceptance/r40/desktop-live-uia.mjs +121 -0
  8. package/bin/deadbyte-desktop-input-r40.exe +0 -0
  9. package/bin/deadbyte-desktop-input-r40.obj +0 -0
  10. package/bin/deadbyte-desktop-observer-r40.exe +0 -0
  11. package/bin/deadbyte-desktop-observer-r40.obj +0 -0
  12. package/bin/deadbyte-desktop-uia-r40.exe +0 -0
  13. package/bin/deadbyte-desktop-uia-r40.obj +0 -0
  14. package/controller/README.TXT +7 -5
  15. package/controller/deadbyte-controller.config.psd1.example +3 -0
  16. package/controller/deadbyte-controller.ps1 +149 -4
  17. package/controller/deadbyte-desktop-policy.json +41 -0
  18. package/docs/ARCHITECTURE.md +6 -1
  19. package/docs/superpowers/specs/2026-09-19-r40-desktop-computer-use-design.md +214 -0
  20. package/native/deadbyte-desktop-input.cpp +455 -0
  21. package/native/deadbyte-desktop-observer.cpp +253 -0
  22. package/native/deadbyte-desktop-uia.cpp +307 -0
  23. package/package.json +10 -8
  24. package/scripts/build-desktop-r40.ps1 +106 -0
  25. package/scripts/deferred-slot-operation-r40.mjs +126 -0
  26. package/scripts/final-closure-r40.mjs +114 -0
  27. package/scripts/final-closure-verify-r40.mjs +212 -0
  28. package/scripts/gate-windows-r40.ps1 +43 -0
  29. package/scripts/init-desktop-policy-r40.ps1 +79 -0
  30. package/scripts/r40-compact-ledger-probe.mjs +4 -0
  31. package/scripts/release-parity-r40.mjs +212 -0
  32. package/scripts/release-parity-tests-r40.ps1 +9 -0
  33. package/scripts/windows-gate-evidence-r40.mjs +52 -0
  34. package/src/autonomous-context-engine.mjs +2 -1
  35. package/src/autonomous-planner-contracts.mjs +19 -1
  36. package/src/autonomous-planner.mjs +22 -0
  37. package/src/autonomous-runtime.mjs +144 -24
  38. package/src/capability-ledger.mjs +6 -1
  39. package/src/compact-gateway-mcp-tools.mjs +14 -2
  40. package/src/deadbyte-cli.mjs +72 -44
  41. package/src/desktop-contract.mjs +263 -0
  42. package/src/desktop-input-r40.mjs +148 -0
  43. package/src/desktop-mcp-tools-r40.mjs +126 -0
  44. package/src/desktop-memory-r40.mjs +272 -0
  45. package/src/desktop-observer-r40.mjs +231 -0
  46. package/src/desktop-policy-r40.mjs +110 -0
  47. package/src/desktop-runtime-r40.mjs +318 -0
  48. package/src/desktop-uia-r40.mjs +123 -0
  49. package/src/mcp-server.mjs +74 -19
  50. package/src/r40-compact-disarmed-probe.mjs +71 -0
  51. package/src/r40-full-surface-probe.mjs +34 -0
  52. package/src/release-generation.mjs +38 -5
  53. package/src/remote-console.mjs +41 -0
  54. package/src/remote-live-log.mjs +208 -45
  55. package/src/remote-mcp-desktop-r40-smoke-client.mjs +70 -0
  56. package/src/remote-mcp-r40-parity-client.mjs +200 -0
  57. package/src/remote-mcp-smoke-client.mjs +20 -6
  58. package/src/remote-result-journal.mjs +10 -2
  59. package/src/runtime-update.mjs +55 -17
  60. package/src/tool-audit.mjs +29 -2
  61. package/src/version.mjs +1 -1
  62. package/test/autonomous-capability-r36.test.mjs +43 -1
  63. package/test/autonomous-desktop-r40.test.mjs +188 -0
  64. package/test/capability-ledger-r36.test.mjs +1 -0
  65. package/test/cli-entrypoint.test.mjs +1 -1
  66. package/test/compact-catalog-r36.test.mjs +2 -2
  67. package/test/compact-gateway-r36.test.mjs +56 -4
  68. package/test/compact-max-catalog-r40.test.mjs +95 -0
  69. package/test/controller-desktop-r40.test.mjs +85 -0
  70. package/test/core.test.mjs +1 -1
  71. package/test/desktop-adversarial-r40.test.mjs +62 -0
  72. package/test/desktop-contract-r40.test.mjs +200 -0
  73. package/test/desktop-input-r40.test.mjs +63 -0
  74. package/test/desktop-mcp-tools-r40.test.mjs +61 -0
  75. package/test/desktop-memory-r40.test.mjs +141 -0
  76. package/test/desktop-observer-r40.test.mjs +119 -0
  77. package/test/desktop-policy-r40.test.mjs +111 -0
  78. package/test/desktop-runtime-r40.test.mjs +278 -0
  79. package/test/desktop-uia-r40.test.mjs +42 -0
  80. package/test/full-max-catalog-r40.test.mjs +91 -0
  81. package/test/helpers/autonomous-r34-fixture.mjs +3 -3
  82. package/test/r33-closeout-regression.test.mjs +7 -7
  83. package/test/r33-finalization.test.mjs +14 -14
  84. package/test/r37-remote-tool-call-timeline.test.mjs +6 -5
  85. package/test/r40-finalization.test.mjs +118 -0
  86. package/test/release-generation.test.mjs +41 -9
  87. package/test/release-version.test.mjs +19 -61
  88. package/test/remote-console-r40.test.mjs +38 -0
  89. package/test/remote-live-log-r37.test.mjs +89 -32
  90. package/test/remote-r24-catalog.test.mjs +12 -3
  91. package/test/remote-result-journal.test.mjs +15 -0
  92. package/test/remote-session-cli.test.mjs +69 -46
  93. package/test/runtime-self-update.test.mjs +44 -0
@@ -1,4 +1,4 @@
1
- import { randomBytes } from 'node:crypto';
1
+ import { createHash, randomBytes } from 'node:crypto';
2
2
  import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client';
3
3
 
4
4
  const endpointText = process.argv[2];
@@ -51,6 +51,9 @@ const r36CompactTools = [
51
51
  'process_session'
52
52
  ].sort();
53
53
  if (r36CompactTools.length !== 41) throw new Error(`R36 compact catalog constant drift: ${r36CompactTools.length}`);
54
+ const r40CompactTools = [...r36CompactTools, 'desktop_control'].sort();
55
+ if (r40CompactTools.length !== 42) throw new Error(`R40 compact catalog constant drift: ${r40CompactTools.length}`);
56
+ const sha256 = bytes => createHash('sha256').update(bytes).digest('hex');
54
57
  const allowedCatalogs = [];
55
58
  for (const machine of [false, true]) for (const host of [false, true]) for (const agent of [false, true]) {
56
59
  for (const processPlane of [false, true]) for (const machineFs of [false, true]) for (const observation of [false, true]) for (const codingMode of ['none', 'coding', 'autonomous']) {
@@ -93,7 +96,9 @@ try {
93
96
  if (client.getProtocolEra() !== 'modern') throw new Error(`expected modern MCP era, got ${String(client.getProtocolEra())}`);
94
97
  const listed = await client.listTools();
95
98
  const names = listed.tools.map(t => t.name).sort();
96
- const compactProfile = JSON.stringify(names) === JSON.stringify(r36CompactTools);
99
+ const exactR36Compact = JSON.stringify(names) === JSON.stringify(r36CompactTools);
100
+ const exactR40Compact = JSON.stringify(names) === JSON.stringify(r40CompactTools);
101
+ const compactProfile = exactR36Compact || exactR40Compact;
97
102
  const catalogOk = compactProfile || allowedCatalogs.some(expected => JSON.stringify(names) === JSON.stringify(expected));
98
103
  if (!catalogOk) throw new Error(`tool catalog mismatch: ${JSON.stringify(names)}`);
99
104
  for (const tool of listed.tools) {
@@ -117,14 +122,23 @@ try {
117
122
  let agent = null;
118
123
  if (compactProfile) {
119
124
  const manifest = gateway(await client.callTool({ name: 'capability_manifest', arguments: {} }), 'capability_manifest');
120
- if (manifest.schema !== 'deadbyte.capability-manifest.v1' || manifest.profile !== 'compact' ||
125
+ const manifestV1 = manifest.schema === 'deadbyte.capability-manifest.v1' && exactR36Compact;
126
+ const manifestV2 = manifest.schema === 'deadbyte.capability-manifest.v2' && exactR40Compact;
127
+ if ((!manifestV1 && !manifestV2) || manifest.profile !== 'compact' ||
121
128
  !Array.isArray(manifest.capabilities) || !/^[0-9a-f]{64}$/.test(String(manifest.ledger_sha256 ?? ''))) {
122
- throw new Error('R36 compact capability manifest malformed');
129
+ throw new Error('compact capability manifest malformed');
130
+ }
131
+ if (manifestV2) {
132
+ const expectedCatalogSha = sha256(Buffer.from(JSON.stringify(names), 'utf8'));
133
+ if (manifest.tool_count !== names.length || JSON.stringify(manifest.tool_catalog) !== JSON.stringify(names) ||
134
+ manifest.catalog_sha256 !== expectedCatalogSha) {
135
+ throw new Error('R40 compact capability manifest catalog attestation mismatch');
136
+ }
123
137
  }
124
138
  const required = manifest.capabilities.filter(item => item?.production === true && item?.host_only !== true);
125
139
  for (const item of required) {
126
140
  if (typeof item.compact_tool !== 'string' || !names.includes(item.compact_tool)) {
127
- throw new Error(`R36 compact capability route missing: ${String(item.id)}`);
141
+ throw new Error(`compact capability route missing: ${String(item.id)}`);
128
142
  }
129
143
  }
130
144
  const machine = gateway(await client.callTool({ name: 'machine_observe', arguments: { op: 'machine_status' } }), 'machine_status');
@@ -132,7 +146,7 @@ try {
132
146
  const ping = gateway(await client.callTool({ name: 'machine_observe', arguments: { op: 'machine_ping', nonce } }), 'machine_ping');
133
147
  if (ping.nonce !== nonce || ping.machine_id !== machine.machine_id ||
134
148
  ping.release_id !== machine.release_id || ping.release_manifest_sha256 !== machine.release_manifest_sha256) {
135
- throw new Error('R36 compact machine ping binding mismatch');
149
+ throw new Error('compact machine ping binding mismatch');
136
150
  }
137
151
  compact = {
138
152
  profile: 'compact',
@@ -6,6 +6,7 @@ export const REMOTE_RESULT_JOURNAL_SCHEMA='deadbyte.remote-result-journal.v1';
6
6
  const SESSION=/^[0-9a-f]{24}$/;
7
7
  const HASH=/^[0-9a-f]{64}$/;
8
8
  const STATUS=new Set(['ok','error']);
9
+ const SIDE_EFFECT_STATES=new Set(['unknown']);
9
10
  const EVENT_TYPES=new Set(['tool.call','tool.result']);
10
11
 
11
12
  function text(value,label,max){
@@ -40,10 +41,17 @@ function validateEvent(raw,index=null){
40
41
  observed_at_utc:observed,payload_sha256:raw.payload_sha256,arguments:sanitizeAuditArgs(rawArgs)});
41
42
  }
42
43
  if(!STATUS.has(raw.status)) throw new Error('remote result status invalid');
44
+ const durationMs=raw.duration_ms===undefined?null:raw.duration_ms;
45
+ if(durationMs!==null&&(!Number.isFinite(durationMs)||durationMs<0||durationMs>86_400_000)) throw new Error('remote result duration_ms invalid');
46
+ const reason=raw.reason===undefined||raw.reason===null?null:text(raw.reason,'remote result reason',512);
47
+ const sideEffectState=raw.side_effect_state===undefined||raw.side_effect_state===null?null:raw.side_effect_state;
48
+ if(sideEffectState!==null&&!SIDE_EFFECT_STATES.has(sideEffectState)) throw new Error('remote result side_effect_state invalid');
43
49
  return Object.freeze({session_id:sessionId,seq,event_type:eventType,request_id:requestId,tool,status:raw.status,
44
- observed_at_utc:observed,payload_sha256:raw.payload_sha256});
50
+ observed_at_utc:observed,payload_sha256:raw.payload_sha256,
51
+ ...(durationMs===null?{}:{duration_ms:durationMs}),...(reason===null?{}:{reason}),
52
+ ...(sideEffectState===null?{}:{side_effect_state:sideEffectState})});
45
53
  }
46
- function identity(item){return `${item.event_type}\0${item.request_id}\0${item.status??''}\0${item.payload_sha256}`;}
54
+ function identity(item){return `${item.event_type}\0${item.request_id}\0${item.status??''}\0${item.side_effect_state??''}\0${item.payload_sha256}`;}
47
55
  export function parseRemoteResultCursor(cursor){
48
56
  if(typeof cursor!=='string') throw new Error('remote result cursor invalid');
49
57
  const match=/^r1:([0-9a-f]{24}):(0|[1-9][0-9]{0,15})$/.exec(cursor);
@@ -45,14 +45,17 @@ function normalizePersistentPublicOrigin(value){
45
45
  return publicOrigin.origin;
46
46
  }
47
47
  export function parseUpdateArgs(argv){
48
- const out={stage:null,developmentRoot:null,expectedVersion:null,expectedManifest:null,sourceRoot:null,restoreStandingGrants:false,recoverOfflinePredecessor:false,enableMachineFs:false,enableObservation:false,enableAutonomousSupervisor:false,cloudTransport:null,bridgeBootstrap:null,processPolicyBootstrap:null,semanticLspBootstrap:null,hostCommandPins:[]};
49
- const cloud={},bridge={},processPolicy={},semanticLsp={},seen=new Set(),hostPinIds=new Set();
48
+ const out={stage:null,developmentRoot:null,expectedVersion:null,expectedManifest:null,sourceRoot:null,restoreStandingGrants:false,recoverOfflinePredecessor:false,enableMachineFs:false,enableObservation:false,enableDesktop:false,enableAutonomousSupervisor:false,cloudTransport:null,bridgeBootstrap:null,processPolicyBootstrap:null,desktopPolicyBootstrap:null,semanticLspBootstrap:null,hostCommandPins:[]};
49
+ const cloud={},bridge={},processPolicy={},desktopPolicy={},semanticLsp={},seen=new Set(),hostPinIds=new Set();
50
50
  const bridgeFlags=new Map([
51
51
  ['--bridge-root','root'],['--bridge-script-sha256','scriptSha256']
52
52
  ]);
53
53
  const processPolicyFlags=new Map([
54
54
  ['--process-policy','path'],['--process-policy-sha256','sha256']
55
55
  ]);
56
+ const desktopPolicyFlags=new Map([
57
+ ['--desktop-policy','path'],['--desktop-policy-sha256','sha256']
58
+ ]);
56
59
  const semanticLspFlags=new Map([
57
60
  ['--semantic-lsp-config','path'],['--semantic-lsp-config-sha256','sha256']
58
61
  ]);
@@ -61,16 +64,17 @@ export function parseUpdateArgs(argv){
61
64
  ['--cloud-token-file','tokenFilePath'],['--cloud-token-sha256','tokenFileSha256'],
62
65
  ['--cloud-child-working-directory','childWorkingDirectory'],['--cloud-public-origin','publicOrigin']
63
66
  ]);
64
- const valued=new Set(['--stage','--development-root','--expected-version','--expected-manifest','--source-root','--host-command-pin',...bridgeFlags.keys(),...processPolicyFlags.keys(),...semanticLspFlags.keys(),...cloudFlags.keys()]);
67
+ const valued=new Set(['--stage','--development-root','--expected-version','--expected-manifest','--source-root','--host-command-pin',...bridgeFlags.keys(),...processPolicyFlags.keys(),...desktopPolicyFlags.keys(),...semanticLspFlags.keys(),...cloudFlags.keys()]);
65
68
  for(let index=0;index<argv.length;index++){
66
69
  const arg=argv[index];
67
- if(arg==='--restore-standing-grants'||arg==='--recover-offline-predecessor'||arg==='--enable-machine-fs'||arg==='--enable-observation'||arg==='--enable-autonomous-supervisor'){
70
+ if(arg==='--restore-standing-grants'||arg==='--recover-offline-predecessor'||arg==='--enable-machine-fs'||arg==='--enable-observation'||arg==='--enable-desktop'||arg==='--enable-autonomous-supervisor'){
68
71
  if(seen.has(arg)) throw new Error(`duplicate self-update argument '${arg}'`);
69
72
  seen.add(arg);
70
73
  if(arg==='--restore-standing-grants') out.restoreStandingGrants=true;
71
74
  else if(arg==='--recover-offline-predecessor') out.recoverOfflinePredecessor=true;
72
75
  else if(arg==='--enable-machine-fs') out.enableMachineFs=true;
73
76
  else if(arg==='--enable-observation') out.enableObservation=true;
77
+ else if(arg==='--enable-desktop') out.enableDesktop=true;
74
78
  else out.enableAutonomousSupervisor=true;
75
79
  continue;
76
80
  }
@@ -95,6 +99,7 @@ export function parseUpdateArgs(argv){
95
99
  else if(arg==='--source-root') out.sourceRoot=path.resolve(value);
96
100
  else if(bridgeFlags.has(arg)) bridge[bridgeFlags.get(arg)]=value;
97
101
  else if(processPolicyFlags.has(arg)) processPolicy[processPolicyFlags.get(arg)]=value;
102
+ else if(desktopPolicyFlags.has(arg)) desktopPolicy[desktopPolicyFlags.get(arg)]=value;
98
103
  else if(semanticLspFlags.has(arg)) semanticLsp[semanticLspFlags.get(arg)]=value;
99
104
  else cloud[cloudFlags.get(arg)]=value;
100
105
  }
@@ -116,6 +121,13 @@ export function parseUpdateArgs(argv){
116
121
  if(!HEX64.test(sha256)) throw new Error('process policy SHA-256 must be lowercase hex');
117
122
  out.processPolicyBootstrap={path:path.resolve(processPolicy.path),sha256};
118
123
  }
124
+ const desktopPolicyFields=[...desktopPolicyFlags.values()],desktopPolicyPresent=desktopPolicyFields.filter(key=>desktopPolicy[key]!==undefined);
125
+ if(desktopPolicyPresent.length!==0&&desktopPolicyPresent.length!==desktopPolicyFields.length) throw new Error('desktop policy bootstrap requires path plus SHA-256 as one pair');
126
+ if(desktopPolicyPresent.length===desktopPolicyFields.length){
127
+ const sha256=String(desktopPolicy.sha256).toLowerCase();
128
+ if(!HEX64.test(sha256)) throw new Error('desktop policy SHA-256 must be lowercase hex');
129
+ out.desktopPolicyBootstrap={path:path.resolve(desktopPolicy.path),sha256};
130
+ }
119
131
  const semanticLspFields=[...semanticLspFlags.values()],semanticLspPresent=semanticLspFields.filter(key=>semanticLsp[key]!==undefined);
120
132
  if(semanticLspPresent.length!==0&&semanticLspPresent.length!==semanticLspFields.length) throw new Error('semantic LSP config bootstrap requires path plus SHA-256 as one pair');
121
133
  if(semanticLspPresent.length===semanticLspFields.length){
@@ -321,6 +333,21 @@ export async function prepareProcessPolicyBootstrap(bootstrap){
321
333
  return {...pinned,policy};
322
334
  }
323
335
 
336
+ export async function prepareDesktopPolicyBootstrap(bootstrap){
337
+ if(!bootstrap) return null;
338
+ const pinned=await verifyPinnedRegularFile(bootstrap.path,bootstrap.sha256,'desktop policy');
339
+ const policy=await readJson(pinned.path);
340
+ if(policy?.schema!=='deadbyte.desktop-policy.v1'||policy?.enabled!==true) throw new Error('desktop policy schema/enabled mismatch');
341
+ for(const name of ['observer','input','grounder']){
342
+ const provider=policy[name];
343
+ if(!provider||typeof provider.executable!=='string'||!path.isAbsolute(provider.executable)||!HEX64.test(String(provider.sha256??''))) {
344
+ throw new Error(`desktop policy ${name} pin invalid`);
345
+ }
346
+ }
347
+ if(!Array.isArray(policy.allowed_actions)||policy.allowed_actions.length<1) throw new Error('desktop policy allowed_actions invalid');
348
+ return {...pinned,policy};
349
+ }
350
+
324
351
  export async function prepareSemanticLspConfigBootstrap(bootstrap){
325
352
  if(!bootstrap) return null;
326
353
  const pinned=await verifyPinnedRegularFile(bootstrap.path,bootstrap.sha256,'semantic LSP config');
@@ -460,6 +487,7 @@ export function parseControllerStatus(text){
460
487
  local_ready:/Local ready\s*:\s*True/.test(text),
461
488
  coding_armed:/Coding plane\s*:\s*ARMED \[standing-grant\]/.test(text),
462
489
  autonomous_armed:/Autonomous\s*:\s*ARMED \[standing-grant\]/.test(text),
490
+ desktop_armed:/Desktop plane\s*:\s*ARMED \[standing-grant\]/.test(text),
463
491
  host_armed:/Host plane\s*:\s*ARMED \[standing-grant\]/.test(text)
464
492
  };
465
493
  }
@@ -664,13 +692,14 @@ async function loadControlInputs({stateRoot,sourceRoot,pointerInfo}){
664
692
  ctx=legacyContext(sourceRoot); configPath=path.join(ctx.source_root,'controller','deadbyte-controller.config.psd1');
665
693
  }
666
694
  let config=await loadPowerShellDataFile(configPath); config=await normalizeComponentIdentities(config);
667
- const codingPath=String(config.CodingPolicyFile??''); const autonomousPath=String(config.AutonomousPolicyFile??''); const hostPath=String(config.HostPolicyFile??''); const processPath=String(config.ProcessPolicyFile??'');
695
+ const codingPath=String(config.CodingPolicyFile??''); const autonomousPath=String(config.AutonomousPolicyFile??''); const hostPath=String(config.HostPolicyFile??''); const processPath=String(config.ProcessPolicyFile??''); const desktopPath=String(config.DesktopPolicyFile??'');
668
696
  if(!path.isAbsolute(codingPath)||!path.isAbsolute(autonomousPath)) throw new Error('coding/autonomous policy paths must be absolute');
669
697
  if(processPath&&!path.isAbsolute(processPath)) throw new Error('process policy path must be absolute');
670
- const codingPolicy=await readJson(codingPath); const autonomousPolicy=await readJson(autonomousPath); const hostPolicy=hostPath?await readJson(hostPath):null; const processPolicy=processPath?await readJson(processPath):null;
698
+ if(desktopPath&&!path.isAbsolute(desktopPath)) throw new Error('desktop policy path must be absolute');
699
+ const codingPolicy=await readJson(codingPath); const autonomousPolicy=await readJson(autonomousPath); const hostPolicy=hostPath?await readJson(hostPath):null; const processPolicy=processPath?await readJson(processPath):null; const desktopPolicy=desktopPath?await readJson(desktopPath):null;
671
700
  const legacyCore=typeof config.CoreRoot==='string'&&config.CoreRoot?path.resolve(config.CoreRoot):ctx.core_root;
672
701
  const workspaceRoot=typeof config.WorkspaceRoot==='string'&&config.WorkspaceRoot?path.resolve(config.WorkspaceRoot):path.join(legacyCore,'workspaces');
673
- return {ctx,configPath,config,codingPolicy,autonomousPolicy,hostPolicy,processPolicy,workspaceRoot};
702
+ return {ctx,configPath,config,codingPolicy,autonomousPolicy,hostPolicy,processPolicy,desktopPolicy,workspaceRoot};
674
703
  }
675
704
  export function assertDevelopmentRootSafe(developmentRoot,stateRoot){
676
705
  const dev=path.resolve(developmentRoot),state=path.resolve(stateRoot);
@@ -724,17 +753,18 @@ async function rollbackLaunchers(plan){
724
753
  }
725
754
  async function revokeAuthority(ctx,prior){
726
755
  let revoked=false;
756
+ if(prior.desktop_armed){ controllerCaptured(ctx,'disarmdesktop',15000); revoked=true; }
727
757
  if(prior.autonomous_armed){ controllerCaptured(ctx,'disarmautonomous',15000); revoked=true; }
728
758
  if(prior.coding_armed){ controllerCaptured(ctx,'disarmcoding',15000); revoked=true; }
729
759
  const status=parseControllerStatus(controllerCaptured(ctx,'status',15000));
730
- if(status.coding_armed||status.autonomous_armed) throw new Error('standing authority revoke gate failed');
760
+ if(status.coding_armed||status.autonomous_armed||status.desktop_armed) throw new Error('standing authority revoke gate failed');
731
761
  if(status.host_armed) throw new Error('host plane became armed during revoke');
732
762
  return {revoked,status};
733
763
  }
734
764
  async function restoreStandingAuthority(ctx,prior,enabled){
735
765
  if(!enabled){
736
766
  const status=parseControllerStatus(controllerCaptured(ctx,'status',15000));
737
- if(status.coding_armed||status.autonomous_armed) throw new Error('standing authority unexpectedly survived slot switch');
767
+ if(status.coding_armed||status.autonomous_armed||status.desktop_armed) throw new Error('standing authority unexpectedly survived slot switch');
738
768
  if(status.host_armed) throw new Error('host plane became armed during slot switch');
739
769
  return status;
740
770
  }
@@ -744,6 +774,7 @@ async function restoreStandingAuthority(ctx,prior,enabled){
744
774
  const status=parseControllerStatus(controllerCaptured(ctx,'status',15000));
745
775
  if(status.coding_armed!==prior.coding_armed) throw new Error('coding standing-grant restore gate failed');
746
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');
747
778
  if(status.host_armed) throw new Error('host plane became armed during slot switch');
748
779
  return status;
749
780
  }
@@ -795,7 +826,7 @@ function publicPointer(pointer){
795
826
  release_root:pointer.release_root,generation_root:pointer.generation_root,generation_manifest_sha256:pointer.generation_manifest_sha256,
796
827
  launcher_sha256:pointer.launcher_sha256,config_path:pointer.config_path};
797
828
  }
798
- export async function performSlotUpdate({stage,developmentRoot,expectedVersion,expectedManifest,sourceRoot=null,stateRoot=DEFAULT_STATE_ROOT,restoreStandingGrants=false,recoverOfflinePredecessor=false,cloudTransport=null,bridgeBootstrap=null,processPolicyBootstrap=null,semanticLspBootstrap=null,enableMachineFs=false,enableObservation=false,enableAutonomousSupervisor=false,hostCommandPins=[]}){
829
+ export async function performSlotUpdate({stage,developmentRoot,expectedVersion,expectedManifest,sourceRoot=null,stateRoot=DEFAULT_STATE_ROOT,restoreStandingGrants=false,recoverOfflinePredecessor=false,cloudTransport=null,bridgeBootstrap=null,processPolicyBootstrap=null,desktopPolicyBootstrap=null,semanticLspBootstrap=null,enableMachineFs=false,enableObservation=false,enableDesktop=false,enableAutonomousSupervisor=false,hostCommandPins=[]}){
799
830
  if(process.platform!=='win32') throw new Error('R27 slot update is Windows-only');
800
831
  const state=path.resolve(stateRoot),stageRoot=path.resolve(stage),development=assertDevelopmentRootSafe(developmentRoot,state);
801
832
  const key=await loadUpdateKey(state); const updateId=`r27-${expectedManifest.slice(0,16)}-${Date.now().toString(36)}-${randomBytes(4).toString('hex')}`;
@@ -820,6 +851,8 @@ export async function performSlotUpdate({stage,developmentRoot,expectedVersion,e
820
851
  control.processPolicy=processBootstrap.policy;
821
852
  control.config.ProcessRuntimeEnabled=true;
822
853
  }
854
+ const desktopBootstrap=await prepareDesktopPolicyBootstrap(desktopPolicyBootstrap);
855
+ if(desktopBootstrap) control.desktopPolicy=desktopBootstrap.policy;
823
856
  const semanticBootstrap=await prepareSemanticLspConfigBootstrap(semanticLspBootstrap);
824
857
  if(semanticBootstrap){
825
858
  control.config.SemanticLspConfigFile=semanticBootstrap.path;
@@ -834,6 +867,10 @@ export async function performSlotUpdate({stage,developmentRoot,expectedVersion,e
834
867
  if(control.config.CodingRuntimeEnabled!==true) throw new Error('ObservationRuntimeEnabled requires CodingRuntimeEnabled');
835
868
  control.config.ObservationRuntimeEnabled=true;
836
869
  }
870
+ if(enableDesktop){
871
+ if(!control.desktopPolicy) throw new Error('DesktopRuntimeEnabled requires pinned desktop policy bootstrap or existing policy');
872
+ control.config.DesktopRuntimeEnabled=true;
873
+ }
837
874
  control.config=await prepareBridgeSpec(control.config,bridgeBootstrap);
838
875
  control.config=await prepareNamedTunnelCloudSpec(control.config,{releaseRoot:slot.release_root,bootstrap:cloudTransport});
839
876
  control.config=await prepareAutonomousSupervisorSpec(control.config,{releaseRoot:slot.release_root,enable:enableAutonomousSupervisor});
@@ -860,7 +897,7 @@ export async function performSlotUpdate({stage,developmentRoot,expectedVersion,e
860
897
  launchers=await launcherPlan(state);
861
898
  const generation=await createGenerationBundle({stateRoot:state,releaseRoot:slot.release_root,developmentRoot:development,
862
899
  workspaceRoot:workspace.target,releaseId:slot.release_id,currentConfig:control.config,codingPolicy:control.codingPolicy,
863
- autonomousPolicy:control.autonomousPolicy,hostPolicy:control.hostPolicy,processPolicy:control.processPolicy});
900
+ autonomousPolicy:control.autonomousPolicy,hostPolicy:control.hostPolicy,processPolicy:control.processPolicy,desktopPolicy:control.desktopPolicy});
864
901
  newPointer={schema:'deadbyte.active-release.v1',release_id:slot.release_id,version:expectedVersion,manifest_sha256:expectedManifest,
865
902
  release_root:slot.release_root,generation_root:generation.generation_root,generation_manifest_sha256:generation.generation_manifest_sha256,
866
903
  launcher_sha256:launchers.launcher_sha256,config_path:generation.config_path};
@@ -875,7 +912,7 @@ export async function performSlotUpdate({stage,developmentRoot,expectedVersion,e
875
912
  coding_repin:{requested:hostCommandPins.length>0,repinned:authorityPreparation.codingRepinned},new_pointer:publicPointer(newPointer)});
876
913
  predecessor=await capturePredecessor(control.ctx,recoverOfflinePredecessor); oldRuntime=predecessor.runtime; priorStatus=predecessor.status;
877
914
  await writeState(statePath,journal,'old_runtime_verified',{predecessor_mode:predecessor.mode,old_runtime:generationTuple(oldRuntime),
878
- prior_authority:{coding_armed:priorStatus.coding_armed,autonomous_armed:priorStatus.autonomous_armed,host_armed:priorStatus.host_armed}});
915
+ prior_authority:{coding_armed:priorStatus.coding_armed,autonomous_armed:priorStatus.autonomous_armed,desktop_armed:priorStatus.desktop_armed,host_armed:priorStatus.host_armed}});
879
916
  const revoke=await revokeAuthority(control.ctx,priorStatus); revoked=revoke.revoked;
880
917
  await writeState(statePath,journal,'authority_revoked',{revoked_before_switch:revoked});
881
918
  if(oldRuntime){
@@ -897,7 +934,7 @@ export async function performSlotUpdate({stage,developmentRoot,expectedVersion,e
897
934
  await writeState(statePath,journal,'fresh_runtime_verified',{new_runtime:generationTuple(newRuntime),stabilization});
898
935
  const finalAuthority=await restoreStandingAuthority(newCtx,priorStatus,restoreStandingGrants);
899
936
  await writeState(statePath,journal,'authority_verified',{final_authority:{coding_armed:finalAuthority.coding_armed,
900
- autonomous_armed:finalAuthority.autonomous_armed,host_armed:finalAuthority.host_armed}});
937
+ autonomous_armed:finalAuthority.autonomous_armed,desktop_armed:finalAuthority.desktop_armed,host_armed:finalAuthority.host_armed}});
901
938
  const body={schema:UPDATE_SCHEMA,update_id:updateId,release_version:expectedVersion,candidate_manifest_sha256:expectedManifest,
902
939
  completed_at_utc:new Date().toISOString(),activation_boundary:'active-release-pointer',mechanism:'immutable-release-slot-plus-atomic-pointer-rename',
903
940
  predecessor_mode:predecessor.mode,prior_pointer:publicPointer(pointerInfo.pointer),active_pointer:publicPointer(newPointer),release_slot:{release_id:slot.release_id,release_root:slot.release_root,
@@ -908,8 +945,9 @@ export async function performSlotUpdate({stage,developmentRoot,expectedVersion,e
908
945
  host_repin:{requested:hostCommandPins.length>0,repinned:authorityPreparation.hostRepinned},
909
946
  coding_repin:{requested:hostCommandPins.length>0,repinned:authorityPreparation.codingRepinned},old_runtime:generationTuple(oldRuntime),new_runtime:generationTuple(newRuntime),stabilization,
910
947
  authority:{prior:{coding_armed:priorStatus.coding_armed,
911
- autonomous_armed:priorStatus.autonomous_armed,host_armed:priorStatus.host_armed},revoked_before_switch:revoked,restore_requested:restoreStandingGrants,
912
- final:{coding_armed:finalAuthority.coding_armed,autonomous_armed:finalAuthority.autonomous_armed,host_armed:finalAuthority.host_armed}},rollback_performed:false};
948
+ autonomous_armed:priorStatus.autonomous_armed,desktop_armed:priorStatus.desktop_armed,host_armed:priorStatus.host_armed},revoked_before_switch:revoked,restore_requested:restoreStandingGrants,
949
+ 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};
913
951
  const receipt=await writeEnvelope(receiptPath,body,key);
914
952
  await writeState(statePath,journal,'completed',{receipt_path:receiptPath,receipt_sha256:receipt.receipt_sha256});
915
953
  return {ok:true,update_id:updateId,state_path:statePath,receipt_path:receiptPath,receipt_sha256:receipt.receipt_sha256,
@@ -1042,8 +1080,8 @@ export async function runSlotUpdateCli(argv=process.argv.slice(2),{stateRoot=DEF
1042
1080
  const args=parseUpdateArgs(argv);
1043
1081
  const result=await performSlotUpdate({stage:args.stage,developmentRoot:args.developmentRoot,expectedVersion:args.expectedVersion,
1044
1082
  expectedManifest:args.expectedManifest,sourceRoot:args.sourceRoot,stateRoot,restoreStandingGrants:args.restoreStandingGrants,recoverOfflinePredecessor:args.recoverOfflinePredecessor,
1045
- cloudTransport:args.cloudTransport,bridgeBootstrap:args.bridgeBootstrap,processPolicyBootstrap:args.processPolicyBootstrap,semanticLspBootstrap:args.semanticLspBootstrap,enableMachineFs:args.enableMachineFs,
1046
- enableObservation:args.enableObservation,enableAutonomousSupervisor:args.enableAutonomousSupervisor,hostCommandPins:args.hostCommandPins});
1083
+ cloudTransport:args.cloudTransport,bridgeBootstrap:args.bridgeBootstrap,processPolicyBootstrap:args.processPolicyBootstrap,desktopPolicyBootstrap:args.desktopPolicyBootstrap,semanticLspBootstrap:args.semanticLspBootstrap,enableMachineFs:args.enableMachineFs,
1084
+ enableObservation:args.enableObservation,enableDesktop:args.enableDesktop,enableAutonomousSupervisor:args.enableAutonomousSupervisor,hostCommandPins:args.hostCommandPins});
1047
1085
  console.log(`R27_SLOT_UPDATE_PASS version=${args.expectedVersion}`);
1048
1086
  console.log(`UPDATE_ID ${result.update_id}`); console.log(`RECEIPT_SHA256 ${result.receipt_sha256}`);
1049
1087
  console.log(`STATE_PATH ${result.state_path}`); console.log(`ACTIVE_RELEASE ${result.pointer.release_id}`);
@@ -82,15 +82,42 @@ function outcomeReceiptHashes(result){
82
82
  visit(result?.structuredContent??result);
83
83
  return [...found].sort();
84
84
  }
85
+ function safeOutcomeReason(value){
86
+ if(value===null||value===undefined) return null;
87
+ let text=typeof value==='string'?value:(value instanceof Error?String(value.message??value):null);
88
+ if(text===null) return null;
89
+ text=text.replace(/[\\r\\n\\t]+/g,' ').replace(/\\s{2,}/g,' ').trim();
90
+ if(!text) return null;
91
+ const originalBytes=Buffer.from(text,'utf8');
92
+ if(secretValuePattern.test(text)) return `<redacted reason bytes=${originalBytes.length} sha256=${sha256(originalBytes)}>`;
93
+ const max=240;
94
+ if(text.length>max) text=`${text.slice(0,max-1)}…`;
95
+ return text;
96
+ }
97
+ function resultOutcomeReason(result){
98
+ const structured=result?.structuredContent;
99
+ const candidates=[
100
+ structured?.error,structured?.message,structured?.reason,structured?.result?.error,structured?.result?.message,
101
+ ...(Array.isArray(result?.content)?result.content.filter(item=>item?.type==='text').map(item=>item.text):[])
102
+ ];
103
+ for(const candidate of candidates){
104
+ const reason=safeOutcomeReason(candidate);
105
+ if(reason) return reason;
106
+ }
107
+ return null;
108
+ }
85
109
  function summarizeOutcome(result,error,durationMs){
86
110
  const duration=Math.max(0,Math.round(Number(durationMs)||0));
87
111
  if(error){
88
112
  const message=String(error?.message??error);
89
- return {status:'threw',is_error:true,duration_ms:duration,receipt_sha256s:[],error:{name:String(error?.name||'Error').slice(0,128),byte_count:Buffer.byteLength(message,'utf8'),sha256:sha256(Buffer.from(message,'utf8'))}};
113
+ return {status:'threw',is_error:true,duration_ms:duration,receipt_sha256s:[],reason:safeOutcomeReason(message),
114
+ error:{name:String(error?.name||'Error').slice(0,128),byte_count:Buffer.byteLength(message,'utf8'),sha256:sha256(Buffer.from(message,'utf8'))}};
90
115
  }
91
116
  const structured=result?.structuredContent;
92
117
  const status=typeof structured?.status==='string'?structured.status:(result?.isError===true?'error':'ok');
93
- return {status:String(status).slice(0,128),is_error:result?.isError===true,duration_ms:duration,receipt_sha256s:outcomeReceiptHashes(result)};
118
+ const isError=result?.isError===true;
119
+ return {status:String(status).slice(0,128),is_error:isError,duration_ms:duration,receipt_sha256s:outcomeReceiptHashes(result),
120
+ ...(isError?{reason:resultOutcomeReason(result)}:{})};
94
121
  }
95
122
 
96
123
  async function appendSynced(file,line){
package/src/version.mjs CHANGED
@@ -1,4 +1,4 @@
1
- export const DEADBYTE_VERSION = '0.11.3';
1
+ export const DEADBYTE_VERSION = '0.12.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';
@@ -32,7 +32,11 @@ const typedCases=[
32
32
  ['machine_fs_delete',{kind:'machine_fs_delete',path:'empty',expected_source_identity:H}],
33
33
  ['observation_search_start',{kind:'observation_search_start',path:'.',pattern:'FAIL',case_sensitive:false,max_results:100,max_files:1000,max_bytes:1048576,max_file_bytes:262144}],
34
34
  ['observation_search_page',{kind:'observation_search_page',search_id:H,cursor:null,limit:50}],
35
- ['observation_system_status',{kind:'observation_system_status'}]
35
+ ['observation_system_status',{kind:'observation_system_status'}],
36
+ ['desktop_status',{kind:'desktop_status'}],
37
+ ['desktop_observe',{kind:'desktop_observe'}],
38
+ ['desktop_target',{kind:'desktop_target',frame_id:H,uia_query:{kind:'root',value:'',max_results:1,ordinal:0}}],
39
+ ['desktop_action',{kind:'desktop_action',action_kind:'click_primary',expected_frame_id:H,target_id:null,point:{x:10,y:20},scroll_delta:null,key_chord:null,text:null}]
36
40
  ];
37
41
 
38
42
  test('R36 planner parses first-class capability actions without delegate wrapper',()=>{
@@ -44,6 +48,20 @@ test('R36 planner parses first-class capability actions without delegate wrapper
44
48
  }
45
49
  });
46
50
 
51
+ test('R40 T7 planner rejects ambiguous Desktop actions before runtime',()=>{
52
+ assert.throws(()=>parseR34PlannerDecision(wrap({
53
+ kind:'desktop_action',action_kind:'click_primary',expected_frame_id:H,
54
+ target_id:H,point:{x:1,y:2},scroll_delta:null,key_chord:null,text:null
55
+ }),policy),/exactly one/);
56
+ assert.throws(()=>parseR34PlannerDecision(wrap({
57
+ kind:'desktop_action',action_kind:'key_chord',expected_frame_id:H,
58
+ target_id:null,point:null,scroll_delta:null,key_chord:['CTRL','CTRL'],text:null
59
+ }),policy),/duplicate/);
60
+ assert.throws(()=>parseR34PlannerDecision(wrap({
61
+ kind:'desktop_target',frame_id:H,uia_query:{kind:'root',value:'not-empty',max_results:1,ordinal:0}
62
+ }),policy),/root query value/);
63
+ });
64
+
47
65
  test('R36 legacy delegate is accepted only for a known operation and normalized into typed action',()=>{
48
66
  const parsed=parseR34PlannerDecision(wrap({kind:'delegate',operation:{kind:'process_status',session_id:H}}),policy);
49
67
  assert.equal(parsed.action.kind,'process_status');
@@ -62,6 +80,30 @@ test('R36 planner prompt exposes configured capability manifest and never advert
62
80
  assert.doesNotMatch(prompt,/host\.exec|host_file|host authority/i);
63
81
  });
64
82
 
83
+ test('R40 T6 autonomous planner receives bounded Desktop memory projection as context only',async()=>{
84
+ let calls=0;
85
+ const desktopMemoryProvider={plannerContext:async()=>{
86
+ calls++;
87
+ return Object.freeze({
88
+ schema:'deadbyte.desktop-memory-planner.v1',
89
+ snapshot_sha256:'1'.repeat(64),memory_sha256:'2'.repeat(64),policy_sha256:'3'.repeat(64),
90
+ source_receipt_count:7,last_verified_desktop:{frame_id:'4'.repeat(64),desktop_epoch:'5'.repeat(64),topology_sha256:'6'.repeat(64)},
91
+ windows:[],grounded_targets:[],rejected_target_fingerprints:[],recent_effects:[],unresolved_unknown_effects:[]
92
+ });
93
+ }};
94
+ const f=await createR34Fixture({desktopMemoryProvider});
95
+ try{
96
+ const goal=await f.runtime.submit({submissionId:'r40-t6-memory',rootId:'project',title:'desktop memory context',goal:'inspect project',acceptance:'planner sees authenticated desktop summary'});
97
+ const result=await f.runtime.run({goalId:goal.goal_id,maxCycles:1});
98
+ assert.equal(result.status,'input_required');
99
+ assert.equal(calls,1);
100
+ assert.match(result.planner_request.prompt,/deadbyte\.desktop-memory-planner\.v1/);
101
+ assert.match(result.planner_request.prompt,/"desktop_memory"/);
102
+ assert.match(result.planner_request.prompt,/"source_receipt_count":7/);
103
+ assert.doesNotMatch(result.planner_request.prompt,/data_base64|authority_instance_nonce|SECRET/);
104
+ }finally{await f.cleanup();}
105
+ });
106
+
65
107
  test('R36 progress fingerprint ignores rationale but binds capability, evidence, session and cursor identity',()=>{
66
108
  const base={before:{mutation_epoch:2},after:{mutation_epoch:2},decision:{kind:'process_read',rationale:'first'},evidence:{capability_id:'process.read',session_id:H,cursor:10,observation_sha256:'b'.repeat(64),receipt_sha256:'c'.repeat(64)}};
67
109
  const a=judgeProgress(base);
@@ -0,0 +1,188 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { createHash } from 'node:crypto';
4
+ import { createR34Fixture,sendPlannerDecision,inspectDecision } from './helpers/autonomous-r34-fixture.mjs';
5
+
6
+ const H=v=>createHash('sha256').update(String(v)).digest('hex');
7
+ const FRAME=H('desktop-frame');
8
+ const GROUNDED_FRAME=H('desktop-grounded-frame');
9
+ const TARGET=H('desktop-target');
10
+ const POLICY=H('desktop-policy');
11
+ const WIN={hwnd:'0x1234',pid:77,process_image_sha256:H('desktop-app'),title_sha256:H('desktop-title')};
12
+
13
+ function fakeDesktopRuntime({armed=false,unknown=false}={}){
14
+ const calls={status:0,observe:0,ground:0,action:0,verify:0};
15
+ const receipts=new Map();
16
+ let seq=0;
17
+ const issue=kind=>{
18
+ const receipt_id=(++seq).toString(16).padStart(24,'0');
19
+ const receipt_sha256=H(kind+':'+receipt_id);
20
+ receipts.set(receipt_id,{ok:true,receipt_id,receipt_sha256,kind});
21
+ return {receipt_id,receipt_sha256};
22
+ };
23
+ const frame=id=>({frame_id:id,desktop_epoch:H('epoch'),topology_sha256:H('topology'),
24
+ observed_at_utc:'2026-09-19T10:00:00.000Z',bounds:{left:0,top:0,width:1920,height:1080},
25
+ active_window:WIN,image_sha256:H('image:'+id)});
26
+ return {
27
+ calls,
28
+ async status(){calls.status++;return {status:'ok',configured:true,armed,reason:armed?'standing-grant':'disarmed',
29
+ action_kinds:armed?['click_primary']:[],policy_sha256:POLICY,observer_backend:'fake'};},
30
+ async observe(){calls.observe++;const r=issue('desktop_observation');return {status:'ok',frame:frame(FRAME),backend:'fake',byte_length:4,...r};},
31
+ async ground({frameId,query}){calls.ground++;assert.equal(frameId,FRAME);assert.equal(query.kind,'root');
32
+ const r=issue('desktop_grounding');return {status:'ok',frame:frame(GROUNDED_FRAME),
33
+ target:{target_id:TARGET,frame_id:GROUNDED_FRAME,rect:{left:100,top:100,width:200,height:80},source:'uia'},
34
+ grounding:{query_sha256:H('query'),query_kind:'root',match_count:1,ordinal:0,provider_receipt_sha256:H('provider'),...r}};},
35
+ async action(args){calls.action++;
36
+ assert.equal(args.effectContext.goalId.length,64);
37
+ if(unknown){
38
+ assert.equal(args.expectedFrameId,FRAME);assert.equal(args.effectContext.observationHeadSha256,FRAME);
39
+ const e=new Error('transport lost after SendInput');e.side_effect_state='unknown';const r=issue('desktop_action_outcome_unknown');
40
+ e.receipt_id=r.receipt_id;e.receipt_sha256=r.receipt_sha256;throw e;
41
+ }
42
+ assert.equal(args.expectedFrameId,GROUNDED_FRAME);assert.equal(args.targetId,TARGET);
43
+ assert.equal(args.effectContext.observationHeadSha256,GROUNDED_FRAME);
44
+ const started=issue('desktop_action_started'),post=issue('desktop_post_observation'),done=issue('desktop_action_completed');
45
+ return {status:'ok',injected:true,side_effect_state:'confirmed_injected',
46
+ action:{intent_id:args.intentId,kind:'click_primary',frame_id:GROUNDED_FRAME,desktop_epoch:H('epoch'),
47
+ topology_sha256:H('topology'),expected_window:WIN,target_id:TARGET,point:{x:200,y:140},
48
+ scroll_delta:null,key_chord:null,text:null,text_sha256:null,action_sha256:H('action')},
49
+ guard:{guard_sha256:H('guard')},post_observation:{change_observed:true,window_changed:false,
50
+ post_observation_sha256:H('post-observation')},action_chain:{chain_sha256:H('chain')},
51
+ effect_id:H('effect'),effect_fingerprint:H('effect-fp'),effect_completion:{schema:'deadbyte.effect-completion.v1',result_sha256:H('effect-result')},
52
+ started_receipt_id:started.receipt_id,started_receipt_sha256:started.receipt_sha256,
53
+ post_receipt_id:post.receipt_id,post_receipt_sha256:post.receipt_sha256,
54
+ receipt_id:done.receipt_id,receipt_sha256:done.receipt_sha256};},
55
+ async verifyReceipt({receiptId}){calls.verify++;const r=receipts.get(receiptId);if(!r)throw new Error('unknown fake receipt');return r;}
56
+ };
57
+ }
58
+
59
+ const plan=steps=>({schema:'deadbyte.autonomous-decision.v2',kind:'plan_create',title:'Desktop flow',
60
+ reason:'prove bounded desktop loop',steps:steps.map(step_id=>({step_id,intent:step_id}))});
61
+ const complete=(step_id,tag)=>({schema:'deadbyte.autonomous-decision.v2',kind:'step_complete',step_id,
62
+ evidence:[{kind:'desktop',id:tag,sha256:H(tag)}]});
63
+
64
+ async function toAct(f,goal,result){
65
+ result=await sendPlannerDecision(f.runtime,goal.goal_id,result,inspectDecision());
66
+ assert.equal(result.loop.phase,'ACTING');
67
+ return result;
68
+ }
69
+
70
+ test('T7 capability manifest exposes Desktop only when runtime exists and never Host',async()=>{
71
+ const d=fakeDesktopRuntime();
72
+ const f=await createR34Fixture({desktopRuntime:d});
73
+ try{
74
+ const goal=await f.runtime.submit({submissionId:'r40-t7-manifest',rootId:'project',title:'desktop manifest',goal:'inspect desktop',acceptance:'desktop capability visible'});
75
+ const result=await f.runtime.run({goalId:goal.goal_id,maxCycles:1});
76
+ assert.equal(result.status,'input_required');
77
+ assert.match(result.planner_request.prompt,/"id":"desktop\.observe"/);
78
+ assert.match(result.planner_request.prompt,/"id":"desktop\.action"/);
79
+ assert.match(result.planner_request.prompt,/"authority":"desktop"/);
80
+ assert.doesNotMatch(result.planner_request.prompt,/host\.exec|host_file/);
81
+ }finally{await f.cleanup();}
82
+ });
83
+
84
+ test('T7 read-only Desktop observe works while Desktop mutation authority is DISARMED',async()=>{
85
+ const d=fakeDesktopRuntime({armed:false});
86
+ const f=await createR34Fixture({desktopRuntime:d});
87
+ try{
88
+ const goal=await f.runtime.submit({submissionId:'r40-t7-observe',rootId:'project',title:'desktop observe',goal:'observe desktop',acceptance:'signed observation'});
89
+ let result=await f.runtime.run({goalId:goal.goal_id,maxCycles:2});
90
+ result=await sendPlannerDecision(f.runtime,goal.goal_id,result,plan(['observe']));
91
+ result=await toAct(f,goal,result);
92
+ result=await sendPlannerDecision(f.runtime,goal.goal_id,result,{schema:'deadbyte.autonomous-decision.v2',kind:'act',action:{kind:'desktop_observe'}});
93
+ assert.equal(result.loop.phase,'VERIFYING');
94
+ assert.equal(d.calls.observe,1);assert.equal(d.calls.action,0);assert.equal(d.calls.verify,1);
95
+ const journal=await f.runtime.events({goalId:goal.goal_id,fromSeq:1,limit:200});
96
+ const completed=journal.events.find(e=>e.type==='delegated_action_completed'&&e.payload?.operation?.kind==='desktop_observe');
97
+ assert.equal(completed.payload.capability_id,'desktop.observe');
98
+ assert.equal(completed.payload.evidence.kind,'desktop_receipts');
99
+ }finally{await f.cleanup();}
100
+ });
101
+
102
+ test('T7 Autonomous authority cannot substitute for missing Desktop mutation grant',async()=>{
103
+ const d=fakeDesktopRuntime({armed:false});
104
+ const f=await createR34Fixture({desktopRuntime:d});
105
+ try{
106
+ const goal=await f.runtime.submit({submissionId:'r40-t7-disarmed',rootId:'project',title:'desktop disarmed',goal:'click only if separately authorized',acceptance:'no input without Desktop grant'});
107
+ let result=await f.runtime.run({goalId:goal.goal_id,maxCycles:2});
108
+ result=await sendPlannerDecision(f.runtime,goal.goal_id,result,plan(['act']));
109
+ result=await toAct(f,goal,result);
110
+ result=await sendPlannerDecision(f.runtime,goal.goal_id,result,{schema:'deadbyte.autonomous-decision.v2',kind:'act',action:{
111
+ kind:'desktop_action',action_kind:'click_primary',expected_frame_id:FRAME,target_id:null,point:{x:20,y:20},
112
+ scroll_delta:null,key_chord:null,text:null
113
+ }});
114
+ assert.equal(result.status,'paused');
115
+ assert.equal(result.reason,'desktop_authority_unavailable');
116
+ assert.equal(d.calls.action,0);
117
+ const journal=await f.runtime.events({goalId:goal.goal_id,fromSeq:1,limit:200});
118
+ assert.ok(journal.events.some(e=>e.type==='waiting_entered'&&e.payload?.reason==='desktop_authority_unavailable'));
119
+ assert.equal(journal.events.some(e=>e.type==='delegated_action_started'&&e.payload?.operation?.kind==='desktop_action'),false);
120
+ }finally{await f.cleanup();}
121
+ });
122
+
123
+ test('T7 armed flow binds observe to UIA target to one Desktop action and verifies every Desktop receipt',async()=>{
124
+ const d=fakeDesktopRuntime({armed:true});
125
+ const f=await createR34Fixture({desktopRuntime:d});
126
+ try{
127
+ const goal=await f.runtime.submit({submissionId:'r40-t7-flow',rootId:'project',title:'desktop flow',goal:'observe target click',acceptance:'one receipt-verified action'});
128
+ let result=await f.runtime.run({goalId:goal.goal_id,maxCycles:2});
129
+ result=await sendPlannerDecision(f.runtime,goal.goal_id,result,plan(['observe','target','act']));
130
+
131
+ result=await toAct(f,goal,result);
132
+ result=await sendPlannerDecision(f.runtime,goal.goal_id,result,{schema:'deadbyte.autonomous-decision.v2',kind:'act',action:{kind:'desktop_observe'}});
133
+ result=await sendPlannerDecision(f.runtime,goal.goal_id,result,complete('observe','observe-proof'));
134
+
135
+ result=await toAct(f,goal,result);
136
+ result=await sendPlannerDecision(f.runtime,goal.goal_id,result,{schema:'deadbyte.autonomous-decision.v2',kind:'act',action:{
137
+ kind:'desktop_target',frame_id:FRAME,uia_query:{kind:'root',value:'',max_results:1,ordinal:0}
138
+ }});
139
+ result=await sendPlannerDecision(f.runtime,goal.goal_id,result,complete('target','target-proof'));
140
+
141
+ result=await toAct(f,goal,result);
142
+ result=await sendPlannerDecision(f.runtime,goal.goal_id,result,{schema:'deadbyte.autonomous-decision.v2',kind:'act',action:{
143
+ kind:'desktop_action',action_kind:'click_primary',expected_frame_id:GROUNDED_FRAME,target_id:TARGET,point:null,
144
+ scroll_delta:null,key_chord:null,text:null
145
+ }});
146
+ assert.equal(result.loop.phase,'VERIFYING');
147
+ assert.equal(d.calls.observe,1);assert.equal(d.calls.ground,1);assert.equal(d.calls.action,1);
148
+ assert.equal(d.calls.verify,5);
149
+
150
+ const journal=await f.runtime.events({goalId:goal.goal_id,fromSeq:1,limit:200});
151
+ const completed=journal.events.filter(e=>e.type==='delegated_action_completed'&&String(e.payload?.operation?.kind).startsWith('desktop_'));
152
+ assert.deepEqual(completed.map(e=>e.payload.operation.kind),['desktop_observe','desktop_target','desktop_action']);
153
+ const action=completed.at(-1);
154
+ assert.equal(action.payload.capability_id,'desktop.action');
155
+ assert.equal(action.payload.evidence.kind,'desktop_receipts');
156
+ assert.deepEqual(action.payload.evidence.receipts.map(x=>x.kind),[
157
+ 'desktop_action_started','desktop_post_observation','desktop_action_completed'
158
+ ]);
159
+ assert.equal(Object.hasOwn(action.payload.result.action_summary,'text'),false);
160
+ }finally{await f.cleanup();}
161
+ });
162
+
163
+ test('T7 UNKNOWN Desktop outcome pauses for critic/replan and is never replayed',async()=>{
164
+ const d=fakeDesktopRuntime({armed:true,unknown:true});
165
+ const f=await createR34Fixture({desktopRuntime:d});
166
+ try{
167
+ const goal=await f.runtime.submit({submissionId:'r40-t7-unknown',rootId:'project',title:'desktop unknown',goal:'never replay unknown',acceptance:'pause on uncertain side effect'});
168
+ let result=await f.runtime.run({goalId:goal.goal_id,maxCycles:2});
169
+ result=await sendPlannerDecision(f.runtime,goal.goal_id,result,plan(['observe','act']));
170
+ result=await toAct(f,goal,result);
171
+ result=await sendPlannerDecision(f.runtime,goal.goal_id,result,{schema:'deadbyte.autonomous-decision.v2',kind:'act',action:{kind:'desktop_observe'}});
172
+ result=await sendPlannerDecision(f.runtime,goal.goal_id,result,complete('observe','observe-proof-unknown'));
173
+ result=await toAct(f,goal,result);
174
+ result=await sendPlannerDecision(f.runtime,goal.goal_id,result,{schema:'deadbyte.autonomous-decision.v2',kind:'act',action:{
175
+ kind:'desktop_action',action_kind:'click_primary',expected_frame_id:FRAME,target_id:null,point:{x:40,y:40},
176
+ scroll_delta:null,key_chord:null,text:null
177
+ }});
178
+ assert.equal(result.status,'paused');
179
+ assert.equal(result.reason,'desktop_outcome_unknown');
180
+ assert.equal(d.calls.action,1);
181
+ const journal=await f.runtime.events({goalId:goal.goal_id,fromSeq:1,limit:200});
182
+ assert.ok(journal.events.some(e=>e.type==='waiting_entered'&&e.payload?.reason==='desktop_outcome_unknown'&&e.payload?.resume_phase==='CRITIC'));
183
+ assert.ok(journal.events.some(e=>e.type==='goal_paused'&&e.payload?.side_effect_state==='unknown'));
184
+ const again=await f.runtime.run({goalId:goal.goal_id,maxCycles:2});
185
+ assert.equal(again.status,'paused');
186
+ assert.equal(d.calls.action,1,'UNKNOWN action was replayed');
187
+ }finally{await f.cleanup();}
188
+ });
@@ -36,6 +36,7 @@ test('R36 capability ledger rejects duplicate ids', () => {
36
36
 
37
37
  test('R36 capability ledger validates authority and canonicalizes identity', () => {
38
38
  assert.throws(() => createCapabilityLedger([entry({authority:'root'})]), /authority/i);
39
+ assert.doesNotThrow(()=>createCapabilityLedger([entry({authority:'desktop'})]));
39
40
  const a=createCapabilityLedger([
40
41
  entry({id:'process.read',full_tool:'process_session_read',compact_tool:'process_session',compact_op:'read',runtime_flag:'DEADBYTE_PROCESS_RUNTIME',authority:'process'}),
41
42
  entry({})