deadbyte-mcp 0.14.0 → 0.15.1

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 (157) hide show
  1. package/AGENTS.md +11 -18
  2. package/CONTEXT.md +3 -3
  3. package/MANIFEST.SHA256 +156 -70
  4. package/PROVENANCE.json +10 -0
  5. package/R43-PARENT.json +20 -0
  6. package/R44-DONORS.json +99 -0
  7. package/R44-PARENT.json +18 -0
  8. package/R45-PARENT.json +18 -0
  9. package/README.txt +19 -14
  10. package/RELEASE.json +37 -0
  11. package/SBOM.cdx.json +46 -0
  12. package/TEST-MATRIX.json +31 -0
  13. package/bin/deadbyte-hardware-observer-r45.exe +0 -0
  14. package/bin/deadbyte-hardware-observer-r45.obj +0 -0
  15. package/controller/README.TXT +5 -6
  16. package/controller/deadbyte-controller.ps1 +2 -2
  17. package/docs/ARCHITECTURE.md +8 -1
  18. package/docs/R44-ARCHITECTURE.md +37 -0
  19. package/native/deadbyte-hardware-observer.cpp +308 -0
  20. package/package.json +18 -8
  21. package/scripts/autonomous-tests.ps1 +2 -2
  22. package/scripts/build-hardware-r45.ps1 +71 -0
  23. package/scripts/deferred-slot-operation-r43.mjs +126 -0
  24. package/scripts/deferred-slot-operation-r44.mjs +126 -0
  25. package/scripts/deferred-slot-operation-r45.mjs +126 -0
  26. package/scripts/derive-r45-parent.mjs +24 -0
  27. package/scripts/derive-r45-release.mjs +72 -0
  28. package/scripts/derive-r45-tests.mjs +20 -0
  29. package/scripts/final-closure-r43.mjs +115 -0
  30. package/scripts/final-closure-r44.mjs +121 -0
  31. package/scripts/final-closure-r45.mjs +121 -0
  32. package/scripts/final-closure-verify-r43.mjs +213 -0
  33. package/scripts/final-closure-verify-r44.mjs +220 -0
  34. package/scripts/final-closure-verify-r45.mjs +220 -0
  35. package/scripts/finalize-r43.mjs +217 -0
  36. package/scripts/finalize-r44.mjs +217 -0
  37. package/scripts/finalize-r45.mjs +219 -0
  38. package/scripts/full-regression-r43.mjs +20 -0
  39. package/scripts/full-regression-r44.mjs +20 -0
  40. package/scripts/full-regression-r45.mjs +20 -0
  41. package/scripts/full-regression-summary-r45.mjs +14 -0
  42. package/scripts/gate-windows-r43.ps1 +36 -0
  43. package/scripts/gate-windows-r44.ps1 +36 -0
  44. package/scripts/gate-windows-r45.ps1 +44 -0
  45. package/scripts/init-host-policy.ps1 +39 -1
  46. package/scripts/inspect-active-release-r45.mjs +20 -0
  47. package/scripts/release-manifest.mjs +3 -1
  48. package/scripts/release-parity-r43.mjs +218 -0
  49. package/scripts/release-parity-r44.mjs +242 -0
  50. package/scripts/release-parity-r45.mjs +253 -0
  51. package/scripts/release-parity-tests-r43.ps1 +8 -0
  52. package/scripts/release-parity-tests-r44.ps1 +8 -0
  53. package/scripts/release-parity-tests-r45.ps1 +8 -0
  54. package/scripts/verify-r43-parent.mjs +126 -0
  55. package/scripts/verify-r44-parent.mjs +142 -0
  56. package/scripts/verify-r45-parent.mjs +142 -0
  57. package/scripts/windows-gate-evidence-r43.mjs +55 -0
  58. package/scripts/windows-gate-evidence-r44.mjs +55 -0
  59. package/scripts/windows-gate-evidence-r45.mjs +60 -0
  60. package/src/agent-backend-r44.mjs +254 -0
  61. package/src/autonomous-output-contracts.mjs +1 -1
  62. package/src/autonomous-policy.mjs +41 -4
  63. package/src/autonomous-runtime.mjs +37 -19
  64. package/src/autonomous-supervisor-host.mjs +12 -2
  65. package/src/capability-ledger.mjs +3 -1
  66. package/src/codex-app-server-r44.mjs +154 -0
  67. package/src/coding-plane.mjs +66 -19
  68. package/src/compact-gateway-mcp-tools.mjs +4 -2
  69. package/src/deadbyte-cli.mjs +17 -9
  70. package/src/doctor-r45.mjs +65 -0
  71. package/src/durable-job-r44.mjs +145 -0
  72. package/src/external-job-provider-r44.mjs +126 -0
  73. package/src/host-plane.mjs +156 -87
  74. package/src/job-supervisor-host-r44.mjs +112 -0
  75. package/src/machine-fs-smoke-client.mjs +1 -1
  76. package/src/machine-fs.mjs +21 -10
  77. package/src/mcp-host-smoke-client.mjs +1 -1
  78. package/src/mcp-server.mjs +84 -26
  79. package/src/mcp-smoke-client.mjs +1 -1
  80. package/src/native-diff-v2.mjs +12 -7
  81. package/src/native-diff.mjs +32 -9
  82. package/src/observation-mcp-tools.mjs +5 -1
  83. package/src/observation-output-contracts.mjs +8 -0
  84. package/src/observation-runtime.mjs +33 -2
  85. package/src/observation-smoke-client.mjs +7 -3
  86. package/src/operator-surface.mjs +2 -2
  87. package/src/output-contracts.mjs +27 -11
  88. package/src/process-policy.mjs +19 -0
  89. package/src/process-runtime.mjs +5 -2
  90. package/src/process-smoke-client.mjs +2 -2
  91. package/src/provider-circuit.mjs +8 -2
  92. package/src/r44-compact-disarmed-probe.mjs +74 -0
  93. package/src/r44-full-surface-probe.mjs +34 -0
  94. package/src/r45-compact-disarmed-probe.mjs +74 -0
  95. package/src/r45-full-surface-probe.mjs +34 -0
  96. package/src/release-generation.mjs +1 -0
  97. package/src/remote-device-r44.mjs +205 -0
  98. package/src/remote-mcp-autonomous-smoke-client.mjs +4 -4
  99. package/src/remote-mcp-r44-parity-client.mjs +203 -0
  100. package/src/remote-mcp-r45-parity-client.mjs +206 -0
  101. package/src/remote-mcp-smoke-client.mjs +7 -4
  102. package/src/runtime-update.mjs +100 -39
  103. package/src/skill-mcp-tools-r44.mjs +37 -0
  104. package/src/skill-plane-r44.mjs +203 -0
  105. package/src/tunnel-stdio-compat-smoke.mjs +1 -1
  106. package/src/version.mjs +1 -1
  107. package/test/agent-backend-r44.test.mjs +115 -0
  108. package/test/agent-runtime.test.mjs +4 -3
  109. package/test/autonomous-loop.test.mjs +121 -11
  110. package/test/autonomous-runtime.test.mjs +3 -2
  111. package/test/capability-ledger-r36.test.mjs +3 -3
  112. package/test/cli-entrypoint.test.mjs +4 -4
  113. package/test/codex-app-server-r44.test.mjs +115 -0
  114. package/test/codex-autonomous-r44.test.mjs +87 -0
  115. package/test/coding-plane.test.mjs +3 -2
  116. package/test/coding-runtime.test.mjs +36 -1
  117. package/test/compact-catalog-r36.test.mjs +3 -3
  118. package/test/compact-max-catalog-r40.test.mjs +4 -2
  119. package/test/compact-max-catalog-r44.test.mjs +97 -0
  120. package/test/compact-max-catalog-r45.test.mjs +99 -0
  121. package/test/contained.test.mjs +23 -11
  122. package/test/controller.test.mjs +5 -1
  123. package/test/core.test.mjs +1 -1
  124. package/test/durable-job-r44.test.mjs +75 -0
  125. package/test/external-job-provider-r44.test.mjs +46 -0
  126. package/test/full-max-catalog-r40.test.mjs +5 -3
  127. package/test/full-max-catalog-r44.test.mjs +91 -0
  128. package/test/full-max-catalog-r45.test.mjs +91 -0
  129. package/test/hardware-observation-r45.test.mjs +44 -0
  130. package/test/host-inline-r45.test.mjs +67 -0
  131. package/test/host-plane.test.mjs +15 -1
  132. package/test/machine-mcp-integration.test.mjs +1 -0
  133. package/test/native-diff.test.mjs +28 -0
  134. package/test/observation-mcp-tools.test.mjs +4 -3
  135. package/test/output-schema.test.mjs +3 -2
  136. package/test/package-boundary.test.mjs +1 -1
  137. package/test/process-policy.test.mjs +22 -1
  138. package/test/production-docs.test.mjs +3 -3
  139. package/test/prompts-resources.test.mjs +1 -1
  140. package/test/r33-closeout-regression.test.mjs +20 -36
  141. package/test/r33-finalization.test.mjs +7 -7
  142. package/test/r43-finalization.test.mjs +31 -0
  143. package/test/r43-finalizer.test.mjs +34 -0
  144. package/test/r43-parent.test.mjs +29 -0
  145. package/test/r44-finalization.test.mjs +58 -0
  146. package/test/r44-finalizer.test.mjs +48 -0
  147. package/test/r44-parent.test.mjs +25 -0
  148. package/test/r45-autonomous-quick-test.test.mjs +10 -0
  149. package/test/r45-finalization.test.mjs +84 -0
  150. package/test/release-generation.test.mjs +1 -0
  151. package/test/release-version.test.mjs +65 -20
  152. package/test/remote-device-r44.test.mjs +104 -0
  153. package/test/runtime-host-bootstrap-r45.test.mjs +46 -0
  154. package/test/runtime-self-update.test.mjs +42 -0
  155. package/test/skill-mcp-r44.test.mjs +38 -0
  156. package/test/skill-plane-r44.test.mjs +93 -0
  157. package/test/standing-grants.test.mjs +5 -1
