diff --git a/experiments/analysis-efficiency/command-observation-costs.py b/experiments/analysis-efficiency/command-observation-costs.py new file mode 100644 index 0000000000..f2fcaaf818 --- /dev/null +++ b/experiments/analysis-efficiency/command-observation-costs.py @@ -0,0 +1,32 @@ +"""Count all actual client JSON inputs and preserve bounded family resource receipts.""" +from pathlib import Path +import argparse, json, hashlib +import tiktoken +parser=argparse.ArgumentParser(description=__doc__) +parser.add_argument('--root',type=Path,required=True) +parser.add_argument('--output',type=Path,required=True) +args=parser.parse_args();lab=args.root;enc=tiktoken.get_encoding('cl100k_base') +compact=lambda value:json.dumps(value,ensure_ascii=False,separators=(',',':'),allow_nan=False) +count=lambda value:len(enc.encode(compact(value),disallowed_special=())) +def resources(root): + scope=json.loads(Path(str(root)+'.log').read_text().splitlines()[-1]);o=scope['payload_resource_observation'] + assert scope['abort'] is None and scope['payload_resource_observation_verified'] and o['payload_exit_code']==0 and o['payload_cleanup_verified'] + for event in o['resource_events'].values():assert not any(event.values()) + return {'wall_seconds':scope['elapsed_seconds'],'cpu_seconds':o['cpu_stat']['usage_usec']/1e6,'family_peak_mib':o['memory_peak_bytes']/1048576,'payload_cleanup_verified':True,'final_whole_scope_counters_verified':scope['resource_counters_verified']} +wire=[];sdk=[] +for root in sorted(lab.glob('final-wire-*')): + if not root.is_dir():continue + report=json.loads((root/'report.json').read_text());requests=json.loads((root/'model-requests.json').read_text());assert report['status'] in ['passed','expected-incomplete-task'] and not report['cleanup_failures'] and report['appserver_exit_code']==0 and report['owned_scope_drain']['remaining_processes']==[] + rows=[] + for index,captured in enumerate(requests): + body=captured['body'];namespace=[n for n in body.get('tools',[]) if n.get('type')=='namespace' and n.get('name')=='mcp__rea_audit'];compaction=not namespace + if compaction:assert 'CONTEXT CHECKPOINT COMPACTION' in compact(body['input'][-1]) + else:assert len(namespace)==1 and len(namespace[0]['tools'])==210 + rows.append({'index':index+1,'kind':'compaction' if compaction else 'normal','whole_json_tokens_proxy':count(body),'rea_namespace_tokens_proxy':0 if not namespace else count(namespace[0]),'rea_function_count':0 if not namespace else len(namespace[0]['tools'])}) + wire.append({'case':root.name,'task_completed':report['task_completed'],'mode':report['mode'],'budget':report['observations']['tool_output_budget'],'requests':len(rows),'compactions':sum(r['kind']=='compaction' for r in rows),'whole_json_tokens_proxy':sum(r['whole_json_tokens_proxy'] for r in rows),'request_details':rows,**resources(root)}) +for root in sorted(lab.glob('final-sdk-*')): + if not root.is_dir():continue + report=json.loads((root/'report.json').read_text());assert report['status']=='passed' and not report['cleanup_failures'] and not report['receipt_failures'] + sdk.append({'case':root.name,'provider':report['provider'],'symbol':report['symbol'],'detail':report['detail'],'warm':report['warm'],'tool_calls':len(report['calls']),'exported_bundle_bytes':(root/'evidence.json').stat().st_size,'disassembly_calls':[r for r in report['calls'] if r['phase'].startswith('disassembly:')],**resources(root)}) +receipt={'measurement_scope':'Actual public SDK and Codex0.163 app-server with real GDB/BFD. Local deterministic SSE fixture and observed-ID checkpoint; no AI inference, model usage/quality/acceptance or billing claim. Whole compact JSON serialization tokens are proxies and include all schemas/replays/compaction. Incomplete default baseline is excluded from completed-workflow gains. Fresh processes have unflushed OS caches; warm SDK cases repeat the selected disassembly in the same server. Native target never executed.','tokenizer':{'package':tiktoken.__version__,'encoding':enc.name},'runtime_pins':{variant:{k:v for k,v in json.loads((lab/(variant+'-manifest.json')).read_text()).items() if k!='files'} for variant in ['baseline','candidate']},'native':json.loads((lab/'native-prerequisite-manifest.json').read_text()),'wire':wire,'sdk':sdk} +args.output.write_text(json.dumps(receipt,indent=2)+'\n');print(json.dumps({'wire':[{k:v for k,v in row.items() if k!='request_details'} for row in wire],'sdk':[{k:v for k,v in row.items() if k!='disassembly_calls'} for row in sdk]},indent=2)) diff --git a/experiments/analysis-efficiency/command-observation-oracle.py b/experiments/analysis-efficiency/command-observation-oracle.py new file mode 100644 index 0000000000..a63613347e --- /dev/null +++ b/experiments/analysis-efficiency/command-observation-oracle.py @@ -0,0 +1,22 @@ +from pathlib import Path +import struct,hashlib,json,argparse +parser=argparse.ArgumentParser(description='Independent ELF64 little-endian native-command byte oracle');parser.add_argument('--input',type=Path,required=True);parser.add_argument('--output',type=Path,required=True);args=parser.parse_args();path=args.input;b=path.read_bytes();assert b[:6]==b'\x7fELF\x02\x01' +shoff=struct.unpack_from('=name + if names[name:end] not in [b'execute_command',b'execute_command_internal'] or info&15!=2:continue + target=sections[section];file_offset=target[4]+value-target[3] + assert target[4]<=file_offset and file_offset+size<=target[4]+target[5] + rows.append({'symbol_table_section':si,'symbol_ordinal':ordinal,'name':names[name:end].decode('ascii'),'address':value,'bytes':size,'file_offset':file_offset,'first_16_bytes_hex':b[file_offset:file_offset+16].hex()}) +assert rows +result={'path':str(path),'artifact_sha256':hashlib.sha256(b).hexdigest(),'artifact_bytes':len(b),'elf_class':64,'byte_order':'little','elf_machine':struct.unpack_from(' row.name === values.symbol)); +const inputBytes = await readFile(values.input); +assert.equal( + createHash("sha256").update(inputBytes).digest("hex"), + oracle.artifact_sha256, +); +const require = createRequire(join(values.runtime, "package.json")); +const { Client } = await import( + pathToFileURL(require.resolve("@modelcontextprotocol/client")) +); +const { StdioClientTransport } = await import( + pathToFileURL(require.resolve("@modelcontextprotocol/client/stdio")) +); +await mkdir(values.output, { mode: 0o700 }); +const account = await mkdtemp(join(tmpdir(), "rea-command-views-")); +const client = new Client({ name: "command-observation-task", version: "1" }); +const transport = new StdioClientTransport({ + command: process.execPath, + args: [join(values.runtime, "scripts/rea.mjs"), "mcp"], + env: { + HOME: account, + PATH: process.env.PATH, + NODE_OPTIONS: "--max-old-space-size=2048", + UV_THREADPOOL_SIZE: "1", + REA_LOG_LEVEL: "silent", + }, + stderr: "pipe", +}); +const serverLog = createWriteStream(join(values.output, "server.log")); +const serverLogFinished = finished(serverLog); +serverLogFinished.catch(() => {}); +transport.stderr?.pipe(serverLog); +const transcript = []; +const report = { + question: + "Read architecture and the complete byte-annotated disassembly of one selected native function; export complete Evidence. No inferior execution.", + provider: values.provider, + symbol: values.symbol, + detail: values.detail, + warm: values.warm, + calls: [], + observations: [], +}; +const call = createRetainedQueryCaller(client, (receipt, event) => { + report.calls.push(receipt); + transcript.push(event); +}); +let sessionId; +try { + await client.connect(transport); + const catalog = await client.listTools(); + await writeFile(join(values.output, "catalog.json"), JSON.stringify(catalog)); + const definitions = new Map(catalog.tools.map((tool) => [tool.name, tool])); + const originalCall = client.callTool.bind(client); + client.callTool = (input, options) => + originalCall(input, { + ...options, + timeout: 60000, + toolDefinition: definitions.get(input.name), + }); + const observe = async (name, input, phase = name) => { + const response = await call(name, input, phase); + assert.ok(!response.isError, JSON.stringify(response)); + report.observations.push({ + name, + phase, + evidence: response.structuredContent, + }); + return response.structuredContent; + }; + const completeParents = []; + const disassemble = async (phase) => { + const selection = values.detail === "summary" ? { detail: "summary" } : {}; + const result = + values.provider === "gdb" + ? await observe( + "gdb_console", + { + session_id: sessionId, + command: `disassemble /r ${values.symbol}`, + ...selection, + }, + phase, + ) + : await observe( + "inspect_with_objdump", + { + path: values.input, + operation: "disassemble", + symbol: values.symbol, + ...selection, + }, + phase, + ); + completeParents.push(result); + }; + if (values.provider === "gdb") { + const started = await observe("start_gdb_session", {}); + sessionId = started.session_id; + for (const command of [ + "set auto-load off", + "set debuginfod enabled off", + `file ${values.input}`, + "show architecture", + ]) + await observe("gdb_console", { session_id: sessionId, command }); + } else + await observe("inspect_with_objdump", { + path: values.input, + operation: "file_headers", + }); + await disassemble("disassembly:fresh"); + if (values.warm) await disassemble("disassembly:repeat"); + if (sessionId) { + await observe("close_gdb_session", { session_id: sessionId }); + sessionId = undefined; + } + await observe("export_evidence_bundle", { + path: join(values.output, "evidence.json"), + }); + const bundle = JSON.parse( + await readFile(join(values.output, "evidence.json"), "utf8"), + ); + for (const delivered of completeParents) { + const parent = + values.detail === "complete" + ? delivered + : bundle.records.find( + (record) => + record.evidence_id === + delivered.normalized_result.parent_evidence_id, + ); + assert.ok(parent); + assert.equal(parent.normalized_result.completion_status, "complete"); + assert.equal(parent.normalized_result.output_truncated, false); + if (values.provider === "gdb") assert.equal(parent.subject, null); + else assert.equal(parent.subject.digest.sha256, oracle.artifact_sha256); + if (values.detail === "summary") { + const selected = delivered.normalized_result.summary; + const { available_counts: counts, ...facts } = selected; + const { console, target, log, ...records } = parent.normalized_result; + assert.deepEqual( + values.provider === "gdb" ? facts : selected, + values.provider === "gdb" ? records : parent.normalized_result, + ); + if (values.provider === "gdb") + assert.equal(counts.console_utf16_units, console.length); + assert.equal(delivered.raw_result, null); + } + } + report.status = "passed"; +} catch (error) { + report.status = "failed"; + report.failure = String(error?.stack ?? error); + process.exitCode = 1; +} finally { + if (sessionId) + try { + const closed = await call( + "close_gdb_session", + { session_id: sessionId }, + "cleanup:gdb", + ); + assert.ok(!closed.isError); + } catch (error) { + report.gdb_cleanup_failure = String(error); + process.exitCode = 1; + } + if ( + !(await finalizeRetainedQueryReceipt({ + report, + client, + transport, + account, + serverLog, + serverLogFinished, + output: values.output, + transcript, + })) + ) + process.exitCode = 1; +} diff --git a/experiments/analysis-efficiency/command-observation-verify.py b/experiments/analysis-efficiency/command-observation-verify.py new file mode 100644 index 0000000000..2f8671f296 --- /dev/null +++ b/experiments/analysis-efficiency/command-observation-verify.py @@ -0,0 +1,28 @@ +from pathlib import Path +import json,re,hashlib,argparse +parser=argparse.ArgumentParser(description='Match every native instruction byte against independently mapped ELF file ranges');parser.add_argument('--root',type=Path,required=True);args=parser.parse_args();lab=args.root;oracle=json.loads((lab/'elf-oracle.json').read_text());image=Path(oracle['path']).read_bytes();assert hashlib.sha256(image).hexdigest()==oracle['artifact_sha256'];rows=[] +for directory in sorted(lab.iterdir()): + bundle=directory/'evidence.json' + if not bundle.is_file():continue + for parent in json.loads(bundle.read_text())['records']: + if parent['operation'] not in ['gdb_console','inspect_with_objdump']:continue + params=parent['parameters'];gdb=parent['operation']=='gdb_console' + symbol=params.get('command','').removeprefix('disassemble /r ') if gdb else params.get('symbol') + if symbol not in {s['name'] for s in oracle['symbols']}:continue + spec=next(s for s in oracle['symbols'] if s['name']==symbol) + report=parent['normalized_result'];assert report['completion_status']=='complete' and not report['output_truncated'] + if gdb: + assert parent['subject'] is None + console='\n'.join(line for line in report['records'] if line.startswith('~'));assert console==report['console'] + text=''.join(json.loads(line[1:]) for line in report['records'] if line.startswith('~')) + pattern=r'^\s*(?:=>\s*)?0x([0-9a-f]+)[^:]*:\s*((?:[0-9a-f]{2}(?:\s+|$))+)' + else: + assert parent['subject']['digest']['sha256']==oracle['artifact_sha256'];text=report['stdout'];pattern=r'^\s*([0-9a-f]+):\s*((?:[0-9a-f]{2}(?:\s+|$))+)' + address=spec['address'];collected=bytearray();count=0 + for line in text.splitlines(): + m=re.match(pattern,line) + if not m:continue + actual=int(m[1],16);raw=bytes.fromhex(m[2]);assert actual==address+len(collected),(directory,actual,address+len(collected));collected.extend(raw);count+=1 + expected=image[spec['file_offset']:spec['file_offset']+spec['bytes']];assert collected==expected,(directory,len(collected),len(expected));rows.append({'case':directory.name,'mode':'gdb' if gdb else 'objdump','symbol':symbol,'byte_bearing_lines':count,'bytes':len(collected),'exact_file_bytes':True,'parent_evidence_id':parent['evidence_id']}) +assert rows +(lab/'native-final-verification.json').write_text(json.dumps({'status':'passed','artifact_sha256':oracle['artifact_sha256'],'records':rows},indent=2)+'\n');print(json.dumps(rows)) diff --git a/experiments/analysis-efficiency/command-observation-views.json b/experiments/analysis-efficiency/command-observation-views.json new file mode 100644 index 0000000000..cf723ab4ab --- /dev/null +++ b/experiments/analysis-efficiency/command-observation-views.json @@ -0,0 +1,1328 @@ +{ + "measurement_scope": "Actual public SDK and Codex0.163 app-server with real GDB/BFD. Local deterministic SSE fixture and observed-ID checkpoint; no AI inference, model usage/quality/acceptance or billing claim. Whole compact JSON serialization tokens are proxies and include all schemas/replays/compaction. Incomplete default baseline is excluded from completed-workflow gains. Fresh processes have unflushed OS caches; warm SDK cases repeat the selected disassembly in the same server. Native target never executed.", + "tokenizer": { + "package": "0.12.0", + "encoding": "cl100k_base" + }, + "runtime_pins": { + "baseline": { + "source_base": "c2858d2aa0916f0dd2d073d848b8c2195d9bf6b3", + "patch_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "archive_sha256": "7f844f4eb5684e6db42d08b01303bf7c7a5232ba98787da97549a2689210a568" + }, + "candidate": { + "source_base": "c2858d2aa0916f0dd2d073d848b8c2195d9bf6b3", + "patch_sha256": "d172193be7b6cc1e8cc98e085d56e33658f4ddf92f892e33262ac16b7114c873", + "archive_sha256": "c391fb272173364917d421458d99a77a26e6c7ee27641c8e95a4a66123be5d68" + } + }, + "native": { + "gdb": { + "selected_path": "/usr/bin/gdb", + "resolved_path": "/usr/bin/gdb", + "bytes": 10395104, + "sha256": "762f9d48202dd341e170d8302543f35622417b4e39bfce9a270d06943702e754", + "version": "GNU gdb (Debian 13.1-3) 13.1" + }, + "objdump": { + "selected_path": "/usr/bin/objdump", + "resolved_path": "/usr/bin/x86_64-linux-gnu-objdump", + "bytes": 371264, + "sha256": "19717049e8ecd98cfbb17fd9eb25e9fd896ecec2fc4af6b931f3dd0bc4e903de", + "version": "GNU objdump (GNU Binutils for Debian) 2.40" + }, + "target": { + "selected_path": "/usr/bin/bash", + "resolved_path": "/usr/bin/bash", + "bytes": 1265648, + "sha256": "55b89ab22bee4792a210f493a53fb066accd5d30b69837c28d98be5ff863efcf" + }, + "cutter": { + "REA_CUTTER_BRIDGE_DIR_configured": false, + "host_default_bridge_directory": "/home/yann/.cache/rea/cutter-bridge", + "host_default_exists": false, + "scope": "No configured selector and default directory absent; undisclosed installations or sessions remain unverified." + }, + "adb": { + "REA_ADB_PATH_configured": false, + "default_public_tool_outcome": "capability_unavailable: selected adb not found" + } + }, + "wire": [ + { + "case": "final-wire-baseline-default", + "task_completed": false, + "mode": "complete", + "budget": "client default", + "requests": 9, + "compactions": 0, + "whole_json_tokens_proxy": 584416, + "request_details": [ + { + "index": 1, + "kind": "normal", + "whole_json_tokens_proxy": 61472, + "rea_namespace_tokens_proxy": 52281, + "rea_function_count": 210 + }, + { + "index": 2, + "kind": "normal", + "whole_json_tokens_proxy": 61607, + "rea_namespace_tokens_proxy": 52281, + "rea_function_count": 210 + }, + { + "index": 3, + "kind": "normal", + "whole_json_tokens_proxy": 62054, + "rea_namespace_tokens_proxy": 52281, + "rea_function_count": 210 + }, + { + "index": 4, + "kind": "normal", + "whole_json_tokens_proxy": 62561, + "rea_namespace_tokens_proxy": 52281, + "rea_function_count": 210 + }, + { + "index": 5, + "kind": "normal", + "whole_json_tokens_proxy": 63156, + "rea_namespace_tokens_proxy": 52281, + "rea_function_count": 210 + }, + { + "index": 6, + "kind": "normal", + "whole_json_tokens_proxy": 63735, + "rea_namespace_tokens_proxy": 52281, + "rea_function_count": 210 + }, + { + "index": 7, + "kind": "normal", + "whole_json_tokens_proxy": 69795, + "rea_namespace_tokens_proxy": 52281, + "rea_function_count": 210 + }, + { + "index": 8, + "kind": "normal", + "whole_json_tokens_proxy": 69932, + "rea_namespace_tokens_proxy": 52281, + "rea_function_count": 210 + }, + { + "index": 9, + "kind": "normal", + "whole_json_tokens_proxy": 70104, + "rea_namespace_tokens_proxy": 52281, + "rea_function_count": 210 + } + ], + "wall_seconds": 8.548, + "cpu_seconds": 7.752407, + "family_peak_mib": 795.40625, + "payload_cleanup_verified": true, + "final_whole_scope_counters_verified": false + }, + { + "case": "final-wire-baseline-large", + "task_completed": true, + "mode": "complete", + "budget": 350000, + "requests": 10, + "compactions": 1, + "whole_json_tokens_proxy": 1269405, + "request_details": [ + { + "index": 1, + "kind": "normal", + "whole_json_tokens_proxy": 61488, + "rea_namespace_tokens_proxy": 52281, + "rea_function_count": 210 + }, + { + "index": 2, + "kind": "normal", + "whole_json_tokens_proxy": 61627, + "rea_namespace_tokens_proxy": 52281, + "rea_function_count": 210 + }, + { + "index": 3, + "kind": "normal", + "whole_json_tokens_proxy": 62082, + "rea_namespace_tokens_proxy": 52281, + "rea_function_count": 210 + }, + { + "index": 4, + "kind": "normal", + "whole_json_tokens_proxy": 62592, + "rea_namespace_tokens_proxy": 52281, + "rea_function_count": 210 + }, + { + "index": 5, + "kind": "normal", + "whole_json_tokens_proxy": 63186, + "rea_namespace_tokens_proxy": 52281, + "rea_function_count": 210 + }, + { + "index": 6, + "kind": "normal", + "whole_json_tokens_proxy": 63769, + "rea_namespace_tokens_proxy": 52281, + "rea_function_count": 210 + }, + { + "index": 7, + "kind": "compaction", + "whole_json_tokens_proxy": 708561, + "rea_namespace_tokens_proxy": 0, + "rea_function_count": 0 + }, + { + "index": 8, + "kind": "normal", + "whole_json_tokens_proxy": 61882, + "rea_namespace_tokens_proxy": 52281, + "rea_function_count": 210 + }, + { + "index": 9, + "kind": "normal", + "whole_json_tokens_proxy": 62023, + "rea_namespace_tokens_proxy": 52281, + "rea_function_count": 210 + }, + { + "index": 10, + "kind": "normal", + "whole_json_tokens_proxy": 62195, + "rea_namespace_tokens_proxy": 52281, + "rea_function_count": 210 + } + ], + "wall_seconds": 9.551, + "cpu_seconds": 8.831977, + "family_peak_mib": 805.2265625, + "payload_cleanup_verified": true, + "final_whole_scope_counters_verified": false + }, + { + "case": "final-wire-baseline-small", + "task_completed": true, + "mode": "complete", + "budget": 30000, + "requests": 9, + "compactions": 0, + "whole_json_tokens_proxy": 605133, + "request_details": [ + { + "index": 1, + "kind": "normal", + "whole_json_tokens_proxy": 61486, + "rea_namespace_tokens_proxy": 52281, + "rea_function_count": 210 + }, + { + "index": 2, + "kind": "normal", + "whole_json_tokens_proxy": 61627, + "rea_namespace_tokens_proxy": 52281, + "rea_function_count": 210 + }, + { + "index": 3, + "kind": "normal", + "whole_json_tokens_proxy": 62075, + "rea_namespace_tokens_proxy": 52281, + "rea_function_count": 210 + }, + { + "index": 4, + "kind": "normal", + "whole_json_tokens_proxy": 62582, + "rea_namespace_tokens_proxy": 52281, + "rea_function_count": 210 + }, + { + "index": 5, + "kind": "normal", + "whole_json_tokens_proxy": 63189, + "rea_namespace_tokens_proxy": 52281, + "rea_function_count": 210 + }, + { + "index": 6, + "kind": "normal", + "whole_json_tokens_proxy": 63773, + "rea_namespace_tokens_proxy": 52281, + "rea_function_count": 210 + }, + { + "index": 7, + "kind": "normal", + "whole_json_tokens_proxy": 76647, + "rea_namespace_tokens_proxy": 52281, + "rea_function_count": 210 + }, + { + "index": 8, + "kind": "normal", + "whole_json_tokens_proxy": 76790, + "rea_namespace_tokens_proxy": 52281, + "rea_function_count": 210 + }, + { + "index": 9, + "kind": "normal", + "whole_json_tokens_proxy": 76964, + "rea_namespace_tokens_proxy": 52281, + "rea_function_count": 210 + } + ], + "wall_seconds": 7.906, + "cpu_seconds": 7.195052, + "family_peak_mib": 788.88671875, + "payload_cleanup_verified": true, + "final_whole_scope_counters_verified": false + }, + { + "case": "final-wire-candidate-default", + "task_completed": true, + "mode": "summary", + "budget": "client default", + "requests": 9, + "compactions": 0, + "whole_json_tokens_proxy": 580243, + "request_details": [ + { + "index": 1, + "kind": "normal", + "whole_json_tokens_proxy": 61693, + "rea_namespace_tokens_proxy": 52500, + "rea_function_count": 210 + }, + { + "index": 2, + "kind": "normal", + "whole_json_tokens_proxy": 61834, + "rea_namespace_tokens_proxy": 52500, + "rea_function_count": 210 + }, + { + "index": 3, + "kind": "normal", + "whole_json_tokens_proxy": 62293, + "rea_namespace_tokens_proxy": 52500, + "rea_function_count": 210 + }, + { + "index": 4, + "kind": "normal", + "whole_json_tokens_proxy": 62807, + "rea_namespace_tokens_proxy": 52500, + "rea_function_count": 210 + }, + { + "index": 5, + "kind": "normal", + "whole_json_tokens_proxy": 63409, + "rea_namespace_tokens_proxy": 52500, + "rea_function_count": 210 + }, + { + "index": 6, + "kind": "normal", + "whole_json_tokens_proxy": 64001, + "rea_namespace_tokens_proxy": 52500, + "rea_function_count": 210 + }, + { + "index": 7, + "kind": "normal", + "whole_json_tokens_proxy": 67918, + "rea_namespace_tokens_proxy": 52500, + "rea_function_count": 210 + }, + { + "index": 8, + "kind": "normal", + "whole_json_tokens_proxy": 68057, + "rea_namespace_tokens_proxy": 52500, + "rea_function_count": 210 + }, + { + "index": 9, + "kind": "normal", + "whole_json_tokens_proxy": 68231, + "rea_namespace_tokens_proxy": 52500, + "rea_function_count": 210 + } + ], + "wall_seconds": 7.989, + "cpu_seconds": 7.265101, + "family_peak_mib": 781.34375, + "payload_cleanup_verified": true, + "final_whole_scope_counters_verified": false + }, + { + "case": "final-wire-candidate-large", + "task_completed": true, + "mode": "summary", + "budget": 350000, + "requests": 9, + "compactions": 0, + "whole_json_tokens_proxy": 1091290, + "request_details": [ + { + "index": 1, + "kind": "normal", + "whole_json_tokens_proxy": 61716, + "rea_namespace_tokens_proxy": 52500, + "rea_function_count": 210 + }, + { + "index": 2, + "kind": "normal", + "whole_json_tokens_proxy": 61854, + "rea_namespace_tokens_proxy": 52500, + "rea_function_count": 210 + }, + { + "index": 3, + "kind": "normal", + "whole_json_tokens_proxy": 62305, + "rea_namespace_tokens_proxy": 52500, + "rea_function_count": 210 + }, + { + "index": 4, + "kind": "normal", + "whole_json_tokens_proxy": 62811, + "rea_namespace_tokens_proxy": 52500, + "rea_function_count": 210 + }, + { + "index": 5, + "kind": "normal", + "whole_json_tokens_proxy": 63411, + "rea_namespace_tokens_proxy": 52500, + "rea_function_count": 210 + }, + { + "index": 6, + "kind": "normal", + "whole_json_tokens_proxy": 63992, + "rea_namespace_tokens_proxy": 52500, + "rea_function_count": 210 + }, + { + "index": 7, + "kind": "normal", + "whole_json_tokens_proxy": 238252, + "rea_namespace_tokens_proxy": 52500, + "rea_function_count": 210 + }, + { + "index": 8, + "kind": "normal", + "whole_json_tokens_proxy": 238389, + "rea_namespace_tokens_proxy": 52500, + "rea_function_count": 210 + }, + { + "index": 9, + "kind": "normal", + "whole_json_tokens_proxy": 238560, + "rea_namespace_tokens_proxy": 52500, + "rea_function_count": 210 + } + ], + "wall_seconds": 9.127, + "cpu_seconds": 8.370296, + "family_peak_mib": 770.76171875, + "payload_cleanup_verified": true, + "final_whole_scope_counters_verified": false + }, + { + "case": "final-wire-candidate-small", + "task_completed": true, + "mode": "summary", + "budget": 30000, + "requests": 9, + "compactions": 0, + "whole_json_tokens_proxy": 580070, + "request_details": [ + { + "index": 1, + "kind": "normal", + "whole_json_tokens_proxy": 61700, + "rea_namespace_tokens_proxy": 52500, + "rea_function_count": 210 + }, + { + "index": 2, + "kind": "normal", + "whole_json_tokens_proxy": 61837, + "rea_namespace_tokens_proxy": 52500, + "rea_function_count": 210 + }, + { + "index": 3, + "kind": "normal", + "whole_json_tokens_proxy": 62289, + "rea_namespace_tokens_proxy": 52500, + "rea_function_count": 210 + }, + { + "index": 4, + "kind": "normal", + "whole_json_tokens_proxy": 62797, + "rea_namespace_tokens_proxy": 52500, + "rea_function_count": 210 + }, + { + "index": 5, + "kind": "normal", + "whole_json_tokens_proxy": 63395, + "rea_namespace_tokens_proxy": 52500, + "rea_function_count": 210 + }, + { + "index": 6, + "kind": "normal", + "whole_json_tokens_proxy": 63968, + "rea_namespace_tokens_proxy": 52500, + "rea_function_count": 210 + }, + { + "index": 7, + "kind": "normal", + "whole_json_tokens_proxy": 67876, + "rea_namespace_tokens_proxy": 52500, + "rea_function_count": 210 + }, + { + "index": 8, + "kind": "normal", + "whole_json_tokens_proxy": 68017, + "rea_namespace_tokens_proxy": 52500, + "rea_function_count": 210 + }, + { + "index": 9, + "kind": "normal", + "whole_json_tokens_proxy": 68191, + "rea_namespace_tokens_proxy": 52500, + "rea_function_count": 210 + } + ], + "wall_seconds": 8.618, + "cpu_seconds": 7.831511, + "family_peak_mib": 785.86328125, + "payload_cleanup_verified": true, + "final_whole_scope_counters_verified": false + } + ], + "sdk": [ + { + "case": "final-sdk-baseline-gdb-large", + "provider": "gdb", + "symbol": "execute_command_internal", + "detail": "complete", + "warm": false, + "tool_calls": 8, + "exported_bundle_bytes": 1310073, + "disassembly_calls": [ + { + "name": "gdb_console", + "input": { + "session_id": "ae48c61a-c147-49fe-ae56-6497631474e3", + "command": "disassemble /r execute_command_internal" + }, + "phase": "disassembly:fresh", + "elapsed_ms": 1156.7173110000003, + "reply_bytes": 2763983, + "is_error": false + } + ], + "wall_seconds": 6.965, + "cpu_seconds": 6.729326, + "family_peak_mib": 595.0234375, + "payload_cleanup_verified": true, + "final_whole_scope_counters_verified": false + }, + { + "case": "final-sdk-baseline-gdb-small", + "provider": "gdb", + "symbol": "execute_command", + "detail": "complete", + "warm": false, + "tool_calls": 8, + "exported_bundle_bytes": 31020, + "disassembly_calls": [ + { + "name": "gdb_console", + "input": { + "session_id": "b25a7498-5ccf-4aa2-bae9-7517ad8ae39a", + "command": "disassemble /r execute_command" + }, + "phase": "disassembly:fresh", + "elapsed_ms": 11.674629000000095, + "reply_bytes": 54123, + "is_error": false + } + ], + "wall_seconds": 6.631, + "cpu_seconds": 6.101502, + "family_peak_mib": 536.32421875, + "payload_cleanup_verified": true, + "final_whole_scope_counters_verified": false + }, + { + "case": "final-sdk-baseline-gdb-warm", + "provider": "gdb", + "symbol": "execute_command_internal", + "detail": "complete", + "warm": true, + "tool_calls": 9, + "exported_bundle_bytes": 2614510, + "disassembly_calls": [ + { + "name": "gdb_console", + "input": { + "session_id": "e853876c-e2b1-4bea-a0c6-e4703df0b02f", + "command": "disassemble /r execute_command_internal" + }, + "phase": "disassembly:fresh", + "elapsed_ms": 1084.606151, + "reply_bytes": 2763983, + "is_error": false + }, + { + "name": "gdb_console", + "input": { + "session_id": "e853876c-e2b1-4bea-a0c6-e4703df0b02f", + "command": "disassemble /r execute_command_internal" + }, + "phase": "disassembly:repeat", + "elapsed_ms": 1100.088831, + "reply_bytes": 2763983, + "is_error": false + } + ], + "wall_seconds": 7.831, + "cpu_seconds": 7.613457, + "family_peak_mib": 585.32421875, + "payload_cleanup_verified": true, + "final_whole_scope_counters_verified": false + }, + { + "case": "final-sdk-baseline-objdump-large", + "provider": "objdump", + "symbol": "execute_command_internal", + "detail": "complete", + "warm": false, + "tool_calls": 3, + "exported_bundle_bytes": 474288, + "disassembly_calls": [ + { + "name": "inspect_with_objdump", + "input": { + "path": "/usr/bin/bash", + "operation": "disassemble", + "symbol": "execute_command_internal" + }, + "phase": "disassembly:fresh", + "elapsed_ms": 1191.5375009999998, + "reply_bytes": 964889, + "is_error": false + } + ], + "wall_seconds": 7.737, + "cpu_seconds": 7.334486, + "family_peak_mib": 537.0078125, + "payload_cleanup_verified": true, + "final_whole_scope_counters_verified": false + }, + { + "case": "final-sdk-baseline-objdump-small", + "provider": "objdump", + "symbol": "execute_command", + "detail": "complete", + "warm": false, + "tool_calls": 3, + "exported_bundle_bytes": 13136, + "disassembly_calls": [ + { + "name": "inspect_with_objdump", + "input": { + "path": "/usr/bin/bash", + "operation": "disassemble", + "symbol": "execute_command" + }, + "phase": "disassembly:fresh", + "elapsed_ms": 1232.2390910000004, + "reply_bytes": 22467, + "is_error": false + } + ], + "wall_seconds": 7.599, + "cpu_seconds": 7.110224, + "family_peak_mib": 527.7890625, + "payload_cleanup_verified": true, + "final_whole_scope_counters_verified": false + }, + { + "case": "final-sdk-baseline-objdump-warm", + "provider": "objdump", + "symbol": "execute_command_internal", + "detail": "complete", + "warm": true, + "tool_calls": 4, + "exported_bundle_bytes": 474288, + "disassembly_calls": [ + { + "name": "inspect_with_objdump", + "input": { + "path": "/usr/bin/bash", + "operation": "disassemble", + "symbol": "execute_command_internal" + }, + "phase": "disassembly:fresh", + "elapsed_ms": 1329.52002, + "reply_bytes": 964889, + "is_error": false + }, + { + "name": "inspect_with_objdump", + "input": { + "path": "/usr/bin/bash", + "operation": "disassemble", + "symbol": "execute_command_internal" + }, + "phase": "disassembly:repeat", + "elapsed_ms": 1069.72696, + "reply_bytes": 964889, + "is_error": false + } + ], + "wall_seconds": 8.549, + "cpu_seconds": 7.819274, + "family_peak_mib": 549.41796875, + "payload_cleanup_verified": true, + "final_whole_scope_counters_verified": false + }, + { + "case": "final-sdk-candidate-gdb-default", + "provider": "gdb", + "symbol": "execute_command", + "detail": "complete", + "warm": false, + "tool_calls": 8, + "exported_bundle_bytes": 31020, + "disassembly_calls": [ + { + "name": "gdb_console", + "input": { + "session_id": "bd108943-b8a0-4d17-aaf2-12703cf3245d", + "command": "disassemble /r execute_command" + }, + "phase": "disassembly:fresh", + "elapsed_ms": 7.148729999999887, + "reply_bytes": 54123, + "is_error": false + } + ], + "wall_seconds": 5.376, + "cpu_seconds": 5.139502, + "family_peak_mib": 532.73828125, + "payload_cleanup_verified": true, + "final_whole_scope_counters_verified": false + }, + { + "case": "final-sdk-candidate-gdb-large", + "provider": "gdb", + "symbol": "execute_command_internal", + "detail": "summary", + "warm": false, + "tool_calls": 8, + "exported_bundle_bytes": 1640022, + "disassembly_calls": [ + { + "name": "gdb_console", + "input": { + "session_id": "ced14819-b4e5-4001-940d-74aeb3ffee76", + "command": "disassemble /r execute_command_internal", + "detail": "summary" + }, + "phase": "disassembly:fresh", + "elapsed_ms": 924.0110299999997, + "reply_bytes": 700453, + "is_error": false + } + ], + "wall_seconds": 6.66, + "cpu_seconds": 6.451299, + "family_peak_mib": 545.83203125, + "payload_cleanup_verified": true, + "final_whole_scope_counters_verified": false + }, + { + "case": "final-sdk-candidate-gdb-small", + "provider": "gdb", + "symbol": "execute_command", + "detail": "summary", + "warm": false, + "tool_calls": 8, + "exported_bundle_bytes": 39541, + "disassembly_calls": [ + { + "name": "gdb_console", + "input": { + "session_id": "20d27f39-3d3a-4338-97bd-c3593d1a14fa", + "command": "disassemble /r execute_command", + "detail": "summary" + }, + "phase": "disassembly:fresh", + "elapsed_ms": 14.95060999999987, + "reply_bytes": 18009, + "is_error": false + } + ], + "wall_seconds": 5.447, + "cpu_seconds": 5.23989, + "family_peak_mib": 535.83984375, + "payload_cleanup_verified": true, + "final_whole_scope_counters_verified": false + }, + { + "case": "final-sdk-candidate-gdb-warm", + "provider": "gdb", + "symbol": "execute_command_internal", + "detail": "summary", + "warm": true, + "tool_calls": 9, + "exported_bundle_bytes": 3274329, + "disassembly_calls": [ + { + "name": "gdb_console", + "input": { + "session_id": "1fdd278f-ee18-4fa4-bb8f-c1cd94f15177", + "command": "disassemble /r execute_command_internal", + "detail": "summary" + }, + "phase": "disassembly:fresh", + "elapsed_ms": 1014.8414000000002, + "reply_bytes": 700453, + "is_error": false + }, + { + "name": "gdb_console", + "input": { + "session_id": "1fdd278f-ee18-4fa4-bb8f-c1cd94f15177", + "command": "disassemble /r execute_command_internal", + "detail": "summary" + }, + "phase": "disassembly:repeat", + "elapsed_ms": 1015.94805, + "reply_bytes": 700453, + "is_error": false + } + ], + "wall_seconds": 7.412, + "cpu_seconds": 7.161854, + "family_peak_mib": 534.37890625, + "payload_cleanup_verified": true, + "final_whole_scope_counters_verified": false + }, + { + "case": "final-sdk-candidate-objdump-default", + "provider": "objdump", + "symbol": "execute_command", + "detail": "complete", + "warm": false, + "tool_calls": 3, + "exported_bundle_bytes": 13136, + "disassembly_calls": [ + { + "name": "inspect_with_objdump", + "input": { + "path": "/usr/bin/bash", + "operation": "disassemble", + "symbol": "execute_command" + }, + "phase": "disassembly:fresh", + "elapsed_ms": 872.0469199999998, + "reply_bytes": 22467, + "is_error": false + } + ], + "wall_seconds": 6.674, + "cpu_seconds": 6.296232, + "family_peak_mib": 529.1640625, + "payload_cleanup_verified": true, + "final_whole_scope_counters_verified": false + }, + { + "case": "final-sdk-candidate-objdump-large", + "provider": "objdump", + "symbol": "execute_command_internal", + "detail": "summary", + "warm": false, + "tool_calls": 3, + "exported_bundle_bytes": 712656, + "disassembly_calls": [ + { + "name": "inspect_with_objdump", + "input": { + "path": "/usr/bin/bash", + "operation": "disassemble", + "symbol": "execute_command_internal", + "detail": "summary" + }, + "phase": "disassembly:fresh", + "elapsed_ms": 1196.0160100000003, + "reply_bytes": 487219, + "is_error": false + } + ], + "wall_seconds": 7.499, + "cpu_seconds": 6.935195, + "family_peak_mib": 536.60546875, + "payload_cleanup_verified": true, + "final_whole_scope_counters_verified": false + }, + { + "case": "final-sdk-candidate-objdump-small", + "provider": "objdump", + "symbol": "execute_command", + "detail": "summary", + "warm": false, + "tool_calls": 3, + "exported_bundle_bytes": 20919, + "disassembly_calls": [ + { + "name": "inspect_with_objdump", + "input": { + "path": "/usr/bin/bash", + "operation": "disassemble", + "symbol": "execute_command", + "detail": "summary" + }, + "phase": "disassembly:fresh", + "elapsed_ms": 1023.8277900000003, + "reply_bytes": 15990, + "is_error": false + } + ], + "wall_seconds": 7.699, + "cpu_seconds": 7.263709, + "family_peak_mib": 532.5, + "payload_cleanup_verified": true, + "final_whole_scope_counters_verified": false + }, + { + "case": "final-sdk-candidate-objdump-warm", + "provider": "objdump", + "symbol": "execute_command_internal", + "detail": "summary", + "warm": true, + "tool_calls": 4, + "exported_bundle_bytes": 712656, + "disassembly_calls": [ + { + "name": "inspect_with_objdump", + "input": { + "path": "/usr/bin/bash", + "operation": "disassemble", + "symbol": "execute_command_internal", + "detail": "summary" + }, + "phase": "disassembly:fresh", + "elapsed_ms": 1342.6134499999998, + "reply_bytes": 487219, + "is_error": false + }, + { + "name": "inspect_with_objdump", + "input": { + "path": "/usr/bin/bash", + "operation": "disassemble", + "symbol": "execute_command_internal", + "detail": "summary" + }, + "phase": "disassembly:repeat", + "elapsed_ms": 939.490941, + "reply_bytes": 487219, + "is_error": false + } + ], + "wall_seconds": 8.496, + "cpu_seconds": 7.858788, + "family_peak_mib": 550.5625, + "payload_cleanup_verified": true, + "final_whole_scope_counters_verified": false + } + ], + "oracle": { + "path": "/usr/bin/bash", + "artifact_sha256": "55b89ab22bee4792a210f493a53fb066accd5d30b69837c28d98be5ff863efcf", + "artifact_bytes": 1265648, + "elf_class": 64, + "byte_order": "little", + "elf_machine": 62, + "symbols": [ + { + "symbol_table_section": 6, + "symbol_ordinal": 544, + "name": "execute_command_internal", + "address": 294400, + "bytes": 15428, + "file_offset": 294400, + "first_16_bytes_hex": "415741564155415455534881ecc80000" + }, + { + "symbol_table_section": 6, + "symbol_ordinal": 578, + "name": "execute_command", + "address": 309840, + "bytes": 239, + "file_offset": 309840, + "first_16_bytes_hex": "41544c8d25289a0a00554889fdbf1000" + } + ], + "scope": "Independent ELF64 little-endian dynamic symbol/section mapping; no target execution or REA parser." + }, + "byte_verification": { + "status": "passed", + "scope": "Each native byte-bearing line matches contiguous independent ELF file ranges; objdump line counts include byte continuations.", + "records": [ + { + "case": "final-sdk-baseline-gdb-large", + "mode": "gdb", + "symbol": "execute_command_internal", + "byte_bearing_lines": 3365, + "bytes": 15428, + "exact_file_bytes": true, + "parent_evidence_id": "ev_ec972c91ca8ad1688b91dd1cb5022ad57f39bc683e17d469e909e1d2453ec047" + }, + { + "case": "final-sdk-baseline-gdb-small", + "mode": "gdb", + "symbol": "execute_command", + "byte_bearing_lines": 66, + "bytes": 239, + "exact_file_bytes": true, + "parent_evidence_id": "ev_b5a0e1ec8bf0e69ae489820cb92da38b7120075582d68c2a8bef65e3f66ec136" + }, + { + "case": "final-sdk-baseline-gdb-warm", + "mode": "gdb", + "symbol": "execute_command_internal", + "byte_bearing_lines": 3365, + "bytes": 15428, + "exact_file_bytes": true, + "parent_evidence_id": "ev_4587fc260fca9c062361e1334eb55f31cab1878c41dfe34461d1a5bda55c485a" + }, + { + "case": "final-sdk-baseline-gdb-warm", + "mode": "gdb", + "symbol": "execute_command_internal", + "byte_bearing_lines": 3365, + "bytes": 15428, + "exact_file_bytes": true, + "parent_evidence_id": "ev_f204aa41ddb44fdf677c9acc47d93592567fd6ba155d06b4eee81089af25b37e" + }, + { + "case": "final-sdk-baseline-objdump-large", + "mode": "objdump", + "symbol": "execute_command_internal", + "byte_bearing_lines": 3447, + "bytes": 15428, + "exact_file_bytes": true, + "parent_evidence_id": "ev_60eb1e87e0a2cdb1506bdc38f2a81feeb50ea9cd9b3b366c76496299ac67f27d" + }, + { + "case": "final-sdk-baseline-objdump-small", + "mode": "objdump", + "symbol": "execute_command", + "byte_bearing_lines": 67, + "bytes": 239, + "exact_file_bytes": true, + "parent_evidence_id": "ev_7283f73d06bfc092e61fe8f7f7cd22caee6a4eb3d0e245974e02bbd2f695edab" + }, + { + "case": "final-sdk-baseline-objdump-warm", + "mode": "objdump", + "symbol": "execute_command_internal", + "byte_bearing_lines": 3447, + "bytes": 15428, + "exact_file_bytes": true, + "parent_evidence_id": "ev_60eb1e87e0a2cdb1506bdc38f2a81feeb50ea9cd9b3b366c76496299ac67f27d" + }, + { + "case": "final-sdk-candidate-gdb-default", + "mode": "gdb", + "symbol": "execute_command", + "byte_bearing_lines": 66, + "bytes": 239, + "exact_file_bytes": true, + "parent_evidence_id": "ev_6a1fa914f561364502501dcaf6e965cd7369d1fa6c33af266f4ea6263c7a1f45" + }, + { + "case": "final-sdk-candidate-gdb-large", + "mode": "gdb", + "symbol": "execute_command_internal", + "byte_bearing_lines": 3365, + "bytes": 15428, + "exact_file_bytes": true, + "parent_evidence_id": "ev_65bbf6278cca8c78fefa4845d35181f5662efc3bd255464447cba6281eb8564d" + }, + { + "case": "final-sdk-candidate-gdb-small", + "mode": "gdb", + "symbol": "execute_command", + "byte_bearing_lines": 66, + "bytes": 239, + "exact_file_bytes": true, + "parent_evidence_id": "ev_e20167b50a8d1a1fc856162790804103999e2c70887e488539ea48f2a4d6eb15" + }, + { + "case": "final-sdk-candidate-gdb-warm", + "mode": "gdb", + "symbol": "execute_command_internal", + "byte_bearing_lines": 3365, + "bytes": 15428, + "exact_file_bytes": true, + "parent_evidence_id": "ev_7f190259b6190b59bab8a99391fd80853b5bb3345bc3a38abc1c63a195a4aa6e" + }, + { + "case": "final-sdk-candidate-gdb-warm", + "mode": "gdb", + "symbol": "execute_command_internal", + "byte_bearing_lines": 3365, + "bytes": 15428, + "exact_file_bytes": true, + "parent_evidence_id": "ev_b23289fde8468f005c0172e479ff7ff6aa1121338b6a771aec8184c5c82e7aee" + }, + { + "case": "final-sdk-candidate-objdump-default", + "mode": "objdump", + "symbol": "execute_command", + "byte_bearing_lines": 67, + "bytes": 239, + "exact_file_bytes": true, + "parent_evidence_id": "ev_7283f73d06bfc092e61fe8f7f7cd22caee6a4eb3d0e245974e02bbd2f695edab" + }, + { + "case": "final-sdk-candidate-objdump-large", + "mode": "objdump", + "symbol": "execute_command_internal", + "byte_bearing_lines": 3447, + "bytes": 15428, + "exact_file_bytes": true, + "parent_evidence_id": "ev_60eb1e87e0a2cdb1506bdc38f2a81feeb50ea9cd9b3b366c76496299ac67f27d" + }, + { + "case": "final-sdk-candidate-objdump-small", + "mode": "objdump", + "symbol": "execute_command", + "byte_bearing_lines": 67, + "bytes": 239, + "exact_file_bytes": true, + "parent_evidence_id": "ev_7283f73d06bfc092e61fe8f7f7cd22caee6a4eb3d0e245974e02bbd2f695edab" + }, + { + "case": "final-sdk-candidate-objdump-warm", + "mode": "objdump", + "symbol": "execute_command_internal", + "byte_bearing_lines": 3447, + "bytes": 15428, + "exact_file_bytes": true, + "parent_evidence_id": "ev_60eb1e87e0a2cdb1506bdc38f2a81feeb50ea9cd9b3b366c76496299ac67f27d" + }, + { + "case": "final-wire-baseline-default", + "mode": "gdb", + "symbol": "execute_command", + "byte_bearing_lines": 66, + "bytes": 239, + "exact_file_bytes": true, + "parent_evidence_id": "ev_3b74f7b1cee2871850020bce2d87b87573bd6c84df1ad963f6cb597cb77f7cf3" + }, + { + "case": "final-wire-baseline-large", + "mode": "gdb", + "symbol": "execute_command_internal", + "byte_bearing_lines": 3365, + "bytes": 15428, + "exact_file_bytes": true, + "parent_evidence_id": "ev_5a7b679d00d23d273edc47821f456042b00bc3a3676709f89a5566b624a31343" + }, + { + "case": "final-wire-baseline-small", + "mode": "gdb", + "symbol": "execute_command", + "byte_bearing_lines": 66, + "bytes": 239, + "exact_file_bytes": true, + "parent_evidence_id": "ev_67f1cf02a74ec018f00d43995e614f17ba6d41ed6b4cb004e55d85b9d75d91d3" + }, + { + "case": "final-wire-candidate-default", + "mode": "gdb", + "symbol": "execute_command", + "byte_bearing_lines": 66, + "bytes": 239, + "exact_file_bytes": true, + "parent_evidence_id": "ev_acc5e55ed735ad5373bc3c84192e0086f9aeb20e0a8a325aa0bb2143f3fc591f" + }, + { + "case": "final-wire-candidate-large", + "mode": "gdb", + "symbol": "execute_command_internal", + "byte_bearing_lines": 3365, + "bytes": 15428, + "exact_file_bytes": true, + "parent_evidence_id": "ev_349a7579b41a7ec0c01c8e62adb8130ede0b5817117bcffa153b334c315b670b" + }, + { + "case": "final-wire-candidate-small", + "mode": "gdb", + "symbol": "execute_command", + "byte_bearing_lines": 66, + "bytes": 239, + "exact_file_bytes": true, + "parent_evidence_id": "ev_9d4e66b8a2fe99f6fe83291cc8653b9dd57dcb8811494aedfaf7b653daf5b291" + } + ] + }, + "cli": { + "status": "passed", + "records": [ + { + "case": "symbol-baseline", + "exit_code": 0, + "stdout_bytes": 10871 + }, + { + "case": "symbol-candidate-default", + "exit_code": 0, + "stdout_bytes": 10871 + }, + { + "case": "symbol-summary", + "exit_code": 0, + "stdout_bytes": 7825 + }, + { + "case": "symbol-no-overwrite", + "exit_code": 1, + "stdout_bytes": 11268 + }, + { + "case": "architectures-baseline", + "exit_code": 0, + "stdout_bytes": 5827 + }, + { + "case": "architectures-candidate-default", + "exit_code": 0, + "stdout_bytes": 5827 + }, + { + "case": "architectures-summary", + "exit_code": 0, + "stdout_bytes": 4914 + }, + { + "case": "architectures-no-overwrite", + "exit_code": 1, + "stdout_bytes": 6231 + }, + { + "case": "missing-destination", + "exit_code": 1, + "stdout_bytes": 670 + }, + { + "case": "gdb-summary", + "exit_code": 0, + "stdout_bytes": 2527 + } + ], + "scope": "Actual CLI/GDB/BFD; target not executed; subjectless GDB and architectures preserved; default exact Evidence identical; bundles nonoverwriting." + }, + "validation": { + "focused_tests": { + "files": 12, + "tests": 320, + "status": "passed" + }, + "check_fast": "passed on fourth invocation; earlier host-memory stops and fixture type failure are not passes", + "resource_notes": "No experiment exceeded resource bounds or reported OOM. Final whole-scope counters are unknown when the cgroup disappeared. Initial fixture assertion failures and host-memory check stops were preserved." + }, + "driver_sha256": { + "command-observation-wire.py": "6d7794df8d9ee9b4dc897363cb2b4ebe2c4f705f7ebba6db40a24aabace4844b", + "command-observation-oracle.py": "ebded792de2af81eb52dac0aae92717e9e281962afcb051c7f59d20e972e23a3", + "command-observation-costs.py": "d52e265d29880e18cd7d03a208a2000e359997a2d0717c1eb0e498af0ebd6231", + "command-observation-queries.mjs": "0a7a68a747f5a23d7e819aeca4204f0a04bd5bfbd2e0d508bb45dfee0b6ce3ab", + "command-observation-verify.py": "7b983a228477ccffa61293531afc99176d69b506961cff5ee18a660d1e9e733c" + }, + "review_followup": { + "parent_commit": "b361d5de54a7a2f618a80af5846fad55f8f3f7d8", + "production_source_changes": false, + "changes": [ + "Shared acceptance oracle rejects incomplete summary delivery under every budget; only complete/default-budget baseline may remain incomplete.", + "Removed unused --export option; native export remains mandatory in the eight-call question.", + "Nine CLI regressions use registered argument/option codecs and real file publication with controlled captures; no real-provider claim from these tests." + ], + "checks": { + "python_acceptance_regressions": 3, + "cli_tests_passed": 16, + "new_cli_regressions": 9, + "focused_cli_typecheck": "passed with unchanged strict compiler settings and transitive source dependencies", + "standard_static_tasks": "lint:built, format:check, knip, metadata:check, jscpd, scan:todos all passed" + }, + "wire_revalidation": [ + { + "case": "review-wire-baseline-verified", + "status": "expected-incomplete-task", + "task_completed": false, + "tool_calls": 8, + "requests": 9, + "payload_cleanup_verified": true + }, + { + "case": "review-wire-summary-final", + "status": "passed", + "task_completed": true, + "tool_calls": 8, + "requests": 9, + "payload_cleanup_verified": true + }, + { + "case": "review-wire-large-final", + "status": "passed", + "task_completed": true, + "tool_calls": 8, + "requests": 9, + "payload_cleanup_verified": true + } + ], + "updated_driver_sha256": { + "command-observation-wire.py": "1124a3eda1779a78e37cac6b004f3593fadea62d310f6801bdf71062413549d6", + "command_observation_delivery.py": "5c42fc767b859f9f7ba71bac6b83ea05db1bb9639ac438ce2741a1e01d55a260", + "test_command_observation_delivery.py": "71d835a58aa7541d54a01326ca9001df0d63959e79f813cd0debfdc0d7837343" + }, + "measurement_limits": "Original timing/token cohorts and their source/runtime pins are unchanged. Review revalidation is correctness evidence, not a replacement paired timing cohort." + } +} diff --git a/experiments/analysis-efficiency/command-observation-views.md b/experiments/analysis-efficiency/command-observation-views.md new file mode 100644 index 0000000000..9753328383 --- /dev/null +++ b/experiments/analysis-efficiency/command-observation-views.md @@ -0,0 +1,139 @@ +# Retained command observations + +Addresses [#5584](https://github.com/morluto/rea/issues/5584). GDB command Evidence +repeats ordered MI records in stream groups and a raw mirror; objdump repeats +stdout/stderr in its raw mirror. Explicit `detail: summary` retains the complete +parent and projects existing owner facts once. Default complete Evidence is +unchanged. This adds no tool, MI decoder, live-state cache or command authority. + +GDB summaries preserve **every ordered record**, including asynchronous and +unrecognized records, correlated MI result, command, completion/truncation, +invalid-UTF-8 and process-exit facts. They omit the convenience console/target/log +groupings and report their original UTF-16 sizes. Objdump summaries preserve +opaque stdout/stderr, exit/signal, completion and pre/post digest-change facts. +Both carry source parameters/provider/profile, parent identity, limitations and +unknowns. Subjectless observations stay subjectless. Historical observations do +not establish current target state or global coverage. The existing +`inspect_analysis_view` summary operation works after debugger close. + +The domain codec now owns the command fact shapes used by contracts and GDB's +capture type; protocol interpretation stays with its provider. MCP requires +retention before command execution. CLI uses the existing shared projection and +nonoverwriting bundle publisher after provider cleanup: + +```sh +rea inspect-with-objdump /usr/bin/bash disassemble --symbol execute_command \ + --detail summary --evidence_output /tmp/bash-command-evidence.json --json +``` + +## Real input and correctness + +Linux x86-64, Node 24.18.0/npm 11.16.0; GNU GDB 13.1 (Debian 13.1-3), +GNU objdump 2.40, installed Bash ELF 1,265,648 bytes, SHA-256 +`55b89ab22bee4792a210f493a53fb066accd5d30b69837c28d98be5ff863efcf`. +The [receipt](command-observation-views.json) pins native binaries, baseline main +`c2858d2aa0916f0dd2d073d848b8c2195d9bf6b3`, source patch and packaged runtime digests. +No target execution, attach, GUI or database mutation occurred. + +An [independent ELF reader](command-observation-oracle.py) maps dynamic symbols +to exact file ranges: `execute_command`, 239 bytes at `0x4ba50`, and +`execute_command_internal`, 15,428 bytes at `0x47e00`. The +[verifier](command-observation-verify.py) checks every disassembled byte and its +contiguous address against those ranges. GDB reports 66/3,365 instructions; +objdump byte lines also include continuations, so their count is not an +instruction count. Ordered summary facts match each exported complete parent. +GDB Evidence has a null subject; the external oracle does not manufacture a +target binding. Actual CLI default objdump Evidence matches the baseline exactly, +including identity; symbol/pathless summaries preserve it in bundles. CLI checks +also cover rejected overwrite, missing destination before dispatch, and a real +GDB command with a caller-owned PTY that remains available after debugger exit. + +## Whole tasks and resource costs + +The [SDK driver](command-observation-queries.mjs) discovers all 210 tools, reads +architecture and symbol-scoped disassembly, closes GDB where applicable, exports +complete Evidence and finalizes owned resources. GDB uses 8 calls; objdump uses 3. +Warm cases repeat disassembly in the same process, adding one call. Fresh-process +cases have **unflushed OS caches**, not disk-cold inputs. Native commands are +rerun; results are not reused as current live state. + +The [actual Codex 0.163 app-server driver](command-observation-wire.py) uses a +loopback deterministic SSE fixture, no AI inference or paid service. All 210 +function schemas are present in every normal model-facing request. Matched tasks +use full input/output profiles; only disassembly selects summary. The +[counter](command-observation-costs.py) counts compact JSON serialization with +`tiktoken 0.12.0 / cl100k_base` across **every** request, including schemas, +replayed results and the actual client's local compaction request. The fixture +answers compaction with observed identifiers and the retained-body location. +These are serialization-token proxies, not model rendering, acceptance, answer +quality, actual usage or billing. Compaction behavior is qualified only for this +client/configuration; no inference quality is established. + +| Completed GDB task | JSON token proxy before → after | Requests | Wall seconds | CPU seconds | Family peak MiB | +| ------------------------------ | ------------------------------: | -------: | ------------: | ------------: | --------------: | +| Small, 30,000 per-tool budget | 605,133 → 580,070 | 9 → 9 | 7.906 → 8.618 | 7.195 → 7.832 | 788.9 → 785.9 | +| Large, 350,000 per-tool budget | 1,269,405 → 1,091,290 | 10 → 9 | 9.551 → 9.127 | 8.832 → 8.370 | 805.2 → 770.8 | + +Whole-task context proxies decrease **4.14% / 14.03%**, rather than the much +larger reply-byte reduction. The large baseline makes one compaction request; +the candidate makes none. The small task is slower. These single paired samples +do not establish a general speedup. With the client's default output budget, +the small baseline delivers truncated invalid inner JSON and does **not** +complete the question; summary completes it. That incomplete baseline is +excluded from the percentage comparison. + +| SDK task | Reply bytes before → after | Whole wall seconds | CPU seconds | Family peak MiB | +| ------------- | -------------------------: | -----------------: | ------------: | --------------: | +| GDB small | 54,123 → 18,009 | 6.631 → 5.447 | 6.102 → 5.240 | 536.3 → 535.8 | +| GDB large | 2,763,983 → 700,453 | 6.965 → 6.660 | 6.729 → 6.451 | 595.0 → 545.8 | +| Objdump small | 22,467 → 15,990 | 7.599 → 7.699 | 7.110 → 7.264 | 527.8 → 532.5 | +| Objdump large | 964,889 → 487,219 | 7.737 → 7.499 | 7.334 → 6.935 | 537.0 → 536.6 | + +Same-process repeat disassembly takes 1,100 → 1,016 ms for GDB and +1,070 → 939 ms for objdump in these observations. Whole warm-task CPU improves +7.613 → 7.162 s for GDB but increases 7.819 → 7.859 s for objdump. Timings include +existing ownership checks; no check was removed or cached. + +Retaining a summary also retains an additional derived record: large GDB bundles +grow 1,310,073 → 1,640,022 bytes; objdump 474,288 → 712,656. This trades retained +storage for smaller delivery; long-session heap growth is unmeasured. Retained-data lifetime follows the existing investigation ledger and cleanup +owners. Output still scales +with the requested complete disassembly; summary is not a fixed-size preview. + +Each experiment runs in one owned systemd scope, one CPU, no swap, 4 GiB hard / +3 GiB early memory thresholds and host-memory admission/stop thresholds. Recorded +payload exit and cleanup are verified, with zero OOM/resource events. Final +whole-scope counters disappear after teardown and remain explicitly unverified. +The receipt includes CPU time, wall time, peak family memory, warm cases and +bundle growth. Build/check scopes use separate bounded budgets. Host-memory +check stops and initial fixture failures are preserved locally and are not passes. + +## Coverage and reproduction + +320 focused tests cover the new SDK schemas/views plus existing GDB queue, +malformed output, cancellation, process exit, target identity and cleanup cases, +objdump symbol selection and command admission. Real provider evidence is from +the CLI/SDK/client runs above, not simulated-provider tests. + +Run the oracle for the selected Bash image, then use the SDK driver with +`--runtime`, `--input`, `--oracle`, `--output`, `--provider gdb|objdump`, +`--symbol`, `--detail complete|summary` and optional `--warm`. The wire driver +also takes the fixed Codex/Node paths, `--mode`, `--symbol` and `--budget`; it always performs the +export (eight tool calls). Run each through `resource-run.py --observe-payload-exit`, preserving +its sibling `.log`; the verifier and counter accept `--root` for those results. + +Default Rizin execution returned ENOENT with no configured alternative. Cutter +reported no live bridge; no configured selector or default bridge directory was +present. Real Rizin/Cutter performance and GUI workflows remain unverified. +Their command semantics were not changed. Existing objdump native symbol +selection was reused. No claim covers arbitrary GDB target execution or another +host client's output budget. + +Review follow-up adds nine CLI publication regressions (16 CLI tests including the +existing cleanup cases), three deterministic delivery-oracle tests, and a focused +strict typecheck of the new CLI boundary and its transitive source. Real client +small/default and large/generous summary runs pass the strengthened oracle and +export complete parents; only the explicit complete/default baseline may finish +with an incomplete answer. The unused export switch was removed: export is +mandatory in this question. Product source and the original paired measurements +are unchanged; the receipt records these additional validation runs separately. diff --git a/experiments/analysis-efficiency/command-observation-wire.py b/experiments/analysis-efficiency/command-observation-wire.py new file mode 100644 index 0000000000..e9bc82c74b --- /dev/null +++ b/experiments/analysis-efficiency/command-observation-wire.py @@ -0,0 +1,301 @@ +"""Actual Codex wire capture with a deterministic local SSE backend; no AI inference.""" +import argparse +import http.server +import threading +import asyncio +import hashlib +import json +import os +from pathlib import Path +import re +import time +import traceback +import subprocess +from command_observation_delivery import verify_command_delivery + + +async def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--runtime", type=Path, required=True) + parser.add_argument("--codex", type=Path, required=True) + parser.add_argument("--node", type=Path, required=True) + parser.add_argument("--input-profile", choices=["full", "compact"], default="full") + parser.add_argument("--output-profile", choices=["full", "off"], default="full") + parser.add_argument("--input", type=Path, required=True) + parser.add_argument("--oracle", type=Path, required=True) + parser.add_argument("--expected-tools", type=int, default=210) + parser.add_argument("--mode", choices=["complete", "summary"], required=True) + parser.add_argument("--question", choices=["disassembly"], default="disassembly") + parser.add_argument("--symbol", choices=["execute_command", "execute_command_internal"], required=True) + parser.add_argument("--budget", type=int, default=30000) + args = parser.parse_args() + oracle=json.loads(args.oracle.read_text()) + target=str(args.input) + assert args.input.is_absolute() and args.oracle.is_absolute() and args.output.is_absolute() + assert hashlib.sha256(args.input.read_bytes()).hexdigest()==oracle["artifact_sha256"] + def delivered(pieces): + assert isinstance(pieces,str) and pieces.startswith("Wall time: "), pieces[:300] + _,sep,payload=pieces.partition("Output:\n");assert sep + try: value=json.loads(payload) + except json.JSONDecodeError: return {"_invalid_json_payload":payload} + return value["result"] if "result" in value and "normalized_result" not in value else value + def outputs_of(body): + return {item["call_id"]:delivered(item["output"]) for item in body.get("input",[]) if item.get("type")=="function_call_output"} + steps=8 + args.output.mkdir(mode=0o700) + audit_home = args.output / "account" + codex_state = audit_home / ".codex" + mcp_home = audit_home / "mcp" + workspace = audit_home / "workspace" + for directory in [codex_state, mcp_home, workspace]: + directory.mkdir(parents=True) + captured = [] + known_outputs = {} + state = {"normal_requests":0,"compactions":0} + class Capture(http.server.BaseHTTPRequestHandler): + def log_message(self, *_): + pass + def do_POST(self): + size = int(self.headers["Content-Length"]) + assert 0 < size <= 16 * 1024 * 1024 + assert self.headers.get("Content-Encoding", "identity") == "identity" + body = json.loads(self.rfile.read(size)) + captured.append({"path": self.path, "body": body}) + (args.output / "model-requests.json").write_text(json.dumps(captured) + "\n") + http_index = len(captured) + known_outputs.update(outputs_of(body)) + names = [t.get("name", "") for ns in body.get("tools", []) if ns.get("type") == "namespace" and ns.get("name") == "mcp__rea_audit" for t in ns.get("tools", [])] + if not names: + last=body.get('input',[])[-1] + assert 'CONTEXT CHECKPOINT COMPACTION' in json.dumps(last) + state['compactions']+=1;assert state['compactions']<=1 + evidence=known_outputs['call_local_6'];assert 'normalized_result' in evidence + captured_result=evidence['normalized_result'];command_report=captured_result.get('summary',captured_result) + checkpoint={'fixture':'deterministic protocol checkpoint; no AI inference or model-quality claim','task':'Full native Bash function disassembly has been delivered to this compaction request. Retain complete Evidence and finish owned cleanup.','path':target,'artifact_sha256_external_oracle':oracle['artifact_sha256'],'symbol':args.symbol,'session_id':known_outputs['call_local_1']['session_id'],'parent_evidence_id':captured_result.get('parent_evidence_id',evidence['evidence_id']),'subject':evidence['subject'],'completion_status':command_report['completion_status'],'output_truncated':command_report['output_truncated'],'native_console_utf16_units':command_report.get('available_counts',{}).get('console_utf16_units',len(command_report.get('console','').encode('utf-16-le'))//2),'full_body_location':'same-session retained Evidence; export next to '+str(args.output/'evidence.json'),'remaining':['close_gdb_session','export_evidence_bundle']} + text=json.dumps(checkpoint) + item={"type":"message","id":f"msg_checkpoint_{state['compactions']}","role":"assistant","status":"completed","content":[{"type":"output_text","text":text,"annotations":[]}]} + captured[-1]['kind']='deterministic-compaction' + else: + state['normal_requests']+=1;index=state['normal_requests'];assert index<=steps+1 + captured[-1]['kind']='tool-planning-or-final' + if index<=steps: + if index==1: + operation="start_gdb_session";arguments={} + else: + session=known_outputs["call_local_1"]["session_id"] + if index<=6: + command=['set auto-load off','set debuginfod enabled off',f'file {target}','show architecture',f'disassemble /r {args.symbol}'][index-2] + operation="gdb_console";arguments={"session_id":session,"command":command} + if index==6 and args.mode=="summary":arguments["detail"]="summary" + elif index==7: + operation="close_gdb_session";arguments={"session_id":session} + else: + operation="export_evidence_bundle";arguments={"path":str(args.output/'evidence.json')} + selected=[name for name in names if name.endswith(operation)];assert len(selected)==1 + item={"type":"function_call","id":f"fc_local_{index}","call_id":f"call_local_{index}","name":selected[0],"namespace":"mcp__rea_audit","arguments":json.dumps(arguments),"status":"completed"} + else: + item={"type":"message","id":"msg_local_done","role":"assistant","status":"completed","content":[{"type":"output_text","text":"Scripted local GDB disassembly task complete.","annotations":[]}]} + response_id=f"resp_local_{http_index}" + base = {"id": response_id, "object": "response", "model": body.get("model"), "status": "completed", "output": [item]} + events = [ + {"type": "response.created", "response": {**base, "status": "in_progress", "output": []}}, + {"type": "response.output_item.added", "output_index": 0, "item": {**item, "status": "in_progress"}}, + {"type": "response.output_item.done", "output_index": 0, "item": item}, + {"type": "response.completed", "response": base}, + ] + wire = "".join("event: " + e["type"] + "\ndata: " + json.dumps(e) + "\n\n" for e in events).encode() + self.send_response(200) + self.send_header("Content-Type", "text/event-stream") + self.send_header("Content-Length", str(len(wire))) + self.end_headers() + self.wfile.write(wire) + endpoint = http.server.HTTPServer(("127.0.0.1", 0), Capture) + endpoint_thread = threading.Thread(target=endpoint.serve_forever, daemon=True) + endpoint_thread.start() + task_path = f"{args.node.parent}:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + mcp_env = {"HOME": str(mcp_home), "PATH": task_path, + "NODE_OPTIONS": "--max-old-space-size=2048", "UV_THREADPOOL_SIZE": "1", + "REA_LOG_LEVEL": "silent", "REA_MCP_INPUT_SCHEMA_PROFILE": args.input_profile, + "REA_MCP_OUTPUT_SCHEMA_PROFILE": args.output_profile} + config = 'model_provider = "rea_capture"\nmodel = "rea-wire-probe"\n[analytics]\nenabled = false\n[mcp_servers.rea_audit]\n' + config += f'command = {json.dumps(str(args.node))}\n' + config += f'args = {json.dumps([str(args.runtime / "scripts/rea.mjs"), "mcp"])}\n' + config += 'startup_timeout_sec = 60\ntool_timeout_sec = 60\n' + config += '[mcp_servers.rea_audit.env]\n' + config += ''.join(f'{key} = {json.dumps(value)}\n' for key, value in mcp_env.items()) + for name in ['start_gdb_session','gdb_console','close_gdb_session','export_evidence_bundle']: + config += f'[mcp_servers.rea_audit.tools.{name}]\napproval_mode = "approve"\n' + if args.budget: config += f'output_token_limit = {args.budget}\n' + config += f'\n[model_providers.rea_capture]\nname = "Local scripted capture"\nbase_url = "http://127.0.0.1:{endpoint.server_port}/v1"\nwire_api = "responses"\nrequires_openai_auth = false\nsupports_websockets = false\nrequest_max_retries = 0\nstream_max_retries = 0\n' + (codex_state / "config.toml").write_text(config) + (args.output / "declared-config.toml").write_text(config) + environment = {"HOME": str(audit_home), "CODEX_HOME": str(codex_state), "PATH": task_path, + "NODE_OPTIONS": "--max-old-space-size=2048", "UV_THREADPOOL_SIZE": "1"} + report = {"question": "Return architecture and complete byte-annotated disassembly for one selected native Bash function and export complete Evidence. No target execution.", + "client": "Codex app-server 0.163.0", "model_turns_started": 1, "backend": "local deterministic SSE fixture; no AI model or paid service", + "actual_model_usage": None, "input_profile": args.input_profile, + "output_profile": args.output_profile, "calls": [], "observations": [], + "unexpected_server_requests": [], "cleanup_failures": []} + client_version = subprocess.check_output([str(args.codex), "--version"], env=environment, cwd=workspace, text=True, timeout=10).strip() + assert client_version == "codex-cli 0.163.0", client_version + transcript = [] + pending = {} + completed_turn = asyncio.Event() + next_id = 0 + stderr = (args.output / "server.log").open("wb") + process = await asyncio.create_subprocess_exec(str(args.codex), "app-server", "--stdio", + stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE, stderr=stderr, + cwd=workspace, env=environment, limit=16 * 1024 * 1024) + + async def send(message): + encoded = (json.dumps(message, separators=(",", ":")) + "\n").encode() + transcript.append({"direction": "client", "bytes": len(encoded), "message": message}) + process.stdin.write(encoded) + await process.stdin.drain() + + async def read_messages(): + try: + while line := await process.stdout.readline(): + value = json.loads(line) + transcript.append({"direction": "server", "bytes": len(line), "message": value}) + if value.get("method") == "turn/completed": + report["turn"] = value["params"]["turn"] + completed_turn.set() + if "id" in value and "method" not in value: + future = pending.pop(value["id"], None) + if future and not future.done(): + future.set_result(value) + elif "id" in value: + report["unexpected_server_requests"].append(value["method"]) + await send({"id": value["id"], "error": {"code": -32601, "message": "Unexpected request in scripted metadata probe"}}) + raise RuntimeError("app-server stdout closed") + except Exception as error: + for future in pending.values(): + if not future.done(): + future.set_exception(error) + + reader = asyncio.create_task(read_messages()) + + async def request(method, params): + nonlocal next_id + assert method in ["initialize", "thread/start", "mcpServerStatus/list", "mcpServer/tool/call", "turn/start"] + next_id += 1 + message = {"id": next_id, "method": method, "params": params} + future = asyncio.get_running_loop().create_future() + pending[next_id] = future + start = time.monotonic() + await send(message) + reply = await asyncio.wait_for(future, 90) + report["calls"].append({"method": method, "elapsed_ms": (time.monotonic() - start) * 1000, + "error": reply.get("error")}) + if "error" in reply: + raise RuntimeError(json.dumps(reply["error"])[:4000]) + return reply["result"] + + try: + await request("initialize", {"clientInfo": {"name": "rea_host_probe", "title": "REA host compatibility probe", "version": "1"}, + "capabilities": {"experimentalApi": True, "explicitGatewayOauth": True}}) + await send({"method": "initialized"}) + created = await request("thread/start", {"cwd": str(workspace), "ephemeral": True, + "approvalPolicy": "never", "sandbox": "read-only"}) + thread_id = created["thread"]["id"] + assert not created["thread"].get("turns", []) + inventory = await request("mcpServerStatus/list", {"threadId": thread_id, + "serverName": "rea_audit", "detail": "toolsAndAuthOnly", "limit": 10}) + assert inventory.get("nextCursor") is None + assert len(inventory["data"]) == 1 + server = inventory["data"][0] + assert server["name"] == "rea_audit" and server.get("toolsError") is None + tools = list(server["tools"].values()) + names = [tool["name"] for tool in tools] + assert len(names) == len(set(names)) == args.expected_tools + assert "gdb_console" in names + output_schemas = sum(tool.get("outputSchema") is not None for tool in tools) + assert output_schemas == (args.expected_tools if args.output_profile == "full" else 0) + raw = json.dumps(inventory, separators=(",", ":"), ensure_ascii=False).encode() + (args.output / "catalog.json").write_bytes(raw) + report["catalog"] = {"tools": len(tools), "output_schemas": output_schemas, + "bytes": len(raw), "sha256": hashlib.sha256(raw).hexdigest(), + "server_info": server.get("serverInfo"), "runtime_status": server.get("runtimeStatus")} + await request("turn/start", {"threadId":thread_id,"input":[{"type":"text","text":f"Read architecture and the complete byte-annotated disassembly of {args.symbol} in the selected Bash executable. Retain and export complete Evidence, then close the owned debugger. The local fixture scripts the required tools; no AI inference or target execution is used."}]}) + await asyncio.wait_for(completed_turn.wait(), 90) + assert report["turn"]["status"] == "completed", report["turn"] + assert state["normal_requests"]==steps+1 + assert len(captured)==steps+1+state["compactions"] + outputs=known_outputs + assert len(outputs)==steps + bundle=json.loads((args.output/'evidence.json').read_text()) + evidence=next(e for e in bundle['records'] if e['operation']=='gdb_console' and e['parameters']['command']==f'disassemble /r {args.symbol}') + assert evidence['subject'] is None + assert evidence['normalized_result']['completion_status']=='complete' and not evidence['normalized_result']['output_truncated'] + delivered_result=outputs['call_local_6'] + delivery_complete=verify_command_delivery(delivered_result,evidence,args.mode,args.budget) + if delivery_complete and args.mode=='summary': + assert next(e for e in bundle['records'] if e['evidence_id']==delivered_result['evidence_id'])==delivered_result + report['task_completed']=delivery_complete + report['observations']={'parent_evidence_id':evidence['evidence_id'],'exported':True,'native_console_utf16_units':len(evidence['normalized_result']['console'].encode('utf-16-le'))//2,'client_delivered_full_evidence':delivery_complete,'selected_symbol':args.symbol,'tool_output_budget':args.budget or 'client default','subject':evidence['subject']} + report["captured_requests"]=len(captured) + report["compaction_requests"]=state["compactions"] + report["compaction_backend"]="Deterministic checkpoint of observed identifiers and retained-body location; no AI inference or quality claim." + report["mode"]=args.mode + report["request_tool_counts"] = [len(c["body"].get("tools", [])) for c in captured] + assert not report["unexpected_server_requests"] + assert hashlib.sha256(args.input.read_bytes()).hexdigest()==oracle["artifact_sha256"] + report["status"] = "passed" if report["task_completed"] else "expected-incomplete-task" + except Exception: + report["status"] = "failed" + report["failure"] = traceback.format_exc()[-6000:] + finally: + process.stdin.close() + try: + await asyncio.wait_for(process.wait(), 15) + except asyncio.TimeoutError: + report["cleanup_failures"].append("app-server did not exit after stdin close; terminated owned process") + process.terminate() + try: + await asyncio.wait_for(process.wait(), 5) + except asyncio.TimeoutError: + process.kill() + await process.wait() + await reader + # app-server can acknowledge exit before its MCP child finishes EOF + # cleanup. Wait on the allocated resource scope; never target a PID. + try: + groups = [line[3:] for line in Path("/proc/self/cgroup").read_text().splitlines() + if line.startswith("0::")] + assert len(groups) == 1 + group = Path("/sys/fs/cgroup") / groups[0].lstrip("/") + assert re.fullmatch(r"rea-efficiency-[0-9a-f]{32}\.scope", group.name) + observer = os.getppid() + observer_args = Path(f"/proc/{observer}/cmdline").read_bytes().split(b"\0") + assert any(Path(os.fsdecode(value)).name == "resource_payload.py" for value in observer_args if value) + expected = {os.getpid(), observer} + until = time.monotonic() + 10 + while True: + actors = {int(value) for value in (group / "cgroup.procs").read_text().splitlines()} + assert expected <= actors + remaining = sorted(actors - expected) + if not remaining or time.monotonic() >= until: + break + await asyncio.sleep(0.05) + report["owned_scope_drain"] = {"remaining_processes": remaining} + if remaining: + report["cleanup_failures"].append("owned MCP child did not settle after app-server exit") + except (OSError, ValueError, AssertionError) as error: + report["owned_scope_drain"] = {"status": "unavailable", "reason": type(error).__name__} + report["cleanup_failures"].append("allocated resource-scope cleanup could not be verified") + endpoint.shutdown() + endpoint.server_close() + endpoint_thread.join(5) + stderr.close() + report["appserver_exit_code"] = process.returncode + if process.returncode != 0: + report["cleanup_failures"].append(f"app-server exit code: {process.returncode}") + (args.output / "transcript.json").write_text(json.dumps(transcript) + "\n") + (args.output / "report.json").write_text(json.dumps(report, indent=2) + "\n") + print(json.dumps({key: report.get(key) for key in ["status", "failure", "catalog", "observations", "cleanup_failures", "appserver_exit_code"]})) + return 0 if report["status"] in ["passed","expected-incomplete-task"] and not report["cleanup_failures"] else 1 + + +raise SystemExit(asyncio.run(main())) diff --git a/experiments/analysis-efficiency/command_observation_delivery.py b/experiments/analysis-efficiency/command_observation_delivery.py new file mode 100644 index 0000000000..7939b43d0d --- /dev/null +++ b/experiments/analysis-efficiency/command_observation_delivery.py @@ -0,0 +1,39 @@ +"""Acceptance of the captured GDB answer, separate from native execution success.""" + + +def verify_command_delivery(delivered, parent, mode, budget): + assert mode in ('complete', 'summary') + if '_invalid_json_payload' in delivered: + assert mode == 'complete' and budget == 0, ( + 'Incomplete delivery is allowed only for the ' + 'complete/default-budget baseline' + ) + return False + if mode == 'complete': + assert delivered == parent + return True + + selected = delivered['normalized_result'] + assert selected['parent_evidence_id'] == parent['evidence_id'] + assert selected['artifact'] is None + profile = parent['analysis_profile'] + expected_profile = None if profile is None else profile['digest'] + assert selected['source_analysis_profile_digest'] == expected_profile + assert selected['source_parameters'] == parent['parameters'] + assert selected['source_provider'] == parent['provider'] + assert delivered['raw_result'] is None and delivered['subject'] is None + expected = { + key: value for key, value in parent['normalized_result'].items() + if key not in ('console', 'target', 'log') + } + actual = { + key: value for key, value in selected['summary'].items() + if key != 'available_counts' + } + assert actual == expected + counts = {'records': len(expected['records'])} + for name in ('console', 'target', 'log'): + text = parent['normalized_result'][name] + counts[name + '_utf16_units'] = len(text.encode('utf-16-le')) // 2 + assert selected['summary']['available_counts'] == counts + return True diff --git a/experiments/analysis-efficiency/test_command_observation_delivery.py b/experiments/analysis-efficiency/test_command_observation_delivery.py new file mode 100644 index 0000000000..f65cc58486 --- /dev/null +++ b/experiments/analysis-efficiency/test_command_observation_delivery.py @@ -0,0 +1,81 @@ +"""A truncated summary must never qualify as a completed investigation.""" +from copy import deepcopy +import unittest +from command_observation_delivery import verify_command_delivery + + +class CommandDelivery(unittest.TestCase): + def test_truncation_is_accepted_only_for_complete_default_baseline(self): + truncated = {'_invalid_json_payload': '{truncated'} + self.assertFalse(verify_command_delivery(truncated, {}, 'complete', 0)) + for mode, budget in [ + ('summary', 0), ('summary', 30000), ('complete', 30000), + ]: + with self.subTest(mode=mode, budget=budget): + with self.assertRaises(AssertionError): + verify_command_delivery(truncated, {}, mode, budget) + + def test_valid_complete_answer_can_complete_under_either_budget(self): + parent = {'evidence_id': 'parent'} + for budget in [0, 30000]: + self.assertTrue( + verify_command_delivery(parent, parent, 'complete', budget) + ) + with self.assertRaises(AssertionError): + verify_command_delivery({'error': 'failed command'}, parent, 'complete', 0) + + def test_summary_must_preserve_every_record_and_accurate_stream_counts(self): + report = { + 'command': 'disassemble /r entry', 'mi': '7^done', + 'records': ['~"🧪\\n"', '=async', 'unknown', '7^done'], + 'console': '🧪', 'target': '', 'log': '', + 'completion_status': 'complete', 'output_truncated': False, + } + parent = { + 'evidence_id': 'parent', + 'parameters': {'command': report['command']}, + 'provider': {'id': 'gdb'}, 'analysis_profile': None, + 'normalized_result': report, + } + facts = { + key: value for key, value in report.items() + if key not in ('console', 'target', 'log') + } + selected = { + 'evidence_id': 'derived', 'raw_result': None, 'subject': None, + 'normalized_result': { + 'parent_evidence_id': 'parent', 'artifact': None, + 'source_analysis_profile_digest': None, + 'source_parameters': parent['parameters'], + 'source_provider': parent['provider'], + 'summary': { + **facts, + 'available_counts': { + 'records': 4, 'console_utf16_units': 2, + 'target_utf16_units': 0, 'log_utf16_units': 0, + }, + }, + }, + } + self.assertTrue(verify_command_delivery(selected, parent, 'summary', 0)) + for damage in ['records', 'counts', 'parameters', 'profile']: + bad = deepcopy(selected) + result = bad['normalized_result'] + if damage == 'records': + result['summary']['records'].pop(1) + elif damage == 'counts': + result['summary']['available_counts']['console_utf16_units'] = 1 + elif damage == 'profile': + result['source_analysis_profile_digest'] = 'wrong' + else: + result['source_parameters'] = {} + with self.subTest(damage=damage): + with self.assertRaises(AssertionError): + verify_command_delivery(bad, parent, 'summary', 0) + parent['analysis_profile'] = {'digest': 'recorded-profile'} + selected['normalized_result']['source_analysis_profile_digest'] = 'recorded-profile' + self.assertTrue(verify_command_delivery(selected, parent, 'summary', 0)) + + +if __name__ == '__main__': + unittest.main() diff --git a/src/application/reverse/ReverseEngineeringCommandPort.ts b/src/application/reverse/ReverseEngineeringCommandPort.ts index 947af38dd2..92a014e83f 100644 --- a/src/application/reverse/ReverseEngineeringCommandPort.ts +++ b/src/application/reverse/ReverseEngineeringCommandPort.ts @@ -1,6 +1,6 @@ import type { EvidenceProvider } from "../../domain/evidence.js"; import type { - ObjdumpInput, + ObjdumpSelection, RizinInput, } from "../../domain/reverseEngineering.js"; @@ -14,6 +14,6 @@ export interface ReverseEngineeringProviderCommand { /** Provider-specific command construction stays outside the application layer. */ export interface ReverseEngineeringCommandPort { - objdump(input: ObjdumpInput): ReverseEngineeringProviderCommand; + objdump(input: ObjdumpSelection): ReverseEngineeringProviderCommand; rizin(input: RizinInput): ReverseEngineeringProviderCommand; } diff --git a/src/application/reverse/ReverseEngineeringService.ts b/src/application/reverse/ReverseEngineeringService.ts index b9592e0703..b28db77b21 100644 --- a/src/application/reverse/ReverseEngineeringService.ts +++ b/src/application/reverse/ReverseEngineeringService.ts @@ -3,6 +3,11 @@ import { createReadStream } from "node:fs"; import { stat as statPath } from "node:fs/promises"; import type { Stats } from "node:fs"; +import { + requireAnalysisViewRetention, + summarizeRetainedAnalysis, + type AnalysisViewRetention, +} from "../analysisView/AnalysisViewService.js"; import { AnalysisAccessDeniedError, AnalysisCancelledError, @@ -179,29 +184,41 @@ export class ReverseEngineeringService { if (!cleanup.ok) throw cleanup.error; } - inspectWithObjdump( + async inspectWithObjdump( rawInput: unknown, options: { readonly signal?: AbortSignal } = {}, + retention: AnalysisViewRetention = { + evidenceById: undefined, + recordEvidence: undefined, + }, ): Promise> { const parsed = objdumpInputSchema.safeParse(rawInput); if (!parsed.success) - return Promise.resolve( - err(new AnalysisInputError("inspect_with_objdump")), + return err(new AnalysisInputError("inspect_with_objdump")); + const { detail, ...input } = parsed.data; + if (detail === "summary") { + const ready = requireAnalysisViewRetention( + "inspect_with_objdump", + retention, ); - const input = parsed.data; + if (!ready.ok) return ready; + } const command = this.#commands.objdump(input); let parameters: Readonly>; if (input.operation === "disassemble") { const { symbol, ...base } = input; parameters = symbol === undefined ? base : { ...base, symbol }; } else parameters = input; - return this.#observe( + const observed = await this.#observe( "inspect_with_objdump", command, input.operation === "architectures" ? undefined : input.path, parameters, options.signal, ); + return observed.ok && detail === "summary" + ? summarizeRetainedAnalysis(observed.value, retention) + : observed; } executeRizinCommand( diff --git a/src/cli/reverseEngineeringCommands.ts b/src/cli/reverseEngineeringCommands.ts index 8df885b537..a027246f16 100644 --- a/src/cli/reverseEngineeringCommands.ts +++ b/src/cli/reverseEngineeringCommands.ts @@ -22,6 +22,11 @@ import { gdbOneShotCommandMayInvalidateDetachProof, } from "../gdb/GdbSessionManager.js"; import { RizinDebugSessionManager } from "../rizin/RizinDebugSessionManager.js"; +import { + analysisEvidenceDeliveryOptions, + cliEvidenceDeliveryInputError, + publishCliAnalysisEvidence, +} from "./analysisEvidenceDelivery.js"; const executeWithCleanup = async ( service: ReverseEngineeringService, @@ -117,6 +122,7 @@ export const registerReverseEngineeringCommands = ( .describe("GNU objdump inspection operation to run."), }), options: z.object({ + ...analysisEvidenceDeliveryOptions, symbol: z .string() .optional() @@ -133,6 +139,15 @@ export const registerReverseEngineeringCommands = ( run: ({ args, options }) => withCommandCancellation((signal) => logCliCommand(logger, CLI_COMMANDS.inspectWithObjdump, async () => { + const delivery = { + providerId: "gnu.objdump", + operation: "inspect_with_objdump", + signal, + detail: options.detail, + evidenceOutput: options.evidence_output, + }; + const inputError = cliEvidenceDeliveryInputError(delivery); + if (inputError !== undefined) return projectAnalysisError(inputError); const result = await executeWithCleanup(service, () => service.inspectWithObjdump( { @@ -145,19 +160,32 @@ export const registerReverseEngineeringCommands = ( { signal }, ), ); - return result.ok ? result.value : projectAnalysisError(result.error); + return result.ok + ? publishCliAnalysisEvidence(result.value, delivery) + : projectAnalysisError(result.error); }), ), }); cli.command(CLI_COMMANDS.listObjdumpArchitectures, { description: "List BFD targets advertised by the installed GNU objdump", args: z.object({}), - run: () => + options: z.object(analysisEvidenceDeliveryOptions), + run: ({ options }) => withCommandCancellation((signal) => logCliCommand( logger, CLI_COMMANDS.listObjdumpArchitectures, async () => { + const delivery = { + providerId: "gnu.objdump", + operation: "inspect_with_objdump", + signal, + detail: options.detail, + evidenceOutput: options.evidence_output, + }; + const inputError = cliEvidenceDeliveryInputError(delivery); + if (inputError !== undefined) + return projectAnalysisError(inputError); const result = await executeWithCleanup(service, () => service.inspectWithObjdump( { operation: "architectures" }, @@ -165,7 +193,7 @@ export const registerReverseEngineeringCommands = ( ), ); return result.ok - ? result.value + ? publishCliAnalysisEvidence(result.value, delivery) : projectAnalysisError(result.error); }, ), @@ -206,10 +234,20 @@ export const registerReverseEngineeringCommands = ( .describe( "Caller-selected durable TTY for output from a live GDB inferior.", ), + ...analysisEvidenceDeliveryOptions, }), run: ({ args, options }) => withCommandCancellation((signal) => logCliCommand(logger, CLI_COMMANDS.debugWithGdb, async () => { + const delivery = { + providerId: "gdb", + operation: "gdb_console", + signal, + detail: options.detail, + evidenceOutput: options.evidence_output, + }; + const inputError = cliEvidenceDeliveryInputError(delivery); + if (inputError !== undefined) return projectAnalysisError(inputError); if (gdbOneShotCommandMayInvalidateDetachProof(args.command)) return projectAnalysisError( new AnalysisInputError("gdb_console", undefined, [ @@ -223,34 +261,52 @@ export const registerReverseEngineeringCommands = ( ); const manager = createGdbManager(environment); let sessionId: string | undefined; - try { - const started = await manager.start(signal, { - inferiorTty: options.inferiorTty, - }); - if (!started.ok) return projectAnalysisError(started.error); - sessionId = started.value.session_id; - const executed = await manager.execute( - sessionId, - args.command, - signal, - ); - return executed.ok - ? executed.value.evidence - : projectAnalysisError(executed.error); - } finally { - let cleanupFailed = false; - if (sessionId !== undefined) { - try { - const closed = await manager.close(sessionId); - cleanupFailed = !closed.ok; - if (!closed.ok) + const completed = await (async () => { + try { + const started = await manager.start(signal, { + inferiorTty: options.inferiorTty, + }); + if (!started.ok) return projectAnalysisError(started.error); + sessionId = started.value.session_id; + const executed = await manager.execute( + sessionId, + args.command, + signal, + ); + return executed.ok + ? executed.value.evidence + : projectAnalysisError(executed.error); + } finally { + let cleanupFailed = false; + if (sessionId !== undefined) { + try { + const closed = await manager.close(sessionId); + cleanupFailed = !closed.ok; + if (!closed.ok) + logger.error( + { + command: CLI_COMMANDS.debugWithGdb, + cleanup: projectAnalysisError(closed.error), + }, + "GDB cleanup failed after collecting command output", + ); + } catch (cause) { + cleanupFailed = true; logger.error( { command: CLI_COMMANDS.debugWithGdb, - cleanup: projectAnalysisError(closed.error), + cleanup: + cause instanceof Error + ? cause.message + : "Unknown cleanup failure", }, "GDB cleanup failed after collecting command output", ); + } + } + try { + await manager.closeAll(); + cleanupFailed = false; } catch (cause) { cleanupFailed = true; logger.error( @@ -261,28 +317,13 @@ export const registerReverseEngineeringCommands = ( ? cause.message : "Unknown cleanup failure", }, - "GDB cleanup failed after collecting command output", + "GDB fallback cleanup failed", ); } + if (cleanupFailed) process.exitCode = 1; } - try { - await manager.closeAll(); - cleanupFailed = false; - } catch (cause) { - cleanupFailed = true; - logger.error( - { - command: CLI_COMMANDS.debugWithGdb, - cleanup: - cause instanceof Error - ? cause.message - : "Unknown cleanup failure", - }, - "GDB fallback cleanup failed", - ); - } - if (cleanupFailed) process.exitCode = 1; - } + })(); + return publishCliAnalysisEvidence(completed, delivery); }), ), }); diff --git a/src/contracts/analysisViewToolContracts.ts b/src/contracts/analysisViewToolContracts.ts index 56b76ea7e6..f103a619af 100644 --- a/src/contracts/analysisViewToolContracts.ts +++ b/src/contracts/analysisViewToolContracts.ts @@ -17,7 +17,7 @@ export const ANALYSIS_VIEW_TOOL_CONTRACTS = [ ...toolContractMetadata("inspect_analysis_view"), kind: "application", description: - 'Inspect selected views of completed Go build metadata, binary-layout, JavaScript application, native analyze_function, inspect_artifact, inventory_artifact, inspect_wasm_artifact, inspect_macho, decode_interface_builder, inspect_evm_interface, inspect_managed_artifact, inspect_managed_members, inspect_android_package, inspect_dart_aot, inspect_il2cpp_metadata, inspect_model_artifact or inspect_web_network_capture Evidence without repeating analysis. Historical capture summaries return identity and counts without bodies or archive inventories. Capture views page retained records: view {kind: "capture", detail: "preview" | "complete", offset: 0, limit: 20}; preview selects recorded headline fields and binary descriptors, complete returns exact records with all sidecars. Offsets follow the selected parent record order, preserving original producer ordinals. Paging never reopens the source or reruns the decoder. Go metadata uses kind go, collection dependencies/settings, exact paths/keys or indices, fields and pagination. Row index refers to the original collection; empty selections return no rows, and dependency coverage remains unknown. For inspect_pcap_capture parents, kind pcap pages endpoints, dns-queries, tls-sni, protocols, http-uris or unattributed-addresses. Select an exact query for dns-addresses, or a tshark/capinfos stream for UTF-16 diagnostics text. Page totals count retained facts; source coverage remains explicit. Qlog summaries keep profile/scope and counts; kind qlog pages traces, streams, loss-declarations, incomplete-records, contributions, ranges or final-sizes. Except traces, select trace_index; stream-specific facets also require exact stream_id and direction. Facts retain all-recorded-events-before-selection scope, and interval overlap or endpoint loss declarations do not prove retransmission or network loss. The api-surface view groups retained HAR requests by method, origin and format-rule path template; templated segments are inferences. It reads no headers or bodies and reports no query values. The environment-manifest view projects the same HAR requests on the primary origin (most JSON-like records) into a replayable tool surface: view {kind: "environment-manifest", template_rules: ["after-uid-literal", "opaque-token", "sibling-variance"], include_query_parameter_names: true}. Each tool has a deterministic name, a path template and parameters from the four format rules plus the selected opt-in rules, observed query parameter names, a value-free body shape for POST/PUT/PATCH, status and content-type sets and record ordinals; records are pointers into the parent, excluded groups carry a reason and three fixed unknowns are always stated. Templates, parameters and names are inferences; the view copies no header, cookie or body values and refuses parents inspected with sensitive values. A capture_browser_scenario parent that selected response bodies or header values projects the same way, one record per network transaction in request order; it relies on capture-time redaction of declared secrets instead of that refusal, and a transaction whose URL was redacted is left unrepresented. The environment-state view extracts, from the same parents, the initial state the recorded reads observed: view {kind: "environment-state", template_rules: [...]} with the rules of the manifest it accompanies. It reads response bodies recorded before the first related write and reports entity types with an identity field (proved by a path parameter or inferred from a field name), entities in collection order with per-view values where views differ, documents for non-parameterized non-entity paths written by POST/PUT/PATCH (item-shaped and delete-only paths stay recorded), and a render plan per read tool: item and list views reproduce their latest recordings from the state, and every other read tool is a recorded view with a reason. Types and identity are inferences; values are recorded data, so it refuses parents inspected with sensitive values, and a scenario parent must have selected response bodies; its values keep the capture redaction markers, and a write whose URL it redacted ends the state of every type. For a retained record use source: {kind: "retained-evidence", evidence_id: ""}. Use normalized_result.parent_evidence_id from a summary receipt. Source kind is required. Portable source uses {kind: "inline", evidence: }. A completed Evidence JSON file on this host uses {kind: "evidence-file", path: ""}: REA reads and authenticates it, then projects it like retained Evidence, so a large capture is bounded by the file reader rather than by inline-input accounting. For layout, JavaScript, inventory, WASM, Mach-O, Interface Builder, EVM, managed artifact, Android package and Dart AOT sources, request a summary with view: {kind: "summary"}; summary is a view kind and has no facet. The WASM summary gives validation, counts, WAT digest/length, tool profile and execution/network states without reading WAT text. Select a layout mitigations/linkage facet, one section/symbol/module, or a stable page. Module pages include assets, bundled and source modules; use node_id for ambiguous or unavailable paths. Typed module references use view {kind: "javascript", collection: "modules", projection: "references", where: {path: "main.js", kind: "javascript-module"}, offset: 0, limit: 64}; where matches exact retained strings (null selects unavailable paths). Rows include canonical identity, retained source-range count and observation support states. Source coverage and selection remain explicit; exhausting matching rows does not prove runtime loading or complete analysis. For exact file/native facts and processing gaps, use view {kind: "javascript-facts", where: {paths: ["package.json"]}, property_keys: ["name", "operation", "analysis_status"]}. Rows page original observations with canonical identities and Evidence; missing properties remain omitted. Paths match recorded artifact-path/source-range locations; optional kinds filter nodes. To discover field names without values, opt into include_available_property_keys: true with property_keys: []. Canonical identity and Evidence are separate from properties; keep each node_id/observation_id pair together when composing results. Inventory uses view: {kind: "inventory", collection: "occurrences", logical_paths: ["package.json"], offset: 0, limit: 64}. Inventory views page nodes, occurrences, edges or integrity contradictions. Occurrences accept exact logical_paths and occurrence_ids; nodes accept artifact_ids. Batch filters intersect (omitted means all available, [] means none); fields selects columns while retaining identity links. source_complete describes retained graph membership; source_coverage describes producer coverage, including unknown encrypted PCK directories. Page exhaustion does not prove absence in a partial source. WASM views use kind wasm and facet sections/imports/exports/candidates/glue/wat/headers/details/tool_profile; text facets use UTF-16 code units and source forms remain exact. Mach-O views use kind macho and facet entrypoints/architectures/build_metadata/load_commands/dependencies/imports/exports/segments/provenance; source_exhaustive false means unreturned rows may exist. Interface Builder views use kind interface-builder and collection documents/nodes/edges/coverage; nodes and edges accept an exact document relative_path. EVM summary returns all inferred interface candidates, carrier/decoded identities and explicit unknowns without hex or duplicate stdout. EVM views use kind evm and facet functions/bytecode_hex/stdout/stderr; function pages preserve the original candidate rows, and text offsets/limits count UTF-16 code units (hex has two characters per decoded byte). Exhausting candidate pages does not establish global interface completeness. Managed artifact summaries preserve complete identity/classification, metadata status, module/MVID, frameworks, coverage and limitations without unrelated inventories. Managed pages use kind managed and collection references/resources/attributes, retaining source coverage and build-local token scope; page exhaustion does not establish complete metadata. Managed member summaries keep identity, selection with unmatched names, coverage and per-collection counts; managed-members pages use collection types/exported_types/fields/methods/member_refs/call_edges/field_accesses/user_strings and carry the source selection and coverage. Android package summaries retain all declarations, engine identity and counts without decoded manifest text. Android manifest pages use kind android, facet manifest, offset and limit in UTF-16 code units; source_status preserves partial decoded coverage even when the retained text page is exhausted. Dart AOT summary preserves target/ABI/library identity, snapshot headers and pool lower bounds. String pages use kind dart-aot, collection package_uris/dart_uris/class_like_tokens; source coverage remains partial when the producer says partial. Protobuf views use kind protobuf, collection bodies/frames/fields, with record_ordinals, body_pointers and fields; field pages also select frame_indices, field_ids, field_numbers or schema_names. Byte identity, offset basis and body status remain present. Text and schema_value are opt-in page columns. Model views use kind model-artifact and collection metadata (keys), tensors (names/types) or types (derived totals), with indices, fields and paging. Header SHA-256 scopes declarations; full-file identity remains unknown unless full hashing was selected. Arrays retain typed eight-element previews with explicit omissions. IL2CPP metadata views use kind il2cpp-metadata and collection sections/images/types/methods/fields/properties/events/parameters/string_literals/code_gen_modules/method_addresses/return_types/field_types/parameter_types. Select indices, fields and pages; named rows accept names, types also accept image_indices/namespaces/declaring_types, and methods/members accept declaring_types. Parameters and parameter_types accept method_indices; code_gen_modules accept image_indices; method_addresses accept addresses/states, preserving every folded alternative. Row index is scoped to the metadata digest; native pages add binary_source. Uncollected native tables have null counts and remain unavailable. Desktop directory views use kind desktop-application and collection files/omissions/candidates/components/next_probes, with fields and pagination; files and omissions accept exact paths or their stable IDs, and candidates accept families. Source coverage and stable row IDs remain available. Native inventory Evidence from list_names, list/search_procedures or list/search_strings uses view: {kind: "native-inventory", offset: 0, limit: 64, fields: ["value"]}. Rows keep source_index and address even with selected fields. Summary counts describe retained rows; provider discovery coverage remains unknown. Native function Evidence requires kind native: view: {kind: "native", facet: "procedure"}. Facets select procedure, pseudocode, assembly, basic blocks, comments, callers/callees, references, unresolved calls, referenced strings/names, native API and high-pcode. Use nonnegative offset and a positive caller-selected limit (defaults 0 and 64); pseudocode uses UTF-16 code units, other native pages use rows. Returns artifact identity, parent Evidence ID, a distinct view digest, coverage, limitations and unknowns. Actual serialized size determines MCP transport admission; choose a smaller page or export the complete retained Evidence if needed.', + 'Inspect selected views of completed GDB/objdump command observations, Go build metadata, binary-layout, JavaScript application, native analyze_function, inspect_artifact, inventory_artifact, inspect_wasm_artifact, inspect_macho, decode_interface_builder, inspect_evm_interface, inspect_managed_artifact, inspect_managed_members, inspect_android_package, inspect_dart_aot, inspect_il2cpp_metadata, inspect_model_artifact or inspect_web_network_capture Evidence without repeating analysis. Command summary views preserve ordered GDB records or opaque objdump stdout/stderr once, source parameters, completion facts and parent identity. They do not establish current target state. Historical capture summaries return identity and counts without bodies or archive inventories. Capture views page retained records: view {kind: "capture", detail: "preview" | "complete", offset: 0, limit: 20}; preview selects recorded headline fields and binary descriptors, complete returns exact records with all sidecars. Offsets follow the selected parent record order, preserving original producer ordinals. Paging never reopens the source or reruns the decoder. Go metadata uses kind go, collection dependencies/settings, exact paths/keys or indices, fields and pagination. Row index refers to the original collection; empty selections return no rows, and dependency coverage remains unknown. For inspect_pcap_capture parents, kind pcap pages endpoints, dns-queries, tls-sni, protocols, http-uris or unattributed-addresses. Select an exact query for dns-addresses, or a tshark/capinfos stream for UTF-16 diagnostics text. Page totals count retained facts; source coverage remains explicit. Qlog summaries keep profile/scope and counts; kind qlog pages traces, streams, loss-declarations, incomplete-records, contributions, ranges or final-sizes. Except traces, select trace_index; stream-specific facets also require exact stream_id and direction. Facts retain all-recorded-events-before-selection scope, and interval overlap or endpoint loss declarations do not prove retransmission or network loss. The api-surface view groups retained HAR requests by method, origin and format-rule path template; templated segments are inferences. It reads no headers or bodies and reports no query values. The environment-manifest view projects the same HAR requests on the primary origin (most JSON-like records) into a replayable tool surface: view {kind: "environment-manifest", template_rules: ["after-uid-literal", "opaque-token", "sibling-variance"], include_query_parameter_names: true}. Each tool has a deterministic name, a path template and parameters from the four format rules plus the selected opt-in rules, observed query parameter names, a value-free body shape for POST/PUT/PATCH, status and content-type sets and record ordinals; records are pointers into the parent, excluded groups carry a reason and three fixed unknowns are always stated. Templates, parameters and names are inferences; the view copies no header, cookie or body values and refuses parents inspected with sensitive values. A capture_browser_scenario parent that selected response bodies or header values projects the same way, one record per network transaction in request order; it relies on capture-time redaction of declared secrets instead of that refusal, and a transaction whose URL was redacted is left unrepresented. The environment-state view extracts, from the same parents, the initial state the recorded reads observed: view {kind: "environment-state", template_rules: [...]} with the rules of the manifest it accompanies. It reads response bodies recorded before the first related write and reports entity types with an identity field (proved by a path parameter or inferred from a field name), entities in collection order with per-view values where views differ, documents for non-parameterized non-entity paths written by POST/PUT/PATCH (item-shaped and delete-only paths stay recorded), and a render plan per read tool: item and list views reproduce their latest recordings from the state, and every other read tool is a recorded view with a reason. Types and identity are inferences; values are recorded data, so it refuses parents inspected with sensitive values, and a scenario parent must have selected response bodies; its values keep the capture redaction markers, and a write whose URL it redacted ends the state of every type. For a retained record use source: {kind: "retained-evidence", evidence_id: ""}. Use normalized_result.parent_evidence_id from a summary receipt. Source kind is required. Portable source uses {kind: "inline", evidence: }. A completed Evidence JSON file on this host uses {kind: "evidence-file", path: ""}: REA reads and authenticates it, then projects it like retained Evidence, so a large capture is bounded by the file reader rather than by inline-input accounting. For layout, JavaScript, inventory, WASM, Mach-O, Interface Builder, EVM, managed artifact, Android package and Dart AOT sources, request a summary with view: {kind: "summary"}; summary is a view kind and has no facet. The WASM summary gives validation, counts, WAT digest/length, tool profile and execution/network states without reading WAT text. Select a layout mitigations/linkage facet, one section/symbol/module, or a stable page. Module pages include assets, bundled and source modules; use node_id for ambiguous or unavailable paths. Typed module references use view {kind: "javascript", collection: "modules", projection: "references", where: {path: "main.js", kind: "javascript-module"}, offset: 0, limit: 64}; where matches exact retained strings (null selects unavailable paths). Rows include canonical identity, retained source-range count and observation support states. Source coverage and selection remain explicit; exhausting matching rows does not prove runtime loading or complete analysis. For exact file/native facts and processing gaps, use view {kind: "javascript-facts", where: {paths: ["package.json"]}, property_keys: ["name", "operation", "analysis_status"]}. Rows page original observations with canonical identities and Evidence; missing properties remain omitted. Paths match recorded artifact-path/source-range locations; optional kinds filter nodes. To discover field names without values, opt into include_available_property_keys: true with property_keys: []. Canonical identity and Evidence are separate from properties; keep each node_id/observation_id pair together when composing results. Inventory uses view: {kind: "inventory", collection: "occurrences", logical_paths: ["package.json"], offset: 0, limit: 64}. Inventory views page nodes, occurrences, edges or integrity contradictions. Occurrences accept exact logical_paths and occurrence_ids; nodes accept artifact_ids. Batch filters intersect (omitted means all available, [] means none); fields selects columns while retaining identity links. source_complete describes retained graph membership; source_coverage describes producer coverage, including unknown encrypted PCK directories. Page exhaustion does not prove absence in a partial source. WASM views use kind wasm and facet sections/imports/exports/candidates/glue/wat/headers/details/tool_profile; text facets use UTF-16 code units and source forms remain exact. Mach-O views use kind macho and facet entrypoints/architectures/build_metadata/load_commands/dependencies/imports/exports/segments/provenance; source_exhaustive false means unreturned rows may exist. Interface Builder views use kind interface-builder and collection documents/nodes/edges/coverage; nodes and edges accept an exact document relative_path. EVM summary returns all inferred interface candidates, carrier/decoded identities and explicit unknowns without hex or duplicate stdout. EVM views use kind evm and facet functions/bytecode_hex/stdout/stderr; function pages preserve the original candidate rows, and text offsets/limits count UTF-16 code units (hex has two characters per decoded byte). Exhausting candidate pages does not establish global interface completeness. Managed artifact summaries preserve complete identity/classification, metadata status, module/MVID, frameworks, coverage and limitations without unrelated inventories. Managed pages use kind managed and collection references/resources/attributes, retaining source coverage and build-local token scope; page exhaustion does not establish complete metadata. Managed member summaries keep identity, selection with unmatched names, coverage and per-collection counts; managed-members pages use collection types/exported_types/fields/methods/member_refs/call_edges/field_accesses/user_strings and carry the source selection and coverage. Android package summaries retain all declarations, engine identity and counts without decoded manifest text. Android manifest pages use kind android, facet manifest, offset and limit in UTF-16 code units; source_status preserves partial decoded coverage even when the retained text page is exhausted. Dart AOT summary preserves target/ABI/library identity, snapshot headers and pool lower bounds. String pages use kind dart-aot, collection package_uris/dart_uris/class_like_tokens; source coverage remains partial when the producer says partial. Protobuf views use kind protobuf, collection bodies/frames/fields, with record_ordinals, body_pointers and fields; field pages also select frame_indices, field_ids, field_numbers or schema_names. Byte identity, offset basis and body status remain present. Text and schema_value are opt-in page columns. Model views use kind model-artifact and collection metadata (keys), tensors (names/types) or types (derived totals), with indices, fields and paging. Header SHA-256 scopes declarations; full-file identity remains unknown unless full hashing was selected. Arrays retain typed eight-element previews with explicit omissions. IL2CPP metadata views use kind il2cpp-metadata and collection sections/images/types/methods/fields/properties/events/parameters/string_literals/code_gen_modules/method_addresses/return_types/field_types/parameter_types. Select indices, fields and pages; named rows accept names, types also accept image_indices/namespaces/declaring_types, and methods/members accept declaring_types. Parameters and parameter_types accept method_indices; code_gen_modules accept image_indices; method_addresses accept addresses/states, preserving every folded alternative. Row index is scoped to the metadata digest; native pages add binary_source. Uncollected native tables have null counts and remain unavailable. Desktop directory views use kind desktop-application and collection files/omissions/candidates/components/next_probes, with fields and pagination; files and omissions accept exact paths or their stable IDs, and candidates accept families. Source coverage and stable row IDs remain available. Native inventory Evidence from list_names, list/search_procedures or list/search_strings uses view: {kind: "native-inventory", offset: 0, limit: 64, fields: ["value"]}. Rows keep source_index and address even with selected fields. Summary counts describe retained rows; provider discovery coverage remains unknown. Native function Evidence requires kind native: view: {kind: "native", facet: "procedure"}. Facets select procedure, pseudocode, assembly, basic blocks, comments, callers/callees, references, unresolved calls, referenced strings/names, native API and high-pcode. Use nonnegative offset and a positive caller-selected limit (defaults 0 and 64); pseudocode uses UTF-16 code units, other native pages use rows. Returns artifact identity, parent Evidence ID, a distinct view digest, coverage, limitations and unknowns. Actual serialized size determines MCP transport admission; choose a smaller page or export the complete retained Evidence if needed.', inputSchema: inspectAnalysisViewInputSchema, outputSchema: evidenceResultOf(analysisViewResultSchema), examples: [ diff --git a/src/contracts/gdbToolContracts.ts b/src/contracts/gdbToolContracts.ts index 03e4868f49..de3526202b 100644 --- a/src/contracts/gdbToolContracts.ts +++ b/src/contracts/gdbToolContracts.ts @@ -1,25 +1,16 @@ import { z } from "zod"; import { localPathStringSchema } from "../domain/localPath.js"; +import { + commandEvidenceDetailSchema, + gdbCommandResultSchema, +} from "../domain/commandObservation.js"; +import { gdbCommandSummaryViewResultSchema } from "../domain/analysisView/analysisView.js"; import type { ToolContract } from "./toolContractTypes.js"; -import { evidenceResultOf } from "./toolOutputSchemaPrimitives.js"; +import { analysisDetailOutputSchemaOf } from "./analysisDetail.js"; import { toolContractMetadata } from "./toolEffects.js"; const sessionId = z.string().uuid(); -const gdbCommandResult = z.strictObject({ - command: z.string(), - mi: z.string(), - records: z.array(z.string()), - console: z.string(), - target: z.string(), - log: z.string(), - process_exit_observed: z.boolean().optional(), - process_exit_code: z.number().int().nullable().optional(), - process_exit_signal: z.string().nullable().optional(), - output_truncated: z.boolean(), - output_invalid_utf8: z.literal(true).optional(), - completion_status: z.enum(["complete", "unknown"]), -}); /** Persistent GDB/MI session contracts hosted by the existing REA MCP server. */ export const GDB_TOOL_CONTRACTS = [ @@ -47,12 +38,16 @@ export const GDB_TOOL_CONTRACTS = [ ...toolContractMetadata("gdb_console"), kind: "native-provider", description: - "Run one unrestricted GDB CLI command through MI interpreter-exec console and return raw MI, console, target, and log records inline with Evidence. A command timeout preserves collected records with completion_status unknown and stops the owned debugger session. Commands can execute shell, alter target state, access files, and use network. Closing the debugger never implies target termination. In a shared MCP server, this admitted command closes any active binary-analysis session; after its effects have settled, call open_binary before further analysis. Close an active Ghidra session explicitly first.", + "Run one unrestricted GDB CLI command through MI interpreter-exec console and return raw MI, console, target, and log records inline with Evidence. Default detail complete preserves the original Evidence. Explicit detail summary retains that complete parent and returns every ordered MI record once, omitting duplicate stream groupings and raw mirrors; parent_evidence_id supports later inspect_analysis_view summary queries and complete Evidence export. A command timeout preserves collected records with completion_status unknown and stops the owned debugger session. Commands can execute shell, alter target state, access files, and use network. Closing the debugger never implies target termination. In a shared MCP server, this admitted command closes any active binary-analysis session; after its effects have settled, call open_binary before further analysis. Close an active Ghidra session explicitly first.", inputSchema: z.strictObject({ session_id: sessionId, command: z.string().min(1), + detail: commandEvidenceDetailSchema, }), - outputSchema: evidenceResultOf(gdbCommandResult), + outputSchema: analysisDetailOutputSchemaOf( + gdbCommandResultSchema, + gdbCommandSummaryViewResultSchema, + ), examples: [ { title: "Inspect loaded symbols", diff --git a/src/contracts/reverseEngineeringToolContracts.ts b/src/contracts/reverseEngineeringToolContracts.ts index 41b7d5b7d0..c194ad7cb0 100644 --- a/src/contracts/reverseEngineeringToolContracts.ts +++ b/src/contracts/reverseEngineeringToolContracts.ts @@ -1,4 +1,5 @@ -import { z } from "zod"; +import { commandObservationSchema } from "../domain/commandObservation.js"; +import { objdumpSummaryViewResultSchema } from "../domain/analysisView/analysisView.js"; import { objdumpInputSchema, @@ -6,22 +7,9 @@ import { } from "../domain/reverseEngineering.js"; import type { ToolContract } from "./toolContractTypes.js"; import { evidenceResultOf } from "./toolOutputSchemaPrimitives.js"; +import { analysisDetailOutputSchemaOf } from "./analysisDetail.js"; import { toolContractMetadata } from "./toolEffects.js"; -const commandObservationSchema = z.strictObject({ - stdout: z.string(), - stderr: z.string(), - exit_code: z.union([z.string(), z.number(), z.null()]), - signal: z.string().nullable(), - output_truncated: z.boolean(), - completion_status: z.enum(["complete", "unknown"]), - artifact_sha256_after: z - .string() - .regex(/^[a-f0-9]{64}$/u) - .nullable(), - artifact_changed: z.boolean().nullable(), -}); - /** Caller-selected GNU objdump and Rizin observations. */ export const REVERSE_ENGINEERING_TOOL_CONTRACTS = [ { @@ -29,9 +17,12 @@ export const REVERSE_ENGINEERING_TOOL_CONTRACTS = [ ...toolContractMetadata("inspect_with_objdump"), kind: "native-provider", description: - "Run caller-selected GNU objdump inspection against a local artifact, or request operation=architectures without a path to list BFD targets advertised by the installed binary. For operation=disassemble, optional symbol selects native symbol-scoped disassembly before output collection; omission preserves all code sections. Matching and symbol boundaries follow GNU objdump. Preserves decoded stdout and stderr inline with SHA-256 Evidence for artifact inspections, up to a shared 16 MiB combined limit; invalid UTF-8 bytes are represented by replacement characters and flagged in Evidence limitations. output_truncated reports output beyond that limit and completion_status is unknown if buffer enforcement terminated the process. The raw output is authoritative and REA does not claim a stable normalized format. DWARF does not follow local debug links by default. Set follow_debug_links to true to follow them; identities of external debug files are not bound to Evidence. REA clears DEBUGINFOD_URLS for objdump so DWARF processing does not make debuginfod network requests. Architecture output is subjectless and does not establish support for any artifact.", + "Run caller-selected GNU objdump inspection against a local artifact, or request operation=architectures without a path to list BFD targets advertised by the installed binary. For operation=disassemble, optional symbol selects native symbol-scoped disassembly before output collection; omission preserves all code sections. Matching and symbol boundaries follow GNU objdump. Preserves decoded stdout and stderr inline with SHA-256 Evidence for artifact inspections, up to a shared 16 MiB combined limit; invalid UTF-8 bytes are represented by replacement characters and flagged in Evidence limitations. output_truncated reports output beyond that limit and completion_status is unknown if buffer enforcement terminated the process. The raw output is authoritative and REA does not claim a stable normalized format. DWARF does not follow local debug links by default. Set follow_debug_links to true to follow them; identities of external debug files are not bound to Evidence. REA clears DEBUGINFOD_URLS for objdump so DWARF processing does not make debuginfod network requests. Default detail complete returns original Evidence. Explicit detail summary retains the complete parent and returns its command observation once, without the raw mirror; parent_evidence_id supports later inspect_analysis_view summary queries and complete Evidence export. Architecture output is subjectless and does not establish support for any artifact.", inputSchema: objdumpInputSchema, - outputSchema: evidenceResultOf(commandObservationSchema), + outputSchema: analysisDetailOutputSchemaOf( + commandObservationSchema, + objdumpSummaryViewResultSchema, + ), examples: [ { title: "Disassemble code sections", diff --git a/src/domain/analysisView/analysisView.ts b/src/domain/analysisView/analysisView.ts index fb34dbfc7f..29ca29cb99 100644 --- a/src/domain/analysisView/analysisView.ts +++ b/src/domain/analysisView/analysisView.ts @@ -5,6 +5,12 @@ import { modelArtifactViewItemsSchema, modelArtifactViewContextSchema, } from "./modelArtifactView.js"; +import { + commandSummaryContext, + gdbCommandSummarySchema, + projectCommandObservationView, +} from "./commandObservationView.js"; +import { commandObservationSchema } from "../commandObservation.js"; import { goBinarySummarySchema, goBinaryViewContextSchema, @@ -378,6 +384,8 @@ const viewResultBase = { parent_operation: z.enum([ "inspect_binary_layout", "inspect_go_binary", + "gdb_console", + "inspect_with_objdump", "inspect_desktop_application", "analyze_javascript_application", "analyze_function", @@ -412,6 +420,23 @@ const summaryViewResultBase = { ...viewResultBase, } as const; +/** Opt-in command delivery keeps output once and retains the complete parent. */ +export const gdbCommandSummaryViewResultSchema = z.strictObject({ + ...summaryViewResultBase, + ...commandSummaryContext, + parent_operation: z.literal("gdb_console"), + artifact: artifactSchema.nullable(), + summary: gdbCommandSummarySchema, +}); + +export const objdumpSummaryViewResultSchema = z.strictObject({ + ...summaryViewResultBase, + ...commandSummaryContext, + parent_operation: z.literal("inspect_with_objdump"), + artifact: artifactSchema.nullable(), + summary: commandObservationSchema, +}); + /** Opt-in Go delivery retains original bytes and exposes build identity/counts. */ export const goBinarySummaryViewResultSchema = z.strictObject({ ...summaryViewResultBase, @@ -618,6 +643,8 @@ export const analysisViewResultSchema = z.discriminatedUnion("kind", [ view: environmentStateViewRequestSchema, }), z.discriminatedUnion("parent_operation", [ + gdbCommandSummaryViewResultSchema, + objdumpSummaryViewResultSchema, goBinarySummaryViewResultSchema, binaryLayoutSummaryViewResultSchema, desktopApplicationSummaryViewResultSchema, @@ -903,6 +930,8 @@ const parentArtifactPath = (parent: AnalysisViewParent): string => { /** Named source owners for views that need no extra decoder or accumulator. */ const retainedViewProjectors = new Map([ + ["gdb_console", projectCommandObservationView], + ["inspect_with_objdump", projectCommandObservationView], ["inspect_go_binary", projectGoBinaryView], ...NATIVE_INVENTORY_OPERATIONS.map( (operation) => [operation, projectNativeInventoryView] as const, diff --git a/src/domain/analysisView/commandObservationView.ts b/src/domain/analysisView/commandObservationView.ts new file mode 100644 index 0000000000..530331138c --- /dev/null +++ b/src/domain/analysisView/commandObservationView.ts @@ -0,0 +1,176 @@ +import { z } from "zod"; +import type { AnalysisError } from "../analysisErrorBase.js"; +import { + commandObservationSchema, + gdbCommandResultSchema, +} from "../commandObservation.js"; +import { digestSchema } from "../digests.js"; +import { providerSchema } from "../evidence.js"; +import { analysisInputErrorFromIssues } from "../inputIssueProjection.js"; +import { jsonValueSchema } from "../jsonValue.js"; +import { err, ok, type Result } from "../result.js"; +import type { + AnalysisViewParent, + AnalysisViewRequest, + UnsignedAnalysisView, +} from "./analysisView.js"; +import { + boundedViewMessages, + completeWithinViewCoverage, +} from "./analysisViewCoverage.js"; +import { analysisViewInputError } from "./analysisViewErrors.js"; +import { parseAnalysisViewResult } from "./analysisViewParent.js"; + +export const commandSummaryContext = { + source_provider: providerSchema.nullable(), + source_analysis_profile_digest: digestSchema.nullable(), + source_parameters: jsonValueSchema, +}; + +/** Keep every ordered record, including asynchronous and unrecognized records. */ +export const gdbCommandSummarySchema = gdbCommandResultSchema + .omit({ console: true, target: true, log: true }) + .extend({ + available_counts: z.strictObject({ + records: z.number().int().nonnegative(), + console_utf16_units: z.number().int().nonnegative(), + target_utf16_units: z.number().int().nonnegative(), + log_utf16_units: z.number().int().nonnegative(), + }), + }); + +/** Select existing owner facts without parsing a command or its output protocol. */ +export const projectCommandObservationView = ( + parent: AnalysisViewParent, + view: AnalysisViewRequest, +): Result => { + if (view.kind !== "summary") + return err( + analysisViewInputError([ + { + path: ["view"], + reason: "invalid_value", + message: "Command observations support summary views.", + }, + ]), + ); + if ( + parent.operation !== "gdb_console" && + parent.operation !== "inspect_with_objdump" + ) + return err( + analysisViewInputError([ + { + path: ["source", "operation"], + reason: "invalid_value", + message: "Unsupported command-observation operation.", + }, + ]), + ); + const parameters = jsonValueSchema.safeParse(parent.parameters ?? null); + if (!parameters.success) + return err( + analysisInputErrorFromIssues( + "inspect_analysis_view", + parameters.error.issues.map((issue) => ({ + ...issue, + path: ["source", "parameters", ...issue.path], + })), + { source: { parameters: parent.parameters } }, + { cause: parameters.error }, + ), + ); + const artifact = + parent.subject === null + ? null + : parent.subject === undefined + ? (parent.artifact ?? null) + : { + path: parent.subject.local_path, + sha256: parent.subject.digest.sha256, + }; + const shared = { + kind: "summary" as const, + view, + parent_evidence_id: parent.evidenceId, + parent_digest: parent.evidenceId.slice(3), + artifact, + source_provider: parent.provider ?? null, + source_analysis_profile_digest: parent.analysisProfileDigest ?? null, + source_parameters: parameters.data, + coverage: completeWithinViewCoverage(1, 1), + limitations: boundedViewMessages( + parent.limitations, + parent.evidenceId, + "limitations", + ), + unknowns: boundedViewMessages( + [ + "This retained command observation does not establish current target state or global analysis coverage.", + ], + parent.evidenceId, + "unknowns", + ), + }; + if (parent.operation === "gdb_console") { + const parsed = parseAnalysisViewResult(gdbCommandResultSchema, parent); + if (!parsed.ok) return parsed; + const selection = parameters.data; + if ( + typeof selection === "object" && + selection !== null && + !Array.isArray(selection) && + typeof selection.command === "string" && + selection.command !== parsed.value.command + ) + return err( + analysisViewInputError([ + { + path: ["source", "parameters", "command"], + reason: "invalid_value", + message: + "The captured command conflicts with its source parameters.", + }, + ]), + ); + const { console, target, log, ...records } = parsed.value; + return ok({ + ...shared, + parent_operation: "gdb_console", + summary: { + ...records, + available_counts: { + records: records.records.length, + console_utf16_units: console.length, + target_utf16_units: target.length, + log_utf16_units: log.length, + }, + }, + }); + } + const parsed = parseAnalysisViewResult(commandObservationSchema, parent); + if (!parsed.ok) return parsed; + const report = parsed.value; + if ( + artifact !== null && + report.artifact_sha256_after !== null && + report.artifact_changed !== null && + report.artifact_changed !== + (artifact.sha256 !== report.artifact_sha256_after) + ) + return err( + analysisViewInputError([ + { + path: ["source", "normalized_result", "artifact_changed"], + reason: "invalid_value", + message: + "The recorded artifact-change flag conflicts with the observed digests.", + }, + ]), + ); + return ok({ + ...shared, + parent_operation: "inspect_with_objdump", + summary: report, + }); +}; diff --git a/src/domain/commandObservation.ts b/src/domain/commandObservation.ts new file mode 100644 index 0000000000..77322c1258 --- /dev/null +++ b/src/domain/commandObservation.ts @@ -0,0 +1,44 @@ +import { z } from "zod"; + +/** Opaque producer text and the command capture's explicit completion facts. */ +export const commandObservationSchema = z.strictObject({ + stdout: z.string(), + stderr: z.string(), + exit_code: z.union([z.string(), z.number(), z.null()]), + signal: z.string().nullable(), + output_truncated: z.boolean(), + completion_status: z.enum(["complete", "unknown"]), + artifact_sha256_after: z + .string() + .regex(/^[a-f0-9]{64}$/u) + .nullable(), + artifact_changed: z.boolean().nullable(), +}); + +/** The GDB owner retains ordered records and derives its stream groupings. */ +export const gdbCommandResultSchema = z.strictObject({ + command: z.string(), + mi: z.string(), + records: z.array(z.string()), + console: z.string(), + target: z.string(), + log: z.string(), + process_exit_observed: z.boolean().exactOptional(), + process_exit_code: z.number().int().nullable().exactOptional(), + process_exit_signal: z.string().nullable().exactOptional(), + output_truncated: z.boolean(), + output_invalid_utf8: z.literal(true).exactOptional(), + completion_status: z.enum(["complete", "unknown"]), +}); + +/** Read-only capture facts; protocol interpretation stays with the provider. */ +export type GdbCommandResult = Readonly< + Omit, "records"> +> & { readonly records: readonly string[] }; + +export const commandEvidenceDetailSchema = z + .enum(["complete", "summary"]) + .default("complete") + .describe( + "complete returns the original Evidence; summary retains that complete parent and returns command output once with completion and source context.", + ); diff --git a/src/domain/reverseEngineering.ts b/src/domain/reverseEngineering.ts index 58ad7ffb38..a011ea3d45 100644 --- a/src/domain/reverseEngineering.ts +++ b/src/domain/reverseEngineering.ts @@ -1,4 +1,5 @@ import { z } from "zod"; +import { commandEvidenceDetailSchema } from "./commandObservation.js"; export const OBJDUMP_ARTIFACT_OPERATIONS = [ "file_headers", @@ -35,11 +36,17 @@ const objdumpDisassemblyInputSchema = objdumpArtifactInputSchema.extend({ .optional(), }); -/** Caller-selected artifact facet or pathless provider capability query. */ +const objdumpArchitecturesInputSchema = z.strictObject({ + operation: z.literal("architectures"), +}); + +/** Caller-selected native selection and independent Evidence delivery. */ export const objdumpInputSchema = z.discriminatedUnion("operation", [ - objdumpArtifactInputSchema, - objdumpDisassemblyInputSchema, - z.strictObject({ operation: z.literal("architectures") }), + objdumpArtifactInputSchema.extend({ detail: commandEvidenceDetailSchema }), + objdumpDisassemblyInputSchema.extend({ detail: commandEvidenceDetailSchema }), + objdumpArchitecturesInputSchema.extend({ + detail: commandEvidenceDetailSchema, + }), ]); /** Caller-selected Rizin command against one local artifact. */ @@ -56,5 +63,8 @@ export const rizinInputSchema = z }) .strict(); -export type ObjdumpInput = z.input; +export type ObjdumpSelection = + | z.input + | z.input + | z.input; export type RizinInput = z.input; diff --git a/src/gdb/GdbSessionManager.ts b/src/gdb/GdbSessionManager.ts index dcde0c90f7..c714ee7535 100644 --- a/src/gdb/GdbSessionManager.ts +++ b/src/gdb/GdbSessionManager.ts @@ -15,6 +15,7 @@ import { type AnalysisErrorOptions, } from "../domain/analysisErrorBase.js"; import { createEvidence, type Evidence } from "../domain/evidence.js"; +import type { GdbCommandResult } from "../domain/commandObservation.js"; import { err, ok, type Result } from "../domain/result.js"; import { ProviderProcessSupervisor, @@ -89,21 +90,6 @@ interface PendingCommand { timer: ReturnType; } -export interface GdbCommandResult { - readonly command: string; - readonly mi: string; - readonly records: readonly string[]; - readonly console: string; - readonly target: string; - readonly log: string; - readonly process_exit_observed?: boolean; - readonly process_exit_code?: number | null; - readonly process_exit_signal?: NodeJS.Signals | null; - readonly output_truncated: boolean; - readonly completion_status: "complete" | "unknown"; - readonly output_invalid_utf8?: true; -} - class GdbCommandTimeoutError extends AnalysisTimeoutError { constructor( readonly partial: GdbCommandResult, diff --git a/src/objdump/ObjdumpCommand.ts b/src/objdump/ObjdumpCommand.ts index 823f91760f..ae899672c9 100644 --- a/src/objdump/ObjdumpCommand.ts +++ b/src/objdump/ObjdumpCommand.ts @@ -1,4 +1,4 @@ -import type { ObjdumpInput } from "../domain/reverseEngineering.js"; +import type { ObjdumpSelection } from "../domain/reverseEngineering.js"; import { absoluteCallerPath } from "../filesystem/callerPath.js"; export const OBJDUMP_PROVIDER_IDENTITY = { @@ -8,7 +8,7 @@ export const OBJDUMP_PROVIDER_IDENTITY = { } as const; /** Build GNU objdump argv without invoking a shell or following DWARF links by default. */ -export const objdumpCommand = (input: ObjdumpInput): readonly string[] => { +export const objdumpCommand = (input: ObjdumpSelection): readonly string[] => { if (input.operation === "architectures") return ["-i"]; const argumentsByOperation = { file_headers: ["-f"], diff --git a/src/server/createServer.ts b/src/server/createServer.ts index 8f8cf90867..d3eee29988 100644 --- a/src/server/createServer.ts +++ b/src/server/createServer.ts @@ -525,6 +525,7 @@ export const createServer = ( operation, rebindFailure, ), + evidenceById, ); registerFridaTools( server, @@ -554,6 +555,7 @@ export const createServer = ( ? undefined : (evidence) => session.recordEvidence(evidence), session, + evidenceById, ); registerRizinDebugTools( server, diff --git a/src/server/registerGdbTools.ts b/src/server/registerGdbTools.ts index 3b8fa44777..6ce117ef5c 100644 --- a/src/server/registerGdbTools.ts +++ b/src/server/registerGdbTools.ts @@ -3,9 +3,14 @@ import type { Logger } from "pino"; import type { GdbSessionManager } from "../gdb/GdbSessionManager.js"; import type { EvidenceWriter } from "../application/investigation/InvestigationRecordPort.js"; +import type { EvidenceLookup } from "../application/EvidenceInputResolver.js"; +import { + requireAnalysisViewRetention, + summarizeRetainedAnalysis, +} from "../application/analysisView/AnalysisViewService.js"; import { toolContract } from "../contracts/toolContracts.js"; import { evidenceSchema } from "../domain/evidence.js"; -import { err } from "../domain/result.js"; +import { err, ok } from "../domain/result.js"; import { AnalysisInputError } from "../domain/analysisErrorCore.js"; import { runOwnedCleanupWithExternalMutationAdmission, @@ -29,6 +34,7 @@ export const registerGdbTools = ( delivery: ToolResultDelivery, recordEvidence?: EvidenceWriter["recordEvidence"], session?: ExternalMutationAdmission, + evidenceById?: EvidenceLookup, ): void => { const start = toolContract("start_gdb_session"); const consoleContract = toolContract("gdb_console"); @@ -70,6 +76,14 @@ export const registerGdbTools = ( consoleContract.name, toolRegistrationOptions(consoleContract), async (input, context: ServerContext) => { + const retention = { recordEvidence, evidenceById }; + if (input.detail === "summary") { + const ready = requireAnalysisViewRetention( + consoleContract.name, + retention, + ); + if (!ready.ok) return delivery.toCallToolResult(ready, consoleContract); + } const execute = () => logToolExecution(logger, consoleContract.name, () => manager.execute( @@ -78,7 +92,7 @@ export const registerGdbTools = ( context.mcpReq.signal, ), ); - const result = await runWithExternalMutationAdmission( + const executed = await runWithExternalMutationAdmission( session, consoleContract.name, context.mcpReq.signal, @@ -86,6 +100,11 @@ export const registerGdbTools = ( (value) => value.evidence, () => manager.retryCleanupFailures(), ); + const result = !executed.ok + ? executed + : input.detail === "summary" + ? summarizeRetainedAnalysis(executed.value.evidence, retention) + : ok(executed.value.evidence); if (!result.ok) { const partial = result.error.partialObservation; const parsed = evidenceSchema.safeParse(partial); @@ -101,9 +120,9 @@ export const registerGdbTools = ( } return delivery.toCallToolResult(result, consoleContract); } - const recorded = recordEvidence?.(result.value.evidence); + const recorded = recordEvidence?.(result.value); return delivery.toEvidenceToolResult( - result.value.evidence, + result.value, consoleContract, recorded, ); diff --git a/src/server/registerReverseEngineeringTools.ts b/src/server/registerReverseEngineeringTools.ts index 49ccbb1ff3..8e686dcf11 100644 --- a/src/server/registerReverseEngineeringTools.ts +++ b/src/server/registerReverseEngineeringTools.ts @@ -7,6 +7,7 @@ import type { import type { Logger } from "pino"; import type { ReverseEngineeringService } from "../application/reverse/ReverseEngineeringService.js"; +import type { EvidenceLookup } from "../application/EvidenceInputResolver.js"; import type { EvidenceWriter } from "../application/investigation/InvestigationRecordPort.js"; import type { Evidence } from "../domain/evidence.js"; import { toolContract } from "../contracts/toolContracts.js"; @@ -38,6 +39,7 @@ export const registerReverseEngineeringTools = ( error: AnalysisError, ) => AnalysisError, ) => Promise, AnalysisError>>, + evidenceById?: EvidenceLookup, ): void => { const objdump = toolContract("inspect_with_objdump"); const rizin = toolContract("execute_rizin_command"); @@ -52,9 +54,13 @@ export const registerReverseEngineeringTools = ( delivery, async () => { const result = await logToolExecution(logger, objdump.name, () => - service.inspectWithObjdump(input, { - signal: context.mcpReq.signal, - }), + service.inspectWithObjdump( + input, + { + signal: context.mcpReq.signal, + }, + { recordEvidence, evidenceById }, + ), ); if (!result.ok) return deliverFailure( diff --git a/tests/boundary/cli/commandEvidenceDelivery.test.ts b/tests/boundary/cli/commandEvidenceDelivery.test.ts new file mode 100644 index 0000000000..eecf5f10f7 --- /dev/null +++ b/tests/boundary/cli/commandEvidenceDelivery.test.ts @@ -0,0 +1,209 @@ +import { Cli, z } from "incur"; +import pino from "pino"; +import { readFile, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { expect, it, onTestFinished, vi } from "vitest"; +import { registerReverseEngineeringCommands } from "../../../src/cli/reverseEngineeringCommands.js"; +import type { CliInstance } from "../../../src/cli/types.js"; +import { CLI_COMMANDS } from "../../../src/cliCommandNames.js"; +import { + createEvidence, + parseEvidence, + evidenceSchema, +} from "../../../src/domain/evidence.js"; +import { ok } from "../../../src/domain/result.js"; +import { GdbSessionManager } from "../../../src/gdb/GdbSessionManager.js"; +import { TestReverseEngineeringService } from "../../support/reverseEngineeringService.js"; +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; + +const kinds = ["objdump", "architectures", "gdb"] as const; +const fixture = async (kind: (typeof kinds)[number]) => { + const directory = await createTestTempDirectory("rea-command-delivery-"); + const path = join(directory, "input.bin"); + await writeFile(path, "local CLI publication fixture"); + const runProvider = vi.fn(async () => ({ + stdout: "opaque provider output\n", + stderr: "provider diagnostic\n", + })); + const service = new TestReverseEngineeringService({ + environment: {}, + run: runProvider, + }); + const closeService = vi.spyOn(service, "close"); + const manager = new GdbSessionManager({ platform: "linux", environment: {} }); + const sessionId = "00000000-0000-4000-8000-000000000001"; + const report = { + command: "show architecture", + mi: "5^done", + records: ['~"architecture\\n"', "unknown record", "5^done"], + console: '~"architecture\\n"', + target: "", + log: "", + completion_status: "complete" as const, + output_truncated: false, + }; + const parent = createEvidence( + undefined, + { id: "gdb", name: "GDB", version: null }, + { + operation: "gdb_console", + parameters: { session_id: sessionId, command: report.command }, + result: report, + rawResult: report, + }, + ); + const start = vi + .spyOn(manager, "start") + .mockResolvedValue(ok({ session_id: sessionId, mi_version: "mi3" })); + vi.spyOn(manager, "execute").mockResolvedValue( + ok({ result: report, evidence: parent }), + ); + const close = vi + .spyOn(manager, "close") + .mockResolvedValue(ok({ target_state: "none_observed" })); + const closeAll = vi.spyOn(manager, "closeAll").mockResolvedValue(undefined); + const createManager = vi.fn(() => manager); + const cli = Cli.create("rea-cli-delivery-test", { + version: "1", + sync: false, + }); + registerReverseEngineeringCommands( + cli as unknown as CliInstance, + pino({ enabled: false }), + {}, + createManager, + () => service, + ); + onTestFinished(async () => { + await service.close(); + await manager.closeAll(); + }); + const commandName = + kind === "gdb" + ? CLI_COMMANDS.debugWithGdb + : kind === "objdump" + ? CLI_COMMANDS.inspectWithObjdump + : CLI_COMMANDS.listObjdumpArchitectures; + const command = Cli.toCommands.get(cli)?.get(commandName); + if (command === undefined) throw new Error("CLI command was not registered"); + const argsSchema = Reflect.get(command, "args"); + const optionsSchema = Reflect.get(command, "options"); + const execute = Reflect.get(command, "run"); + if ( + !(argsSchema instanceof z.ZodObject) || + !(optionsSchema instanceof z.ZodObject) || + typeof execute !== "function" + ) + throw new Error("Missing CLI command schemas or callback"); + const args = + kind === "gdb" + ? { command: report.command } + : kind === "objdump" + ? { path, operation: "file_headers" } + : {}; + const invoke = async (options: Record): Promise => + execute({ + args: argsSchema.parse(args), + options: optionsSchema.parse({ + ...(kind === "gdb" ? { inferiorTty: "/dev/pts/caller-fixture" } : {}), + ...options, + }), + }); + return { + directory, + invoke, + runProvider, + createManager, + start, + close, + closeAll, + closeService, + parent, + report, + }; +}; + +it.each(kinds)( + "publishes %s summaries with exact complete parents", + async (kind) => { + const f = await fixture(kind); + const destination = join(f.directory, "complete.json"); + const output = z + .object({ + summary: evidenceSchema, + evidence_bundle: z.object({ path: z.string(), bytes: z.number() }), + }) + .parse( + await f.invoke({ detail: "summary", evidence_output: destination }), + ); + const selected = parseEvidence(output.summary); + const serialized = await readFile(destination, "utf8"); + const bundle = JSON.parse(serialized); + expect(output.evidence_bundle.bytes).toBe(Buffer.byteLength(serialized)); + expect(bundle.records).toHaveLength(1); + const parent = parseEvidence(bundle.records[0]); + expect(parent.raw_result).toEqual(parent.normalized_result); + expect(selected.normalized_result).toMatchObject({ + parent_evidence_id: parent.evidence_id, + source_parameters: parent.parameters, + source_provider: parent.provider, + }); + expect(selected.raw_result).toBeNull(); + expect(output.evidence_bundle.path).toBe(destination); + if (kind === "gdb") { + expect(parent).toEqual(f.parent); + expect(selected.normalized_result).toMatchObject({ + artifact: null, + summary: { records: f.report.records }, + }); + expect(f.close).toHaveBeenCalledOnce(); + expect(f.closeAll).toHaveBeenCalledOnce(); + } else { + expect(selected.normalized_result).toMatchObject({ + summary: parent.normalized_result, + }); + expect(f.runProvider).toHaveBeenCalledOnce(); + expect(f.closeService).toHaveBeenCalledOnce(); + } + if (kind !== "objdump") { + expect(parent.subject).toBeNull(); + expect(selected.subject).toBeNull(); + expect(selected.normalized_result).toMatchObject({ artifact: null }); + } + }, +); + +it.each(kinds)( + "rejects %s summary without a destination before provider work", + async (kind) => { + const f = await fixture(kind); + const output = await f.invoke({ detail: "summary" }); + expect(output).toMatchObject({ code: "invalid_request" }); + expect(JSON.stringify(output)).toContain("evidence_output"); + expect(f.createManager).not.toHaveBeenCalled(); + expect(f.start).not.toHaveBeenCalled(); + expect(f.runProvider).not.toHaveBeenCalled(); + }, +); + +it.each(kinds)( + "preserves an existing %s destination and reports publication failure with source Evidence", + async (kind) => { + const f = await fixture(kind); + const destination = join(f.directory, "existing.json"); + const original = Buffer.from("caller-selected content\n"); + await writeFile(destination, original); + const output = await f.invoke({ + detail: "summary", + evidence_output: destination, + }); + expect(await readFile(destination)).toEqual(original); + expect(output).toHaveProperty("code"); + expect(output).not.toHaveProperty("summary"); + expect(output).toHaveProperty("details.partial_observation.evidence_id"); + if (kind === "gdb") { + expect(f.close).toHaveBeenCalledOnce(); + expect(f.closeAll).toHaveBeenCalledOnce(); + } else expect(f.closeService).toHaveBeenCalledOnce(); + }, +); diff --git a/tests/boundary/cli/reverseEngineeringCommands.test.ts b/tests/boundary/cli/reverseEngineeringCommands.test.ts index bc1d6a3b4b..29a61acf0e 100644 --- a/tests/boundary/cli/reverseEngineeringCommands.test.ts +++ b/tests/boundary/cli/reverseEngineeringCommands.test.ts @@ -150,7 +150,7 @@ it("lists objdump architectures without inventing an artifact path", async () => if (command === undefined) throw new Error("architecture command was not registered"); - const result = await command.run({}); + const result = await command.run({ options: { detail: "complete" } }); expect(result).toEqual(evidence); expect(inspectWithObjdump).toHaveBeenCalledWith( diff --git a/tests/boundary/mcp/commandObservationViews.test.ts b/tests/boundary/mcp/commandObservationViews.test.ts new file mode 100644 index 0000000000..fae3d31a1d --- /dev/null +++ b/tests/boundary/mcp/commandObservationViews.test.ts @@ -0,0 +1,340 @@ +import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; +import pino from "pino"; +import { expect, it, onTestFinished, vi } from "vitest"; +import { EvidenceMcpServer } from "../../../src/server/EvidenceMcpServer.js"; +import { + registerGdbTools, + type GdbToolManager, +} from "../../../src/server/registerGdbTools.js"; +import { registerReverseEngineeringTools } from "../../../src/server/registerReverseEngineeringTools.js"; +import { registerAnalysisViewTool } from "../../../src/server/registerAnalysisViewTool.js"; +import { createToolResultDelivery } from "../../../src/server/toolResult.js"; +import { + createEvidence, + parseEvidence, + type Evidence, +} from "../../../src/domain/evidence.js"; +import { ok } from "../../../src/domain/result.js"; +import { inspectAnalysisView } from "../../../src/application/analysisView/AnalysisViewService.js"; +import { TestReverseEngineeringService } from "../../support/reverseEngineeringService.js"; +import { advertisedMcpToolSchemas } from "../../fixtures/advertisedMcpTool.js"; + +const sessionId = "00000000-0000-4000-8000-000000000001"; +const capture = { + command: "show architecture", + mi: "7^done", + records: [ + '~"architecture\\n"', + '=thread-created,id="1"', + "unrecognized 🧪 record", + "7^done", + ], + console: '~"architecture\\n"', + target: "", + log: "", + process_exit_observed: false, + process_exit_code: null, + process_exit_signal: null, + output_truncated: true, + output_invalid_utf8: true as const, + completion_status: "unknown" as const, +}; +const gdbEvidence = ( + parameters = { session_id: sessionId, command: capture.command }, +) => + createEvidence( + undefined, + { id: "gdb", name: "GDB", version: null }, + { + operation: "gdb_console", + parameters, + result: capture, + rawResult: capture, + limitations: ["Capture is partial."], + }, + ); + +const connect = async (retain = true) => { + const ledger = new Map(); + const record = (evidence: Evidence) => { + ledger.set(evidence.evidence_id, evidence); + return ok("added" as const); + }; + const lookup = (id: string) => ledger.get(id); + const delivery = createToolResultDelivery(2 * 1024 * 1024); + const server = new EvidenceMcpServer( + { name: "command-views", version: "1" }, + {}, + retain ? record : undefined, + delivery, + ); + const execute = vi.fn(async () => + ok({ result: capture, evidence: gdbEvidence() }), + ); + const manager: GdbToolManager = { + start: async () => + ok({ session_id: sessionId, mi_version: "mi3" as const }), + execute, + status: () => undefined, + close: async () => ok({ target_state: "none_observed" as const }), + retryCleanupFailures: async () => ok(null), + }; + const run = vi.fn(async () => ({ + stdout: "BFD advertised targets\n", + stderr: "diagnostic\n", + })); + const service = new TestReverseEngineeringService({ run }); + const logger = pino({ enabled: false }); + registerGdbTools( + server, + manager, + logger, + delivery, + retain ? record : undefined, + undefined, + retain ? lookup : undefined, + ); + registerReverseEngineeringTools( + server, + service, + logger, + delivery, + retain ? record : undefined, + undefined, + undefined, + undefined, + retain ? lookup : undefined, + ); + registerAnalysisViewTool( + server, + logger, + retain ? lookup : undefined, + retain ? record : undefined, + ); + const client = new Client({ name: "command-view-client", version: "1" }); + onTestFinished(async () => { + await client.close(); + await server.close(); + await service.close(); + }); + const [ct, st] = InMemoryTransport.createLinkedPair(); + await server.connect(st); + await client.connect(ct); + return { client, ledger, execute, run }; +}; + +it("delivers ordered GDB records once and queries their retained parent after close through advertised SDK schemas", async () => { + const { client, ledger } = await connect(); + const schemas = await advertisedMcpToolSchemas(client, "gdb_console"); + const viewSchemas = await advertisedMcpToolSchemas( + client, + "inspect_analysis_view", + ); + const input = { session_id: sessionId, command: capture.command }; + const full = await client.callTool({ name: "gdb_console", arguments: input }); + const summary = await client.callTool({ + name: "gdb_console", + arguments: { ...input, detail: "summary" }, + }); + for (const reply of [full, summary]) { + expect(reply.isError).not.toBe(true); + expect( + schemas.ajv.validate(schemas.outputSchema, reply.structuredContent), + JSON.stringify(schemas.ajv.errors), + ).toBe(true); + } + expect(parseEvidence(full.structuredContent)).toEqual(gdbEvidence()); + const selected = parseEvidence(summary.structuredContent); + expect(selected.raw_result).toBeNull(); + expect(selected.subject).toBeNull(); + expect(selected.normalized_result).toMatchObject({ + parent_evidence_id: gdbEvidence().evidence_id, + artifact: null, + source_provider: gdbEvidence().provider, + source_parameters: gdbEvidence().parameters, + summary: { + records: capture.records, + mi: capture.mi, + completion_status: "unknown", + output_truncated: true, + output_invalid_utf8: true, + process_exit_code: null, + available_counts: { + records: 4, + console_utf16_units: capture.console.length, + target_utf16_units: 0, + log_utf16_units: 0, + }, + }, + }); + expect(selected.normalized_result).not.toHaveProperty("summary.console"); + expect(selected.normalized_result).not.toHaveProperty("summary.target"); + expect(selected.normalized_result).not.toHaveProperty("summary.log"); + await client.callTool({ + name: "close_gdb_session", + arguments: { session_id: sessionId }, + }); + const queried = await client.callTool({ + name: "inspect_analysis_view", + arguments: { + source: { + kind: "retained-evidence", + evidence_id: gdbEvidence().evidence_id, + }, + view: { kind: "summary" }, + }, + }); + expect( + viewSchemas.ajv.validate( + viewSchemas.outputSchema, + queried.structuredContent, + ), + JSON.stringify(viewSchemas.ajv.errors), + ).toBe(true); + expect(parseEvidence(queried.structuredContent)).toEqual(selected); + expect(ledger.get(gdbEvidence().evidence_id)).toEqual(gdbEvidence()); +}); + +it("keeps pathless objdump observations subjectless and preserves the default complete capture", async () => { + const { client, ledger } = await connect(); + const schemas = await advertisedMcpToolSchemas( + client, + "inspect_with_objdump", + ); + expect( + schemas.ajv.validate(schemas.inputSchema, { + operation: "architectures", + detail: "summary", + }), + ).toBe(true); + const full = await client.callTool({ + name: "inspect_with_objdump", + arguments: { operation: "architectures" }, + }); + const selected = await client.callTool({ + name: "inspect_with_objdump", + arguments: { operation: "architectures", detail: "summary" }, + }); + for (const reply of [full, selected]) + expect( + schemas.ajv.validate(schemas.outputSchema, reply.structuredContent), + JSON.stringify(schemas.ajv.errors), + ).toBe(true); + const parent = parseEvidence(full.structuredContent); + const summary = parseEvidence(selected.structuredContent); + expect(summary.normalized_result).toMatchObject({ + parent_evidence_id: parent.evidence_id, + artifact: null, + source_parameters: { operation: "architectures" }, + summary: parent.normalized_result, + }); + expect(summary.raw_result).toBeNull(); + expect(summary.subject).toBeNull(); + expect(ledger.get(parent.evidence_id)).toEqual(parent); +}); + +it("rejects summary without retention before executing either native command", async () => { + const { client, execute, run } = await connect(false); + for (const input of [ + { + name: "gdb_console", + arguments: { + session_id: sessionId, + command: capture.command, + detail: "summary", + }, + }, + { + name: "inspect_with_objdump", + arguments: { operation: "architectures", detail: "summary" }, + }, + ]) + expect((await client.callTool(input)).isError).toBe(true); + expect(execute).not.toHaveBeenCalled(); + expect(run).not.toHaveBeenCalled(); +}); + +it("rejects malformed historical GDB facts and conflicting source commands", () => { + const source = gdbEvidence({ + session_id: sessionId, + command: "a different command", + }); + expect( + inspectAnalysisView({ + source: { kind: "inline", evidence: source }, + view: { kind: "summary" }, + }).ok, + ).toBe(false); + const malformed = createEvidence(undefined, source.provider, { + operation: "gdb_console", + parameters: {}, + result: { ...capture, records: [123] }, + }); + expect( + inspectAnalysisView({ + source: { kind: "inline", evidence: malformed }, + view: { kind: "summary" }, + }).ok, + ).toBe(false); +}); + +it.each([true, false, null])( + "preserves opaque objdump capture and unknown/change facts (%s)", + (changed) => { + const report = { + stdout: "opaque output 🧪\n", + stderr: "diagnostic", + exit_code: "ETIMEDOUT", + signal: "SIGTERM", + output_truncated: true, + completion_status: "unknown", + artifact_sha256_after: + changed === null ? null : (changed ? "b" : "a").repeat(64), + artifact_changed: changed, + }; + const parent = createEvidence( + { + path: "/artifacts/input", + sha256: "a".repeat(64), + format: "elf", + architecture: "x86_64", + }, + { id: "gnu.objdump", name: "GNU objdump", version: null }, + { + operation: "inspect_with_objdump", + parameters: { path: "/artifacts/input", operation: "symbols" }, + result: report, + rawResult: report, + }, + ); + const selected = inspectAnalysisView({ + source: { kind: "inline", evidence: parent }, + view: { kind: "summary" }, + }); + expect(selected.ok).toBe(true); + if (!selected.ok) throw selected.error; + expect(selected.value.normalized_result).toMatchObject({ summary: report }); + if (changed !== null) { + const conflicting = createEvidence( + { + path: "/artifacts/input", + sha256: "a".repeat(64), + format: "elf", + architecture: "x86_64", + }, + parent.provider, + { + operation: parent.operation, + parameters: parent.parameters, + result: { ...report, artifact_changed: !changed }, + }, + ); + expect( + inspectAnalysisView({ + source: { kind: "inline", evidence: conflicting }, + view: { kind: "summary" }, + }).ok, + ).toBe(false); + } + }, +);