deadbyte-mcp 0.15.2 → 0.15.3
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/MANIFEST.SHA256 +54 -33
- package/package.json +88 -88
- package/scripts/autonomous-build.ps1 +1 -0
- package/scripts/autonomous-tests.ps1 +1 -0
- package/scripts/check-agent-backend-r46.mjs +77 -0
- package/scripts/check-atomic-resource-r46.mjs +84 -0
- package/scripts/check-autonomous-race-r46.mjs +160 -0
- package/scripts/check-coding-task-deadline-r46.mjs +59 -0
- package/scripts/check-deadline-cancellation-r46.mjs +49 -0
- package/scripts/check-process-deadline-r46.mjs +52 -0
- package/scripts/check-r46-background.mjs +5 -0
- package/scripts/extract-r46-failures.mjs +5 -0
- package/scripts/full-regression-r46.mjs +20 -12
- package/scripts/gate-windows-r46.ps1 +2 -0
- package/scripts/inspect-r46-active.mjs +2 -0
- package/scripts/r47-core-failures.mjs +1 -0
- package/scripts/release-manifest.mjs +1 -1
- package/scripts/run-r46-full-regression-background.mjs +7 -0
- package/scripts/run-r46-windows-gate-background.mjs +6 -0
- package/scripts/run-r46-windows-gate-background.ps1 +10 -0
- package/scripts/windows-gate-evidence-r46.mjs +4 -3
- package/scripts/workload-contract-regression-r47.mjs +14 -0
- package/scripts/workload-stream-regression.mjs +16 -0
- package/scripts/workload-substrate-r47-regression.mjs +12 -0
- package/src/agent-backend-r44.mjs +75 -10
- package/src/atomic-mutation.mjs +55 -0
- package/src/autonomous-mcp-tools.mjs +4 -3
- package/src/autonomous-output-contracts.mjs +16 -3
- package/src/autonomous-runtime.mjs +83 -30
- package/src/coding-mcp-tools.mjs +2 -2
- package/src/coding-output-contracts.mjs +18 -4
- package/src/coding-plane.mjs +3 -24
- package/src/coding-search.mjs +11 -2
- package/src/coding-task-runtime.mjs +41 -6
- package/src/deadline-cancellation.mjs +83 -0
- package/src/machine-fs-smoke-client.mjs +1 -1
- package/src/machine-fs.mjs +3 -23
- package/src/mcp-server.mjs +6 -0
- package/src/observation-smoke-client.mjs +1 -1
- package/src/process-mcp-tools.mjs +11 -3
- package/src/process-output-contracts.mjs +23 -0
- package/src/process-runtime.mjs +54 -5
- package/src/process-smoke-client.mjs +1 -1
- package/src/resource-identity.mjs +59 -0
- package/src/runtime-update.mjs +526 -162
- package/src/workload-substrate.mjs +87 -0
- package/test/coding-runtime.test.mjs +12 -0
- package/test/coding-search-r36.test.mjs +7 -0
- package/test/full-max-catalog-r46.test.mjs +2 -2
- package/test/helpers/autonomous-r34-fixture.mjs +10 -1
- package/test/process-mcp-tools.test.mjs +5 -2
- package/test/process-runtime.test.mjs +18 -1
- package/test/r45-finalization.test.mjs +1 -1
- package/test/release-version.test.mjs +1 -1
- package/test/runtime-self-update.test.mjs +118 -1
- package/test/workload-substrate.test.mjs +22 -0
|
@@ -8,9 +8,19 @@ const hex64 = z.string().regex(HEX64_RE);
|
|
|
8
8
|
const receiptId = z.string().regex(HEX24_RE);
|
|
9
9
|
const taskId = z.string().regex(TASK_ID_RE);
|
|
10
10
|
const stepId = z.string().regex(STEP_ID_RE);
|
|
11
|
+
const streamCoverage = z.strictObject({
|
|
12
|
+
logical_first_seq:z.number().int().min(1), logical_last_seq:z.number().int().min(0),
|
|
13
|
+
retained_first_seq:z.number().int().min(1), retained_last_seq:z.number().int().min(0),
|
|
14
|
+
complete:z.boolean(), truncated:z.boolean(),
|
|
15
|
+
missing_ranges:z.array(z.strictObject({from:z.number().int().min(1),to:z.number().int().min(0)})).max(100000),
|
|
16
|
+
logical_item_count:z.number().int().min(0), retained_item_count:z.number().int().min(0),
|
|
17
|
+
logical_bytes:z.number().int().min(0), retained_bytes:z.number().int().min(0),
|
|
18
|
+
dropped_items:z.number().int().min(0), dropped_bytes:z.number().int().min(0), status:z.enum(['open','closed'])
|
|
19
|
+
});
|
|
11
20
|
const jsonObject = z.record(z.string(), z.json());
|
|
12
21
|
const taskState = z.enum(['pending', 'running', 'paused', 'failed', 'cancelled', 'succeeded']);
|
|
13
22
|
const codingKind = z.enum(['checkpoint', 'write', 'replace', 'command', 'benchmark']);
|
|
23
|
+
const deadline = z.strictObject({ schema:z.literal('deadbyte.deadline.v1'), id:z.string().nullable(), created_at_ms:z.number(), timeout_ms:z.number().int().min(1).nullable(), deadline_ms:z.number().nullable() });
|
|
14
24
|
|
|
15
25
|
function requireFields(value, ctx, fields) {
|
|
16
26
|
for (const field of fields) if (value[field] === undefined) ctx.addIssue({ code: 'custom', path: [field], message: `${field} is required` });
|
|
@@ -87,7 +97,9 @@ export const codingSearchOutputSchema = statusContract({
|
|
|
87
97
|
binary_files_skipped: z.number().int().min(0).optional(), truncated: z.boolean().optional(),
|
|
88
98
|
truncation_reason: z.enum(['tree_limit','scan_byte_limit','max_results']).nullable().optional(),
|
|
89
99
|
query_sha256: hex64.optional(), snapshot_sha256: hex64.optional(), page_sha256: hex64.optional(),
|
|
90
|
-
next_cursor: z.string().max(8192).nullable().optional()
|
|
100
|
+
next_cursor: z.string().max(8192).nullable().optional(),
|
|
101
|
+
stream_id: hex64.optional(), stream_generation: z.number().int().min(1).optional(), stream_next_seq: z.number().int().min(1).optional(),
|
|
102
|
+
stream_coverage: streamCoverage.optional() },
|
|
91
103
|
successStatuses: ['ok'], successRequired: ['root_id','path','pattern','target','mode','case_sensitive','matches','files_scanned','bytes_scanned','binary_files_skipped','truncated','truncation_reason','query_sha256','snapshot_sha256','page_sha256','next_cursor']
|
|
92
104
|
});
|
|
93
105
|
const semanticLocationSchema = z.strictObject({
|
|
@@ -222,7 +234,7 @@ export const codingTaskSubmitOutputSchema = statusContract({
|
|
|
222
234
|
successStatuses: ['accepted', 'existing'], successRequired: ['task_id', 'plan_sha256', 'state']
|
|
223
235
|
});
|
|
224
236
|
export const codingTaskRunOutputSchema = z.strictObject({
|
|
225
|
-
status: z.enum(['terminal', 'paused', 'completed', 'cancelled', 'failed', 'yielded', 'succeeded']), error: z.string().optional(), task_id: taskId.optional(),
|
|
237
|
+
status: z.enum(['terminal', 'paused', 'completed', 'cancelled', 'failed', 'yielded', 'succeeded']), error: z.string().optional(), task_id: taskId.optional(), deadline: deadline.optional(),
|
|
226
238
|
...Object.fromEntries(Object.entries(derivedShape).map(([key, schema]) => [key, schema.optional()]))
|
|
227
239
|
}).superRefine((value, ctx) => {
|
|
228
240
|
if (value.error !== undefined) { if (value.status !== 'failed') ctx.addIssue({ code: 'custom', path: ['error'], message: 'error wrapper requires status=failed' }); return; }
|
|
@@ -231,13 +243,15 @@ export const codingTaskRunOutputSchema = z.strictObject({
|
|
|
231
243
|
export const codingTaskStatusOutputSchema = statusContract({
|
|
232
244
|
statuses: ['ok', 'failed'],
|
|
233
245
|
shape: { task_id: taskId.optional(), plan_sha256: hex64.optional(),
|
|
234
|
-
...Object.fromEntries(Object.entries(derivedShape).map(([key, schema]) => [key, schema.optional()])), journal_verified: z.literal(true).optional()
|
|
246
|
+
...Object.fromEntries(Object.entries(derivedShape).map(([key, schema]) => [key, schema.optional()])), journal_verified: z.literal(true).optional(),
|
|
247
|
+
stream_id: taskId.optional(), stream_generation: z.number().int().min(1).optional(), stream_next_seq: z.number().int().min(1).optional(), stream_coverage: streamCoverage.optional() },
|
|
235
248
|
successStatuses: ['ok'], successRequired: ['task_id', 'plan_sha256', ...Object.keys(derivedShape), 'journal_verified']
|
|
236
249
|
});
|
|
237
250
|
export const codingTaskEventsOutputSchema = statusContract({
|
|
238
251
|
statuses: ['ok', 'failed'],
|
|
239
252
|
shape: { task_id: taskId.optional(), plan_sha256: hex64.optional(), journal_verified: z.literal(true).optional(),
|
|
240
|
-
journal_head_sha256: hex64.optional(), events: z.array(eventSchema).max(200).optional()
|
|
253
|
+
journal_head_sha256: hex64.optional(), events: z.array(eventSchema).max(200).optional(),
|
|
254
|
+
stream_id: taskId.optional(), stream_generation: z.number().int().min(1).optional(), stream_next_seq: z.number().int().min(1).optional(), stream_coverage: streamCoverage.optional() },
|
|
241
255
|
successStatuses: ['ok'], successRequired: ['task_id', 'plan_sha256', 'journal_verified', 'journal_head_sha256', 'events']
|
|
242
256
|
});
|
|
243
257
|
export const codingTaskCancelOutputSchema = statusContract({
|
package/src/coding-plane.mjs
CHANGED
|
@@ -10,6 +10,7 @@ import { isInside, validatePortableRelativePath } from './path-policy.mjs';
|
|
|
10
10
|
import { loadPrivateSigningKey, loadPublicVerifyKey } from './trust.mjs';
|
|
11
11
|
import { createCodingSearch } from './coding-search.mjs';
|
|
12
12
|
import { createSearchCursorCodec } from './search-cursor.mjs';
|
|
13
|
+
import { atomicMutation } from './atomic-mutation.mjs';
|
|
13
14
|
|
|
14
15
|
export const CODING_POLICY_SCHEMA = 'deadbyte.coding-policy.v1';
|
|
15
16
|
export const CODING_LEASE_SCHEMA = 'deadbyte.coding-lease.v1';
|
|
@@ -261,30 +262,8 @@ function decodeContent(content, encoding) {
|
|
|
261
262
|
throw new Error('encoding must be utf8 or base64');
|
|
262
263
|
}
|
|
263
264
|
async function atomicWrite(target, bytes, expectedSha256, authorityCheck = null) {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
const temp = path.join(path.dirname(target), `.${path.basename(target)}.deadbyte-coding-${process.pid}-${randomBytes(8).toString('hex')}.tmp`);
|
|
267
|
-
let handle;
|
|
268
|
-
try {
|
|
269
|
-
if (authorityCheck) await authorityCheck();
|
|
270
|
-
handle = await open(temp, fsConstants.O_CREAT | fsConstants.O_EXCL | fsConstants.O_WRONLY, 0o600);
|
|
271
|
-
await handle.writeFile(bytes);
|
|
272
|
-
await handle.sync();
|
|
273
|
-
await handle.close();
|
|
274
|
-
handle = null;
|
|
275
|
-
const race = await targetState(target);
|
|
276
|
-
checkExpected(race, expectedSha256);
|
|
277
|
-
if (authorityCheck) await authorityCheck();
|
|
278
|
-
await rename(temp, target);
|
|
279
|
-
} catch (error) {
|
|
280
|
-
if (handle) await handle.close().catch(() => {});
|
|
281
|
-
await rm(temp, { force: true }).catch(() => {});
|
|
282
|
-
throw error;
|
|
283
|
-
}
|
|
284
|
-
const after = await targetState(target);
|
|
285
|
-
const expectedAfter = sha256Bytes(bytes);
|
|
286
|
-
if (!after.exists || after.sha256 !== expectedAfter) throw new Error('coding write post-commit hash mismatch');
|
|
287
|
-
return { before, after };
|
|
265
|
+
return atomicMutation({ target, bytes, expected: expectedSha256, readState: targetState,
|
|
266
|
+
assertExpected: checkExpected, authorityCheck, tempTag: 'deadbyte-coding' });
|
|
288
267
|
}
|
|
289
268
|
function codingEnvironment() {
|
|
290
269
|
const allowed = new Set([
|
package/src/coding-search.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { canonicalJson } from './canonical-json.mjs';
|
|
3
3
|
import { decodeUtf8Strict } from './text-policy.mjs';
|
|
4
|
+
import { createSequenceStream } from './workload-substrate.mjs';
|
|
4
5
|
|
|
5
6
|
const sha256Object=value=>createHash('sha256').update(canonicalJson(value),'utf8').digest('hex');
|
|
6
7
|
const sha256Bytes=bytes=>createHash('sha256').update(bytes).digest('hex');
|
|
@@ -121,18 +122,26 @@ export function createCodingSearch({tree,fileRead,cursorCodec}){
|
|
|
121
122
|
|
|
122
123
|
const snapshotSha=sha256Object({schema:'deadbyte.coding-search-snapshot.v2',query_sha256:querySha,entries:snapshotEntries});
|
|
123
124
|
if(decodedCursor&&decodedCursor.snapshot_sha256!==snapshotSha) throw new Error('coding search cursor stale snapshot mismatch');
|
|
125
|
+
const stream=createSequenceStream({streamId:querySha,maxItems:Math.max(1,maxResults),maxBytes:Math.max(1024, maxResults*4096)});
|
|
126
|
+
for(const match of matches) stream.append(match,{producer:'coding-search',channel:'match'});
|
|
124
127
|
const offset=decodedCursor?.offset??0;
|
|
125
128
|
if(offset>matches.length) throw new Error('coding search cursor offset exceeds retained results');
|
|
126
|
-
const
|
|
129
|
+
const page=stream.read(offset+1,pageSize);
|
|
130
|
+
if(page.missing) throw new Error('coding search cursor offset is outside retained stream coverage');
|
|
131
|
+
const pageMatches=page.items.map(item=>item.value);
|
|
127
132
|
const nextOffset=offset+pageMatches.length;
|
|
128
133
|
const hasMore=nextOffset<matches.length;
|
|
129
134
|
const nextCursor=hasMore?await cursorCodec.encode({query_sha256:querySha,snapshot_sha256:snapshotSha,offset:nextOffset}):null;
|
|
130
135
|
const pageSha=sha256Object({schema:'deadbyte.coding-search-page.v2',query_sha256:querySha,snapshot_sha256:snapshotSha,offset,matches:pageMatches});
|
|
136
|
+
const coverage=stream.coverage();
|
|
137
|
+
const streamSnapshot=stream.snapshot();
|
|
131
138
|
return {
|
|
132
139
|
status:'ok',root_id:rootId,path,pattern,target,mode,case_sensitive:caseSensitive,
|
|
133
140
|
matches:pageMatches,files_scanned:filesScanned,bytes_scanned:bytesScanned,binary_files_skipped:binaryFilesSkipped,
|
|
134
141
|
truncated:truncated||hasMore,truncation_reason:truncationReason,query_sha256:querySha,snapshot_sha256:snapshotSha,
|
|
135
|
-
page_sha256:pageSha,next_cursor:nextCursor
|
|
142
|
+
page_sha256:pageSha,next_cursor:nextCursor,
|
|
143
|
+
stream_id:stream.streamId,stream_generation:stream.generation,stream_next_seq:streamSnapshot.next_seq,
|
|
144
|
+
stream_coverage:coverage
|
|
136
145
|
};
|
|
137
146
|
};
|
|
138
147
|
}
|
|
@@ -4,6 +4,8 @@ import path from 'node:path';
|
|
|
4
4
|
import { canonicalJson } from './canonical-json.mjs';
|
|
5
5
|
import { validatePortableRelativePath } from './path-policy.mjs';
|
|
6
6
|
import { loadPrivateSigningKey, loadPublicVerifyKey } from './trust.mjs';
|
|
7
|
+
import { createJournalStream } from './workload-substrate.mjs';
|
|
8
|
+
import { createDeadline, deadlineState } from './deadline-cancellation.mjs';
|
|
7
9
|
|
|
8
10
|
export const CODING_TASK_PLAN_SCHEMA = 'deadbyte.coding-task-plan.v1';
|
|
9
11
|
export const CODING_TASK_EVENT_SCHEMA = 'deadbyte.coding-task-event.v1';
|
|
@@ -525,7 +527,11 @@ export function createCodingTaskRuntime({ codingRuntime, signingKeyPath, verifyK
|
|
|
525
527
|
const ctx = taskContext(policy, taskId);
|
|
526
528
|
const plan = await loadPlan(ctx, policy, verifyKeyPath);
|
|
527
529
|
const events = await readEvents(ctx, verifyKeyPath);
|
|
528
|
-
|
|
530
|
+
const stream = createJournalStream({streamId:taskId,events,maxItems:MAX_EVENTS,maxBytes:8*1024*1024,producer:'coding-task',channel:'event'});
|
|
531
|
+
const snapshot = stream.snapshot();
|
|
532
|
+
return { status: 'ok', task_id: taskId, plan_sha256: plan.plan_sha256, ...deriveState(plan, events), journal_verified: true,
|
|
533
|
+
stream_id:snapshot.stream_id, stream_generation:snapshot.generation, stream_next_seq:snapshot.next_seq,
|
|
534
|
+
stream_coverage:stream.coverage() };
|
|
529
535
|
}
|
|
530
536
|
|
|
531
537
|
async function events({ taskId, fromSeq = 1, limit = 100 }) {
|
|
@@ -534,10 +540,15 @@ export function createCodingTaskRuntime({ codingRuntime, signingKeyPath, verifyK
|
|
|
534
540
|
const ctx = taskContext(policy, taskId);
|
|
535
541
|
const plan = await loadPlan(ctx, policy, verifyKeyPath);
|
|
536
542
|
const all = await readEvents(ctx, verifyKeyPath);
|
|
543
|
+
const stream = createJournalStream({streamId:taskId,events:all,maxItems:MAX_EVENTS,maxBytes:8*1024*1024,producer:'coding-task',channel:'event'});
|
|
544
|
+
const page = stream.read(fromSeq,limit);
|
|
545
|
+
const snapshot = stream.snapshot();
|
|
537
546
|
return {
|
|
538
547
|
status: 'ok', task_id: taskId, plan_sha256: plan.plan_sha256, journal_verified: true,
|
|
539
548
|
journal_head_sha256: all.at(-1)?.event_sha256 ?? ZERO_HASH,
|
|
540
|
-
|
|
549
|
+
stream_id: snapshot.stream_id, stream_generation: snapshot.generation, stream_next_seq: snapshot.next_seq,
|
|
550
|
+
stream_coverage: stream.coverage(),
|
|
551
|
+
events: page.items.map(item=>item.value)
|
|
541
552
|
};
|
|
542
553
|
}
|
|
543
554
|
|
|
@@ -590,8 +601,18 @@ export function createCodingTaskRuntime({ codingRuntime, signingKeyPath, verifyK
|
|
|
590
601
|
const journal = await readEvents(ctx, verifyKeyPath);
|
|
591
602
|
return verifyTaskReceipt(ctx, plan, journal, policy, verifyKeyPath, codingRuntime);
|
|
592
603
|
}
|
|
593
|
-
async function run({ taskId, maxSteps = 8 }) {
|
|
604
|
+
async function run({ taskId, maxSteps = 8, timeoutMs = null }) {
|
|
594
605
|
if (!Number.isInteger(maxSteps) || maxSteps < 1 || maxSteps > MAX_STEPS) throw new Error(`max_steps must be in [1, ${MAX_STEPS}]`);
|
|
606
|
+
if (timeoutMs !== null && (!Number.isInteger(timeoutMs) || timeoutMs < 100 || timeoutMs > 900000)) throw new Error('timeout_ms must be null or in [100,900000]');
|
|
607
|
+
const deadline = createDeadline({ timeoutMs });
|
|
608
|
+
const deadlineExpired = () => timeoutMs !== null && deadlineState(deadline).expired;
|
|
609
|
+
const failDeadline = async (ctx, journal, plan, stepId = null) => {
|
|
610
|
+
await appendEvent(ctx, journal, signingKeyPath, 'task_failed', {
|
|
611
|
+
stepId,
|
|
612
|
+
payload: { reason: 'deadline_exceeded', deadline }
|
|
613
|
+
});
|
|
614
|
+
return deriveState(plan, journal);
|
|
615
|
+
};
|
|
595
616
|
const session = await codingRuntime.sessionStatus();
|
|
596
617
|
if (!session.armed) throw new Error(`coding plane is disarmed: ${session.reason}`);
|
|
597
618
|
const ctx = taskContext(policy, taskId);
|
|
@@ -610,10 +631,14 @@ export function createCodingTaskRuntime({ codingRuntime, signingKeyPath, verifyK
|
|
|
610
631
|
if (await consumeCancel(ctx, journal, signingKeyPath, verifyKeyPath)) {
|
|
611
632
|
derived = deriveState(plan, journal); return { status: 'cancelled', task_id: taskId, ...derived };
|
|
612
633
|
}
|
|
613
|
-
await appendEvent(ctx, journal, signingKeyPath, 'run_started', { payload: { max_steps: maxSteps } });
|
|
634
|
+
await appendEvent(ctx, journal, signingKeyPath, 'run_started', { payload: { max_steps: maxSteps, deadline } });
|
|
614
635
|
derived = deriveState(plan, journal);
|
|
615
636
|
let consumed = 0;
|
|
616
637
|
while (consumed < maxSteps) {
|
|
638
|
+
if (deadlineExpired()) {
|
|
639
|
+
derived = await failDeadline(ctx, journal, plan);
|
|
640
|
+
return { status: 'failed', task_id: taskId, ...derived, deadline };
|
|
641
|
+
}
|
|
617
642
|
if (await consumeCancel(ctx, journal, signingKeyPath, verifyKeyPath)) {
|
|
618
643
|
derived = deriveState(plan, journal); return { status: 'cancelled', task_id: taskId, ...derived };
|
|
619
644
|
}
|
|
@@ -638,7 +663,9 @@ export function createCodingTaskRuntime({ codingRuntime, signingKeyPath, verifyK
|
|
|
638
663
|
derived = deriveState(plan, journal);
|
|
639
664
|
consumed += 1;
|
|
640
665
|
try {
|
|
641
|
-
const
|
|
666
|
+
const remainingMs = timeoutMs === null ? null : Math.max(100, Math.floor(deadlineState(deadline).remaining_ms));
|
|
667
|
+
const boundedStep = remainingMs === null ? next : { ...next, timeout_ms: next.timeout_ms === null || next.timeout_ms === undefined ? remainingMs : Math.min(next.timeout_ms, remainingMs) };
|
|
668
|
+
const executed = await executeStep(codingRuntime, plan, boundedStep, attempt);
|
|
642
669
|
if (!executed.outcome.valid) {
|
|
643
670
|
await appendEvent(ctx, journal, signingKeyPath, 'task_paused', {
|
|
644
671
|
stepId: next.step_id, payload: { reason: 'post_execute_receipt_mismatch', detail: executed.outcome.reason }
|
|
@@ -646,6 +673,10 @@ export function createCodingTaskRuntime({ codingRuntime, signingKeyPath, verifyK
|
|
|
646
673
|
derived = deriveState(plan, journal);
|
|
647
674
|
return { status: 'paused', task_id: taskId, ...derived };
|
|
648
675
|
}
|
|
676
|
+
if (deadlineExpired()) {
|
|
677
|
+
derived = await failDeadline(ctx, journal, plan, next.step_id);
|
|
678
|
+
return { status: 'failed', task_id: taskId, ...derived, deadline };
|
|
679
|
+
}
|
|
649
680
|
if (executed.outcome.success) {
|
|
650
681
|
await appendEvent(ctx, journal, signingKeyPath, 'step_succeeded', {
|
|
651
682
|
stepId: next.step_id, payload: { ...executed.outcome.payload, attempt, recovered: false }
|
|
@@ -664,6 +695,10 @@ export function createCodingTaskRuntime({ codingRuntime, signingKeyPath, verifyK
|
|
|
664
695
|
}
|
|
665
696
|
derived = deriveState(plan, journal);
|
|
666
697
|
} catch (error) {
|
|
698
|
+
if (deadlineExpired()) {
|
|
699
|
+
derived = await failDeadline(ctx, journal, plan, next.step_id);
|
|
700
|
+
return { status: 'failed', task_id: taskId, ...derived, deadline };
|
|
701
|
+
}
|
|
667
702
|
const checked = await codingRuntime.verifyReceiptDetailed(receiptId);
|
|
668
703
|
if (checked.ok) {
|
|
669
704
|
const outcome = receiptOutcome(next, checked, requestSha256);
|
|
@@ -717,7 +752,7 @@ export function createCodingTaskRuntime({ codingRuntime, signingKeyPath, verifyK
|
|
|
717
752
|
}
|
|
718
753
|
}
|
|
719
754
|
}
|
|
720
|
-
return { status: 'yielded', task_id: taskId, ...derived };
|
|
755
|
+
return { status: 'yielded', task_id: taskId, ...derived, deadline };
|
|
721
756
|
} finally { await releaseLock(ctx); }
|
|
722
757
|
}
|
|
723
758
|
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
const ID=/^[A-Za-z0-9._:-]{1,128}$/;
|
|
2
|
+
|
|
3
|
+
export function createDeadline({timeoutMs=null, nowMs=Date.now(), id=null}={}) {
|
|
4
|
+
if(timeoutMs!==null && (!Number.isInteger(timeoutMs)||timeoutMs<1)) throw new Error('deadline timeout_ms invalid');
|
|
5
|
+
if(!Number.isFinite(nowMs)) throw new Error('deadline now_ms invalid');
|
|
6
|
+
const deadlineMs=timeoutMs===null?null:nowMs+timeoutMs;
|
|
7
|
+
return Object.freeze({
|
|
8
|
+
schema:'deadbyte.deadline.v1',
|
|
9
|
+
id:id===null?null:validateId(id),
|
|
10
|
+
created_at_ms:nowMs,
|
|
11
|
+
timeout_ms:timeoutMs,
|
|
12
|
+
deadline_ms:deadlineMs
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function deadlineState(deadline,{nowMs=Date.now()}={}) {
|
|
17
|
+
if(!deadline||deadline.schema!=='deadbyte.deadline.v1') throw new Error('deadline invalid');
|
|
18
|
+
if(!Number.isFinite(nowMs)) throw new Error('deadline now_ms invalid');
|
|
19
|
+
if(deadline.deadline_ms===null) return Object.freeze({expired:false,remaining_ms:null});
|
|
20
|
+
const remainingMs=deadline.deadline_ms-nowMs;
|
|
21
|
+
return Object.freeze({expired:remainingMs<=0,remaining_ms:Math.max(0,remainingMs)});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function createCancellationToken({id=null}={}) {
|
|
25
|
+
const controller=new AbortController();
|
|
26
|
+
let requestedAtMs=null;
|
|
27
|
+
let reason=null;
|
|
28
|
+
const request=(value='cancelled')=>{
|
|
29
|
+
if(controller.signal.aborted)return false;
|
|
30
|
+
requestedAtMs=Date.now();
|
|
31
|
+
reason=String(value);
|
|
32
|
+
controller.abort(reason);
|
|
33
|
+
return true;
|
|
34
|
+
};
|
|
35
|
+
return Object.freeze({
|
|
36
|
+
schema:'deadbyte.cancellation.v1',
|
|
37
|
+
id:id===null?null:validateId(id),
|
|
38
|
+
signal:controller.signal,
|
|
39
|
+
request,
|
|
40
|
+
snapshot:()=>Object.freeze({
|
|
41
|
+
requested:controller.signal.aborted,
|
|
42
|
+
requested_at_ms:requestedAtMs,
|
|
43
|
+
reason
|
|
44
|
+
})
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function composeAbortSignal({deadline=null,cancellation=null}={}) {
|
|
49
|
+
const controller=new AbortController();
|
|
50
|
+
let timeout=null;
|
|
51
|
+
const abort=(reason)=>{
|
|
52
|
+
if(!controller.signal.aborted)controller.abort(reason);
|
|
53
|
+
};
|
|
54
|
+
if(cancellation?.signal){
|
|
55
|
+
if(cancellation.signal.aborted)abort(cancellation.signal.reason??'cancelled');
|
|
56
|
+
else cancellation.signal.addEventListener('abort',()=>abort(cancellation.signal.reason??'cancelled'),{once:true});
|
|
57
|
+
}
|
|
58
|
+
if(deadline?.deadline_ms!==null&&deadline?.deadline_ms!==undefined){
|
|
59
|
+
const remaining=Math.max(0,deadline.deadline_ms-Date.now());
|
|
60
|
+
if(remaining===0)abort('deadline_exceeded');
|
|
61
|
+
else timeout=setTimeout(()=>abort('deadline_exceeded'),remaining);
|
|
62
|
+
timeout?.unref?.();
|
|
63
|
+
}
|
|
64
|
+
const close=()=>{if(timeout)clearTimeout(timeout);};
|
|
65
|
+
return Object.freeze({signal:controller.signal,close});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function classifyTermination({deadlineExceeded=false,cancellationRequested=false,physicallyTerminated=false,verified=false}={}) {
|
|
69
|
+
return Object.freeze({
|
|
70
|
+
deadline_exceeded:deadlineExceeded===true,
|
|
71
|
+
cancellation_requested:cancellationRequested===true,
|
|
72
|
+
physical_termination:physicallyTerminated===true,
|
|
73
|
+
verified:verified===true,
|
|
74
|
+
terminal_semantics:
|
|
75
|
+
verified ? (physicallyTerminated ? 'verified_termination' : 'verified_stop')
|
|
76
|
+
: physicallyTerminated ? 'termination_unverified'
|
|
77
|
+
: cancellationRequested ? 'cancellation_requested'
|
|
78
|
+
: deadlineExceeded ? 'deadline_exceeded'
|
|
79
|
+
: 'running'
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function validateId(id){if(typeof id!=='string'||!ID.test(id))throw new Error('deadline/cancellation id invalid');return id;}
|
|
@@ -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','skill_catalog','workspace_open'];
|
|
59
|
+
const baseTools = ['contained_transform','evidence_verify','file_read','host_exec','host_file_read','host_file_write','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
|
@@ -9,6 +9,7 @@ import { assertCodingArmed, recheckCodingAuthority } from './coding-plane.mjs';
|
|
|
9
9
|
import { isInside, validatePortableRelativePath } from './path-policy.mjs';
|
|
10
10
|
import { decodeUtf8Strict } from './text-policy.mjs';
|
|
11
11
|
import { loadPrivateSigningKey, loadPublicVerifyKey } from './trust.mjs';
|
|
12
|
+
import { atomicMutation } from './atomic-mutation.mjs';
|
|
12
13
|
|
|
13
14
|
export const MACHINE_FS_RECEIPT_SCHEMA = 'deadbyte.machine-fs-receipt.v1';
|
|
14
15
|
const RECEIPT_DOMAIN = Buffer.from('DEADBYTE-MACHINE-FS-RECEIPT-V1\0', 'utf8');
|
|
@@ -271,29 +272,8 @@ function requestSha(operation, args) {
|
|
|
271
272
|
}
|
|
272
273
|
|
|
273
274
|
async function atomicWrite(target, bytes, expectedSha256, authorityCheck = null) {
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
const temp = path.join(path.dirname(target), `.${path.basename(target)}.deadbyte-machine-fs-${process.pid}-${randomBytes(8).toString('hex')}.tmp`);
|
|
277
|
-
let handle;
|
|
278
|
-
try {
|
|
279
|
-
if (authorityCheck) await authorityCheck();
|
|
280
|
-
handle = await open(temp, 'wx', 0o600);
|
|
281
|
-
await handle.writeFile(bytes);
|
|
282
|
-
await handle.sync();
|
|
283
|
-
await handle.close();
|
|
284
|
-
handle = null;
|
|
285
|
-
checkExpectedFile(await regularState(target), expectedSha256);
|
|
286
|
-
if (authorityCheck) await authorityCheck();
|
|
287
|
-
await rename(temp, target);
|
|
288
|
-
} catch (error) {
|
|
289
|
-
if (handle) await handle.close().catch(() => {});
|
|
290
|
-
await rm(temp, { force: true }).catch(() => {});
|
|
291
|
-
throw error;
|
|
292
|
-
}
|
|
293
|
-
const after = await regularState(target);
|
|
294
|
-
const expectedAfter = sha256Bytes(bytes);
|
|
295
|
-
if (!after.exists || after.sha256 !== expectedAfter) throw new Error('machine fs post-commit hash mismatch');
|
|
296
|
-
return { before, after };
|
|
275
|
+
return atomicMutation({ target, bytes, expected: expectedSha256, readState: regularState,
|
|
276
|
+
assertExpected: checkExpectedFile, authorityCheck, tempTag: 'deadbyte-machine-fs' });
|
|
297
277
|
}
|
|
298
278
|
|
|
299
279
|
export function createMachineFsRuntime({ policy, signingKeyPath, verifyKeyPath, readWorkerPath = machineFsReadWorkerPath }) {
|
package/src/mcp-server.mjs
CHANGED
|
@@ -15,6 +15,7 @@ import { DEADBYTE_VERSION } from './version.mjs';
|
|
|
15
15
|
import { currentIngressAdapterMode, TUNNEL_STDIO_PROBE_COMPAT_MODE } from './ingress-mode.mjs';
|
|
16
16
|
import { installTunnelStdioProbeCompat } from './tunnel-stdio-compat.mjs';
|
|
17
17
|
import { installStdioConsoleGuard } from './stdio-console-guard.mjs';
|
|
18
|
+
import { recoverInterruptedSlotUpdates } from './runtime-update.mjs';
|
|
18
19
|
import { decodeUtf8Strict } from './text-policy.mjs';
|
|
19
20
|
import { assertHostArmed, hostExec, hostExecInline, hostExecNative, hostFileRead, hostFileWrite, loadHostPolicy } from './host-plane.mjs';
|
|
20
21
|
import { createMachineRuntime } from './machine-identity.mjs';
|
|
@@ -74,6 +75,11 @@ const verifyKeyPath = process.env.DEADBYTE_VERIFY_KEY ?? defaultPublicKeyPath();
|
|
|
74
75
|
const auditRuntimeMode = process.env.DEADBYTE_AUDIT_RUNTIME ?? '0';
|
|
75
76
|
if (!['0','1'].includes(auditRuntimeMode)) throw new Error('DEADBYTE_AUDIT_RUNTIME must be 0 or 1');
|
|
76
77
|
const runtimeStateRoot = path.resolve(process.env.DEADBYTE_STATE_ROOT || path.join(os.homedir(),'.deadbyte-mcp'));
|
|
78
|
+
const interruptedUpdateRecovery = await recoverInterruptedSlotUpdates({ stateRoot: runtimeStateRoot });
|
|
79
|
+
if (interruptedUpdateRecovery.launched > 0) {
|
|
80
|
+
console.error(`[runtime-update-recovery] launched ${interruptedUpdateRecovery.launched} detached supervisor(s)`);
|
|
81
|
+
}
|
|
82
|
+
|
|
77
83
|
const surfaceProfile = capabilityProfileFromEnv(process.env);
|
|
78
84
|
const localPrivilegedPolicyPath = path.join(packageRoot, 'controller', 'deadbyte-host-policy.json');
|
|
79
85
|
const privilegedPolicyExplicit = Object.hasOwn(process.env, 'DEADBYTE_PRIVILEGED_POLICY');
|
|
@@ -51,7 +51,7 @@ const transport=new StdioClientTransport({command:process.execPath,args:[serverP
|
|
|
51
51
|
env:{...getDefaultEnvironment(),DEADBYTE_MACHINE_RUNTIME:'1',DEADBYTE_CODING_RUNTIME:'1',DEADBYTE_OBSERVATION_RUNTIME:'1',
|
|
52
52
|
DEADBYTE_CODING_POLICY:policyPath,DEADBYTE_STATE_ROOT:state,DEADBYTE_SIGNING_KEY:keys.privateKeyPath,DEADBYTE_VERIFY_KEY:keys.publicKeyPath},stderr:'pipe'});
|
|
53
53
|
const stderr=[]; transport.stderr?.on('data',chunk=>stderr.push(Buffer.from(chunk)));
|
|
54
|
-
const base=['contained_transform','evidence_verify','file_read','proof_execute','skill_catalog','workspace_open'];
|
|
54
|
+
const base=['contained_transform','evidence_verify','file_read','host_exec','host_file_read','host_file_write','proof_execute','skill_catalog','workspace_open'];
|
|
55
55
|
const machine=['machine_list','machine_ping','machine_revoke','machine_status'];
|
|
56
56
|
const coding=['coding_benchmark_run','coding_command_run','coding_file_read','coding_file_write','coding_files_read','coding_receipt_verify','coding_search',
|
|
57
57
|
'coding_session_status','coding_stat','coding_symbol_definition','coding_symbol_diagnostics','coding_symbol_implementations','coding_symbol_outline','coding_symbol_references','coding_symbol_rename_preview','coding_symbol_replace',
|
|
@@ -22,10 +22,10 @@ export function registerProcessTools(server, { runtime }) {
|
|
|
22
22
|
description: 'Start one locally registered process profile inside an approved root. Executable identity comes only from local policy.',
|
|
23
23
|
inputSchema: z.strictObject({
|
|
24
24
|
profile_id: id, root_id: id, cwd: z.string().min(1).max(1024).default('.'),
|
|
25
|
-
args: z.array(z.string().max(8192)).max(64).default([])
|
|
25
|
+
args: z.array(z.string().max(8192)).max(64).default([]), timeout_ms: z.number().int().min(100).max(900000).nullable().default(null)
|
|
26
26
|
}), outputSchema: processStartOutputSchema
|
|
27
|
-
}, async ({ profile_id, root_id, cwd, args }) => {
|
|
28
|
-
try { return ok(processStartOutputSchema, { status: 'ok', session: await runtime.start({ profileId: profile_id, rootId: root_id, cwd, args }) }); }
|
|
27
|
+
}, async ({ profile_id, root_id, cwd, args, timeout_ms }) => {
|
|
28
|
+
try { return ok(processStartOutputSchema, { status: 'ok', session: await runtime.start({ profileId: profile_id, rootId: root_id, cwd, args, timeoutMs: timeout_ms }) }); }
|
|
29
29
|
catch (error) { return fail(processStartOutputSchema, 'denied', error); }
|
|
30
30
|
});
|
|
31
31
|
server.registerTool('process_session_read', {
|
|
@@ -66,6 +66,14 @@ export function registerProcessTools(server, { runtime }) {
|
|
|
66
66
|
try { return ok(processListOutputSchema, { status: 'ok', ...(await runtime.list()) }); }
|
|
67
67
|
catch (error) { return fail(processListOutputSchema, 'failed', error); }
|
|
68
68
|
});
|
|
69
|
+
server.registerTool('process_session_cancel', {
|
|
70
|
+
description: 'Request cooperative cancellation of one DEADBYTE-owned process session. Cancellation is recorded separately from physical termination.',
|
|
71
|
+
inputSchema: z.strictObject({ session_id: hex64, reason: z.string().min(1).max(256).default('operator_request') }), outputSchema: processTerminateOutputSchema
|
|
72
|
+
}, async ({ session_id, reason }) => {
|
|
73
|
+
try { return ok(processTerminateOutputSchema, { status: 'ok', session: await runtime.cancel({ sessionId: session_id, reason }) }); }
|
|
74
|
+
catch (error) { return fail(processTerminateOutputSchema, 'denied', error); }
|
|
75
|
+
});
|
|
76
|
+
|
|
69
77
|
server.registerTool('process_session_terminate', {
|
|
70
78
|
description: 'Terminate one DEADBYTE-owned process session through its owned host boundary. Numeric PID is never an input.',
|
|
71
79
|
inputSchema: z.strictObject({ session_id: hex64 }), outputSchema: processTerminateOutputSchema
|
|
@@ -4,6 +4,23 @@ import { structuredToolResult } from './output-contracts.mjs';
|
|
|
4
4
|
const hex64 = z.string().regex(/^[0-9a-f]{64}$/);
|
|
5
5
|
const digits = z.string().regex(/^[0-9]+$/);
|
|
6
6
|
const processState = z.enum(['running','exited','terminated']);
|
|
7
|
+
const termination = z.strictObject({
|
|
8
|
+
deadline_exceeded:z.boolean(), cancellation_requested:z.boolean(), physical_termination:z.boolean(), verified:z.boolean(),
|
|
9
|
+
terminal_semantics:z.string().min(1).max(64)
|
|
10
|
+
});
|
|
11
|
+
const deadline = z.strictObject({
|
|
12
|
+
schema:z.literal('deadbyte.deadline.v1'), id:hex64.nullable(), created_at_ms:z.number(), timeout_ms:z.number().int().positive().nullable(), deadline_ms:z.number().nullable()
|
|
13
|
+
});
|
|
14
|
+
const streamCoverage = z.strictObject({
|
|
15
|
+
logical_first_seq:z.number().int().min(1), logical_last_seq:z.number().int().min(0),
|
|
16
|
+
retained_first_seq:z.number().int().min(1), retained_last_seq:z.number().int().min(0),
|
|
17
|
+
complete:z.boolean(), truncated:z.boolean(),
|
|
18
|
+
missing_ranges:z.array(z.strictObject({from:z.number().int().min(1),to:z.number().int().min(0)})).max(100000),
|
|
19
|
+
logical_item_count:z.number().int().min(0), retained_item_count:z.number().int().min(0),
|
|
20
|
+
logical_bytes:z.number().int().min(0), retained_bytes:z.number().int().min(0),
|
|
21
|
+
dropped_items:z.number().int().min(0), dropped_bytes:z.number().int().min(0),
|
|
22
|
+
status:z.enum(['open','closed'])
|
|
23
|
+
});
|
|
7
24
|
|
|
8
25
|
export const processSessionRecordSchema = z.strictObject({
|
|
9
26
|
session_id: hex64,
|
|
@@ -17,6 +34,12 @@ export const processSessionRecordSchema = z.strictObject({
|
|
|
17
34
|
earliest_offset: z.number().int().nonnegative(),
|
|
18
35
|
stream_head: z.number().int().nonnegative(),
|
|
19
36
|
stream_sha256: hex64,
|
|
37
|
+
stream_id: hex64.nullable(),
|
|
38
|
+
stream_generation: z.number().int().min(1).nullable(),
|
|
39
|
+
stream_next_seq: z.number().int().min(1).nullable(),
|
|
40
|
+
stream_coverage: streamCoverage.nullable(),
|
|
41
|
+
termination: termination.nullable(),
|
|
42
|
+
deadline: deadline.nullable(),
|
|
20
43
|
journal_head_sha256: hex64
|
|
21
44
|
});
|
|
22
45
|
|
package/src/process-runtime.mjs
CHANGED
|
@@ -6,6 +6,8 @@ import { canonicalJson } from './canonical-json.mjs';
|
|
|
6
6
|
import { isInside, validatePortableRelativePath } from './path-policy.mjs';
|
|
7
7
|
import { loadPrivateSigningKey, loadPublicVerifyKey } from './trust.mjs';
|
|
8
8
|
import { assertProcessArmed, recheckProcessAuthority } from './process-policy.mjs';
|
|
9
|
+
import { createSequenceStream } from './workload-substrate.mjs';
|
|
10
|
+
import { classifyTermination, composeAbortSignal, createCancellationToken, createDeadline } from './deadline-cancellation.mjs';
|
|
9
11
|
|
|
10
12
|
const EVENT_SCHEMA = 'deadbyte.process-event.v1';
|
|
11
13
|
const EVENT_DOMAIN = Buffer.from('DEADBYTE-PROCESS-EVENT-V1\0', 'utf8');
|
|
@@ -52,11 +54,18 @@ async function resolveCwd(policy, grant, profile, rootId, cwd) {
|
|
|
52
54
|
}
|
|
53
55
|
|
|
54
56
|
function publicSession(session) {
|
|
57
|
+
const stream = session.recovered_terminal ? null : session.output_stream;
|
|
55
58
|
return {
|
|
56
59
|
session_id: session.session_id, profile_id: session.profile_id, root_id: session.root_id,
|
|
57
60
|
cwd: session.cwd_relative, child_pid: session.child_pid, creation_filetime: session.creation_filetime,
|
|
58
61
|
state: session.state, exit_code: session.exit_code, earliest_offset: session.earliest_offset,
|
|
59
62
|
stream_head: session.stream_head, stream_sha256: session.stream_sha256,
|
|
63
|
+
stream_id: stream?.streamId ?? null,
|
|
64
|
+
stream_generation: stream?.generation ?? null,
|
|
65
|
+
stream_next_seq: stream?.snapshot().next_seq ?? null,
|
|
66
|
+
stream_coverage: stream?.coverage() ?? null,
|
|
67
|
+
termination: session.termination ?? null,
|
|
68
|
+
deadline: session.deadline ?? null,
|
|
60
69
|
journal_head_sha256: session.journal_head_sha256
|
|
61
70
|
};
|
|
62
71
|
}
|
|
@@ -93,6 +102,7 @@ function retainedIndexOf(session, needle, begin = 0) {
|
|
|
93
102
|
function appendOutput(session, chunk) {
|
|
94
103
|
if (!chunk?.length) return;
|
|
95
104
|
const bytes = Buffer.from(chunk);
|
|
105
|
+
session.output_stream?.append(null, { producer: 'process-host', channel: 'merged-output', sizeBytes: bytes.length });
|
|
96
106
|
session.stream_sha256 = sha256Bytes(Buffer.concat([
|
|
97
107
|
Buffer.from(session.stream_sha256, 'hex'), bytes
|
|
98
108
|
]));
|
|
@@ -207,7 +217,8 @@ export function createProcessRuntime({ policy, signingKeyPath, verifyKeyPath })
|
|
|
207
217
|
earliest_offset:terminal.detail.earliest_offset, stream_head:terminal.detail.stream_head,
|
|
208
218
|
stream_sha256:terminal.detail.stream_sha256, output_chunks:[output], retained_output_bytes:output.length, control_buffer:Buffer.alloc(0),
|
|
209
219
|
journal_head_sha256:checked.head_sha256, journal_path:path.join(policy.state_dir,name), event_count:checked.event_count,
|
|
210
|
-
max_retained_output_bytes:profile.max_retained_output_bytes, stderr:'', terminate_requested:terminal.type==='terminated',
|
|
220
|
+
max_retained_output_bytes:profile.max_retained_output_bytes, output_stream:createSequenceStream({streamId:sessionId,maxItems:1000000,maxBytes:profile.max_retained_output_bytes}), stderr:'', terminate_requested:terminal.type==='terminated',
|
|
221
|
+
termination:terminal.detail?.termination ?? null, deadline:terminal.detail?.deadline ?? null, cancellation:null,
|
|
211
222
|
protocol_error:null, transport:profile.transport, win32_input_mode:false, host:null, close_promise:Promise.resolve(),
|
|
212
223
|
recovered_terminal:true
|
|
213
224
|
});
|
|
@@ -281,7 +292,8 @@ export function createProcessRuntime({ policy, signingKeyPath, verifyKeyPath })
|
|
|
281
292
|
}
|
|
282
293
|
}
|
|
283
294
|
|
|
284
|
-
async function start({ profileId, rootId, cwd = '.', args = [] }) {
|
|
295
|
+
async function start({ profileId, rootId, cwd = '.', args = [], timeoutMs = null }) {
|
|
296
|
+
if (timeoutMs !== null && (!Number.isInteger(timeoutMs) || timeoutMs < 100 || timeoutMs > 900000)) throw new Error('process timeout_ms must be in [100, 900000] or null');
|
|
285
297
|
await hydrateTerminalSessions();
|
|
286
298
|
const grant = await assertProcessArmed(policy, profileId, rootId);
|
|
287
299
|
const profile = policy.profiles[profileId];
|
|
@@ -303,16 +315,35 @@ export function createProcessRuntime({ policy, signingKeyPath, verifyKeyPath })
|
|
|
303
315
|
cwd_relative: cwdInfo.relative, child_pid: null, creation_filetime: null, state: 'starting', exit_code: null,
|
|
304
316
|
earliest_offset: 0, stream_head: 0, stream_sha256: ZERO_SHA256, output_chunks: [], retained_output_bytes: 0, control_buffer: Buffer.alloc(0),
|
|
305
317
|
journal_head_sha256: ZERO_SHA256, journal_path: path.join(policy.state_dir, `${sessionId}.jsonl`), event_count: 0,
|
|
306
|
-
max_retained_output_bytes: profile.max_retained_output_bytes, stderr: '', terminate_requested: false, protocol_error: null,
|
|
318
|
+
max_retained_output_bytes: profile.max_retained_output_bytes, output_stream: createSequenceStream({streamId: sessionId, maxItems: 1000000, maxBytes: profile.max_retained_output_bytes}), stderr: '', terminate_requested: false, protocol_error: null,
|
|
319
|
+
deadline: createDeadline({ timeoutMs, id: sessionId }),
|
|
320
|
+
cancellation: createCancellationToken({ id: sessionId }),
|
|
321
|
+
termination: classifyTermination(),
|
|
307
322
|
transport: profile.transport,
|
|
308
323
|
win32_input_mode: false
|
|
309
324
|
};
|
|
310
325
|
await recheckProcessAuthority(policy, grant, profileId, rootId);
|
|
326
|
+
const composed = composeAbortSignal({ deadline: session.deadline, cancellation: session.cancellation });
|
|
327
|
+
session.abort_signal = composed;
|
|
311
328
|
const host = spawn(policy.native_host.path,
|
|
312
329
|
[profile.executable, cwdInfo.resolved, session.job_id, `--deadbyte-transport=${profile.transport}`, ...profile.prefix_args, ...args],
|
|
313
330
|
{ cwd: cwdInfo.resolved, env: processEnvironment(profile), windowsHide: true, shell: false, stdio: ['pipe','pipe','pipe'] });
|
|
314
331
|
session.host = host;
|
|
315
332
|
sessions.set(sessionId, session);
|
|
333
|
+
const abortProcess = () => {
|
|
334
|
+
const reason = String(composed.signal.reason ?? 'cancelled');
|
|
335
|
+
const snapshot = session.cancellation.snapshot();
|
|
336
|
+
session.termination = classifyTermination({
|
|
337
|
+
deadlineExceeded: reason === 'deadline_exceeded',
|
|
338
|
+
cancellationRequested: snapshot.requested,
|
|
339
|
+
physicallyTerminated: false,
|
|
340
|
+
verified: false
|
|
341
|
+
});
|
|
342
|
+
session.terminate_requested = true;
|
|
343
|
+
if (!host.killed) host.kill();
|
|
344
|
+
};
|
|
345
|
+
composed.signal.addEventListener('abort', abortProcess, { once: true });
|
|
346
|
+
if (composed.signal.aborted) abortProcess();
|
|
316
347
|
const ready = new Promise((resolve, reject) => { session.ready_resolve = resolve; session.ready_reject = reject; });
|
|
317
348
|
host.stdout.on('data', chunk => parseHostOutput(session, chunk));
|
|
318
349
|
host.stderr.on('data', chunk => { session.stderr = (session.stderr + chunk.toString('utf8')).slice(-16384); });
|
|
@@ -322,6 +353,13 @@ export function createProcessRuntime({ policy, signingKeyPath, verifyKeyPath })
|
|
|
322
353
|
parseHostOutput(session, null, true);
|
|
323
354
|
if (session.state === 'starting') session.ready_reject?.(new Error(`process host exited before READY: ${session.stderr}`));
|
|
324
355
|
session.exit_code ??= Number.isInteger(code) ? code : null;
|
|
356
|
+
const snapshot = session.cancellation.snapshot();
|
|
357
|
+
session.termination = classifyTermination({
|
|
358
|
+
deadlineExceeded: session.deadline.deadline_ms !== null && Date.now() >= session.deadline.deadline_ms && session.terminate_requested,
|
|
359
|
+
cancellationRequested: snapshot.requested,
|
|
360
|
+
physicallyTerminated: session.terminate_requested,
|
|
361
|
+
verified: true
|
|
362
|
+
});
|
|
325
363
|
const terminalState = session.terminate_requested ? 'terminated' : 'exited';
|
|
326
364
|
try {
|
|
327
365
|
const outputFile = `${session.session_id}.stream.bin`;
|
|
@@ -336,10 +374,13 @@ export function createProcessRuntime({ policy, signingKeyPath, verifyKeyPath })
|
|
|
336
374
|
stream_sha256:session.stream_sha256,
|
|
337
375
|
retained_output_bytes:session.retained_output_bytes,
|
|
338
376
|
retained_output_sha256:retainedOutputSha256,
|
|
377
|
+
termination:session.termination,
|
|
378
|
+
deadline:session.deadline,
|
|
339
379
|
output_file:outputFile
|
|
340
380
|
});
|
|
341
381
|
session.state = terminalState;
|
|
342
382
|
} catch {}
|
|
383
|
+
composed.close();
|
|
343
384
|
resolve();
|
|
344
385
|
});
|
|
345
386
|
});
|
|
@@ -428,11 +469,19 @@ export function createProcessRuntime({ policy, signingKeyPath, verifyKeyPath })
|
|
|
428
469
|
await appendEvent(runtime, session, 'action', { action_id: actionId, logical_bytes: logicalBytes.length, logical_sha256: logicalSha256 });
|
|
429
470
|
return { action_id: actionId, logical_bytes: logicalBytes.length, logical_sha256: logicalSha256, session: publicSession(session) };
|
|
430
471
|
}
|
|
472
|
+
async function cancel({ sessionId, reason = 'operator_request' }) {
|
|
473
|
+
const session = await sessionFor(sessionId);
|
|
474
|
+
if (typeof reason !== 'string' || reason.length < 1 || reason.length > 256) throw new Error('process cancellation reason invalid');
|
|
475
|
+
if (['exited','terminated'].includes(session.state)) return publicSession(session);
|
|
476
|
+
session.cancellation.request(reason);
|
|
477
|
+
await session.close_promise;
|
|
478
|
+
return publicSession(session);
|
|
479
|
+
}
|
|
431
480
|
async function terminate({ sessionId }) {
|
|
432
481
|
const session = await sessionFor(sessionId);
|
|
433
482
|
if (['exited','terminated'].includes(session.state)) return publicSession(session);
|
|
434
483
|
session.terminate_requested = true;
|
|
435
|
-
session.host.kill();
|
|
484
|
+
if (!session.host.killed) session.host.kill();
|
|
436
485
|
await session.close_promise;
|
|
437
486
|
return publicSession(session);
|
|
438
487
|
}
|
|
@@ -463,5 +512,5 @@ export function createProcessRuntime({ policy, signingKeyPath, verifyKeyPath })
|
|
|
463
512
|
return { ok: true, event_count: sequence, head_sha256: previous };
|
|
464
513
|
}
|
|
465
514
|
|
|
466
|
-
return { policy, start, read, write, status, list, action, terminate, verifyEventJournal };
|
|
515
|
+
return { policy, start, read, write, status, list, action, cancel, terminate, verifyEventJournal };
|
|
467
516
|
}
|