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.
- package/AGENTS.md +11 -18
- package/CONTEXT.md +3 -3
- package/MANIFEST.SHA256 +156 -70
- package/PROVENANCE.json +10 -0
- package/R43-PARENT.json +20 -0
- package/R44-DONORS.json +99 -0
- package/R44-PARENT.json +18 -0
- package/R45-PARENT.json +18 -0
- package/README.txt +19 -14
- package/RELEASE.json +37 -0
- package/SBOM.cdx.json +46 -0
- package/TEST-MATRIX.json +31 -0
- package/bin/deadbyte-hardware-observer-r45.exe +0 -0
- package/bin/deadbyte-hardware-observer-r45.obj +0 -0
- package/controller/README.TXT +5 -6
- package/controller/deadbyte-controller.ps1 +2 -2
- package/docs/ARCHITECTURE.md +8 -1
- package/docs/R44-ARCHITECTURE.md +37 -0
- package/native/deadbyte-hardware-observer.cpp +308 -0
- package/package.json +18 -8
- package/scripts/autonomous-tests.ps1 +2 -2
- package/scripts/build-hardware-r45.ps1 +71 -0
- package/scripts/deferred-slot-operation-r43.mjs +126 -0
- package/scripts/deferred-slot-operation-r44.mjs +126 -0
- package/scripts/deferred-slot-operation-r45.mjs +126 -0
- package/scripts/derive-r45-parent.mjs +24 -0
- package/scripts/derive-r45-release.mjs +72 -0
- package/scripts/derive-r45-tests.mjs +20 -0
- package/scripts/final-closure-r43.mjs +115 -0
- package/scripts/final-closure-r44.mjs +121 -0
- package/scripts/final-closure-r45.mjs +121 -0
- package/scripts/final-closure-verify-r43.mjs +213 -0
- package/scripts/final-closure-verify-r44.mjs +220 -0
- package/scripts/final-closure-verify-r45.mjs +220 -0
- package/scripts/finalize-r43.mjs +217 -0
- package/scripts/finalize-r44.mjs +217 -0
- package/scripts/finalize-r45.mjs +219 -0
- package/scripts/full-regression-r43.mjs +20 -0
- package/scripts/full-regression-r44.mjs +20 -0
- package/scripts/full-regression-r45.mjs +20 -0
- package/scripts/full-regression-summary-r45.mjs +14 -0
- package/scripts/gate-windows-r43.ps1 +36 -0
- package/scripts/gate-windows-r44.ps1 +36 -0
- package/scripts/gate-windows-r45.ps1 +44 -0
- package/scripts/init-host-policy.ps1 +39 -1
- package/scripts/inspect-active-release-r45.mjs +20 -0
- package/scripts/release-manifest.mjs +3 -1
- package/scripts/release-parity-r43.mjs +218 -0
- package/scripts/release-parity-r44.mjs +242 -0
- package/scripts/release-parity-r45.mjs +253 -0
- package/scripts/release-parity-tests-r43.ps1 +8 -0
- package/scripts/release-parity-tests-r44.ps1 +8 -0
- package/scripts/release-parity-tests-r45.ps1 +8 -0
- package/scripts/verify-r43-parent.mjs +126 -0
- package/scripts/verify-r44-parent.mjs +142 -0
- package/scripts/verify-r45-parent.mjs +142 -0
- package/scripts/windows-gate-evidence-r43.mjs +55 -0
- package/scripts/windows-gate-evidence-r44.mjs +55 -0
- package/scripts/windows-gate-evidence-r45.mjs +60 -0
- package/src/agent-backend-r44.mjs +254 -0
- package/src/autonomous-output-contracts.mjs +1 -1
- package/src/autonomous-policy.mjs +41 -4
- package/src/autonomous-runtime.mjs +37 -19
- package/src/autonomous-supervisor-host.mjs +12 -2
- package/src/capability-ledger.mjs +3 -1
- package/src/codex-app-server-r44.mjs +154 -0
- package/src/coding-plane.mjs +66 -19
- package/src/compact-gateway-mcp-tools.mjs +4 -2
- package/src/deadbyte-cli.mjs +17 -9
- package/src/doctor-r45.mjs +65 -0
- package/src/durable-job-r44.mjs +145 -0
- package/src/external-job-provider-r44.mjs +126 -0
- package/src/host-plane.mjs +156 -87
- package/src/job-supervisor-host-r44.mjs +112 -0
- package/src/machine-fs-smoke-client.mjs +1 -1
- package/src/machine-fs.mjs +21 -10
- package/src/mcp-host-smoke-client.mjs +1 -1
- package/src/mcp-server.mjs +84 -26
- package/src/mcp-smoke-client.mjs +1 -1
- package/src/native-diff-v2.mjs +12 -7
- package/src/native-diff.mjs +32 -9
- package/src/observation-mcp-tools.mjs +5 -1
- package/src/observation-output-contracts.mjs +8 -0
- package/src/observation-runtime.mjs +33 -2
- package/src/observation-smoke-client.mjs +7 -3
- package/src/operator-surface.mjs +2 -2
- package/src/output-contracts.mjs +27 -11
- package/src/process-policy.mjs +19 -0
- package/src/process-runtime.mjs +5 -2
- package/src/process-smoke-client.mjs +2 -2
- package/src/provider-circuit.mjs +8 -2
- package/src/r44-compact-disarmed-probe.mjs +74 -0
- package/src/r44-full-surface-probe.mjs +34 -0
- package/src/r45-compact-disarmed-probe.mjs +74 -0
- package/src/r45-full-surface-probe.mjs +34 -0
- package/src/release-generation.mjs +1 -0
- package/src/remote-device-r44.mjs +205 -0
- package/src/remote-mcp-autonomous-smoke-client.mjs +4 -4
- package/src/remote-mcp-r44-parity-client.mjs +203 -0
- package/src/remote-mcp-r45-parity-client.mjs +206 -0
- package/src/remote-mcp-smoke-client.mjs +7 -4
- package/src/runtime-update.mjs +100 -39
- package/src/skill-mcp-tools-r44.mjs +37 -0
- package/src/skill-plane-r44.mjs +203 -0
- package/src/tunnel-stdio-compat-smoke.mjs +1 -1
- package/src/version.mjs +1 -1
- package/test/agent-backend-r44.test.mjs +115 -0
- package/test/agent-runtime.test.mjs +4 -3
- package/test/autonomous-loop.test.mjs +121 -11
- package/test/autonomous-runtime.test.mjs +3 -2
- package/test/capability-ledger-r36.test.mjs +3 -3
- package/test/cli-entrypoint.test.mjs +4 -4
- package/test/codex-app-server-r44.test.mjs +115 -0
- package/test/codex-autonomous-r44.test.mjs +87 -0
- package/test/coding-plane.test.mjs +3 -2
- package/test/coding-runtime.test.mjs +36 -1
- package/test/compact-catalog-r36.test.mjs +3 -3
- package/test/compact-max-catalog-r40.test.mjs +4 -2
- package/test/compact-max-catalog-r44.test.mjs +97 -0
- package/test/compact-max-catalog-r45.test.mjs +99 -0
- package/test/contained.test.mjs +23 -11
- package/test/controller.test.mjs +5 -1
- package/test/core.test.mjs +1 -1
- package/test/durable-job-r44.test.mjs +75 -0
- package/test/external-job-provider-r44.test.mjs +46 -0
- package/test/full-max-catalog-r40.test.mjs +5 -3
- package/test/full-max-catalog-r44.test.mjs +91 -0
- package/test/full-max-catalog-r45.test.mjs +91 -0
- package/test/hardware-observation-r45.test.mjs +44 -0
- package/test/host-inline-r45.test.mjs +67 -0
- package/test/host-plane.test.mjs +15 -1
- package/test/machine-mcp-integration.test.mjs +1 -0
- package/test/native-diff.test.mjs +28 -0
- package/test/observation-mcp-tools.test.mjs +4 -3
- package/test/output-schema.test.mjs +3 -2
- package/test/package-boundary.test.mjs +1 -1
- package/test/process-policy.test.mjs +22 -1
- package/test/production-docs.test.mjs +3 -3
- package/test/prompts-resources.test.mjs +1 -1
- package/test/r33-closeout-regression.test.mjs +20 -36
- package/test/r33-finalization.test.mjs +7 -7
- package/test/r43-finalization.test.mjs +31 -0
- package/test/r43-finalizer.test.mjs +34 -0
- package/test/r43-parent.test.mjs +29 -0
- package/test/r44-finalization.test.mjs +58 -0
- package/test/r44-finalizer.test.mjs +48 -0
- package/test/r44-parent.test.mjs +25 -0
- package/test/r45-autonomous-quick-test.test.mjs +10 -0
- package/test/r45-finalization.test.mjs +84 -0
- package/test/release-generation.test.mjs +1 -0
- package/test/release-version.test.mjs +65 -20
- package/test/remote-device-r44.test.mjs +104 -0
- package/test/runtime-host-bootstrap-r45.test.mjs +46 -0
- package/test/runtime-self-update.test.mjs +42 -0
- package/test/skill-mcp-r44.test.mjs +38 -0
- package/test/skill-plane-r44.test.mjs +93 -0
- package/test/standing-grants.test.mjs +5 -1
package/src/host-plane.mjs
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
93
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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',
|
package/src/machine-fs.mjs
CHANGED
|
@@ -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
|
-
|
|
326
|
-
const
|
|
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
|
-
|
|
362
|
-
const
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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({
|