@@ -16,6 +16,7 @@ import { decodeUtf8Strict } from './text-policy.mjs';
16
16
  import { isInside, validatePortableRelativePath } from './path-policy.mjs';
17
17
 
18
18
  export const HOST_POLICY_SCHEMA = 'deadbyte.host-policy.v1';
19
+ export const HOST_POLICY_SCHEMA_V2 = 'deadbyte.host-policy.v2';
19
20
  export const HOST_ARM_SCHEMA = 'deadbyte.host-arm.v1';
20
21
  export const HOST_GRANT_SCHEMA = 'deadbyte.host-grant.v2';
21
22
  export const HOST_DEFAULT_MAX_READ_BYTES = 256 * 1024;
@@ -45,7 +46,7 @@ function finiteInteger(value, name, min, max, fallback) {
45
46
 
46
47
  function normalizePolicyObject(raw, policyPath) {
47
48
  if (!raw || typeof raw !== 'object' || Array.isArray(raw)) throw new Error('host policy must be a JSON object');
48
- if (raw.schema !== HOST_POLICY_SCHEMA) throw new Error('host policy schema mismatch');
49
+ if (![HOST_POLICY_SCHEMA, HOST_POLICY_SCHEMA_V2].includes(raw.schema)) throw new Error('host policy schema mismatch');
49
50
  if (raw.enabled !== true) throw new Error('host policy is not enabled');
50
51
  if (!raw.arm_file || typeof raw.arm_file !== 'string' || !path.isAbsolute(raw.arm_file)) {
51
52
  throw new Error('host policy arm_file must be absolute');
@@ -75,7 +76,10 @@ function normalizePolicyObject(raw, policyPath) {
75
76
  for (const [id, value] of Object.entries(raw.commands)) {
76
77
  if (!COMMAND_ID_RE.test(id)) throw new Error(`invalid host command id '${id}'`);
77
78
  if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error(`host command '${id}' malformed`);
78
- if (value.kind !== 'script') throw new Error(`host command '${id}' kind must be script`);
79
+ const kind=value.kind;
80
+ if (kind!=='script' && !(raw.schema===HOST_POLICY_SCHEMA_V2 && kind==='inline')) {
81
+ throw new Error(`host command '${id}' kind must be script${raw.schema===HOST_POLICY_SCHEMA_V2?' or inline':''}`);
82
+ }
79
83
  if (typeof value.executable !== 'string' || !path.isAbsolute(value.executable)) {
80
84
  throw new Error(`host command '${id}' executable must be absolute`);
81
85
  }
@@ -86,37 +90,60 @@ function normalizePolicyObject(raw, policyPath) {
86
90
  if (rootIds.length === 0 || rootIds.some(rootId => !ROOT_ID_RE.test(rootId) || !roots[rootId])) {
87
91
  throw new Error(`host command '${id}' root_ids invalid`);
88
92
  }
89
- if (!Array.isArray(value.prefix_args) || value.prefix_args.some(x => typeof x !== 'string')) {
93
+ if (!Array.isArray(value.prefix_args) || value.prefix_args.some(x => typeof x !== 'string' || x.includes('\0'))) {
90
94
  throw new Error(`host command '${id}' prefix_args must be strings`);
91
95
  }
92
- if (!Array.isArray(value.extensions) || value.extensions.length === 0 ||
93
- value.extensions.some(x => typeof x !== 'string' || !/^\.[A-Za-z0-9._-]+$/.test(x))) {
94
- throw new Error(`host command '${id}' extensions invalid`);
95
- }
96
- commands[id] = {
97
- kind: 'script',
96
+ const base={
97
+ kind,
98
98
  executable: path.resolve(value.executable),
99
99
  executable_sha256: value.executable_sha256.toLowerCase(),
100
100
  root_ids: [...new Set(rootIds)],
101
101
  prefix_args: [...value.prefix_args],
102
- extensions: value.extensions.map(x => x.toLowerCase()),
103
102
  timeout_ms: finiteInteger(value.timeout_ms, `commands.${id}.timeout_ms`, 100, 300_000, HOST_DEFAULT_TIMEOUT_MS),
104
103
  max_output_bytes: finiteInteger(value.max_output_bytes, `commands.${id}.max_output_bytes`, 1024, 4 * 1024 * 1024, HOST_DEFAULT_MAX_OUTPUT_BYTES)
105
104
  };
105
+ if(kind==='script'){
106
+ if (!Array.isArray(value.extensions) || value.extensions.length === 0 ||
107
+ value.extensions.some(x => typeof x !== 'string' || !/^\.[A-Za-z0-9._-]+$/.test(x))) {
108
+ throw new Error(`host command '${id}' extensions invalid`);
109
+ }
110
+ base.extensions=value.extensions.map(x => x.toLowerCase());
111
+ } else {
112
+ base.max_command_chars=finiteInteger(value.max_command_chars,`commands.${id}.max_command_chars`,1,262144,32768);
113
+ }
114
+ commands[id]=base;
115
+ }
116
+
117
+ let nativeExec={enabled:false,root_ids:[],timeout_ms:HOST_DEFAULT_TIMEOUT_MS,max_output_bytes:HOST_DEFAULT_MAX_OUTPUT_BYTES,max_args:64,max_arg_chars:8192};
118
+ if(raw.schema===HOST_POLICY_SCHEMA_V2 && raw.native_exec!==undefined){
119
+ const value=raw.native_exec;
120
+ if(!value||typeof value!=='object'||Array.isArray(value)) throw new Error('host native_exec malformed');
121
+ if(value.enabled===true){
122
+ const rootIds=Array.isArray(value.root_ids)?value.root_ids:[];
123
+ if(rootIds.length===0||rootIds.some(rootId=>!ROOT_ID_RE.test(rootId)||!roots[rootId])) throw new Error('host native_exec root_ids invalid');
124
+ nativeExec={
125
+ enabled:true,
126
+ root_ids:[...new Set(rootIds)],
127
+ timeout_ms:finiteInteger(value.timeout_ms,'native_exec.timeout_ms',100,300000,HOST_DEFAULT_TIMEOUT_MS),
128
+ max_output_bytes:finiteInteger(value.max_output_bytes,'native_exec.max_output_bytes',1024,4*1024*1024,HOST_DEFAULT_MAX_OUTPUT_BYTES),
129
+ max_args:finiteInteger(value.max_args,'native_exec.max_args',0,64,64),
130
+ max_arg_chars:finiteInteger(value.max_arg_chars,'native_exec.max_arg_chars',1,32768,8192)
131
+ };
132
+ }
106
133
  }
107
134
 
108
135
  return {
109
- schema: HOST_POLICY_SCHEMA,
136
+ schema: raw.schema,
110
137
  enabled: true,
111
138
  policy_path: path.resolve(policyPath),
112
139
  arm_file: path.resolve(raw.arm_file),
113
140
  roots,
114
141
  commands,
142
+ native_exec:nativeExec,
115
143
  max_read_bytes: finiteInteger(raw.max_read_bytes, 'max_read_bytes', 1, 4 * 1024 * 1024, HOST_DEFAULT_MAX_READ_BYTES),
116
144
  max_write_bytes: finiteInteger(raw.max_write_bytes, 'max_write_bytes', 1, 8 * 1024 * 1024, HOST_DEFAULT_MAX_WRITE_BYTES)
117
145
  };
118
146
  }
119
-
120
147
  export async function loadHostPolicy(policyPath) {
121
148
  if (!policyPath) throw new Error('missing DEADBYTE privileged host policy path');
122
149
  const absolute = path.resolve(policyPath);
@@ -157,6 +184,22 @@ export async function assertHostArmed(policy, now = new Date()) {
157
184
  return { arm, standing:false, remaining_ms: expires - now.getTime() };
158
185
  }
159
186
 
187
+ export async function recheckHostAuthority(policy, initial, now = new Date()) {
188
+ if (!initial?.arm || typeof initial.arm.instance_nonce !== 'string') {
189
+ throw new Error('host authority recheck requires an armed initial snapshot');
190
+ }
191
+ let current;
192
+ try { current = await assertHostArmed(policy, now); }
193
+ catch (error) { throw new Error(`host authority changed during operation: ${String(error?.message ?? error)}`); }
194
+ const changed =
195
+ current.arm.schema !== initial.arm.schema ||
196
+ current.arm.instance_nonce !== initial.arm.instance_nonce ||
197
+ current.standing !== initial.standing ||
198
+ (current.arm.expires_at_utc ?? null) !== (initial.arm.expires_at_utc ?? null);
199
+ if (changed) throw new Error('host authority changed during operation');
200
+ return current;
201
+ }
202
+
160
203
  function getRoot(policy, rootId, permission) {
161
204
  if (!ROOT_ID_RE.test(rootId)) throw new Error('invalid host root id');
162
205
  const root = policy.roots[rootId];
@@ -190,7 +233,7 @@ async function nearestExistingParent(candidate) {
190
233
  }
191
234
  }
192
235
 
193
- async function resolveHostWritable(policy, rootId, relativePath) {
236
+ async function resolveHostWritable(policy, rootId, relativePath, authorityCheck = null) {
194
237
  validatePortableRelativePath(relativePath);
195
238
  const root = getRoot(policy, rootId, 'write');
196
239
  const lexical = path.resolve(root.real_path, relativePath);
@@ -201,6 +244,7 @@ async function resolveHostWritable(policy, rootId, relativePath) {
201
244
  const realExisting = await realpath(existingParent);
202
245
  if (!isInside(root.real_path, realExisting)) throw new Error('host parent symlink escapes allowed root');
203
246
 
247
+ if (authorityCheck) await authorityCheck();
204
248
  await mkdir(parent, { recursive: true });
205
249
  const realParent = await realpath(parent);
206
250
  if (!isInside(root.real_path, realParent)) throw new Error('host parent resolves outside allowed root');
@@ -265,8 +309,9 @@ function checkExpectedState(state, expectedSha256) {
265
309
  }
266
310
 
267
311
  export async function hostFileWrite(policy, { rootId, relativePath, content, encoding = 'utf8', expectedSha256 }) {
268
- await assertHostArmed(policy);
269
- const { lexical } = await resolveHostWritable(policy, rootId, relativePath);
312
+ const armed = await assertHostArmed(policy);
313
+ const authorityCheck = () => recheckHostAuthority(policy, armed);
314
+ const { lexical } = await resolveHostWritable(policy, rootId, relativePath, authorityCheck);
270
315
  const bytes = decodeWriteContent(content, encoding);
271
316
  if (bytes.length > policy.max_write_bytes) throw new Error(`host write exceeds ${policy.max_write_bytes} byte limit`);
272
317
 
@@ -276,6 +321,7 @@ export async function hostFileWrite(policy, { rootId, relativePath, content, enc
276
321
  const temp = path.join(path.dirname(lexical), `.${path.basename(lexical)}.deadbyte-${process.pid}-${randomBytes(8).toString('hex')}.tmp`);
277
322
  let handle;
278
323
  try {
324
+ await authorityCheck();
279
325
  handle = await open(temp, fsConstants.O_CREAT | fsConstants.O_EXCL | fsConstants.O_WRONLY, 0o600);
280
326
  await handle.writeFile(bytes);
281
327
  await handle.sync();
@@ -284,6 +330,7 @@ export async function hostFileWrite(policy, { rootId, relativePath, content, enc
284
330
 
285
331
  const raceCheck = await currentTargetState(lexical);
286
332
  checkExpectedState(raceCheck, expectedSha256);
333
+ await authorityCheck();
287
334
  await rename(temp, lexical);
288
335
  } catch (error) {
289
336
  if (handle) await handle.close().catch(() => {});
@@ -325,6 +372,93 @@ function collectBounded(stream, limit, state, label, onExceeded) {
325
372
  });
326
373
  }
327
374
 
375
+ function validateHostArgs(args,maxArgs=64,maxChars=2048,label='host exec args'){
376
+ if(!Array.isArray(args)||args.length>maxArgs||args.some(x=>typeof x!=='string'||x.length>maxChars||x.includes('\0'))){
377
+ throw new Error(`${label} must be <=${maxArgs} strings of <=${maxChars} characters`);
378
+ }
379
+ }
380
+ async function resolveExecCwd(policy,rootId,cwd){
381
+ const root=getRoot(policy,rootId,'exec');
382
+ let cwdResolved=root.real_path;
383
+ if(cwd!=='.'){
384
+ validatePortableRelativePath(cwd);
385
+ cwdResolved=path.resolve(root.real_path,cwd);
386
+ if(!isInside(root.real_path,cwdResolved)) throw new Error('host exec cwd escapes allowed root');
387
+ const realCwd=await realpath(cwdResolved);
388
+ if(!isInside(root.real_path,realCwd)) throw new Error('host exec cwd symlink escapes allowed root');
389
+ const cwdStat=await stat(realCwd);
390
+ if(!cwdStat.isDirectory()) throw new Error('host exec cwd must be a directory');
391
+ cwdResolved=realCwd;
392
+ }
393
+ return cwdResolved;
394
+ }
395
+ async function runHostChild(policy,armed,{executable,argv,cwd,timeoutMs,maxTimeoutMs,maxOutputBytes,authorityPollMs=100}){
396
+ const activeArm=await recheckHostAuthority(policy,armed);
397
+ const grantBound=Number.isFinite(activeArm.remaining_ms)?Math.floor(activeArm.remaining_ms):maxTimeoutMs;
398
+ if(grantBound<100) throw new Error('privileged host plane arm lease expires too soon');
399
+ const requested=timeoutMs===null||timeoutMs===undefined?maxTimeoutMs:finiteInteger(timeoutMs,'timeout_ms',100,maxTimeoutMs,maxTimeoutMs);
400
+ const effectiveTimeout=Math.min(requested,maxTimeoutMs,grantBound);
401
+ const child=spawn(executable,argv,{cwd,env:scrubHostEnvironment(),shell:false,windowsHide:true,detached:false,stdio:['ignore','pipe','pipe']});
402
+ const stdoutState={chunks:[],bytes:0,exceeded:null};
403
+ const stderrState={chunks:[],bytes:0,exceeded:null};
404
+ const kill=()=>{try{child.kill('SIGKILL');}catch{}};
405
+ collectBounded(child.stdout,maxOutputBytes,stdoutState,'stdout',kill);
406
+ collectBounded(child.stderr,maxOutputBytes,stderrState,'stderr',kill);
407
+ let timedOut=false,authorityError=null,timer=null,poller=null;
408
+ const completion=new Promise((resolve,reject)=>{
409
+ child.once('error',reject);
410
+ child.once('close',(code,signal)=>resolve({code,signal}));
411
+ timer=setTimeout(()=>{timedOut=true;kill();},effectiveTimeout); timer.unref?.();
412
+ const poll=Math.max(25,Math.min(1000,Number.isInteger(authorityPollMs)?authorityPollMs:100));
413
+ poller=setInterval(()=>{void recheckHostAuthority(policy,armed).catch(error=>{if(!authorityError){authorityError=error;kill();}});},poll); poller.unref?.();
414
+ });
415
+ let result;
416
+ try{result=await completion;}finally{if(timer)clearTimeout(timer);if(poller)clearInterval(poller);}
417
+ if(authorityError) throw authorityError;
418
+ if(stdoutState.exceeded||stderrState.exceeded) throw new Error(stdoutState.exceeded??stderrState.exceeded);
419
+ const stdoutBytes=Buffer.concat(stdoutState.chunks),stderrBytes=Buffer.concat(stderrState.chunks);
420
+ return {
421
+ exit_code:result.code,signal:result.signal,timed_out:timedOut,timeout_ms:effectiveTimeout,
422
+ stdout:decodeUtf8Strict(stdoutBytes),stderr:decodeUtf8Strict(stderrBytes),
423
+ stdout_sha256:sha256Bytes(stdoutBytes),stderr_sha256:sha256Bytes(stderrBytes),
424
+ argv_sha256:sha256Bytes(Buffer.from(JSON.stringify(argv),'utf8'))
425
+ };
426
+ }
427
+
428
+ export async function hostExecInline(policy,{commandId,rootId,command,cwd='.',timeoutMs=null,authorityPollMs=100}){
429
+ const armed=await assertHostArmed(policy);
430
+ if(!COMMAND_ID_RE.test(commandId)) throw new Error('invalid host command id');
431
+ const spec=policy.commands[commandId];
432
+ if(!spec||spec.kind!=='inline') throw new Error(`host inline command '${commandId}' is not allowed`);
433
+ if(!ROOT_ID_RE.test(rootId)||!spec.root_ids.includes(rootId)) throw new Error(`host command '${commandId}' is not allowed for root '${rootId}'`);
434
+ getRoot(policy,rootId,'exec');
435
+ if(typeof command!=='string'||command.length<1||command.length>spec.max_command_chars||command.includes('\0')) throw new Error('host inline command exceeds policy bounds');
436
+ const actualExecutableHash=await sha256File(spec.executable);
437
+ if(actualExecutableHash!==spec.executable_sha256) throw new Error(`host command '${commandId}' executable hash changed`);
438
+ const cwdResolved=await resolveExecCwd(policy,rootId,cwd);
439
+ const argv=[...spec.prefix_args,command];
440
+ const result=await runHostChild(policy,armed,{executable:spec.executable,argv,cwd:cwdResolved,timeoutMs,maxTimeoutMs:spec.timeout_ms,maxOutputBytes:spec.max_output_bytes,authorityPollMs});
441
+ return {command_id:commandId,mode:'inline',executable_sha256:actualExecutableHash,command_sha256:sha256Bytes(Buffer.from(command,'utf8')),...result};
442
+ }
443
+
444
+ export async function hostExecNative(policy,{rootId,executable,executableSha256,args=[],cwd='.',timeoutMs=null,authorityPollMs=100}){
445
+ const armed=await assertHostArmed(policy);
446
+ const spec=policy.native_exec;
447
+ if(!spec?.enabled) throw new Error('host native exec is not enabled by policy');
448
+ if(!ROOT_ID_RE.test(rootId)||!spec.root_ids.includes(rootId)) throw new Error(`host native exec is not allowed for root '${rootId}'`);
449
+ getRoot(policy,rootId,'exec');
450
+ if(typeof executable!=='string'||!path.isAbsolute(executable)) throw new Error('host native executable must be absolute');
451
+ if(typeof executableSha256!=='string'||!SHA256_RE.test(executableSha256)) throw new Error('executable_sha256 must be a lowercase SHA-256 digest');
452
+ validateHostArgs(args,spec.max_args,spec.max_arg_chars,'host native args');
453
+ const resolved=path.resolve(executable); const info=await stat(resolved);
454
+ if(!info.isFile()) throw new Error('host native executable is not a file');
455
+ const actualExecutableHash=await sha256File(resolved);
456
+ if(actualExecutableHash!==executableSha256) throw new Error(`host native executable hash mismatch: expected ${executableSha256}, found ${actualExecutableHash}`);
457
+ const cwdResolved=await resolveExecCwd(policy,rootId,cwd);
458
+ const result=await runHostChild(policy,armed,{executable:resolved,argv:[...args],cwd:cwdResolved,timeoutMs,maxTimeoutMs:spec.timeout_ms,maxOutputBytes:spec.max_output_bytes,authorityPollMs});
459
+ return {command_id:'native',mode:'native',executable_sha256:actualExecutableHash,...result};
460
+ }
461
+
328
462
  export async function hostExec(policy, {
329
463
  commandId,
330
464
  rootId,
@@ -337,14 +471,12 @@ export async function hostExec(policy, {
337
471
  const armed = await assertHostArmed(policy);
338
472
  if (!COMMAND_ID_RE.test(commandId)) throw new Error('invalid host command id');
339
473
  const command = policy.commands[commandId];
340
- if (!command) throw new Error(`host command '${commandId}' is not allowed`);
341
- if (!Array.isArray(args) || args.length > 64 || args.some(x => typeof x !== 'string' || x.length > 2048 || x.includes('\0'))) {
342
- throw new Error('host exec args must be <=64 strings of <=2048 characters');
343
- }
474
+ if (!command || command.kind!=='script') throw new Error(`host command '${commandId}' is not allowed as a script command`);
475
+ validateHostArgs(args,64,2048,'host exec args');
344
476
  if (typeof scriptSha256 !== 'string' || !SHA256_RE.test(scriptSha256)) throw new Error('script_sha256 must be a lowercase SHA-256 digest');
345
477
 
346
478
  if (!ROOT_ID_RE.test(rootId) || !command.root_ids.includes(rootId)) throw new Error(`host command '${commandId}' is not allowed for root '${rootId}'`);
347
- const root = getRoot(policy, rootId, 'exec');
479
+ getRoot(policy, rootId, 'exec');
348
480
  const script = await resolveHostExisting(policy, rootId, scriptPath, 'exec');
349
481
  const extension = path.extname(script.resolved).toLowerCase();
350
482
  if (!command.extensions.includes(extension)) throw new Error(`script extension '${extension}' is not allowed for '${commandId}'`);
@@ -353,71 +485,8 @@ export async function hostExec(policy, {
353
485
 
354
486
  const actualExecutableHash = await sha256File(command.executable);
355
487
  if (actualExecutableHash !== command.executable_sha256) throw new Error(`host command '${commandId}' executable hash changed`);
356
-
357
- let cwdResolved = root.real_path;
358
- if (cwd !== '.') {
359
- validatePortableRelativePath(cwd);
360
- cwdResolved = path.resolve(root.real_path, cwd);
361
- if (!isInside(root.real_path, cwdResolved)) throw new Error('host exec cwd escapes allowed root');
362
- const realCwd = await realpath(cwdResolved);
363
- if (!isInside(root.real_path, realCwd)) throw new Error('host exec cwd symlink escapes allowed root');
364
- const cwdStat = await stat(realCwd);
365
- if (!cwdStat.isDirectory()) throw new Error('host exec cwd must be a directory');
366
- cwdResolved = realCwd;
367
- }
368
-
369
- if (Number.isFinite(armed.remaining_ms) && armed.remaining_ms < 100) throw new Error('privileged host plane arm lease expires too soon');
370
- const requestedTimeout = timeoutMs === null ? command.timeout_ms : finiteInteger(timeoutMs, 'timeout_ms', 100, command.timeout_ms, command.timeout_ms);
371
- const grantBound = Number.isFinite(armed.remaining_ms) ? armed.remaining_ms : command.timeout_ms;
372
- const effectiveTimeout = Math.max(100, Math.min(requestedTimeout, command.timeout_ms, grantBound));
373
- const argv = [...command.prefix_args, script.resolved, ...args];
374
-
375
- const child = spawn(command.executable, argv, {
376
- cwd: cwdResolved,
377
- env: scrubHostEnvironment(),
378
- shell: false,
379
- windowsHide: true,
380
- detached: false,
381
- stdio: ['ignore', 'pipe', 'pipe']
382
- });
383
-
384
- const stdoutState = { chunks: [], bytes: 0, exceeded: null };
385
- const stderrState = { chunks: [], bytes: 0, exceeded: null };
386
- collectBounded(child.stdout, command.max_output_bytes, stdoutState, 'stdout', () => child.kill('SIGKILL'));
387
- collectBounded(child.stderr, command.max_output_bytes, stderrState, 'stderr', () => child.kill('SIGKILL'));
388
-
389
- let timedOut = false;
390
- let timer;
391
- const completion = new Promise((resolve, reject) => {
392
- child.once('error', reject);
393
- child.once('close', (code, signal) => resolve({ code, signal }));
394
- timer = setTimeout(() => {
395
- timedOut = true;
396
- child.kill('SIGKILL');
397
- }, effectiveTimeout);
398
- timer.unref?.();
399
- });
400
- let result;
401
- try {
402
- result = await completion;
403
- } finally {
404
- if (timer) clearTimeout(timer);
405
- }
406
-
407
- if (stdoutState.exceeded || stderrState.exceeded) {
408
- throw new Error(stdoutState.exceeded ?? stderrState.exceeded);
409
- }
410
- const stdoutBytes = Buffer.concat(stdoutState.chunks);
411
- const stderrBytes = Buffer.concat(stderrState.chunks);
412
- return {
413
- command_id: commandId,
414
- executable_sha256: actualExecutableHash,
415
- script_sha256: actualScriptHash,
416
- exit_code: result.code,
417
- signal: result.signal,
418
- timed_out: timedOut,
419
- timeout_ms: effectiveTimeout,
420
- stdout: decodeUtf8Strict(stdoutBytes),
421
- stderr: decodeUtf8Strict(stderrBytes)
422
- };
488
+ const cwdResolved=await resolveExecCwd(policy,rootId,cwd);
489
+ const argv=[...command.prefix_args,script.resolved,...args];
490
+ const result=await runHostChild(policy,armed,{executable:command.executable,argv,cwd:cwdResolved,timeoutMs,maxTimeoutMs:command.timeout_ms,maxOutputBytes:command.max_output_bytes});
491
+ return {command_id:commandId,mode:'script',executable_sha256:actualExecutableHash,script_sha256:actualScriptHash,...result};
423
492
  }
@@ -0,0 +1,112 @@
1
+ import { createHash, randomBytes, timingSafeEqual } from 'node:crypto';
2
+ import { closeSync, openSync } from 'node:fs';
3
+ import { lstat, mkdir, open, readFile, realpath, rename, rm } from 'node:fs/promises';
4
+ import net from 'node:net';
5
+ import path from 'node:path';
6
+ import { spawn } from 'node:child_process';
7
+
8
+ const HEX64=/^[0-9a-f]{64}$/;
9
+ const ID=/^[A-Za-z0-9._-]{1,64}$/;
10
+ const sha=bytes=>createHash('sha256').update(bytes).digest('hex');
11
+ function insist(ok,message){if(!ok)throw new Error(message);}
12
+ async function atomicJson(file,value){
13
+ const bytes=Buffer.from(`${JSON.stringify(value,null,2)}\n`,'utf8');
14
+ const temp=`${file}.${process.pid}.${randomBytes(6).toString('hex')}.tmp`;
15
+ const h=await open(temp,'wx');try{await h.writeFile(bytes);await h.sync();}finally{await h.close();}
16
+ await rename(temp,file);
17
+ }
18
+ const arg=process.argv.indexOf('--state-dir');
19
+ if(arg<0||!process.argv[arg+1])throw new Error('R44 job supervisor host requires --state-dir');
20
+ const stateDir=await realpath(path.resolve(process.argv[arg+1]));
21
+ const authPath=path.join(stateDir,'supervisor-auth.json');
22
+ const endpointPath=path.join(stateDir,'supervisor-endpoint.json');
23
+ const auth=JSON.parse(await readFile(authPath,'utf8'));
24
+ insist(auth?.schema==='deadbyte.job-supervisor-auth.v1'&&HEX64.test(auth.token??''),'supervisor auth state invalid');
25
+ const authBytes=Buffer.from(auth.token,'hex');
26
+ const instanceId=randomBytes(32).toString('hex');
27
+ const jobs=new Map();
28
+ const logs=path.join(stateDir,'logs');await mkdir(logs,{recursive:true});
29
+
30
+ function publicHandle(job){
31
+ return {handle_id:job.handle_id,pid:job.pid,creation_id:job.creation_id,profile_id:job.profile_id,state:job.state,exit_code:job.exit_code??null};
32
+ }
33
+ function exactJob(input){
34
+ insist(input&&typeof input==='object','supervisor handle missing');
35
+ const job=jobs.get(input.handle_id);insist(job,'unknown supervisor handle');
36
+ insist(job.pid===input.pid&&job.creation_id===input.creation_id&&job.profile_id===input.profile_id,'supervisor process identity mismatch');
37
+ return job;
38
+ }
39
+ async function verifyExecutable(file,expected){
40
+ insist(path.isAbsolute(file)&&HEX64.test(expected??''),'supervisor executable identity invalid');
41
+ const st=await lstat(file);insist(st.isFile()&&!st.isSymbolicLink(),'supervisor executable must be real file');
42
+ insist(sha(await readFile(file))===expected,'supervisor executable hash mismatch');
43
+ }
44
+ async function handle(op,args){
45
+ if(op==='hello')return {kind:'external-supervisor',instance_id:instanceId,pid:process.pid};
46
+ if(op==='start'){
47
+ insist(args&&HEX64.test(args.job_id??'')&&ID.test(args.profile_id??''),'supervisor start identity invalid');
48
+ insist(Array.isArray(args.argv)&&args.argv.length<=128&&args.argv.every(v=>typeof v==='string'&&v.length<=8192),'supervisor argv invalid');
49
+ await verifyExecutable(args.executable,args.executable_sha256);
50
+ insist(typeof args.cwd==='string'&&path.isAbsolute(args.cwd),'supervisor cwd must be absolute');
51
+ const cwd=await realpath(args.cwd);const cwdSt=await lstat(cwd);insist(cwdSt.isDirectory()&&!cwdSt.isSymbolicLink(),'supervisor cwd invalid');
52
+ const handleId=randomBytes(32).toString('hex'),creationId=randomBytes(32).toString('hex');
53
+ const stdoutPath=path.join(logs,`${handleId}.stdout.log`),stderrPath=path.join(logs,`${handleId}.stderr.log`);
54
+ const outFd=openSync(stdoutPath,'a'),errFd=openSync(stderrPath,'a');
55
+ let child;
56
+ try{
57
+ child=spawn(args.executable,args.argv,{cwd,windowsHide:true,shell:false,detached:true,stdio:['ignore',outFd,errFd]});
58
+ }finally{closeSync(outFd);closeSync(errFd);}
59
+ insist(Number.isInteger(child.pid)&&child.pid>0,'supervisor child spawn failed');
60
+ const job={handle_id:handleId,pid:child.pid,creation_id:creationId,profile_id:args.profile_id,state:'running',exit_code:null,
61
+ child,stdout_path:stdoutPath,stderr_path:stderrPath,started_at_utc:new Date().toISOString()};
62
+ jobs.set(handleId,job);
63
+ child.once('exit',(code)=>{job.state='terminated';job.exit_code=Number.isInteger(code)?code:null;});
64
+ child.once('error',()=>{job.state='failed';});
65
+ child.unref();
66
+ return publicHandle(job);
67
+ }
68
+ if(op==='reattach'||op==='status')return publicHandle(exactJob(args));
69
+ if(op==='terminate'){
70
+ const job=exactJob(args);
71
+ if(job.state==='running'){
72
+ const exited=new Promise(resolve=>job.child.once('exit',resolve));
73
+ job.child.kill();
74
+ await Promise.race([exited,new Promise(resolve=>setTimeout(resolve,3000))]);
75
+ if(job.state==='running')job.state='terminated';
76
+ }
77
+ return publicHandle(job);
78
+ }
79
+ if(op==='shutdown'){
80
+ for(const job of jobs.values())if(job.state==='running')job.child.kill();
81
+ return {status:'shutting_down'};
82
+ }
83
+ throw new Error('unknown supervisor operation');
84
+ }
85
+ function authOk(token){
86
+ if(!HEX64.test(token??''))return false;
87
+ const candidate=Buffer.from(token,'hex');return candidate.length===authBytes.length&&timingSafeEqual(candidate,authBytes);
88
+ }
89
+
90
+ const server=net.createServer(socket=>{
91
+ socket.setEncoding('utf8');let buffer='';
92
+ socket.on('data',chunk=>{
93
+ buffer+=chunk;
94
+ if(Buffer.byteLength(buffer,'utf8')>1024*1024){socket.destroy();return;}
95
+ const nl=buffer.indexOf('\n');if(nl<0)return;
96
+ const line=buffer.slice(0,nl);buffer='';
97
+ void(async()=>{
98
+ let request;try{request=JSON.parse(line);}catch{socket.end(JSON.stringify({ok:false,error:'invalid JSON'})+'\n');return;}
99
+ if(!authOk(request?.token)){socket.end(JSON.stringify({ok:false,error:'unauthorized'})+'\n');return;}
100
+ try{
101
+ const result=await handle(request.op,request.args??{});
102
+ socket.end(JSON.stringify({ok:true,result})+'\n');
103
+ if(request.op==='shutdown'){
104
+ setTimeout(async()=>{server.close();await rm(endpointPath,{force:true}).catch(()=>{});process.exit(0);},50).unref();
105
+ }
106
+ }catch(error){socket.end(JSON.stringify({ok:false,error:String(error?.message??error)})+'\n');}
107
+ })();
108
+ });
109
+ });
110
+ await new Promise((resolve,reject)=>{server.once('error',reject);server.listen(0,'127.0.0.1',resolve);});
111
+ const address=server.address();insist(address&&typeof address==='object','supervisor listen address invalid');
112
+ await atomicJson(endpointPath,{schema:'deadbyte.job-supervisor-endpoint.v1',host:'127.0.0.1',port:address.port,instance_id:instanceId,pid:process.pid,created_at_utc:new Date().toISOString()});
@@ -56,7 +56,7 @@ const transport = new StdioClientTransport({
56
56
  });
57
57
  const stderrChunks = [];
58
58
  transport.stderr?.on('data', chunk => stderrChunks.push(Buffer.from(chunk)));
59
- const baseTools = ['contained_transform','evidence_verify','file_read','proof_execute','workspace_open'];
59
+ const baseTools = ['contained_transform','evidence_verify','file_read','proof_execute','skill_catalog','workspace_open'];
60
60
  const codingTools = [
61
61
  'coding_benchmark_run','coding_command_run','coding_file_read','coding_file_write','coding_files_read',
62
62
  'coding_receipt_verify','coding_search','coding_session_status','coding_stat',
@@ -3,7 +3,7 @@ import { createReadStream } from 'node:fs';
3
3
  import { lstat, mkdir as fsMkdir, open, readFile, readdir, realpath, rename, rm, rmdir } from 'node:fs/promises';
4
4
  import path from 'node:path';
5
5
  import { canonicalJson } from './canonical-json.mjs';
6
- import { assertCodingArmed } from './coding-plane.mjs';
6
+ import { assertCodingArmed, recheckCodingAuthority } from './coding-plane.mjs';
7
7
  import { isInside, validatePortableRelativePath } from './path-policy.mjs';
8
8
  import { decodeUtf8Strict } from './text-policy.mjs';
9
9
  import { loadPrivateSigningKey, loadPublicVerifyKey } from './trust.mjs';
@@ -219,18 +219,20 @@ function requestSha(operation, args) {
219
219
  return sha256Object({ schema: 'deadbyte.machine-fs-request.v1', operation, args });
220
220
  }
221
221
 
222
- async function atomicWrite(target, bytes, expectedSha256) {
222
+ async function atomicWrite(target, bytes, expectedSha256, authorityCheck = null) {
223
223
  const before = await regularState(target);
224
224
  checkExpectedFile(before, expectedSha256);
225
225
  const temp = path.join(path.dirname(target), `.${path.basename(target)}.deadbyte-machine-fs-${process.pid}-${randomBytes(8).toString('hex')}.tmp`);
226
226
  let handle;
227
227
  try {
228
+ if (authorityCheck) await authorityCheck();
228
229
  handle = await open(temp, 'wx', 0o600);
229
230
  await handle.writeFile(bytes);
230
231
  await handle.sync();
231
232
  await handle.close();
232
233
  handle = null;
233
234
  checkExpectedFile(await regularState(target), expectedSha256);
235
+ if (authorityCheck) await authorityCheck();
234
236
  await rename(temp, target);
235
237
  } catch (error) {
236
238
  if (handle) await handle.close().catch(() => {});
@@ -322,8 +324,10 @@ export function createMachineFsRuntime({ policy, signingKeyPath, verifyKeyPath }
322
324
  content_sha256: sha256Bytes(bytes)
323
325
  });
324
326
  const started = nowUtc();
325
- const { before, after } = await atomicWrite(target.lexical, bytes, expectedSha256);
326
- const signed = await signReceipt(policy, signingKeyPath, grant, 'machine_fs_write', request, {
327
+ let activeGrant = grant;
328
+ const authorityCheck = async () => { activeGrant = await recheckCodingAuthority(policy, grant, rootId); return activeGrant; };
329
+ const { before, after } = await atomicWrite(target.lexical, bytes, expectedSha256, authorityCheck);
330
+ const signed = await signReceipt(policy, signingKeyPath, activeGrant, 'machine_fs_write', request, {
327
331
  root_id: rootId, path: target.relative, started_at_utc: started, completed_at_utc: nowUtc(),
328
332
  before_sha256: before.sha256, after_sha256: after.sha256, bytes: after.bytes
329
333
  }, receiptId);
@@ -358,8 +362,10 @@ export function createMachineFsRuntime({ policy, signingKeyPath, verifyKeyPath }
358
362
  old_sha256: sha256Bytes(Buffer.from(oldString, 'utf8')), new_sha256: sha256Bytes(Buffer.from(newString, 'utf8'))
359
363
  });
360
364
  const started = nowUtc();
361
- const { after } = await atomicWrite(found.lexical, nextBytes, expectedSha256);
362
- const signed = await signReceipt(policy, signingKeyPath, grant, 'machine_fs_edit_exact', request, {
365
+ let activeGrant = grant;
366
+ const authorityCheck = async () => { activeGrant = await recheckCodingAuthority(policy, grant, rootId); return activeGrant; };
367
+ const { after } = await atomicWrite(found.lexical, nextBytes, expectedSha256, authorityCheck);
368
+ const signed = await signReceipt(policy, signingKeyPath, activeGrant, 'machine_fs_edit_exact', request, {
363
369
  root_id: rootId, path: found.relative, started_at_utc: started, completed_at_utc: nowUtc(),
364
370
  before_sha256: beforeSha, after_sha256: after.sha256, replacements: count, bytes: after.bytes
365
371
  });
@@ -376,11 +382,12 @@ export function createMachineFsRuntime({ policy, signingKeyPath, verifyKeyPath }
376
382
  catch (error) { if (error?.code !== 'ENOENT') throw error; }
377
383
  const request = requestSha('machine_fs_mkdir', { root_id: rootId, path: target.relative, expected: 'absent' });
378
384
  const started = nowUtc();
385
+ const activeGrant = await recheckCodingAuthority(policy, grant, rootId);
379
386
  await fsMkdir(target.lexical, { recursive: false });
380
387
  try {
381
388
  const identity = await objectIdentity(target.lexical);
382
389
  if (identity.kind !== 'directory') throw new Error('machine fs mkdir post-commit type mismatch');
383
- const signed = await signReceipt(policy, signingKeyPath, grant, 'machine_fs_mkdir', request, {
390
+ const signed = await signReceipt(policy, signingKeyPath, activeGrant, 'machine_fs_mkdir', request, {
384
391
  root_id: rootId, path: target.relative, started_at_utc: started, completed_at_utc: nowUtc(),
385
392
  before_identity_sha256: null, after_identity_sha256: identity.identity_sha256
386
393
  }, receiptId);
@@ -409,11 +416,12 @@ export function createMachineFsRuntime({ policy, signingKeyPath, verifyKeyPath }
409
416
  const started = nowUtc();
410
417
  const race = await objectIdentity(source.resolved);
411
418
  if (race.identity_sha256 !== expectedSourceIdentity) throw new Error(`machine fs source identity mismatch before commit: expected ${expectedSourceIdentity}, found ${race.identity_sha256}`);
419
+ const activeGrant = await recheckCodingAuthority(policy, grant, rootId);
412
420
  await rename(source.lexical, destination.lexical);
413
421
  try {
414
422
  const after = await objectIdentity(destination.lexical);
415
423
  if (after.identity_sha256 !== expectedSourceIdentity) throw new Error('machine fs move post-commit identity mismatch');
416
- const signed = await signReceipt(policy, signingKeyPath, grant, 'machine_fs_move', request, {
424
+ const signed = await signReceipt(policy, signingKeyPath, activeGrant, 'machine_fs_move', request, {
417
425
  root_id: rootId, source_path: source.relative, destination_path: destination.relative,
418
426
  started_at_utc: started, completed_at_utc: nowUtc(), source_identity_sha256: expectedSourceIdentity
419
427
  }, receiptId);
@@ -436,6 +444,7 @@ export function createMachineFsRuntime({ policy, signingKeyPath, verifyKeyPath }
436
444
  const before = await objectIdentity(source.resolved);
437
445
  if (before.identity_sha256 !== expectedSourceIdentity) throw new Error(`machine fs source identity mismatch: expected ${expectedSourceIdentity}, found ${before.identity_sha256}`);
438
446
  const recoveryRoot = path.join(source.root.real_path, '.deadbyte-machine-fs-recovery');
447
+ let activeGrant = await recheckCodingAuthority(policy, grant, rootId);
439
448
  await fsMkdir(recoveryRoot, { recursive: true });
440
449
  const recoveryKey = sha256Object({ source_path: source.relative, identity_sha256: expectedSourceIdentity });
441
450
  const recoveryTarget = path.join(recoveryRoot, recoveryKey);
@@ -443,11 +452,12 @@ export function createMachineFsRuntime({ policy, signingKeyPath, verifyKeyPath }
443
452
  catch (error) { if (error?.code !== 'ENOENT') throw error; }
444
453
  const request = requestSha('machine_fs_delete', { root_id: rootId, path: source.relative, expected_source_identity: expectedSourceIdentity });
445
454
  const started = nowUtc();
455
+ activeGrant = await recheckCodingAuthority(policy, grant, rootId);
446
456
  await rename(source.lexical, recoveryTarget);
447
457
  try {
448
458
  const recovered = await objectIdentity(recoveryTarget);
449
459
  if (recovered.identity_sha256 !== expectedSourceIdentity) throw new Error('machine fs delete recovery identity mismatch');
450
- const signed = await signReceipt(policy, signingKeyPath, grant, 'machine_fs_delete', request, {
460
+ const signed = await signReceipt(policy, signingKeyPath, activeGrant, 'machine_fs_delete', request, {
451
461
  root_id: rootId, path: source.relative, started_at_utc: started, completed_at_utc: nowUtc(),
452
462
  source_identity_sha256: expectedSourceIdentity, recovery_key: recoveryKey
453
463
  });
@@ -485,11 +495,12 @@ export function createMachineFsRuntime({ policy, signingKeyPath, verifyKeyPath }
485
495
  if (race.kind !== 'directory' || race.entries.length !== 0 || race.identity_sha256 !== expectedSourceIdentity) {
486
496
  throw new Error('machine fs rmdir precondition changed before commit');
487
497
  }
498
+ const activeGrant = await recheckCodingAuthority(policy, grant, rootId);
488
499
  await rmdir(source.lexical);
489
500
  try {
490
501
  try { await lstat(source.lexical); throw new Error('machine fs rmdir post-commit path still exists'); }
491
502
  catch (error) { if (error?.code !== 'ENOENT') throw error; }
492
- const signed = await signReceipt(policy, signingKeyPath, grant, 'machine_fs_rmdir_empty', request, {
503
+ const signed = await signReceipt(policy, signingKeyPath, activeGrant, 'machine_fs_rmdir_empty', request, {
493
504
  root_id: rootId, path: source.relative, started_at_utc: started, completed_at_utc: nowUtc(),
494
505
  before_identity_sha256: expectedSourceIdentity, after_identity_sha256: null
495
506
  }, receiptId);
@@ -84,7 +84,7 @@ function structured(result, label) {
84
84
  try {
85
85
  await client.connect(transport);
86
86
  const names = (await client.listTools()).tools.map(t => t.name).sort();
87
- const expected = ['contained_transform', 'evidence_verify', 'file_read', 'host_exec', 'host_file_read', 'host_file_write', 'proof_execute', 'workspace_open'];
87
+ const expected = ['contained_transform', 'evidence_verify', 'file_read', 'host_exec', 'host_file_read', 'host_file_write', 'proof_execute', 'skill_catalog', 'workspace_open'];
88
88
  if (JSON.stringify(names) !== JSON.stringify(expected)) throw new Error(`privileged catalog mismatch: ${JSON.stringify(names)}`);
89
89
 
90
90
  const writeText = structured(await client.callTool({