deadbyte-mcp 0.15.14 → 0.15.15

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.
@@ -4,6 +4,17 @@ 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 operationSnapshot = z.strictObject({
8
+ schema:z.literal('deadbyte.operation-lifecycle.v1'), operation_id:hex64,
9
+ state:z.string().min(1).max(32),
10
+ terminal:z.strictObject({type:z.enum(['success','failure','cancelled','timed_out']).nullable(),sequence:z.number().int().positive().nullable(),event_hash:hex64.nullable()}),
11
+ history_head_sha256:hex64,next_sequence:z.number().int().positive(),overflow:z.object({}).passthrough()
12
+ });
13
+ const operationReceipt = z.strictObject({
14
+ schema:z.literal('deadbyte.operation-terminal-receipt.v1'),operation_id:hex64,session_id:hex64,
15
+ terminal:z.object({}).passthrough(),root_id:z.string().min(1).max(64),policy_sha256:hex64,
16
+ key_id:z.string().min(1).max(128),signature:z.string().min(1).max(512)
17
+ });
7
18
  const termination = z.strictObject({
8
19
  deadline_exceeded:z.boolean(), cancellation_requested:z.boolean(), physical_termination:z.boolean(), verified:z.boolean(),
9
20
  terminal_semantics:z.string().min(1).max(64)
@@ -11,6 +22,12 @@ const termination = z.strictObject({
11
22
  const deadline = z.strictObject({
12
23
  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
24
  });
25
+ const readiness = z.strictObject({
26
+ state:z.enum(['running','ready_for_input','finished']),
27
+ evidence:z.string().min(1).max(64),
28
+ prompt:z.strictObject({profile:z.string().min(1).max(32),prompt:z.string().min(1).max(64)}).nullable(),
29
+ exit_code:z.number().int().nullable()
30
+ });
14
31
  const streamCoverage = z.strictObject({
15
32
  logical_first_seq:z.number().int().min(1), logical_last_seq:z.number().int().min(0),
16
33
  retained_first_seq:z.number().int().min(1), retained_last_seq:z.number().int().min(0),
@@ -40,7 +57,13 @@ export const processSessionRecordSchema = z.strictObject({
40
57
  stream_coverage: streamCoverage.nullable(),
41
58
  termination: termination.nullable(),
42
59
  deadline: deadline.nullable(),
43
- journal_head_sha256: hex64
60
+ readiness: readiness.optional().nullable(),
61
+ journal_head_sha256: hex64,
62
+ operation_id: hex64.nullable().optional(),
63
+ operation: operationSnapshot.nullable().optional(),
64
+ operation_history_head_sha256: hex64.nullable().optional(),
65
+ operation_receipt: operationReceipt.nullable().optional(),
66
+ operation_history_error: z.string().max(4096).nullable().optional()
44
67
  });
45
68
 
46
69
  function resultWithSession(okStatus = 'ok') {
@@ -0,0 +1,120 @@
1
+ const PROMPTS = Object.freeze({
2
+ python: ['>>> ', '... '],
3
+ node: ['> ', '... '],
4
+ powershell: ['PS> ', 'PS C:\\', '>> '],
5
+ cmd: ['> ', 'More? '],
6
+ shell: ['$ ', '# ', '% '],
7
+ mysql: ['mysql> ', ' -> '],
8
+ postgres: ['=# ', '-# '],
9
+ redis: ['redis> ']
10
+ });
11
+
12
+ const COMPLETION = [
13
+ /(?:^|\n)Process finished(?:\r?\n|$)/i,
14
+ /(?:^|\n)Command completed(?:\r?\n|$)/i,
15
+ /(?:^|\n)\[Process completed\]/i,
16
+ /(?:^|\n)Program terminated(?:\r?\n|$)/i,
17
+ /(?:^|\n)Exit code:\s*-?\d+/i
18
+ ];
19
+
20
+ const ERROR_TERMINAL = [
21
+ /^\s*SyntaxError:/m,
22
+ /^\s*TypeError:/m,
23
+ /^\s*ReferenceError:/m,
24
+ /^\s*NameError:/m,
25
+ /^\s*Traceback/m,
26
+ /^\s*Unhandled exception/m
27
+ ];
28
+
29
+ const stripAnsi = value => String(value ?? '').replace(/\x1b\[[0-?]*[ -\/]*[@-~]/g, '');
30
+
31
+ export function normalizeProcessText(value) {
32
+ return stripAnsi(value).replace(/\r\n/g, '\n').replace(/\r/g, '\n');
33
+ }
34
+
35
+ export function detectPrompt(text, profiles = PROMPTS) {
36
+ const normalized = normalizeProcessText(text);
37
+ const lastLine = normalized.split('\n').at(-1) ?? '';
38
+ for (const [profile, prompts] of Object.entries(profiles)) {
39
+ for (const prompt of prompts) {
40
+ if (lastLine.endsWith(prompt) || lastLine === prompt.trimEnd()) {
41
+ return { profile, prompt };
42
+ }
43
+ }
44
+ }
45
+ return null;
46
+ }
47
+
48
+ export function analyzeReadiness({stdout='', stderr='', state='running', exitCode=null} = {}) {
49
+ const out = normalizeProcessText(stdout);
50
+ const err = normalizeProcessText(stderr);
51
+ if (state === 'exited' || state === 'terminated') {
52
+ return Object.freeze({ state: 'finished', evidence: 'process_terminal', prompt: null, exit_code: exitCode });
53
+ }
54
+ const prompt = detectPrompt(out);
55
+ if (prompt) {
56
+ return Object.freeze({ state: 'ready_for_input', evidence: 'prompt', prompt, exit_code: null });
57
+ }
58
+ if (COMPLETION.some(pattern => pattern.test(out)) || ERROR_TERMINAL.some(pattern => pattern.test(err || out))) {
59
+ return Object.freeze({ state: 'finished', evidence: 'terminal_output', prompt: null, exit_code: exitCode });
60
+ }
61
+ if (out.length === 0 && err.length === 0) {
62
+ return Object.freeze({ state: 'running', evidence: 'no_output', prompt: null, exit_code: null });
63
+ }
64
+ return Object.freeze({ state: 'running', evidence: 'output_without_terminal_marker', prompt: null, exit_code: null });
65
+ }
66
+
67
+ export function lineEndingProfile(text) {
68
+ const value = String(text ?? '');
69
+ let crlf = 0, lf = 0, cr = 0;
70
+ for (let i = 0; i < value.length; i += 1) {
71
+ if (value[i] === '\r') {
72
+ if (value[i + 1] === '\n') { crlf += 1; i += 1; } else cr += 1;
73
+ } else if (value[i] === '\n') lf += 1;
74
+ }
75
+ const total = crlf + lf + cr;
76
+ const styles = [crlf > 0, lf > 0, cr > 0].filter(Boolean).length;
77
+ const style = crlf >= lf && crlf >= cr && crlf > 0 ? '\r\n' : lf > 0 ? '\n' : cr > 0 ? '\r' : null;
78
+ return Object.freeze({ style, crlf, lf, cr, total, mixed: styles > 1 });
79
+ }
80
+
81
+ export function normalizeLineEndings(text, target) {
82
+ const lf = String(text ?? '').replace(/\r\n/g, '\n').replace(/\r/g, '\n');
83
+ if (target === '\r\n') return lf.replace(/\n/g, '\r\n');
84
+ if (target === '\r') return lf.replace(/\n/g, '\r');
85
+ return lf;
86
+ }
87
+
88
+ export function createReadinessTracker({maxEvidenceBytes=16384}={}) {
89
+ if (!Number.isInteger(maxEvidenceBytes) || maxEvidenceBytes < 256) {
90
+ throw new Error('maxEvidenceBytes must be >= 256');
91
+ }
92
+ let stdout = '';
93
+ let stderr = '';
94
+ let sequence = 0;
95
+ let last = analyzeReadiness();
96
+
97
+ const append = (channel, chunk) => {
98
+ const value = normalizeProcessText(chunk);
99
+ if (channel === 'stderr') stderr = (stderr + value).slice(-maxEvidenceBytes);
100
+ else stdout = (stdout + value).slice(-maxEvidenceBytes);
101
+ sequence += 1;
102
+ last = analyzeReadiness({stdout, stderr});
103
+ return Object.freeze({sequence, channel, readiness: last});
104
+ };
105
+
106
+ const updateTerminal = (state, exitCode) => {
107
+ last = analyzeReadiness({stdout, stderr, state, exitCode});
108
+ sequence += 1;
109
+ return Object.freeze({sequence, readiness: last});
110
+ };
111
+
112
+ const snapshot = () => Object.freeze({
113
+ sequence,
114
+ readiness: last,
115
+ stdout_tail: stdout.slice(-2048),
116
+ stderr_tail: stderr.slice(-2048)
117
+ });
118
+
119
+ return Object.freeze({append, updateTerminal, snapshot});
120
+ }
@@ -2,12 +2,14 @@ import { createHash, randomBytes, sign as cryptoSign, verify as cryptoVerify } f
2
2
  import { appendFile, lstat, mkdir, readFile, readdir, realpath, writeFile } from 'node:fs/promises';
3
3
  import path from 'node:path';
4
4
  import { spawn } from 'node:child_process';
5
+ import { createOperationLifecycle } from './operation-lifecycle.mjs';
5
6
  import { canonicalJson } from './canonical-json.mjs';
6
7
  import { isInside, validatePortableRelativePath } from './path-policy.mjs';
7
8
  import { loadPrivateSigningKey, loadPublicVerifyKey } from './trust.mjs';
8
9
  import { assertProcessArmed, recheckProcessAuthority } from './process-policy.mjs';
9
10
  import { createSequenceStream } from './workload-substrate.mjs';
10
11
  import { classifyTermination, composeAbortSignal, createCancellationToken, createDeadline } from './deadline-cancellation.mjs';
12
+ import { createReadinessTracker } from './process-readiness.mjs';
11
13
 
12
14
  const EVENT_SCHEMA = 'deadbyte.process-event.v1';
13
15
  const EVENT_DOMAIN = Buffer.from('DEADBYTE-PROCESS-EVENT-V1\0', 'utf8');
@@ -18,6 +20,19 @@ const START_TIMEOUT_MS = 8000;
18
20
  const TERMINAL_RECOVERY_SCHEMA = 'deadbyte.process-terminal.v1';
19
21
 
20
22
  const sha256Bytes = bytes => createHash('sha256').update(bytes).digest('hex');
23
+ function textToOperationRecords(text){
24
+ const records=text.split(/\r?\n/).filter(Boolean).map(line=>JSON.parse(line));
25
+ let previous=ZERO_SHA256;
26
+ for(let i=0;i<records.length;i++){
27
+ const record=records[i];
28
+ const {event_hash:eventHash,payload,...body}=record;
29
+ if(body.sequence!==i+1||body.previous_event_hash!==previous||
30
+ body.payload_digest!==sha256Bytes(Buffer.from(canonicalJson(payload ?? {}),'utf8'))||
31
+ eventHash!==sha256Bytes(Buffer.from(canonicalJson(body),'utf8'))) throw new Error('operation history chain mismatch');
32
+ previous=eventHash;
33
+ }
34
+ return records;
35
+ }
21
36
  const nowUtc = () => new Date().toISOString();
22
37
 
23
38
  const STANDARD_PATHEXT = '.COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC';
@@ -81,7 +96,13 @@ function publicSession(session) {
81
96
  stream_coverage: stream?.coverage() ?? null,
82
97
  termination: session.termination ?? null,
83
98
  deadline: session.deadline ?? null,
84
- journal_head_sha256: session.journal_head_sha256
99
+ readiness: session.readiness?.snapshot().readiness ?? null,
100
+ journal_head_sha256: session.journal_head_sha256,
101
+ operation_id: session.operation_lifecycle?.operationId ?? null,
102
+ operation: session.operation_lifecycle?.snapshot() ?? null,
103
+ operation_history_head_sha256: session.operation_history_head_sha256 ?? null,
104
+ operation_receipt: session.operation_receipt ?? null,
105
+ operation_history_error: session.operation_history_error ?? null
85
106
  };
86
107
  }
87
108
  function retainedSlice(session, begin = 0, length = session.retained_output_bytes - begin) {
@@ -173,6 +194,75 @@ export function createProcessRuntime({ policy, signingKeyPath, verifyKeyPath })
173
194
  verifying: () => (verifyPromise ??= loadPublicVerifyKey(verifyKeyPath))
174
195
  };
175
196
 
197
+ async function persistOperationEvent(session, result){
198
+ const recordPath=session.operation_history_path;
199
+ await appendFile(recordPath, canonicalJson(result.event)+'\n', 'utf8');
200
+ session.operation_history_head_sha256=result.event.event_hash;
201
+ session.operation_sequence=result.event.sequence;
202
+ return result;
203
+ }
204
+ async function recordOperationEvent(session,type,payload={}){
205
+ const result=session.operation_lifecycle.emit(type,payload);
206
+ return persistOperationEvent(session,result);
207
+ }
208
+ function queueOperationEvent(session,type,payload={}){
209
+ const next=session.operation_event_chain.then(async ()=>{
210
+ try { return await recordOperationEvent(session,type,payload); }
211
+ catch(error){ session.operation_history_error=String(error?.message ?? error); throw error; }
212
+ });
213
+ session.operation_event_chain=next.catch(()=>undefined);
214
+ return next;
215
+ }
216
+ async function createSignedOperationReceipt(session){
217
+ const body={
218
+ schema:'deadbyte.operation-terminal-receipt.v1',
219
+ operation_id:session.operation_lifecycle.operationId,
220
+ session_id:session.session_id,
221
+ terminal:session.operation_lifecycle.receipt(),
222
+ root_id:session.root_id,
223
+ policy_sha256:runtime.policy.policy_sha256
224
+ };
225
+ const signing=await runtime.signing();
226
+ const canonical=Buffer.from(canonicalJson(body),'utf8');
227
+ const signature=cryptoSign(null,Buffer.concat([Buffer.from('DEADBYTE-OPERATION-RECEIPT-V1\\0','utf8'),canonical]),signing.privateKey).toString('base64');
228
+ const receipt={...body,key_id:signing.keyId,signature};
229
+ await writeFile(session.operation_receipt_path,canonicalJson(receipt)+'\n',{flag:'wx'});
230
+ session.operation_receipt=receipt;
231
+ return receipt;
232
+ }
233
+
234
+ async function verifyOperationEvidence({sessionId}){
235
+ const session=await sessionFor(sessionId);
236
+ const text=await readFile(session.operation_history_path,'utf8');
237
+ const records=text.split(/\r?\n/).filter(Boolean).map(line=>JSON.parse(line));
238
+ let previous=ZERO_SHA256;
239
+ for(let i=0;i<records.length;i++){
240
+ const record=records[i];
241
+ const {event_hash:eventHash,payload,...body}=record;
242
+ if(body.operation_id!==session.operation_lifecycle.operationId || body.sequence!==i+1 || body.previous_event_hash!==previous){
243
+ throw new Error('operation history continuity mismatch');
244
+ }
245
+ const payloadDigest=sha256Bytes(Buffer.from(canonicalJson(payload ?? {}),'utf8'));
246
+ if(body.payload_digest!==payloadDigest || eventHash!==sha256Bytes(Buffer.from(canonicalJson(body),'utf8'))){
247
+ throw new Error('operation history hash mismatch');
248
+ }
249
+ previous=eventHash;
250
+ }
251
+ if(previous!==session.operation_history_head_sha256) throw new Error('operation history head mismatch');
252
+ const receipt=JSON.parse(await readFile(session.operation_receipt_path,'utf8'));
253
+ const {key_id:keyId,signature,...body}=receipt;
254
+ const verifying=await runtime.verifying();
255
+ const canonical=Buffer.from(canonicalJson(body),'utf8');
256
+ if(keyId!==verifying.keyId || !cryptoVerify(null,Buffer.concat([Buffer.from('DEADBYTE-OPERATION-RECEIPT-V1\\0','utf8'),canonical]),verifying.publicKey,Buffer.from(signature,'base64'))){
257
+ throw new Error('operation terminal receipt signature mismatch');
258
+ }
259
+ if(body.operation_id!==session.operation_lifecycle.operationId || body.session_id!==session.session_id ||
260
+ body.terminal?.terminal_event_hash!==previous || body.terminal?.terminal_sequence!==records.length){
261
+ throw new Error('operation terminal receipt/history mismatch');
262
+ }
263
+ return {ok:true,operation_id:body.operation_id,terminal_sequence:body.terminal.terminal_sequence,terminal_event_hash:previous,receipt};
264
+ }
265
+
176
266
  async function readVerifiedJournal(sessionId, journalPath) {
177
267
  const verifying = await runtime.verifying();
178
268
  const text = await readFile(journalPath, 'utf8');
@@ -200,6 +290,7 @@ export function createProcessRuntime({ policy, signingKeyPath, verifyKeyPath })
200
290
  async function hydrateTerminalSessions() {
201
291
  if (hydrationPromise) return hydrationPromise;
202
292
  hydrationPromise = (async () => {
293
+ const verifying = await runtime.verifying();
203
294
  await mkdir(policy.state_dir, { recursive: true });
204
295
  const names = (await readdir(policy.state_dir)).filter(name => /^[0-9a-f]{64}\.jsonl$/.test(name)).sort();
205
296
  for (const name of names) {
@@ -224,8 +315,39 @@ export function createProcessRuntime({ policy, signingKeyPath, verifyKeyPath })
224
315
  !SHA256_RE.test(terminal.detail.stream_sha256 ?? '') || !Number.isInteger(terminal.detail.stream_head) ||
225
316
  !Number.isInteger(terminal.detail.earliest_offset) || terminal.detail.earliest_offset < 0 ||
226
317
  terminal.detail.stream_head < terminal.detail.earliest_offset || terminal.detail.stream_head - terminal.detail.earliest_offset !== output.length) continue;
318
+ const operationHistoryPath=path.join(policy.state_dir,`${sessionId}.operation.jsonl`);
319
+ const operationReceiptPath=path.join(policy.state_dir,`${sessionId}.operation-receipt.json`);
320
+ let operationReceipt, operationRecords, operationId;
321
+ try {
322
+ operationReceipt=JSON.parse(await readFile(operationReceiptPath,'utf8'));
323
+ const {key_id:keyId,signature,...receiptBody}=operationReceipt;
324
+ const receiptCanonical=Buffer.from(canonicalJson(receiptBody),'utf8');
325
+ if(operationReceipt.schema!=='deadbyte.operation-terminal-receipt.v1'||operationReceipt.session_id!==sessionId||
326
+ operationReceipt.policy_sha256!==policy.policy_sha256||keyId!==verifying.keyId||
327
+ !cryptoVerify(null,Buffer.concat([Buffer.from('DEADBYTE-OPERATION-RECEIPT-V1\\0','utf8'),receiptCanonical]),verifying.publicKey,Buffer.from(signature,'base64'))) continue;
328
+ operationId=operationReceipt.operation_id;
329
+ operationRecords=textToOperationRecords(await readFile(operationHistoryPath,'utf8'));
330
+ if(!operationRecords.length||operationRecords.at(-1).event_hash!==operationReceipt.terminal.terminal_event_hash||
331
+ operationRecords.at(-1).sequence!==operationReceipt.terminal.terminal_sequence) continue;
332
+ } catch { continue; }
333
+
334
+ const terminalOperation=operationReceipt.terminal;
335
+ const operationSnapshot={
336
+ schema:'deadbyte.operation-lifecycle.v1',
337
+ operation_id:operationId,
338
+ state:terminalOperation.terminal_type,
339
+ terminal:{type:terminalOperation.terminal_type,sequence:terminalOperation.terminal_sequence,event_hash:terminalOperation.terminal_event_hash},
340
+ history_head_sha256:terminalOperation.head_event_hash,
341
+ next_sequence:terminalOperation.next_sequence,
342
+ overflow:terminalOperation.overflow
343
+ };
227
344
  sessions.set(sessionId, {
228
345
  session_id:sessionId, job_id:null, profile_id:terminal.profile_id, root_id:terminal.root_id,
346
+ operation_lifecycle:{operationId,snapshot:()=>operationSnapshot},
347
+ operation_history_path:operationHistoryPath, operation_receipt_path:operationReceiptPath,
348
+ operation_history_head_sha256:terminalOperation.head_event_hash,
349
+ operation_sequence:terminalOperation.terminal_sequence,
350
+ operation_receipt:operationReceipt, operation_event_chain:Promise.resolve(), operation_history_error:null,
229
351
  cwd_relative:typeof started.detail?.cwd === 'string' ? started.detail.cwd : '.',
230
352
  child_pid:started.detail.child_pid, creation_filetime:String(started.detail.creation_filetime),
231
353
  state:terminal.type, exit_code:Number.isInteger(terminal.detail.exit_code) ? terminal.detail.exit_code : null,
@@ -300,6 +422,7 @@ export function createProcessRuntime({ policy, signingKeyPath, verifyKeyPath })
300
422
  session.child_pid = record.child_pid;
301
423
  session.creation_filetime = String(record.creation_filetime);
302
424
  session.state = 'running';
425
+ queueOperationEvent(session,'running',{child_pid:session.child_pid,creation_filetime:session.creation_filetime}).catch(()=>{});
303
426
  session.ready_resolve?.();
304
427
  } else if (record.child_pid === session.child_pid && Number.isInteger(record.exit_code)) {
305
428
  session.exit_code = record.exit_code;
@@ -327,10 +450,16 @@ export function createProcessRuntime({ policy, signingKeyPath, verifyKeyPath })
327
450
  const sessionId = randomBytes(32).toString('hex');
328
451
  const session = {
329
452
  session_id: sessionId, job_id: randomBytes(32).toString('hex'), profile_id: profileId, root_id: rootId,
453
+ operation_lifecycle:createOperationLifecycle({operationId:randomBytes(32).toString('hex')}),
454
+ operation_history_path:path.join(policy.state_dir, `${sessionId}.operation.jsonl`),
455
+ operation_receipt_path:path.join(policy.state_dir, `${sessionId}.operation-receipt.json`),
456
+ operation_history_head_sha256:ZERO_SHA256, operation_sequence:0, operation_receipt:null, operation_event_chain:Promise.resolve(), operation_history_error:null,
330
457
  cwd_relative: cwdInfo.relative, child_pid: null, creation_filetime: null, state: 'starting', exit_code: null,
331
458
  earliest_offset: 0, stream_head: 0, stream_sha256: ZERO_SHA256, output_chunks: [], retained_output_bytes: 0, control_buffer: Buffer.alloc(0),
332
459
  journal_head_sha256: ZERO_SHA256, journal_path: path.join(policy.state_dir, `${sessionId}.jsonl`), event_count: 0,
333
- 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,
460
+ max_retained_output_bytes: profile.max_retained_output_bytes, output_stream: createSequenceStream({streamId: sessionId, maxItems: 1000000, maxBytes: profile.max_retained_output_bytes}), stderr: '',
461
+ readiness: createReadinessTracker({ maxEvidenceBytes: Math.min(65536, Math.max(4096, profile.max_retained_output_bytes)) }),
462
+ terminate_requested: false, protocol_error: null,
334
463
  deadline: createDeadline({ timeoutMs, id: sessionId }),
335
464
  cancellation: createCancellationToken({ id: sessionId }),
336
465
  termination: classifyTermination(),
@@ -345,6 +474,10 @@ export function createProcessRuntime({ policy, signingKeyPath, verifyKeyPath })
345
474
  { cwd: cwdInfo.resolved, env: processEnvironment(profile), windowsHide: true, shell: false, stdio: ['pipe','pipe','pipe'] });
346
475
  session.host = host;
347
476
  sessions.set(sessionId, session);
477
+ await queueOperationEvent(session,'start',{
478
+ profile_id:profileId,root_id:rootId,cwd:cwdInfo.relative,
479
+ args_sha256:sha256Bytes(Buffer.from(canonicalJson(args),'utf8'))
480
+ });
348
481
  const abortProcess = () => {
349
482
  const reason = String(composed.signal.reason ?? 'cancelled');
350
483
  const snapshot = session.cancellation.snapshot();
@@ -355,19 +488,39 @@ export function createProcessRuntime({ policy, signingKeyPath, verifyKeyPath })
355
488
  verified: false
356
489
  });
357
490
  session.terminate_requested = true;
491
+ if (!session.operation_cancel_transition) {
492
+ session.operation_cancel_transition=true;
493
+ if (snapshot.requested) {
494
+ queueOperationEvent(session,'cancel_requested',{reason:snapshot.reason ?? 'operator_request'}).catch(()=>{});
495
+ }
496
+ queueOperationEvent(session,'canceling',{
497
+ reason:reason,
498
+ deadline_exceeded:reason==='deadline_exceeded',
499
+ cancellation_requested:snapshot.requested
500
+ }).catch(()=>{});
501
+ }
358
502
  if (!host.killed) host.kill();
359
503
  };
360
504
  composed.signal.addEventListener('abort', abortProcess, { once: true });
361
505
  if (composed.signal.aborted) abortProcess();
362
506
  const ready = new Promise((resolve, reject) => { session.ready_resolve = resolve; session.ready_reject = reject; });
363
- host.stdout.on('data', chunk => parseHostOutput(session, chunk));
364
- host.stderr.on('data', chunk => { session.stderr = (session.stderr + chunk.toString('utf8')).slice(-16384); });
507
+ host.stdout.on('data', chunk => {
508
+ session.readiness.append('stdout', chunk);
509
+ parseHostOutput(session, chunk);
510
+ queueOperationEvent(session,'output',{channel:'stdout',bytes:Buffer.byteLength(chunk),payload_digest:sha256Bytes(Buffer.from(chunk))}).catch(()=>{});
511
+ });
512
+ host.stderr.on('data', chunk => {
513
+ session.stderr = (session.stderr + chunk.toString('utf8')).slice(-16384);
514
+ session.readiness.append('stderr', chunk);
515
+ queueOperationEvent(session,'error',{channel:'stderr',bytes:Buffer.byteLength(chunk),payload_digest:sha256Bytes(Buffer.from(chunk))}).catch(()=>{});
516
+ });
365
517
  host.once('error', error => session.ready_reject?.(error));
366
518
  session.close_promise = new Promise(resolve => {
367
519
  host.once('close', async code => {
368
520
  parseHostOutput(session, null, true);
369
521
  if (session.state === 'starting') session.ready_reject?.(new Error(`process host exited before READY: ${session.stderr}`));
370
522
  session.exit_code ??= Number.isInteger(code) ? code : null;
523
+ session.readiness.updateTerminal(session.terminate_requested ? 'terminated' : 'exited', session.exit_code);
371
524
  const snapshot = session.cancellation.snapshot();
372
525
  session.termination = classifyTermination({
373
526
  deadlineExceeded: session.deadline.deadline_ms !== null && Date.now() >= session.deadline.deadline_ms && session.terminate_requested,
@@ -376,6 +529,8 @@ export function createProcessRuntime({ policy, signingKeyPath, verifyKeyPath })
376
529
  verified: true
377
530
  });
378
531
  const terminalState = session.terminate_requested ? 'terminated' : 'exited';
532
+ const deadlineExceeded=session.deadline.deadline_ms !== null && Date.now() >= session.deadline.deadline_ms && session.terminate_requested;
533
+ const operationTerminal=deadlineExceeded ? 'timed_out' : (session.terminate_requested || snapshot.requested ? 'cancelled' : (session.exit_code===0 ? 'success' : 'failure'));
379
534
  try {
380
535
  const outputFile = `${session.session_id}.stream.bin`;
381
536
  const retainedOutput = retainedAll(session);
@@ -393,8 +548,20 @@ export function createProcessRuntime({ policy, signingKeyPath, verifyKeyPath })
393
548
  deadline:session.deadline,
394
549
  output_file:outputFile
395
550
  });
551
+ await queueOperationEvent(session,operationTerminal,{
552
+ exit_code:session.exit_code,
553
+ process_state:terminalState,
554
+ termination:session.termination,
555
+ stream_head:session.stream_head,
556
+ stream_sha256:session.stream_sha256
557
+ });
558
+ await session.operation_event_chain;
559
+ await createSignedOperationReceipt(session);
396
560
  session.state = terminalState;
397
- } catch {}
561
+ } catch(error) {
562
+ session.operation_history_error=String(error?.message ?? error);
563
+ session.state = terminalState;
564
+ }
398
565
  composed.close();
399
566
  resolve();
400
567
  });
@@ -495,6 +662,11 @@ export function createProcessRuntime({ policy, signingKeyPath, verifyKeyPath })
495
662
  async function terminate({ sessionId }) {
496
663
  const session = await sessionFor(sessionId);
497
664
  if (['exited','terminated'].includes(session.state)) return publicSession(session);
665
+ if (!session.operation_cancel_transition) {
666
+ session.operation_cancel_transition=true;
667
+ await queueOperationEvent(session,'cancel_requested',{reason:'physical_termination'});
668
+ await queueOperationEvent(session,'canceling',{reason:'physical_termination',cancellation_requested:false});
669
+ }
498
670
  session.terminate_requested = true;
499
671
  if (!session.host.killed) session.host.kill();
500
672
  await session.close_promise;
@@ -527,5 +699,5 @@ export function createProcessRuntime({ policy, signingKeyPath, verifyKeyPath })
527
699
  return { ok: true, event_count: sequence, head_sha256: previous };
528
700
  }
529
701
 
530
- return { policy, start, read, write, status, list, action, cancel, terminate, verifyEventJournal };
702
+ return { policy, start, read, write, status, list, action, cancel, terminate, verifyEventJournal, verifyOperationEvidence };
531
703
  }