mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
Merge pull request #456 from morluto/agent/mcp-resource-snapshots-context-tools
feat!: make evidence bundles canonical MCP resources
This commit is contained in:
47 files changed
+1311
-392
No files matched your search
@@ -389,7 +389,7 @@ REA handles the app analysis in steps 1–5. The agent performs step 6 with its
|
||||
| Electron analysis | 4 | passive root-confined observation, bounded static application mapping, and evidence-backed static/runtime reconciliation |
|
||||
| JavaScript runtime | 2 | approved attach-only Node/Electron Inspector target discovery plus bounded script and execution-context observation without evaluation or instrumentation |
|
||||
| Application workflows | 12 | bounded cross-layer traces, unique-only version matching, historical-source to bundle mapping, static export return-shape comparison, approved Linux-isolated extracted-module replay, managed-runtime characterization, reconstruction coverage closure, deterministic obligation ledgers, and end-to-end readiness conformance |
|
||||
| Workspace and observation | 19 | target lifecycle, Evidence v2 bundles, direct finite replay-machine evaluation, process/artifact/function comparison, evidence-linked residual-unknown lifecycle |
|
||||
| Workspace and observation | 22 | target lifecycle, Evidence v2 bundle snapshots, aggregate navigation/address context, direct finite replay-machine evaluation, process/artifact/function comparison, evidence-linked residual-unknown lifecycle |
|
||||
|
||||
The public interface describes what the agent is trying to learn. Providers decide how to answer. macOS utilities handle common semantic inspection without launching Hopper; Hopper handles deeper native analysis; the process harness implements controlled behavioral capture.
|
||||
|
||||
|
||||
+1
-1
@@ -170,7 +170,7 @@ rea uninstall --purge-data
|
||||
| تحليل Electron | 4 | مراقبة سلبية ضمن جذور canonical، ورسم ثابت ومحدود للتطبيق، ومطابقة ثابتة/وقت تشغيل قائمة على Evidence |
|
||||
| وقت تشغيل JavaScript | 2 | اكتشاف أهداف Node/Electron Inspector المعتمدة للاتصال فقط، ومراقبة محدودة للبرامج النصية وسياقات التنفيذ دون تقييم أو تجهيز |
|
||||
| سير عمل التطبيقات | 12 | تتبع محدود ومقارنة إصدارات فريدة ومقارنة ثابتة لأشكال قيم إرجاع الصادرات وإعادة تشغيل وحدات معزولة ومعتمدة على Linux وتوصيف وقت التشغيل المُدار وإغلاق تغطية إعادة البناء ودفاتر التزامات حتمية والتحقق الشامل من جاهزية إعادة البناء |
|
||||
| جلسة الملف التنفيذي | 19 | فتح الأهداف وحزم الأدلة وتشغيل آلة إعادة محدودة ومقارنة العمليات والقطع والدوال وسجل المجهولات المتبقية |
|
||||
| جلسة الملف التنفيذي | 22 | فتح الأهداف وحزم الأدلة وتشغيل آلة إعادة محدودة ومقارنة العمليات والقطع والدوال وسجل المجهولات المتبقية |
|
||||
|
||||
## استخدام REA مع وكلاء برمجة آخرين
|
||||
|
||||
|
||||
+1
-1
@@ -160,7 +160,7 @@ REA は手順 1〜5 のバイナリ解析を処理し、手順 6 はエージェ
|
||||
| Electron 解析 | 4 | canonical ファイルルート内の受動観察、有界な静的アプリマッピング、Evidence に基づく静的/実行時の照合 |
|
||||
| JavaScript ランタイム | 2 | 承認済みのアタッチ専用 Node/Electron Inspector ターゲット検出と、評価や計装を行わない有界なスクリプト/実行コンテキスト観察 |
|
||||
| アプリケーション | 12 | 有界なクロスレイヤー追跡、一意限定のバージョン比較、静的なエクスポート戻り値形状比較、承認済み Linux 隔離 extracted-module replay、マネージドランタイム特性評価、再構築カバレッジ閉包、決定論的な義務台帳、エンドツーエンドの準備適合性検証 |
|
||||
| バイナリセッション | 19 | `open_binary`、`binary_session`、証拠バンドル、有限リプレイ状態機械、プロセス/成果物/関数比較、残余未知レジストリ |
|
||||
| バイナリセッション | 22 | `open_binary`、`binary_session`、証拠バンドル、有限リプレイ状態機械、プロセス/成果物/関数比較、残余未知レジストリ |
|
||||
|
||||
## 他のコーディングエージェントで使う
|
||||
|
||||
|
||||
+1
-1
@@ -160,7 +160,7 @@ REA는 1–5단계의 바이너리 분석을 처리합니다. 6단계는 에이
|
||||
| Electron 분석 | 4 | canonical 파일 루트 내 수동 관찰, 제한된 정적 앱 매핑, Evidence 기반 정적/런타임 조정 |
|
||||
| JavaScript 런타임 | 2 | 승인된 연결 전용 Node/Electron Inspector 대상 탐색과 평가·계측 없는 제한된 스크립트 및 실행 컨텍스트 관찰 |
|
||||
| 애플리케이션 | 12 | 제한된 교차 계층 추적, 고유 일치 버전 비교, 정적 export 반환 형태 비교, 승인된 Linux 격리 extracted-module replay, 관리형 런타임 특성화, 재구성 커버리지 폐쇄, 결정론적 의무 원장, 엔드투엔드 준비 적합성 검증 |
|
||||
| 바이너리 세션 | 19 | `open_binary`, `binary_session`, 증거 번들, 유한 replay 상태 머신, 프로세스·아티팩트·함수 비교, 잔여 미지수 레지스트리 |
|
||||
| 바이너리 세션 | 22 | `open_binary`, `binary_session`, 증거 번들, 유한 replay 상태 머신, 프로세스·아티팩트·함수 비교, 잔여 미지수 레지스트리 |
|
||||
|
||||
## 다른 코딩 에이전트에서 사용하기
|
||||
|
||||
|
||||
+1
-1
@@ -160,7 +160,7 @@ REA 负责第 1–5 步中的二进制分析。第 6 步由智能体使用其常
|
||||
| Electron 分析 | 4 | 限定 canonical 文件根的被动观察、有界静态应用映射,以及基于 Evidence 的静态/运行时对账 |
|
||||
| JavaScript 运行时 | 2 | 经批准的只附加 Node/Electron Inspector 目标发现,以及不执行求值或插桩的有界脚本和执行上下文观察 |
|
||||
| 应用工作流 | 12 | 有界跨层追踪、唯一匹配跨版本图比较、静态导出返回形状比较、经批准的 Linux 隔离 extracted-module replay、托管运行时表征、重建覆盖闭合、确定性义务账本和端到端就绪性一致性验证 |
|
||||
| 二进制会话 | 19 | `open_binary`、`binary_session`、证据包、有限重放状态机、进程、制品与函数比较、残余未知项注册表 |
|
||||
| 二进制会话 | 22 | `open_binary`、`binary_session`、证据包、有限重放状态机、进程、制品与函数比较、残余未知项注册表 |
|
||||
|
||||
## 与其他编程智能体一起使用
|
||||
|
||||
|
||||
+21
-5
@@ -60,8 +60,10 @@ shutdown rules still apply; REA never kills a process it cannot prove it owns.
|
||||
|
||||
## Evidence resources
|
||||
|
||||
Evidence remains inline for compatibility and successful Evidence-producing
|
||||
tools also return `resource_link` content. Session resources are connection-local:
|
||||
Evidence-producing tools return compact summaries plus `resource_link` content.
|
||||
Every link is accompanied by an ordinary instruction to copy its opaque URI
|
||||
unchanged and call MCP `resources/read` (Codex: `read_mcp_resource`). Session
|
||||
resources are connection-local:
|
||||
|
||||
- `rea://evidence/{evidenceId}` returns the complete immutable Evidence v2 record.
|
||||
- `rea://evidence/{evidenceId}/section/{section}` returns a bounded result section.
|
||||
@@ -69,9 +71,10 @@ tools also return `resource_link` content. Session resources are connection-loca
|
||||
`protocol`, `nodes`, `occurrences`, and `edges` when present.
|
||||
- `rea://unknown/{unknownId}` returns the current residual-unknown head and its
|
||||
immutable revision history.
|
||||
- `rea://unknowns/active` returns current unresolved heads.
|
||||
- `rea://snapshot/{snapshotDigest}` returns the current immutable analysis
|
||||
snapshot when its canonical content digest matches.
|
||||
- `rea://evidence-bundle/{bundleDigest}` returns immutable canonical bytes
|
||||
retained by `snapshot_evidence_bundle`.
|
||||
- `rea://snapshot/current` returns the mutable native analysis cache as an
|
||||
`available` or `unavailable` state.
|
||||
- `rea://artifact/{manifestId}/{collection}` returns a canonical artifact
|
||||
`nodes`, `occurrences`, or `edges` page with Evidence provenance.
|
||||
- `rea://function/{targetSha256}/{address}` returns a retained function dossier
|
||||
@@ -84,11 +87,24 @@ workspace `resource_link` blocks. Workspace resources preserve revision and
|
||||
`previous_revision_digest` commitments; persistent workspace files remain
|
||||
subject to configured read/write roots.
|
||||
|
||||
`export_evidence_bundle` is file-only and requires `path`; existing files
|
||||
require `overwrite: true`. Use `snapshot_evidence_bundle` and then
|
||||
`resources/read` when the complete bundle is needed within the current session.
|
||||
|
||||
An Evidence URI is discoverability, not authorization. It cannot authorize file
|
||||
access, extraction, mounting, execution, or networking. IDs disappear when the
|
||||
session ends unless the existing Evidence bundle or workspace persistence flow
|
||||
explicitly retains them.
|
||||
|
||||
## Aggregate native context
|
||||
|
||||
`get_navigation_context` combines the selected document, current address, and
|
||||
current/containing procedure. A cursor outside a procedure is represented as
|
||||
`procedure: null`. `inspect_address_context` requires an explicit address and
|
||||
returns bounded name, procedure, comment, inline-comment, and bookmark facets;
|
||||
unsupported facets are local `unavailable` outcomes. The scalar getters remain
|
||||
available while the aggregate contracts are evaluated.
|
||||
|
||||
## Permission policy
|
||||
|
||||
All local side effects use one scope vocabulary: capability, canonical roots,
|
||||
|
||||
+13
-10
@@ -4,14 +4,14 @@
|
||||
"name": "rea-agents",
|
||||
"version": "2.7.0",
|
||||
"sdk": {
|
||||
"server": "2.0.0-beta.4",
|
||||
"client": "2.0.0-beta.4",
|
||||
"core": "2.0.0-beta.4"
|
||||
"server": "2.0.0",
|
||||
"client": "2.0.0",
|
||||
"core": "2.0.0"
|
||||
},
|
||||
"skill_version": "23"
|
||||
},
|
||||
"tools": {
|
||||
"total": 111,
|
||||
"total": 114,
|
||||
"families": [
|
||||
{
|
||||
"id": "direct",
|
||||
@@ -167,7 +167,7 @@
|
||||
{
|
||||
"id": "session",
|
||||
"surface": "session",
|
||||
"count": 19,
|
||||
"count": 22,
|
||||
"tools": [
|
||||
"binary_session",
|
||||
"build_call_path",
|
||||
@@ -180,11 +180,14 @@
|
||||
"correlate_static_and_runtime",
|
||||
"export_evidence_bundle",
|
||||
"find_changed_behavior",
|
||||
"get_navigation_context",
|
||||
"import_evidence_bundle",
|
||||
"inspect_address_context",
|
||||
"list_unknowns",
|
||||
"open_binary",
|
||||
"record_unknown",
|
||||
"run_replay_machine",
|
||||
"snapshot_evidence_bundle",
|
||||
"update_unknown",
|
||||
"verify_reconstruction",
|
||||
"verify_unknown_resolution"
|
||||
@@ -662,16 +665,16 @@
|
||||
"runtime_catalog": {
|
||||
"counts": {
|
||||
"cli_commands": 67,
|
||||
"mcp_tools": 111,
|
||||
"mcp_tools": 114,
|
||||
"mcp_prompts": 6,
|
||||
"mcp_resources": 2,
|
||||
"mcp_resource_templates": 11
|
||||
},
|
||||
"digests": {
|
||||
"tools_sha256": "092dd59990f61b1011ac0cd4f5b8d6a3e4a2cf7ade49c86892963c87b71dc65b",
|
||||
"prompts_sha256": "2dd61fd4e29657e7faf739ef67be7b1f637ddf446e7d73acc0d2fd1d9f06b43e",
|
||||
"resources_sha256": "339a984ea8cd8be5f234adf24395bf0295258d990492005cf6fe1f293d12cd46",
|
||||
"combined_sha256": "461b4dbea03ef60d637f4b74debca4dbea0e1dd423d7d9c9a631e62204528c05",
|
||||
"tools_sha256": "58bc4f72c5f8de4328728ead9f3a28ec5548484b7b9cf2ed85cf0edcc689a63c",
|
||||
"prompts_sha256": "3886a3bf55a448def79e3eb429e6a56eb2ba075423c5cb1e5747642b389ef8fe",
|
||||
"resources_sha256": "e0e872276ebf6f88e916ef24930a99eed27ce4c93e494e60820e9bd8cc8497c3",
|
||||
"combined_sha256": "e83a177f5eca63bbad17886a61f5a1a376d44afe862c51c56e5d38339f7d41f8",
|
||||
"providers_sha256": "918ccd6b8ae442cd5844dcfde55086f458176241c8d166c1e9dd0f242ccd2bb0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"manifest_id": "ecm_8105537d48f95b240afd212bf3e3b975fb27f41d0e1ca313a76e61234619f2e8",
|
||||
"manifest_id": "ecm_c1609d2cc959477e200df1a368ade5e3d68ad6fd4041009b78a5778a3b76e9bc",
|
||||
"verifier": {
|
||||
"id": "rea-managed-conformance",
|
||||
"version": "1"
|
||||
@@ -14,7 +14,7 @@
|
||||
"skill_digests": [
|
||||
{
|
||||
"skill_id": "reverse-engineer-anything",
|
||||
"sha256": "6365fa4341bc7309fd31cf6d1732f95b6fca82984e7f9fd0ab0b9223dc03c501"
|
||||
"sha256": "1398947e9119a5a9af08514413d8cfdd8dcdc9373ebab1ba6eb525126db5ffff"
|
||||
}
|
||||
],
|
||||
"claims": [
|
||||
|
||||
Generated
+13
-13
@@ -15,8 +15,8 @@
|
||||
"@electron/asar": "4.2.0",
|
||||
"@jridgewell/trace-mapping": "0.3.31",
|
||||
"@lydell/node-pty": "1.1.0",
|
||||
"@modelcontextprotocol/client": "2.0.0-beta.4",
|
||||
"@modelcontextprotocol/server": "2.0.0-beta.4",
|
||||
"@modelcontextprotocol/client": "2.0.0",
|
||||
"@modelcontextprotocol/server": "2.0.0",
|
||||
"@xterm/addon-serialize": "0.14.0",
|
||||
"@xterm/headless": "6.0.0",
|
||||
"@zip.js/zip.js": "2.8.26",
|
||||
@@ -369,12 +369,12 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@modelcontextprotocol/client": {
|
||||
"version": "2.0.0-beta.4",
|
||||
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/client/-/client-2.0.0-beta.4.tgz",
|
||||
"integrity": "sha512-VNHA/UXDk7mCpVl+jOg5B4WMRRD2OEl+it360Lhi6HiCbrIB2f6pZ0cqSDKXQVUtZvqnhdQHzZAM9h8EKWhq/A==",
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/client/-/client-2.0.0.tgz",
|
||||
"integrity": "sha512-8f1OghQ2rjzIOfqgUCP+8GiUWqRs89njoWLNqAe8kWmDePv3s1fZXseej+QXemssEuuOvLLmLO/kqM3IQHtISw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/core": "2.0.0-beta.4",
|
||||
"@modelcontextprotocol/core": "2.0.0",
|
||||
"cross-spawn": "^7.0.5",
|
||||
"eventsource": "^3.0.2",
|
||||
"eventsource-parser": "^3.0.0",
|
||||
@@ -387,9 +387,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@modelcontextprotocol/core": {
|
||||
"version": "2.0.0-beta.4",
|
||||
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/core/-/core-2.0.0-beta.4.tgz",
|
||||
"integrity": "sha512-nsMXd4wQBKzmph6r+WOhum+mXjDYljTAqwY/XUg3hLtvNOQ8+JVqBSJOVCMJvx9lXhTpTOPrGZ3BuNiaNPjSvg==",
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/core/-/core-2.0.0.tgz",
|
||||
"integrity": "sha512-pJCEwGG7Lfr/+PQp9ZTwKXNeO5wzbfKL7H3MYpCorM4oFBoQrdjnBgEoqG+RjhsvS1FKrDbKux+M1HhlnGWqcA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"zod": "^4.2.0"
|
||||
@@ -399,12 +399,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@modelcontextprotocol/server": {
|
||||
"version": "2.0.0-beta.4",
|
||||
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/server/-/server-2.0.0-beta.4.tgz",
|
||||
"integrity": "sha512-pjMZcNEt1dOq0aJCcY3b7w1Ayh+qmQN2xlfTELcGV9yiAjEGIczBPBLWWW0k0zzo5T8kiv15jtKPsWeHGxLvLg==",
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/server/-/server-2.0.0.tgz",
|
||||
"integrity": "sha512-YhHWdHfpFMQfd0prsEnxKeS3Qz3ytIGmsS0sth4KDjnacIT7hxk6hXHkJ9KysxlkvTM+WZAtQbbcUhdoP4Hvtw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/core": "2.0.0-beta.4",
|
||||
"@modelcontextprotocol/core": "2.0.0",
|
||||
"zod": "^4.2.0"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
+2
-2
@@ -147,8 +147,8 @@
|
||||
"@electron/asar": "4.2.0",
|
||||
"@jridgewell/trace-mapping": "0.3.31",
|
||||
"@lydell/node-pty": "1.1.0",
|
||||
"@modelcontextprotocol/client": "2.0.0-beta.4",
|
||||
"@modelcontextprotocol/server": "2.0.0-beta.4",
|
||||
"@modelcontextprotocol/client": "2.0.0",
|
||||
"@modelcontextprotocol/server": "2.0.0",
|
||||
"@xterm/addon-serialize": "0.14.0",
|
||||
"@xterm/headless": "6.0.0",
|
||||
"@zip.js/zip.js": "2.8.26",
|
||||
|
||||
@@ -34,7 +34,16 @@ try {
|
||||
skillDestination,
|
||||
{ recursive: true },
|
||||
);
|
||||
const targets = await createTargets(fixtureRoot);
|
||||
const requestedScenarioIds = new Set(
|
||||
(process.env.REA_AGENT_EVAL_SCENARIOS ?? "")
|
||||
.split(",")
|
||||
.map((value) => value.trim())
|
||||
.filter((value) => value.length > 0),
|
||||
);
|
||||
const targets = await createTargets(
|
||||
fixtureRoot,
|
||||
requestedScenarioIds.size === 0 || requestedScenarioIds.has("managed"),
|
||||
);
|
||||
const allScenarios = [
|
||||
{
|
||||
id: "native",
|
||||
@@ -94,13 +103,23 @@ try {
|
||||
prompt:
|
||||
"A user-owned page is already open at http://127.0.0.1:3000 through the approved local debugging endpoint http://127.0.0.1:9222. Inspect what is available without navigating or executing page code, and report any authority or availability limits.",
|
||||
},
|
||||
{
|
||||
id: "navigation-context",
|
||||
expectedFirstTool: "get_navigation_context",
|
||||
requiresEvidence: false,
|
||||
requiredAnswerTermGroups: [["unavailable", "could not", "session"]],
|
||||
prompt:
|
||||
"Report the selected document, current address, and containing procedure from the current native-analysis session in one coherent request. If no native session is available, report that limitation plainly.",
|
||||
},
|
||||
{
|
||||
id: "address-context",
|
||||
expectedFirstTool: "inspect_address_context",
|
||||
requiresEvidence: false,
|
||||
requiredAnswerTermGroups: [["unavailable", "could not", "session"]],
|
||||
prompt:
|
||||
"Inspect explicit address 0x401000 in the current native-analysis session. Report its analyzed name, containing procedure, regular comment, inline comment, and matching bookmarks. If no native session is available, report that limitation plainly.",
|
||||
},
|
||||
];
|
||||
const requestedScenarioIds = new Set(
|
||||
(process.env.REA_AGENT_EVAL_SCENARIOS ?? "")
|
||||
.split(",")
|
||||
.map((value) => value.trim())
|
||||
.filter((value) => value.length > 0),
|
||||
);
|
||||
const scenarios =
|
||||
requestedScenarioIds.size === 0
|
||||
? allScenarios
|
||||
@@ -234,7 +253,7 @@ try {
|
||||
await rm(evaluationRoot, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
async function createTargets(root) {
|
||||
async function createTargets(root, includeManaged) {
|
||||
const javascript = join(root, "desktop-app");
|
||||
await mkdir(join(javascript, "renderer"), { recursive: true });
|
||||
await Promise.all([
|
||||
@@ -275,33 +294,35 @@ async function createTargets(root) {
|
||||
),
|
||||
]);
|
||||
|
||||
const managedProject = join(root, "managed-project");
|
||||
const managedOutput = join(root, "managed-output");
|
||||
await mkdir(managedProject, { recursive: true });
|
||||
await Promise.all([
|
||||
writeFile(
|
||||
join(managedProject, "AgentEval.csproj"),
|
||||
'<Project Sdk="Microsoft.NET.Sdk"><PropertyGroup><OutputType>Exe</OutputType><TargetFramework>net8.0</TargetFramework><ImplicitUsings>enable</ImplicitUsings><Nullable>enable</Nullable></PropertyGroup></Project>\n',
|
||||
),
|
||||
writeFile(
|
||||
join(managedProject, "Program.cs"),
|
||||
'namespace AgentEval; public sealed record Profile(string Id); public static class Program { public static void Main() => Console.WriteLine(new Profile("fixture")); }\n',
|
||||
),
|
||||
]);
|
||||
await runProcess(
|
||||
"dotnet",
|
||||
[
|
||||
"build",
|
||||
join(managedProject, "AgentEval.csproj"),
|
||||
"--configuration",
|
||||
"Release",
|
||||
"--output",
|
||||
managedOutput,
|
||||
"--nologo",
|
||||
],
|
||||
root,
|
||||
120_000,
|
||||
);
|
||||
if (includeManaged) {
|
||||
const managedProject = join(root, "managed-project");
|
||||
await mkdir(managedProject, { recursive: true });
|
||||
await Promise.all([
|
||||
writeFile(
|
||||
join(managedProject, "AgentEval.csproj"),
|
||||
'<Project Sdk="Microsoft.NET.Sdk"><PropertyGroup><OutputType>Exe</OutputType><TargetFramework>net8.0</TargetFramework><ImplicitUsings>enable</ImplicitUsings><Nullable>enable</Nullable></PropertyGroup></Project>\n',
|
||||
),
|
||||
writeFile(
|
||||
join(managedProject, "Program.cs"),
|
||||
'namespace AgentEval; public sealed record Profile(string Id); public static class Program { public static void Main() => Console.WriteLine(new Profile("fixture")); }\n',
|
||||
),
|
||||
]);
|
||||
await runProcess(
|
||||
"dotnet",
|
||||
[
|
||||
"build",
|
||||
join(managedProject, "AgentEval.csproj"),
|
||||
"--configuration",
|
||||
"Release",
|
||||
"--output",
|
||||
managedOutput,
|
||||
"--nologo",
|
||||
],
|
||||
root,
|
||||
120_000,
|
||||
);
|
||||
}
|
||||
return {
|
||||
native: "/bin/true",
|
||||
javascript: javascriptAsar,
|
||||
|
||||
@@ -3,8 +3,8 @@ name: reverse-engineer-anything
|
||||
description: Reverse engineer native, managed, Electron/JavaScript, packaged, and browser applications with REA. Use shipped-artifact or approved runtime evidence to explain features, compare versions, decompile code, or guide a reconstruction. Skip REA for ordinary source-repository architecture analysis.
|
||||
metadata:
|
||||
version: "23"
|
||||
tool_count: 111
|
||||
catalog_digest: "461b4dbea03ef60d637f4b74debca4dbea0e1dd423d7d9c9a631e62204528c05"
|
||||
tool_count: 114
|
||||
catalog_digest: "e83a177f5eca63bbad17886a61f5a1a376d44afe862c51c56e5d38339f7d41f8"
|
||||
---
|
||||
|
||||
# REA
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
import type {
|
||||
AnalysisExecution,
|
||||
AnalysisOperation,
|
||||
AnalysisOperationPort,
|
||||
} from "./AnalysisProvider.js";
|
||||
import { AnalysisOutputError, type AnalysisError } from "../domain/errors.js";
|
||||
import type { JsonValue } from "../domain/jsonValue.js";
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
|
||||
type Facet =
|
||||
| { readonly state: "available"; readonly value: JsonValue }
|
||||
| {
|
||||
readonly state: "unavailable";
|
||||
readonly reason: string;
|
||||
readonly remediation: string;
|
||||
};
|
||||
|
||||
const parameters = (
|
||||
document: string | undefined,
|
||||
address?: string,
|
||||
): Readonly<Record<string, JsonValue>> => ({
|
||||
...(document === undefined ? {} : { document }),
|
||||
...(address === undefined ? {} : { address }),
|
||||
});
|
||||
|
||||
const executionOptions = (
|
||||
signal: AbortSignal | undefined,
|
||||
): { readonly signal?: AbortSignal } =>
|
||||
signal === undefined ? {} : { signal };
|
||||
|
||||
/** Compose the provider's volatile selection state into one coherent query. */
|
||||
export const getNavigationContext = async (
|
||||
analysis: AnalysisOperationPort,
|
||||
input: { readonly document?: string | undefined },
|
||||
signal?: AbortSignal,
|
||||
): Promise<Result<JsonValue, AnalysisError>> => {
|
||||
const document =
|
||||
input.document ??
|
||||
(await analysis.execute("current_document", {}, executionOptions(signal)));
|
||||
if (typeof document !== "string" && !document.ok) return document;
|
||||
const documentName =
|
||||
typeof document === "string" ? document : document.value.result;
|
||||
const documentArgs =
|
||||
typeof documentName === "string" ? parameters(documentName) : {};
|
||||
const address = await analysis.execute(
|
||||
"current_address",
|
||||
documentArgs,
|
||||
executionOptions(signal),
|
||||
);
|
||||
if (!address.ok) return address;
|
||||
if (typeof address.value.result !== "string")
|
||||
return ok({
|
||||
document: documentName,
|
||||
address: address.value.result,
|
||||
procedure: null,
|
||||
});
|
||||
const procedure = await analysis.execute(
|
||||
"resolve_containing_procedure",
|
||||
parameters(
|
||||
typeof documentName === "string" ? documentName : undefined,
|
||||
address.value.result,
|
||||
),
|
||||
executionOptions(signal),
|
||||
);
|
||||
if (!procedure.ok) {
|
||||
if (!isMissingCurrentProcedure(procedure.error)) return procedure;
|
||||
return ok({
|
||||
document: documentName,
|
||||
address: address.value.result,
|
||||
procedure: null,
|
||||
});
|
||||
}
|
||||
return ok({
|
||||
document: documentName,
|
||||
address: address.value.result,
|
||||
procedure: containingProcedureName(procedure.value.result),
|
||||
});
|
||||
};
|
||||
|
||||
/** Inspect bounded provider-neutral facets at one explicit reproducible address. */
|
||||
export const inspectAddressContext = async (
|
||||
analysis: AnalysisOperationPort,
|
||||
input: { readonly address: string; readonly document?: string | undefined },
|
||||
signal?: AbortSignal,
|
||||
): Promise<Result<JsonValue, AnalysisError>> => {
|
||||
const document = await resolveDocument(analysis, input.document, signal);
|
||||
if (!document.ok) return document;
|
||||
const args = parameters(document.value, input.address);
|
||||
const operations = [
|
||||
"address_name",
|
||||
"resolve_containing_procedure",
|
||||
"comment",
|
||||
"inline_comment",
|
||||
"list_bookmarks",
|
||||
] as const satisfies readonly AnalysisOperation[];
|
||||
const results = await Promise.all(
|
||||
operations.map((operation) =>
|
||||
analysis.execute(
|
||||
operation,
|
||||
operation === "list_bookmarks" ? parameters(document.value) : args,
|
||||
executionOptions(signal),
|
||||
),
|
||||
),
|
||||
);
|
||||
for (const result of results) {
|
||||
if (!result.ok && isRequestLevelFailure(result.error)) return result;
|
||||
}
|
||||
const name = facet(results[0], operations[0]);
|
||||
const procedure = facet(results[1], operations[1]);
|
||||
const comment = facet(results[2], operations[2]);
|
||||
const inlineComment = facet(results[3], operations[3]);
|
||||
const bookmarks = facet(results[4], operations[4]);
|
||||
return ok({
|
||||
address: input.address,
|
||||
document: document.value,
|
||||
name,
|
||||
procedure,
|
||||
comment,
|
||||
inline_comment: inlineComment,
|
||||
bookmarks:
|
||||
bookmarks.state === "available"
|
||||
? {
|
||||
state: "available",
|
||||
value: matchingBookmarks(
|
||||
bookmarks.value,
|
||||
resolvedAddress(results[1], input.address),
|
||||
),
|
||||
}
|
||||
: bookmarks,
|
||||
});
|
||||
};
|
||||
|
||||
const resolveDocument = async (
|
||||
analysis: AnalysisOperationPort,
|
||||
document: string | undefined,
|
||||
signal: AbortSignal | undefined,
|
||||
): Promise<Result<string, AnalysisError>> => {
|
||||
if (document !== undefined) return ok(document);
|
||||
const current = await analysis.execute(
|
||||
"current_document",
|
||||
{},
|
||||
executionOptions(signal),
|
||||
);
|
||||
if (!current.ok) return current;
|
||||
return typeof current.value.result === "string"
|
||||
? ok(current.value.result)
|
||||
: err(
|
||||
new AnalysisOutputError(
|
||||
"current_document",
|
||||
"expected a document identity string",
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const facet = (
|
||||
result: Result<AnalysisExecution, AnalysisError> | undefined,
|
||||
operation: AnalysisOperation,
|
||||
): Facet =>
|
||||
result?.ok === true
|
||||
? { state: "available", value: result.value.result }
|
||||
: {
|
||||
state: "unavailable",
|
||||
reason: result?.error.message ?? `${operation} returned no result`,
|
||||
remediation: `Call ${operation} directly to inspect provider availability and diagnostics.`,
|
||||
};
|
||||
|
||||
const isMissingCurrentProcedure = (error: AnalysisError): boolean =>
|
||||
/(?:no procedure exists|not in a procedure)/iu.test(error.message);
|
||||
|
||||
const isRequestLevelFailure = (error: AnalysisError): boolean =>
|
||||
error._tag === "AnalysisCancelledError" ||
|
||||
error._tag === "AnalysisTimeoutError" ||
|
||||
error._tag === "NoBinaryOpenError" ||
|
||||
error._tag === "PermissionRequiredError";
|
||||
|
||||
const containingProcedureName = (value: JsonValue): string | null => {
|
||||
if (typeof value !== "object" || value === null || Array.isArray(value))
|
||||
return null;
|
||||
if (value.found !== true) return null;
|
||||
const procedure = value.procedure;
|
||||
if (
|
||||
typeof procedure !== "object" ||
|
||||
procedure === null ||
|
||||
Array.isArray(procedure)
|
||||
)
|
||||
return null;
|
||||
return typeof procedure.name === "string" ? procedure.name : null;
|
||||
};
|
||||
|
||||
const resolvedAddress = (
|
||||
result: Result<AnalysisExecution, AnalysisError> | undefined,
|
||||
fallback: string,
|
||||
): string => {
|
||||
if (result?.ok !== true) return fallback;
|
||||
const value = result.value.result;
|
||||
if (typeof value !== "object" || value === null || Array.isArray(value))
|
||||
return fallback;
|
||||
return typeof value.query_address === "string"
|
||||
? value.query_address
|
||||
: fallback;
|
||||
};
|
||||
|
||||
const matchingBookmarks = (value: JsonValue, address: string): JsonValue => {
|
||||
if (!Array.isArray(value)) return [];
|
||||
return value.filter(
|
||||
(bookmark) =>
|
||||
typeof bookmark === "object" &&
|
||||
bookmark !== null &&
|
||||
!Array.isArray(bookmark) &&
|
||||
bookmark.address === address,
|
||||
);
|
||||
};
|
||||
@@ -189,6 +189,7 @@ export class BinarySession
|
||||
const imported = this.importAnalysisSnapshot(options.snapshot);
|
||||
if (!imported.ok) return imported;
|
||||
}
|
||||
this.resetSnapshotInvalidation();
|
||||
return ok(target);
|
||||
}
|
||||
await this.#drainCalls();
|
||||
|
||||
@@ -27,6 +27,16 @@ import type {
|
||||
ProviderIdentity,
|
||||
} from "./AnalysisProvider.js";
|
||||
|
||||
/** Compact identity and summary for one retained immutable Evidence bundle. */
|
||||
export interface EvidenceBundleSnapshot {
|
||||
readonly bundleDigest: string;
|
||||
readonly bundleVersion: 2;
|
||||
readonly bytes: number;
|
||||
readonly records: number;
|
||||
readonly unknowns: number;
|
||||
readonly uri: string;
|
||||
}
|
||||
|
||||
/** Target lifecycle used by CLI and MCP without exposing a concrete provider. */
|
||||
export interface BinarySessionPort extends AnalysisOperationPort {
|
||||
open(
|
||||
@@ -49,6 +59,8 @@ export interface BinarySessionPort extends AnalysisOperationPort {
|
||||
hasEvidence(evidenceId: string): boolean;
|
||||
evidenceById(evidenceId: string): Evidence | undefined;
|
||||
exportEvidenceBundle(): EvidenceBundle;
|
||||
snapshotEvidenceBundle(): Result<EvidenceBundleSnapshot, EvidenceLimitError>;
|
||||
retainedEvidenceBundle(digest: string): string | undefined;
|
||||
importEvidenceBundle(
|
||||
bundle: unknown,
|
||||
): Result<number, EvidenceIntegrityError | EvidenceLimitError>;
|
||||
@@ -101,4 +113,5 @@ export interface BinarySessionPort extends AnalysisOperationPort {
|
||||
): AnalysisProfileCommitment | undefined;
|
||||
openCompatibility(): Readonly<Record<string, JsonValue>>;
|
||||
onAvailabilityChanged?(listener: () => void | Promise<void>): () => void;
|
||||
onAnalysisSnapshotChanged?(listener: () => void | Promise<void>): () => void;
|
||||
}
|
||||
@@ -3,11 +3,14 @@ import type { AnalysisSnapshot } from "../domain/analysisSnapshot.js";
|
||||
import type { BinaryTarget } from "../domain/binaryTarget.js";
|
||||
import { createEvidence, type Evidence } from "../domain/evidence.js";
|
||||
import type { EvidenceBundle } from "../domain/evidenceBundle.js";
|
||||
import { evidenceBundleForTarget } from "../domain/evidenceBundle.js";
|
||||
import {
|
||||
evidenceBundleForTarget,
|
||||
serializeEvidenceBundle,
|
||||
} from "../domain/evidenceBundle.js";
|
||||
import {
|
||||
EvidenceIntegrityError,
|
||||
EvidenceLimitError,
|
||||
type AnalysisError,
|
||||
type EvidenceLimitError,
|
||||
type UnknownRegistryError,
|
||||
} from "../domain/errors.js";
|
||||
import {
|
||||
@@ -24,7 +27,8 @@ import type {
|
||||
UnknownStatus,
|
||||
UpdateUnknownInput,
|
||||
} from "../domain/residualUnknown.js";
|
||||
import { err, type Result } from "../domain/result.js";
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
import type { EvidenceBundleSnapshot } from "./BinarySessionPort.js";
|
||||
import type {
|
||||
AnalysisExecution,
|
||||
AnalysisOperation,
|
||||
@@ -44,17 +48,28 @@ export interface ActiveAnalysisBinding {
|
||||
|
||||
/** Owns session evidence, snapshots, workspaces, and residual unknowns. */
|
||||
export abstract class BinarySessionRecords {
|
||||
static readonly MAX_RETAINED_BUNDLES = 16;
|
||||
static readonly MAX_RETAINED_BUNDLE_BYTES = 64 * 1024 * 1024;
|
||||
readonly #evidence = new EvidenceLedger({
|
||||
maxRecords: 10_000,
|
||||
maxBytes: 64 * 1024 * 1024,
|
||||
});
|
||||
readonly #snapshot = new AnalysisSnapshotCache();
|
||||
readonly #retainedBundles = new Map<string, string>();
|
||||
#retainedBundleBytes = 0;
|
||||
readonly #investigationWorkspaces = new Map<string, InvestigationWorkspace>();
|
||||
readonly #coverageWorkspaces = new Map<
|
||||
string,
|
||||
ReconstructionCoverageWorkspace
|
||||
>();
|
||||
#snapshotInvalidated = false;
|
||||
readonly #snapshotListeners = new Set<() => void | Promise<void>>();
|
||||
|
||||
/** Observe changes to the mutable current analysis snapshot resource. */
|
||||
onAnalysisSnapshotChanged(listener: () => void | Promise<void>): () => void {
|
||||
this.#snapshotListeners.add(listener);
|
||||
return () => this.#snapshotListeners.delete(listener);
|
||||
}
|
||||
|
||||
recordEvidence(
|
||||
evidence: Evidence,
|
||||
@@ -62,7 +77,9 @@ export abstract class BinarySessionRecords {
|
||||
"added" | "duplicate",
|
||||
EvidenceIntegrityError | EvidenceLimitError
|
||||
> {
|
||||
return this.#evidence.record(evidence);
|
||||
const recorded = this.#evidence.record(evidence);
|
||||
if (recorded.ok && recorded.value === "added") this.#emitSnapshotChanged();
|
||||
return recorded;
|
||||
}
|
||||
|
||||
hasEvidence(evidenceId: string): boolean {
|
||||
@@ -77,10 +94,55 @@ export abstract class BinarySessionRecords {
|
||||
return this.#evidence.export();
|
||||
}
|
||||
|
||||
snapshotEvidenceBundle(): Result<EvidenceBundleSnapshot, EvidenceLimitError> {
|
||||
const bundle = this.#evidence.export();
|
||||
const encoded = serializeEvidenceBundle(bundle);
|
||||
const digest = createHash("sha256").update(encoded).digest("hex");
|
||||
const existing = this.#retainedBundles.get(digest);
|
||||
if (existing === undefined) {
|
||||
if (
|
||||
this.#retainedBundles.size >= BinarySessionRecords.MAX_RETAINED_BUNDLES
|
||||
)
|
||||
return err(
|
||||
new EvidenceLimitError(
|
||||
"records",
|
||||
BinarySessionRecords.MAX_RETAINED_BUNDLES,
|
||||
),
|
||||
);
|
||||
const bytes = Buffer.byteLength(encoded);
|
||||
if (
|
||||
this.#retainedBundleBytes + bytes >
|
||||
BinarySessionRecords.MAX_RETAINED_BUNDLE_BYTES
|
||||
)
|
||||
return err(
|
||||
new EvidenceLimitError(
|
||||
"bytes",
|
||||
BinarySessionRecords.MAX_RETAINED_BUNDLE_BYTES,
|
||||
),
|
||||
);
|
||||
this.#retainedBundles.set(digest, encoded);
|
||||
this.#retainedBundleBytes += bytes;
|
||||
}
|
||||
return ok({
|
||||
bundleDigest: digest,
|
||||
bundleVersion: 2,
|
||||
bytes: Buffer.byteLength(existing ?? encoded),
|
||||
records: bundle.records.length,
|
||||
unknowns: bundle.unknowns.length,
|
||||
uri: `rea://evidence-bundle/${digest}`,
|
||||
});
|
||||
}
|
||||
|
||||
retainedEvidenceBundle(digest: string): string | undefined {
|
||||
return this.#retainedBundles.get(digest);
|
||||
}
|
||||
|
||||
importEvidenceBundle(
|
||||
bundle: unknown,
|
||||
): Result<number, EvidenceIntegrityError | EvidenceLimitError> {
|
||||
return this.#evidence.import(bundle);
|
||||
const imported = this.#evidence.import(bundle);
|
||||
if (imported.ok && imported.value > 0) this.#emitSnapshotChanged();
|
||||
return imported;
|
||||
}
|
||||
|
||||
protected abstract activeAnalysisBinding(): ActiveAnalysisBinding | undefined;
|
||||
@@ -120,13 +182,15 @@ export abstract class BinarySessionRecords {
|
||||
"Analysis snapshot profile_mismatch: the active target has no concrete analysis profile",
|
||||
),
|
||||
);
|
||||
return this.#snapshot.import(
|
||||
const imported = this.#snapshot.import(
|
||||
snapshot,
|
||||
active === undefined
|
||||
? undefined
|
||||
: { target: active.target, profile: active.profile },
|
||||
(bundle) => this.#evidence.import(bundle),
|
||||
);
|
||||
if (imported.ok) this.#emitSnapshotChanged();
|
||||
return imported;
|
||||
}
|
||||
|
||||
protected matchesSnapshot(
|
||||
@@ -141,6 +205,7 @@ export abstract class BinarySessionRecords {
|
||||
profile: AnalysisProfileCommitment,
|
||||
): void {
|
||||
this.#snapshot.select(target, profile);
|
||||
this.#emitSnapshotChanged();
|
||||
}
|
||||
|
||||
protected lookupSnapshot(
|
||||
@@ -158,25 +223,37 @@ export abstract class BinarySessionRecords {
|
||||
input: Parameters<AnalysisSnapshotCache["record"]>[0],
|
||||
): void {
|
||||
this.#snapshot.record(input);
|
||||
this.#emitSnapshotChanged();
|
||||
}
|
||||
|
||||
protected invalidateSnapshot(): void {
|
||||
this.#snapshot.clear();
|
||||
this.#snapshotInvalidated = true;
|
||||
this.#emitSnapshotChanged();
|
||||
}
|
||||
|
||||
protected resetSnapshotInvalidation(): void {
|
||||
if (!this.#snapshotInvalidated) return;
|
||||
this.#snapshotInvalidated = false;
|
||||
this.#emitSnapshotChanged();
|
||||
}
|
||||
|
||||
protected clearSnapshot(): void {
|
||||
this.#snapshot.clear();
|
||||
this.#emitSnapshotChanged();
|
||||
}
|
||||
|
||||
protected clearSessionRecords(): void {
|
||||
this.#evidence.clear();
|
||||
this.#snapshot.clear();
|
||||
this.#snapshotInvalidated = false;
|
||||
this.#retainedBundles.clear();
|
||||
this.#retainedBundleBytes = 0;
|
||||
this.#emitSnapshotChanged();
|
||||
}
|
||||
|
||||
#emitSnapshotChanged(): void {
|
||||
for (const listener of this.#snapshotListeners) void listener();
|
||||
}
|
||||
|
||||
retainInvestigationWorkspace(
|
||||
@@ -231,21 +308,25 @@ export abstract class BinarySessionRecords {
|
||||
input: RecordUnknownInput,
|
||||
): Result<ResidualUnknown, AnalysisError> {
|
||||
const target = this.activeAnalysisBinding()?.target;
|
||||
return this.#evidence.recordUnknown(
|
||||
const recorded = this.#evidence.recordUnknown(
|
||||
input,
|
||||
unknownMutationEvidence(target, input),
|
||||
);
|
||||
if (recorded.ok) this.#emitSnapshotChanged();
|
||||
return recorded;
|
||||
}
|
||||
|
||||
recordEvidenceWithUnknown(
|
||||
evidence: Evidence,
|
||||
input: RecordUnknownInput,
|
||||
): Result<ResidualUnknown | null, AnalysisError> {
|
||||
return this.#evidence.recordWithUnknown(
|
||||
const recorded = this.#evidence.recordWithUnknown(
|
||||
evidence,
|
||||
input,
|
||||
unknownMutationEvidence(undefined, input),
|
||||
);
|
||||
if (recorded.ok) this.#emitSnapshotChanged();
|
||||
return recorded;
|
||||
}
|
||||
|
||||
updateUnknown(
|
||||
@@ -267,7 +348,9 @@ export abstract class BinarySessionRecords {
|
||||
"Registry mutation evidence records analyst intent, not proof of the answer.",
|
||||
],
|
||||
});
|
||||
return this.#evidence.updateUnknown(input, evidence);
|
||||
const updated = this.#evidence.updateUnknown(input, evidence);
|
||||
if (updated.ok) this.#emitSnapshotChanged();
|
||||
return updated;
|
||||
}
|
||||
|
||||
listUnknowns(
|
||||
@@ -307,3 +390,4 @@ const sortedWorkspaces = <
|
||||
left.revision - right.revision,
|
||||
)
|
||||
.map((workspace) => structuredClone(workspace));
|
||||
import { createHash } from "node:crypto";
|
||||
@@ -111,6 +111,16 @@ const ENHANCED_REQUIREMENTS: Readonly<Record<string, readonly string[]>> = {
|
||||
],
|
||||
};
|
||||
|
||||
const SESSION_COMPOSITION_REQUIREMENTS: Readonly<
|
||||
Record<string, readonly string[]>
|
||||
> = {
|
||||
get_navigation_context: [
|
||||
"current_document",
|
||||
"current_address",
|
||||
"resolve_containing_procedure",
|
||||
],
|
||||
};
|
||||
|
||||
/** Build stable per-operation availability for discovery and tool visibility. */
|
||||
export const buildCapabilityInventory = (
|
||||
sessionStatus: JsonValue,
|
||||
@@ -185,6 +195,9 @@ const clientRequirementsFor = (
|
||||
};
|
||||
|
||||
const availabilityFor = (context: AvailabilityContext): Availability => {
|
||||
const sessionComposition = SESSION_COMPOSITION_REQUIREMENTS[context.name];
|
||||
if (sessionComposition !== undefined)
|
||||
return composedAvailabilityFor(sessionComposition, context.descriptors);
|
||||
const javascriptApplication = javascriptApplicationAvailability(context);
|
||||
if (javascriptApplication !== null) return javascriptApplication;
|
||||
const policyDecision = policyAvailability(context);
|
||||
@@ -366,6 +379,13 @@ const composedAvailability = (
|
||||
reason: "provider_missing",
|
||||
remediation: "No provider composition is declared for this operation.",
|
||||
};
|
||||
return composedAvailabilityFor(requirements, descriptors);
|
||||
};
|
||||
|
||||
const composedAvailabilityFor = (
|
||||
requirements: readonly string[],
|
||||
descriptors: ReadonlyMap<string, ProviderDescriptor>,
|
||||
): Availability => {
|
||||
const missing = requirements.find((operation) => !descriptors.has(operation));
|
||||
if (missing !== undefined)
|
||||
return {
|
||||
|
||||
@@ -35,7 +35,7 @@ const promptCatalog = PROMPT_CONTRACTS.map((contract) => ({
|
||||
/** Canonical fixed resources exposed by every target-free MCP session. */
|
||||
export const MCP_RESOURCE_CATALOG = [
|
||||
{ name: "server-identity", uri: "rea://server/identity" },
|
||||
{ name: "active-residual-unknowns", uri: "rea://unknowns/active" },
|
||||
{ name: "analysis-snapshot", uri: "rea://snapshot/current" },
|
||||
] as const;
|
||||
/** Canonical resource templates exposed by every target-free MCP session. */
|
||||
export const MCP_RESOURCE_TEMPLATE_CATALOG = [
|
||||
@@ -46,8 +46,8 @@ export const MCP_RESOURCE_TEMPLATE_CATALOG = [
|
||||
},
|
||||
{ name: "residual-unknown", uri_template: "rea://unknown/{unknownId}" },
|
||||
{
|
||||
name: "analysis-snapshot",
|
||||
uri_template: "rea://snapshot/{snapshotDigest}",
|
||||
name: "evidence-bundle",
|
||||
uri_template: "rea://evidence-bundle/{bundleDigest}",
|
||||
},
|
||||
{
|
||||
name: "artifact-page",
|
||||
|
||||
@@ -149,9 +149,9 @@ export const PROMPT_CONTRACTS = [
|
||||
"Build function dossiers and corroborate references and call relationships. Keep indirect or truncated paths unknown.",
|
||||
},
|
||||
{
|
||||
tools: ["export_evidence_bundle", "record_unknown"],
|
||||
tools: ["snapshot_evidence_bundle", "record_unknown"],
|
||||
instruction:
|
||||
"Cite retained Evidence IDs. Record residual unknowns only after separate explicit approval for registry mutation.",
|
||||
"Snapshot the canonical bundle, copy its opaque URI unchanged, and read that resource before citing retained Evidence IDs. Record residual unknowns only after separate explicit approval for registry mutation.",
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -198,9 +198,9 @@ export const PROMPT_CONTRACTS = [
|
||||
"For JavaScript/Electron versions, reconstruct each approved artifact independently, optionally reconcile separately retained passive runtime Evidence, then compare authenticated graphs with unique-only identity tiers. Keep ambiguous and incomplete matches unknown.",
|
||||
},
|
||||
{
|
||||
tools: ["export_evidence_bundle", "compare_artifacts"],
|
||||
tools: ["snapshot_evidence_bundle", "compare_artifacts"],
|
||||
instruction:
|
||||
"Resolve any supplied manifest IDs to retained inventory Evidence, validate graph commitments, then compare the complete left and right Evidence page sets.",
|
||||
"Snapshot and read the canonical bundle through its exact resource URI, resolve any supplied manifest IDs to retained inventory Evidence, validate graph commitments, then compare the complete left and right Evidence page sets.",
|
||||
},
|
||||
{
|
||||
tools: ["open_binary", "analyze_function", "compare_functions"],
|
||||
@@ -241,9 +241,9 @@ export const PROMPT_CONTRACTS = [
|
||||
},
|
||||
steps: [
|
||||
{
|
||||
tools: ["export_evidence_bundle"],
|
||||
tools: ["snapshot_evidence_bundle"],
|
||||
instruction:
|
||||
"Load the current canonical bundle and verify that every selected comparison Evidence ID is present and compatible with the intended claim.",
|
||||
"Snapshot the current canonical bundle, copy its opaque URI unchanged, read the resource, and verify that every selected comparison Evidence ID is present and compatible with the intended claim.",
|
||||
},
|
||||
{
|
||||
tools: [
|
||||
@@ -313,12 +313,12 @@ export const PROMPT_CONTRACTS = [
|
||||
},
|
||||
{
|
||||
tools: [
|
||||
"export_evidence_bundle",
|
||||
"snapshot_evidence_bundle",
|
||||
"capture_process_scenario",
|
||||
"correlate_static_and_runtime",
|
||||
],
|
||||
instruction:
|
||||
"Reuse a retained capture when supplied. Otherwise capture only after operator policy and per-call approval, then correlate through explicit hypotheses rather than timing or name coincidence.",
|
||||
"Snapshot and read retained Evidence through the exact resource URI. Reuse a retained capture when supplied. Otherwise capture only after operator policy and per-call approval, then correlate through explicit hypotheses rather than timing or name coincidence.",
|
||||
},
|
||||
{
|
||||
tools: ["record_unknown"],
|
||||
@@ -354,9 +354,9 @@ export const PROMPT_CONTRACTS = [
|
||||
"List current heads first and select only active, session-owned unknown IDs. Preserve their exact revision and requirements.",
|
||||
},
|
||||
{
|
||||
tools: ["export_evidence_bundle", "verify_unknown_resolution"],
|
||||
tools: ["snapshot_evidence_bundle", "verify_unknown_resolution"],
|
||||
instruction:
|
||||
"Inspect cited supporting, contradicting, and mutation Evidence, then validate any resolved head against bundle integrity and authority requirements.",
|
||||
"Snapshot and read the canonical bundle through its exact resource URI, inspect cited supporting, contradicting, and mutation Evidence, then validate any resolved head against bundle integrity and authority requirements.",
|
||||
},
|
||||
{
|
||||
tools: ["update_unknown"],
|
||||
@@ -399,9 +399,9 @@ export const PROMPT_CONTRACTS = [
|
||||
"Inspect capture capability, declared effects, and limits before proposing execution. An unavailable capability is not permission to widen policy.",
|
||||
},
|
||||
{
|
||||
tools: ["export_evidence_bundle"],
|
||||
tools: ["snapshot_evidence_bundle"],
|
||||
instruction:
|
||||
"Review any prior capture for unanswered dimensions, truncation, scenario commitments, and descendant settlement before designing a repeat.",
|
||||
"Snapshot and read the canonical bundle through its exact resource URI, then review any prior capture for unanswered dimensions, truncation, scenario commitments, and descendant settlement before designing a repeat.",
|
||||
},
|
||||
{
|
||||
tools: ["capture_process_scenario"],
|
||||
|
||||
@@ -40,6 +40,8 @@ export const TOOL_EXAMPLE_OVERRIDES: Readonly<
|
||||
find_code_for_string: { query: "authorization failed" },
|
||||
trace_call_path: { start: "0x1000", goal: "0x1100" },
|
||||
open_binary: { path: "/tmp/fixture" },
|
||||
export_evidence_bundle: { path: "/tmp/evidence.json" },
|
||||
inspect_address_context: { address: "0x1000" },
|
||||
import_evidence_bundle: { path: "evidence.json" },
|
||||
capture_process_scenario: {
|
||||
approved: true,
|
||||
|
||||
@@ -375,10 +375,24 @@ export const ENHANCED_TOOL_CONTRACTS = [
|
||||
|
||||
/** Session-owned Evidence bundle export options. */
|
||||
export const exportEvidenceBundleInputSchema = z.strictObject({
|
||||
path: z.string().min(1).optional(),
|
||||
path: z.string().min(1),
|
||||
overwrite: z.boolean().default(false),
|
||||
});
|
||||
|
||||
/** Retain the current canonical Evidence bundle as a session resource. */
|
||||
export const snapshotEvidenceBundleInputSchema = z.strictObject({});
|
||||
|
||||
/** Optional document selection for volatile navigation context. */
|
||||
export const navigationContextInputSchema = z.strictObject({
|
||||
document: z.string().min(1).optional(),
|
||||
});
|
||||
|
||||
/** Explicit reproducible address context query. */
|
||||
export const addressContextInputSchema = z.strictObject({
|
||||
address: z.string().min(1),
|
||||
document: z.string().min(1).optional(),
|
||||
});
|
||||
|
||||
/** Session-owned Evidence bundle import options. */
|
||||
export const importEvidenceBundleInputSchema = z.strictObject({
|
||||
path: z.string().min(1),
|
||||
@@ -403,6 +417,8 @@ export const listUnknownsInputSchema = z.strictObject({
|
||||
.optional(),
|
||||
severity: z.enum(["low", "medium", "high", "critical"]).optional(),
|
||||
domain: z.string().trim().min(1).max(100).optional(),
|
||||
offset: z.number().int().min(0).default(0),
|
||||
limit: z.number().int().min(1).max(500).default(100),
|
||||
});
|
||||
|
||||
/** Exact residual-unknown identity to revalidate. */
|
||||
@@ -429,7 +445,7 @@ export const SESSION_TOOL_CONTRACTS = [
|
||||
),
|
||||
session(
|
||||
"export_evidence_bundle",
|
||||
"Return the session's deterministic Evidence v2 bundle, or atomically write it beneath an operator-approved root. Existing files require overwrite: true; records and manifests use canonical byte-stable ordering.",
|
||||
"Atomically write the session's deterministic Evidence v2 bundle beneath an operator-approved root. Existing files require overwrite: true; records and manifests use canonical byte-stable ordering. For an in-session read, use snapshot_evidence_bundle and read its exact resource URI instead.",
|
||||
exportEvidenceBundleInputSchema,
|
||||
),
|
||||
session(
|
||||
@@ -484,7 +500,7 @@ export const SESSION_TOOL_CONTRACTS = [
|
||||
),
|
||||
session(
|
||||
"list_unknowns",
|
||||
"List current residual-unknown heads in deterministic ID order, with optional exact status, severity, and domain filters. This is read-only; unresolved, contradicted, and non-truth dispositions remain distinct.",
|
||||
"List current residual-unknown heads in deterministic ID order, with optional exact status, severity, and domain filters. Pages default to 100 items; while has_more is true, pass next_offset as offset and continue until false. This is read-only; unresolved, contradicted, and non-truth dispositions remain distinct.",
|
||||
listUnknownsInputSchema,
|
||||
),
|
||||
session(
|
||||
@@ -507,6 +523,21 @@ export const SESSION_TOOL_CONTRACTS = [
|
||||
"Evaluate ordered HTTP and WebSocket events directly against one validated finite replay machine without opening sockets or launching a target. Returns every decision, a capture-value-free transition journal, one redacted action table entry per used transition, captured aliases, final state, and exact configured and consumed limits.",
|
||||
replayMachineRunInputSchema,
|
||||
),
|
||||
session(
|
||||
"snapshot_evidence_bundle",
|
||||
"Retain the current canonical Evidence v2 bundle as an immutable session resource. Returns a compact digest summary and exact opaque URI; copy that URI unchanged and call MCP resources/read (Codex: read_mcp_resource) for the full bundle. Repeating an unchanged snapshot is idempotent.",
|
||||
snapshotEvidenceBundleInputSchema,
|
||||
),
|
||||
session(
|
||||
"get_navigation_context",
|
||||
"Return the selected document, current address, and containing/current procedure in one coherent provider-neutral request. A cursor outside any procedure returns procedure: null. Scalar navigation getters remain available for evaluation and compatibility.",
|
||||
navigationContextInputSchema,
|
||||
),
|
||||
session(
|
||||
"inspect_address_context",
|
||||
"Inspect one explicit reproducible address for its analyzed name, containing procedure, regular and inline comments, and matching bookmarks. Each unsupported facet returns a typed unavailable outcome; xrefs, assembly, and pseudocode remain separate bounded follow-ups.",
|
||||
addressContextInputSchema,
|
||||
),
|
||||
] as const satisfies readonly ToolContract[];
|
||||
|
||||
/** Complete ordered public inventory used by registration and verification. */
|
||||
|
||||
@@ -172,6 +172,9 @@ export const TOOL_EFFECTS: Readonly<Record<string, ToolEffects>> = {
|
||||
writesFilesystem: true,
|
||||
mayDiscardData: true,
|
||||
}),
|
||||
snapshot_evidence_bundle: effects({ mutatesSession: true }),
|
||||
get_navigation_context: effects({ mutatesSession: true }),
|
||||
inspect_address_context: effects({ mutatesSession: true }),
|
||||
import_evidence_bundle: effects({ mutatesSession: true }),
|
||||
capture_process_scenario: effects({
|
||||
mutatesSession: true,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { z } from "zod";
|
||||
import { jsonValueSchema } from "../domain/jsonValue.js";
|
||||
|
||||
import {
|
||||
processCaptureComparisonSchema,
|
||||
processCaptureSchema,
|
||||
} from "../domain/processCapture.js";
|
||||
import { evidenceBundleSchema } from "../domain/evidenceBundle.js";
|
||||
import { residualUnknownSchema } from "../domain/residualUnknown.js";
|
||||
import {
|
||||
functionInstructionWindowSchema,
|
||||
@@ -67,6 +67,15 @@ import {
|
||||
targetKindSchema,
|
||||
} from "./toolOutputSchemaPrimitives.js";
|
||||
|
||||
const contextFacetSchema = z.discriminatedUnion("state", [
|
||||
z.object({ state: z.literal("available"), value: jsonValueSchema }),
|
||||
z.object({
|
||||
state: z.literal("unavailable"),
|
||||
reason: z.string(),
|
||||
remediation: z.string(),
|
||||
}),
|
||||
]);
|
||||
|
||||
/** Exact structured-content schemas shared by direct analysis providers. */
|
||||
export const officialOutputSchemas: Readonly<Record<string, z.ZodObject>> = {
|
||||
address_name: resultOf(nullableText),
|
||||
@@ -403,14 +412,40 @@ export const sessionOutputSchemas: Readonly<Record<string, z.ZodObject>> = {
|
||||
]),
|
||||
),
|
||||
export_evidence_bundle: lifecycleResultOf(
|
||||
z.union([
|
||||
evidenceBundleSchema,
|
||||
z.object({
|
||||
path: z.string(),
|
||||
bytes: z.number().int().min(0),
|
||||
records: z.number().int().min(0),
|
||||
}),
|
||||
]),
|
||||
z.object({
|
||||
path: z.string(),
|
||||
bytes: z.number().int().min(0),
|
||||
records: z.number().int().min(0),
|
||||
unknowns: z.number().int().min(0),
|
||||
}),
|
||||
),
|
||||
snapshot_evidence_bundle: lifecycleResultOf(
|
||||
z.object({
|
||||
bundle_digest: z.string().regex(/^[a-f0-9]{64}$/u),
|
||||
bundle_version: z.literal(2),
|
||||
bytes: z.number().int().min(0),
|
||||
records: z.number().int().min(0),
|
||||
unknowns: z.number().int().min(0),
|
||||
bundle_uri: z.string().regex(/^rea:\/\/evidence-bundle\/[a-f0-9]{64}$/u),
|
||||
}),
|
||||
),
|
||||
get_navigation_context: lifecycleResultOf(
|
||||
z.object({
|
||||
document: z.string(),
|
||||
address: z.string(),
|
||||
procedure: z.union([z.string(), z.null()]),
|
||||
}),
|
||||
),
|
||||
inspect_address_context: lifecycleResultOf(
|
||||
z.object({
|
||||
address: z.string(),
|
||||
document: z.string().nullable(),
|
||||
name: contextFacetSchema,
|
||||
procedure: contextFacetSchema,
|
||||
comment: contextFacetSchema,
|
||||
inline_comment: contextFacetSchema,
|
||||
bookmarks: contextFacetSchema,
|
||||
}),
|
||||
),
|
||||
import_evidence_bundle: lifecycleResultOf(
|
||||
z.object({
|
||||
@@ -427,7 +462,21 @@ export const sessionOutputSchemas: Readonly<Record<string, z.ZodObject>> = {
|
||||
build_call_path: resultOf(callPathResultSchema),
|
||||
correlate_static_and_runtime: resultOf(staticRuntimeCorrelationResultSchema),
|
||||
verify_reconstruction: resultOf(reconstructionVerificationResultSchema),
|
||||
list_unknowns: lifecycleResultOf(z.array(residualUnknownSchema)),
|
||||
list_unknowns: lifecycleResultOf(
|
||||
z.object({
|
||||
items: z.array(
|
||||
z.object({
|
||||
unknown: residualUnknownSchema,
|
||||
uri: z.string().regex(/^rea:\/\/unknown\/unk_[a-f0-9]{64}$/u),
|
||||
}),
|
||||
),
|
||||
offset: z.number().int().min(0),
|
||||
limit: z.number().int().min(1).max(500),
|
||||
total: z.number().int().min(0),
|
||||
next_offset: z.number().int().min(0).nullable(),
|
||||
has_more: z.boolean(),
|
||||
}),
|
||||
),
|
||||
record_unknown: lifecycleResultOf(residualUnknownSchema),
|
||||
update_unknown: lifecycleResultOf(residualUnknownSchema),
|
||||
verify_unknown_resolution: lifecycleResultOf(
|
||||
|
||||
@@ -30,7 +30,7 @@ interface GeneratedPayload {
|
||||
}
|
||||
|
||||
const GENERATED_PAYLOAD_JSON =
|
||||
'{"catalog":[{"name":"address_name","analysisOperation":"address_name","title":"Address Name","description":"Resolve the primary analyzed name at a code or data address. Headless providers require an explicit address; GUI providers may default to their current cursor. Null means the provider has no primary name at that address.","kind":"official-proxy","requiresSession":false,"inputSchema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"document":{"description":"The document name","type":"string"},"address":{"type":"string","description":"A provider-normalized address; default memory uses 0x-prefixed hexadecimal"},"unknown_registry_approved":{"description":"Explicit approval to record typed capability unavailability as a residual unknown","type":"boolean","const":true}},"additionalProperties":false,"examples":[{}]},"outputSchema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"result":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Primary structured result returned by this operation."},"evidence_id":{"type":"string","pattern":"^ev_[a-f0-9]{64}$","description":"Stable identifier of the recorded Evidence v2 observation."},"evidence_uri":{"type":"string","pattern":"^rea:\\\\/\\\\/evidence\\\\/ev_[a-f0-9]{64}$","description":"Session resource URI for the recorded Evidence v2 observation."}},"required":["result","evidence_id","evidence_uri"],"additionalProperties":false},"annotations":{"readOnlyHint":false,"destructiveHint":false,"idempotentHint":true,"openWorldHint":false},"effects":{"mutatesTarget":false,"mutatesSession":true,"writesFilesystem":false,"launchesProcess":false,"accessesNetwork":false,"changesUiState":false,"mayDiscardData":false,"idempotent":true}},{"name":"comment","analysisOperation":"comment","title":"Comment","description":"Read the regular analysis comment at an address, defaulting to the current cursor. This is read-only and returns null when no comment exists; use set_comment to persist a finding.","kind":"official-proxy","requiresSession":false,"inputSchema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"document":{"description":"The document name","type":"string"},"address":{"type":"string","description":"A provider-normalized address; default memory uses 0x-prefixed hexadecimal"},"unknown_registry_approved":{"description":"Explicit approval to record typed capability unavailability as a residual unknown","type":"boolean","const":true}},"additionalProperties":false,"examples":[{}]},"outputSchema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"result":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Primary structured result returned by this operation."},"evidence_id":{"type":"string","pattern":"^ev_[a-f0-9]{64}$","description":"Stable identifier of the recorded Evidence v2 observation."},"evidence_uri":{"type":"string","pattern":"^rea:\\\\/\\\\/evidence\\\\/ev_[a-f0-9]{64}$","description":"Session resource URI for the recorded Evidence v2 observation."}},"required":["result","evidence_id","evidence_uri"],"additionalProperties":false},"annotations":{"readOnlyHint":false,"destructiveHint":false,"idempotentHint":true,"openWorldHint":false},"effects":{"mutatesTarget":false,"mutatesSession":true,"writesFilesystem":false,"launchesProcess":false,"accessesNetwork":false,"changesUiState":false,"mayDiscardData":false,"idempotent":true}},{"name":"current_address","analysisOperation":"current_address","title":"Current Address","description":"Return Hopper\'s current cursor address for the selected document. Use only for interactive navigation state; prefer explicit addresses in reproducible investigations.","kind":"official-proxy","requiresSession":false,"inputSchema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"document":{"description":"The document name","type":"string"},"unknown_registry_approved":{"description":"Explicit approval to record typed capability unavailability as a residual unknown","type":"boolean","const":true}},"additionalProperties":false,"examples":[{}]},"outputSchema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"result":{"type":"string","description":"Primary structured result returned by this operation."},"evidence_id":{"type":"string","pattern":"^ev_[a-f0-9]{64}$","description":"Stable identifier of the recorded Evidence v2 observation."},"evidence_uri":{"type":"string","pattern":"^rea:\\\\/\\\\/evidence\\\\/ev_[a-f0-9]{64}$","description":"Session resource URI for the recorded Evidence v2 observation."}},"required":["result","evidence_id","evidence_uri"],"additionalProperties":false},"annotations":{"readOnlyHint":false,"destructiveHint":false,"idempotentHint":true,"openWorldHint":false},"effects":{"mutatesTarget":false,"mutatesSession":true,"writesFilesystem":false,"launchesProcess":false,"accessesNe
Line truncated
|
||||
'{"catalog":[{"name":"address_name","analysisOperation":"address_name","title":"Address Name","description":"Resolve the primary analyzed name at a code or data address. Headless providers require an explicit address; GUI providers may default to their current cursor. Null means the provider has no primary name at that address.","kind":"official-proxy","requiresSession":false,"inputSchema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"document":{"description":"The document name","type":"string"},"address":{"type":"string","description":"A provider-normalized address; default memory uses 0x-prefixed hexadecimal"},"unknown_registry_approved":{"description":"Explicit approval to record typed capability unavailability as a residual unknown","type":"boolean","const":true}},"additionalProperties":false,"examples":[{}]},"outputSchema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"result":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Primary structured result returned by this operation."},"evidence_id":{"type":"string","pattern":"^ev_[a-f0-9]{64}$","description":"Stable identifier of the recorded Evidence v2 observation."},"evidence_uri":{"type":"string","pattern":"^rea:\\\\/\\\\/evidence\\\\/ev_[a-f0-9]{64}$","description":"Session resource URI for the recorded Evidence v2 observation."}},"required":["result","evidence_id","evidence_uri"],"additionalProperties":false},"annotations":{"readOnlyHint":false,"destructiveHint":false,"idempotentHint":true,"openWorldHint":false},"effects":{"mutatesTarget":false,"mutatesSession":true,"writesFilesystem":false,"launchesProcess":false,"accessesNetwork":false,"changesUiState":false,"mayDiscardData":false,"idempotent":true}},{"name":"comment","analysisOperation":"comment","title":"Comment","description":"Read the regular analysis comment at an address, defaulting to the current cursor. This is read-only and returns null when no comment exists; use set_comment to persist a finding.","kind":"official-proxy","requiresSession":false,"inputSchema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"document":{"description":"The document name","type":"string"},"address":{"type":"string","description":"A provider-normalized address; default memory uses 0x-prefixed hexadecimal"},"unknown_registry_approved":{"description":"Explicit approval to record typed capability unavailability as a residual unknown","type":"boolean","const":true}},"additionalProperties":false,"examples":[{}]},"outputSchema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"result":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Primary structured result returned by this operation."},"evidence_id":{"type":"string","pattern":"^ev_[a-f0-9]{64}$","description":"Stable identifier of the recorded Evidence v2 observation."},"evidence_uri":{"type":"string","pattern":"^rea:\\\\/\\\\/evidence\\\\/ev_[a-f0-9]{64}$","description":"Session resource URI for the recorded Evidence v2 observation."}},"required":["result","evidence_id","evidence_uri"],"additionalProperties":false},"annotations":{"readOnlyHint":false,"destructiveHint":false,"idempotentHint":true,"openWorldHint":false},"effects":{"mutatesTarget":false,"mutatesSession":true,"writesFilesystem":false,"launchesProcess":false,"accessesNetwork":false,"changesUiState":false,"mayDiscardData":false,"idempotent":true}},{"name":"current_address","analysisOperation":"current_address","title":"Current Address","description":"Return Hopper\'s current cursor address for the selected document. Use only for interactive navigation state; prefer explicit addresses in reproducible investigations.","kind":"official-proxy","requiresSession":false,"inputSchema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"document":{"description":"The document name","type":"string"},"unknown_registry_approved":{"description":"Explicit approval to record typed capability unavailability as a residual unknown","type":"boolean","const":true}},"additionalProperties":false,"examples":[{}]},"outputSchema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"result":{"type":"string","description":"Primary structured result returned by this operation."},"evidence_id":{"type":"string","pattern":"^ev_[a-f0-9]{64}$","description":"Stable identifier of the recorded Evidence v2 observation."},"evidence_uri":{"type":"string","pattern":"^rea:\\\\/\\\\/evidence\\\\/ev_[a-f0-9]{64}$","description":"Session resource URI for the recorded Evidence v2 observation."}},"required":["result","evidence_id","evidence_uri"],"additionalProperties":false},"annotations":{"readOnlyHint":false,"destructiveHint":false,"idempotentHint":true,"openWorldHint":false},"effects":{"mutatesTarget":false,"mutatesSession":true,"writesFilesystem":false,"launchesProcess":false,"accessesNe
Line truncated
|
||||
const generatedPayload: unknown = JSON.parse(GENERATED_PAYLOAD_JSON);
|
||||
if (!isGeneratedPayload(generatedPayload))
|
||||
throw new TypeError("Generated MCP catalog payload is invalid");
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
export const PACKAGE_METADATA = {
|
||||
name: "rea-agents",
|
||||
version: "2.7.0", // x-release-please-version
|
||||
serverSdkVersion: "2.0.0-beta.4",
|
||||
clientSdkVersion: "2.0.0-beta.4",
|
||||
coreSdkVersion: "2.0.0-beta.4",
|
||||
serverSdkVersion: "2.0.0",
|
||||
clientSdkVersion: "2.0.0",
|
||||
coreSdkVersion: "2.0.0",
|
||||
skillVersion: "23",
|
||||
} as const;
|
||||
@@ -123,7 +123,7 @@ export const createServer = (
|
||||
{
|
||||
capabilities: {
|
||||
tools: { listChanged: true },
|
||||
resources: { listChanged: true },
|
||||
resources: { listChanged: true, subscribe: true },
|
||||
},
|
||||
inputRequired: {
|
||||
maxRounds: 3,
|
||||
@@ -243,8 +243,6 @@ const createSessionRecorders = (
|
||||
? undefined
|
||||
: (evidence: Parameters<typeof session.recordEvidence>[0]) => {
|
||||
const recorded = session.recordEvidence(evidence);
|
||||
if (recorded.ok && recorded.value === "added")
|
||||
server.sendResourceListChanged();
|
||||
return recorded;
|
||||
},
|
||||
recordEvidenceWithUnknown:
|
||||
@@ -255,7 +253,6 @@ const createSessionRecorders = (
|
||||
input: Parameters<typeof session.recordEvidenceWithUnknown>[1],
|
||||
) => {
|
||||
const recorded = session.recordEvidenceWithUnknown(evidence, input);
|
||||
if (recorded.ok) server.sendResourceListChanged();
|
||||
return recorded;
|
||||
},
|
||||
});
|
||||
|
||||
@@ -276,10 +276,7 @@ const registerApplicationFamily = async ({
|
||||
session === undefined
|
||||
? undefined
|
||||
: (workspace) => {
|
||||
const retained =
|
||||
session.retainReconstructionCoverageWorkspace(workspace);
|
||||
if (retained === "added") server.sendResourceListChanged();
|
||||
return retained;
|
||||
return session.retainReconstructionCoverageWorkspace(workspace);
|
||||
},
|
||||
replay: {
|
||||
policy: options.javascriptReplayPolicy ?? {
|
||||
|
||||
@@ -6,16 +6,12 @@ import {
|
||||
import { createHash } from "node:crypto";
|
||||
|
||||
import canonicalize from "canonicalize";
|
||||
|
||||
import type { BinarySessionPort } from "../application/BinarySessionPort.js";
|
||||
import type { JsonValue } from "../domain/jsonValue.js";
|
||||
import { registerJavaScriptApplicationGraphResource } from "./registerJavaScriptApplicationGraphResource.js";
|
||||
import { registerReconstructionObligationLedgerResource } from "./registerReconstructionObligationLedgerResource.js";
|
||||
import { registerReconstructionReadinessResource } from "./registerReconstructionReadinessResource.js";
|
||||
|
||||
const evidenceUri = (evidenceId: string): string =>
|
||||
`rea://evidence/${evidenceId}`;
|
||||
|
||||
/** Expose immutable session-owned Evidence v2 records as MCP resources. */
|
||||
export const registerEvidenceResources = (
|
||||
server: McpServer,
|
||||
@@ -24,7 +20,7 @@ export const registerEvidenceResources = (
|
||||
registerReconstructionCoverageResource(server, session);
|
||||
registerInvestigationWorkspaceResource(server, session);
|
||||
registerSessionEvidenceResource(server, session);
|
||||
registerActiveResidualUnknownsResource(server, session);
|
||||
registerEvidenceBundleResource(server, session);
|
||||
registerAnalysisSnapshotResource(server, session);
|
||||
registerArtifactPageResource(server, session);
|
||||
registerFunctionDossierResource(server, session);
|
||||
@@ -33,6 +29,11 @@ export const registerEvidenceResources = (
|
||||
registerReconstructionReadinessResource(server, session);
|
||||
registerJavaScriptApplicationGraphResource(server, session);
|
||||
registerResidualUnknownResource(server, session);
|
||||
session.onAnalysisSnapshotChanged?.(() => {
|
||||
void server.server
|
||||
.sendResourceUpdated({ uri: "rea://snapshot/current" })
|
||||
.catch(() => undefined);
|
||||
});
|
||||
};
|
||||
|
||||
const registerReconstructionCoverageResource = (
|
||||
@@ -44,21 +45,7 @@ const registerReconstructionCoverageResource = (
|
||||
new ResourceTemplate(
|
||||
"rea://reconstruction-coverage/{workspaceId}/revision/{revision}",
|
||||
{
|
||||
list: () => ({
|
||||
resources: session
|
||||
.reconstructionCoverageWorkspaces()
|
||||
.map((workspace) => ({
|
||||
uri: coverageWorkspaceUri(
|
||||
workspace.workspace_id,
|
||||
workspace.revision,
|
||||
),
|
||||
name: `${workspace.workspace_id} revision ${String(workspace.revision)}`,
|
||||
title: `${workspace.name} coverage revision ${String(workspace.revision)}`,
|
||||
description:
|
||||
"Immutable evidence-backed reconstruction coverage workspace revision.",
|
||||
mimeType: "application/json",
|
||||
})),
|
||||
}),
|
||||
list: undefined,
|
||||
},
|
||||
),
|
||||
{
|
||||
@@ -89,34 +76,7 @@ const registerInvestigationWorkspaceResource = (
|
||||
server.registerResource(
|
||||
"investigation-workspace-revision",
|
||||
new ResourceTemplate("rea://workspace/{workspaceId}/revision/{revision}", {
|
||||
list: () => ({
|
||||
resources: session.investigationWorkspaces().map((workspace) => ({
|
||||
uri: workspaceUri(workspace.workspace_id, workspace.revision),
|
||||
name: `${workspace.workspace_id} revision ${String(workspace.revision)}`,
|
||||
title: `${workspace.name} revision ${String(workspace.revision)}`,
|
||||
description: "Immutable CAS-linked investigation workspace revision.",
|
||||
mimeType: "application/json",
|
||||
})),
|
||||
}),
|
||||
complete: {
|
||||
workspaceId: (prefix) =>
|
||||
[
|
||||
...new Set(
|
||||
session
|
||||
.investigationWorkspaces()
|
||||
.map(({ workspace_id }) => workspace_id),
|
||||
),
|
||||
].filter((workspaceId) => workspaceId.startsWith(prefix)),
|
||||
revision: (prefix, context) =>
|
||||
session
|
||||
.investigationWorkspaces()
|
||||
.filter(
|
||||
({ workspace_id }) =>
|
||||
context?.arguments?.workspaceId === workspace_id,
|
||||
)
|
||||
.map(({ revision }) => String(revision))
|
||||
.filter((revision) => revision.startsWith(prefix)),
|
||||
},
|
||||
list: undefined,
|
||||
}),
|
||||
{
|
||||
title: "Investigation workspace revision",
|
||||
@@ -145,22 +105,7 @@ const registerSessionEvidenceResource = (
|
||||
server.registerResource(
|
||||
"session-evidence",
|
||||
new ResourceTemplate("rea://evidence/{evidenceId}", {
|
||||
list: () => ({
|
||||
resources: session.exportEvidenceBundle().records.map((evidence) => ({
|
||||
uri: evidenceUri(evidence.evidence_id),
|
||||
name: evidence.evidence_id,
|
||||
title: `${evidence.operation} evidence`,
|
||||
description: `Session-owned Evidence v2 record for ${evidence.operation}.`,
|
||||
mimeType: "application/json",
|
||||
})),
|
||||
}),
|
||||
complete: {
|
||||
evidenceId: (prefix) =>
|
||||
session
|
||||
.exportEvidenceBundle()
|
||||
.records.map(({ evidence_id }) => evidence_id)
|
||||
.filter((evidenceId) => evidenceId.startsWith(prefix)),
|
||||
},
|
||||
list: undefined,
|
||||
}),
|
||||
{
|
||||
title: "Session evidence",
|
||||
@@ -186,23 +131,36 @@ const registerSessionEvidenceResource = (
|
||||
);
|
||||
};
|
||||
|
||||
const registerActiveResidualUnknownsResource = (
|
||||
const registerEvidenceBundleResource = (
|
||||
server: McpServer,
|
||||
session: BinarySessionPort,
|
||||
): void => {
|
||||
server.registerResource(
|
||||
"active-residual-unknowns",
|
||||
"rea://unknowns/active",
|
||||
"evidence-bundle",
|
||||
new ResourceTemplate("rea://evidence-bundle/{bundleDigest}", {
|
||||
list: undefined,
|
||||
}),
|
||||
{
|
||||
title: "Active residual unknowns",
|
||||
description: "Current unresolved session-owned residual unknown heads.",
|
||||
title: "Evidence bundle",
|
||||
description: "Immutable session-retained canonical Evidence v2 bundle.",
|
||||
mimeType: "application/json",
|
||||
},
|
||||
(uri) =>
|
||||
jsonResource(
|
||||
uri.href,
|
||||
session.listUnknowns().filter(({ status }) => status !== "resolved"),
|
||||
),
|
||||
(uri, variables) => {
|
||||
const digest = stringVariable(variables.bundleDigest, uri.href);
|
||||
if (!/^[a-f0-9]{64}$/u.test(digest))
|
||||
throw new ResourceNotFoundError(uri.href);
|
||||
const text = session.retainedEvidenceBundle(digest);
|
||||
if (text === undefined) throw new ResourceNotFoundError(uri.href);
|
||||
return {
|
||||
contents: [
|
||||
{
|
||||
uri: uri.href,
|
||||
mimeType: "application/json",
|
||||
text,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
@@ -212,36 +170,30 @@ const registerAnalysisSnapshotResource = (
|
||||
): void => {
|
||||
server.registerResource(
|
||||
"analysis-snapshot",
|
||||
new ResourceTemplate("rea://snapshot/{snapshotDigest}", {
|
||||
list: () => {
|
||||
const snapshot = session.exportAnalysisSnapshot();
|
||||
if (!snapshot.ok) return { resources: [] };
|
||||
const digest = contentDigest(snapshot.value);
|
||||
return {
|
||||
resources: [
|
||||
{
|
||||
uri: `rea://snapshot/${digest}`,
|
||||
name: digest,
|
||||
title: "Current analysis snapshot",
|
||||
description: "Immutable projection of the current target cache.",
|
||||
mimeType: "application/json",
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
}),
|
||||
"rea://snapshot/current",
|
||||
{
|
||||
title: "Analysis snapshot",
|
||||
description:
|
||||
"Current provider-neutral analysis snapshot by content digest.",
|
||||
"Mutable provider-neutral analysis snapshot for the current native target.",
|
||||
mimeType: "application/json",
|
||||
},
|
||||
(uri, variables) => {
|
||||
const requested = stringVariable(variables.snapshotDigest, uri.href);
|
||||
(uri) => {
|
||||
const snapshot = session.exportAnalysisSnapshot();
|
||||
if (!snapshot.ok || contentDigest(snapshot.value) !== requested)
|
||||
throw new ResourceNotFoundError(uri.href);
|
||||
return jsonResource(uri.href, snapshot.value);
|
||||
if (!snapshot.ok)
|
||||
return jsonResource(uri.href, {
|
||||
state: "unavailable",
|
||||
reason: snapshot.error.message,
|
||||
remediation:
|
||||
"Open a native target with a concrete analysis profile. After metadata mutation, call open_binary again for the current target to begin a fresh cache.",
|
||||
});
|
||||
return jsonResource(uri.href, {
|
||||
state: "available",
|
||||
target: snapshot.value.target,
|
||||
profile: snapshot.value.binding,
|
||||
content_digest: contentDigest(snapshot.value),
|
||||
entry_count: snapshot.value.entries.length,
|
||||
snapshot: snapshot.value,
|
||||
});
|
||||
},
|
||||
);
|
||||
};
|
||||
@@ -333,7 +285,6 @@ const registerEvidenceSectionResource = (
|
||||
new ResourceTemplate("rea://evidence/{evidenceId}/section/{section}", {
|
||||
list: undefined,
|
||||
complete: {
|
||||
evidenceId: (prefix) => evidenceIds(session, prefix),
|
||||
section: (prefix) =>
|
||||
EVIDENCE_SECTIONS.filter((section) => section.startsWith(prefix)),
|
||||
},
|
||||
@@ -372,22 +323,7 @@ const registerResidualUnknownResource = (
|
||||
server.registerResource(
|
||||
"residual-unknown",
|
||||
new ResourceTemplate("rea://unknown/{unknownId}", {
|
||||
list: () => ({
|
||||
resources: session.listUnknowns().map((unknown) => ({
|
||||
uri: `rea://unknown/${unknown.unknown_id}`,
|
||||
name: unknown.unknown_id,
|
||||
title: unknown.question,
|
||||
description: `Current ${unknown.status} residual unknown revision.`,
|
||||
mimeType: "application/json",
|
||||
})),
|
||||
}),
|
||||
complete: {
|
||||
unknownId: (prefix) =>
|
||||
session
|
||||
.listUnknowns()
|
||||
.map(({ unknown_id }) => unknown_id)
|
||||
.filter((unknownId) => unknownId.startsWith(prefix)),
|
||||
},
|
||||
list: undefined,
|
||||
}),
|
||||
{
|
||||
title: "Residual unknown",
|
||||
@@ -408,12 +344,6 @@ const registerResidualUnknownResource = (
|
||||
);
|
||||
};
|
||||
|
||||
const coverageWorkspaceUri = (workspaceId: string, revision: number): string =>
|
||||
`rea://reconstruction-coverage/${workspaceId}/revision/${String(revision)}`;
|
||||
|
||||
const workspaceUri = (workspaceId: string, revision: number): string =>
|
||||
`rea://workspace/${workspaceId}/revision/${String(revision)}`;
|
||||
|
||||
const EVIDENCE_SECTIONS = [
|
||||
"result",
|
||||
"terminal",
|
||||
@@ -425,12 +355,6 @@ const EVIDENCE_SECTIONS = [
|
||||
"edges",
|
||||
] as const;
|
||||
|
||||
const evidenceIds = (session: BinarySessionPort, prefix: string): string[] =>
|
||||
session
|
||||
.exportEvidenceBundle()
|
||||
.records.map(({ evidence_id }) => evidence_id)
|
||||
.filter((evidenceId) => evidenceId.startsWith(prefix));
|
||||
|
||||
const stringVariable = (value: unknown, uri: string): string => {
|
||||
if (typeof value !== "string") throw new ResourceNotFoundError(uri);
|
||||
return value;
|
||||
|
||||
@@ -209,8 +209,7 @@ const runAutomaticInvestigation = async (input: {
|
||||
);
|
||||
if (!investigated.ok) return toCallToolResult(investigated, contract);
|
||||
const workspace = investigated.value.workspace;
|
||||
if (session.retainInvestigationWorkspace(workspace) === "added")
|
||||
input.server.sendResourceListChanged();
|
||||
session.retainInvestigationWorkspace(workspace);
|
||||
const result = changedBehaviorResultSchema.parse(
|
||||
investigated.value.evidence.normalized_result,
|
||||
);
|
||||
|
||||
@@ -19,11 +19,6 @@ export const registerJavaScriptApplicationGraphResource = (
|
||||
{
|
||||
list: undefined,
|
||||
complete: {
|
||||
evidenceId: (prefix) =>
|
||||
session
|
||||
.exportEvidenceBundle()
|
||||
.records.map(({ evidence_id }) => evidence_id)
|
||||
.filter((evidenceId) => evidenceId.startsWith(prefix)),
|
||||
collection: (prefix) =>
|
||||
["nodes", "edges"].filter((name) => name.startsWith(prefix)),
|
||||
},
|
||||
|
||||
@@ -18,17 +18,6 @@ export const registerReconstructionObligationLedgerResource = (
|
||||
"rea://evidence/{evidenceId}/reconstruction-obligation-ledger",
|
||||
{
|
||||
list: undefined,
|
||||
complete: {
|
||||
evidenceId: (prefix) =>
|
||||
session
|
||||
.exportEvidenceBundle()
|
||||
.records.filter(
|
||||
({ predicate_type: predicateType }) =>
|
||||
predicateType === "rea.reconstruction-obligation-ledger/v1",
|
||||
)
|
||||
.map(({ evidence_id: evidenceId }) => evidenceId)
|
||||
.filter((evidenceId) => evidenceId.startsWith(prefix)),
|
||||
},
|
||||
},
|
||||
),
|
||||
{
|
||||
|
||||
@@ -18,17 +18,6 @@ export const registerReconstructionReadinessResource = (
|
||||
"rea://evidence/{evidenceId}/reconstruction-readiness-report",
|
||||
{
|
||||
list: undefined,
|
||||
complete: {
|
||||
evidenceId: (prefix) =>
|
||||
session
|
||||
.exportEvidenceBundle()
|
||||
.records.filter(
|
||||
({ predicate_type: predicateType }) =>
|
||||
predicateType === "rea.reconstruction-readiness-report/v1",
|
||||
)
|
||||
.map(({ evidence_id: evidenceId }) => evidenceId)
|
||||
.filter((evidenceId) => evidenceId.startsWith(prefix)),
|
||||
},
|
||||
},
|
||||
),
|
||||
{
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
exportEvidenceBundleInputSchema,
|
||||
importEvidenceBundleInputSchema,
|
||||
listUnknownsInputSchema,
|
||||
snapshotEvidenceBundleInputSchema,
|
||||
SESSION_TOOL_CONTRACTS,
|
||||
verifyUnknownResolutionInputSchema,
|
||||
} from "../contracts/toolContracts.js";
|
||||
@@ -33,6 +34,7 @@ interface EvidenceToolRegistration {
|
||||
readonly session: BinarySessionPort;
|
||||
readonly exportContract: (typeof SESSION_TOOL_CONTRACTS)[3];
|
||||
readonly importContract: (typeof SESSION_TOOL_CONTRACTS)[4];
|
||||
readonly snapshotContract: (typeof SESSION_TOOL_CONTRACTS)[19];
|
||||
readonly filePolicy: EvidenceFilePolicy;
|
||||
readonly permissionAuthority?: PermissionAuthority;
|
||||
}
|
||||
@@ -43,6 +45,7 @@ export const registerEvidenceTools = (
|
||||
): void => {
|
||||
registerExportEvidenceTool(registration);
|
||||
registerImportEvidenceTool(registration);
|
||||
registerSnapshotEvidenceTool(registration);
|
||||
};
|
||||
|
||||
const registerExportEvidenceTool = ({
|
||||
@@ -64,8 +67,6 @@ const registerExportEvidenceTool = ({
|
||||
if (!parsedInput.ok) return toCallToolResult(parsedInput, exportContract);
|
||||
const parsed = parsedInput.value;
|
||||
const bundle = session.exportEvidenceBundle();
|
||||
if (parsed.path === undefined)
|
||||
return toCallToolResult(ok(bundle), exportContract);
|
||||
const denied = await authorizeEvidencePath({
|
||||
authority: permissionAuthority,
|
||||
capability: "evidence_write",
|
||||
@@ -86,6 +87,7 @@ const registerExportEvidenceTool = ({
|
||||
path: written.value.path,
|
||||
bytes: written.value.bytes,
|
||||
records: bundle.records.length,
|
||||
unknowns: bundle.unknowns.length,
|
||||
}),
|
||||
exportContract,
|
||||
)
|
||||
@@ -94,6 +96,44 @@ const registerExportEvidenceTool = ({
|
||||
);
|
||||
};
|
||||
|
||||
const registerSnapshotEvidenceTool = ({
|
||||
server,
|
||||
session,
|
||||
snapshotContract,
|
||||
}: EvidenceToolRegistration): void => {
|
||||
server.registerTool(
|
||||
snapshotContract.name,
|
||||
toolRegistrationOptions(snapshotContract),
|
||||
(input) => {
|
||||
const parsed = safeParseToolInput(
|
||||
snapshotEvidenceBundleInputSchema,
|
||||
input,
|
||||
snapshotContract.name,
|
||||
);
|
||||
if (!parsed.ok) return toCallToolResult(parsed, snapshotContract);
|
||||
const snapshot = session.snapshotEvidenceBundle();
|
||||
if (!snapshot.ok) return toCallToolResult(snapshot, snapshotContract);
|
||||
const value = {
|
||||
bundle_digest: snapshot.value.bundleDigest,
|
||||
bundle_version: snapshot.value.bundleVersion,
|
||||
bytes: snapshot.value.bytes,
|
||||
records: snapshot.value.records,
|
||||
unknowns: snapshot.value.unknowns,
|
||||
bundle_uri: snapshot.value.uri,
|
||||
} as const;
|
||||
return toCallToolResult(ok(value), snapshotContract, {
|
||||
resourceLinks: [
|
||||
{
|
||||
uri: snapshot.value.uri,
|
||||
name: snapshot.value.bundleDigest,
|
||||
description: "Immutable session-retained Evidence v2 bundle",
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const registerImportEvidenceTool = ({
|
||||
server,
|
||||
session,
|
||||
@@ -123,7 +163,6 @@ const registerImportEvidenceTool = ({
|
||||
const loaded = await readEvidenceBundle(path, filePolicy);
|
||||
if (!loaded.ok) return toCallToolResult(loaded, importContract);
|
||||
const imported = session.importEvidenceBundle(loaded.value);
|
||||
if (imported.ok && imported.value > 0) server.sendResourceListChanged();
|
||||
return imported.ok
|
||||
? toCallToolResult(
|
||||
ok({
|
||||
@@ -191,17 +230,40 @@ export const registerUnknownTools = ({
|
||||
);
|
||||
if (!parsed.ok) return toCallToolResult(parsed, listContract);
|
||||
const filters = parsed.value;
|
||||
const all = session.listUnknowns({
|
||||
...(filters.status === undefined ? {} : { status: filters.status }),
|
||||
...(filters.severity === undefined
|
||||
? {}
|
||||
: { severity: filters.severity }),
|
||||
...(filters.domain === undefined ? {} : { domain: filters.domain }),
|
||||
});
|
||||
const items = all
|
||||
.slice(filters.offset, filters.offset + filters.limit)
|
||||
.map((unknown) => ({
|
||||
unknown,
|
||||
uri: `rea://unknown/${unknown.unknown_id}`,
|
||||
}));
|
||||
const nextOffset =
|
||||
filters.offset + items.length < all.length
|
||||
? filters.offset + items.length
|
||||
: null;
|
||||
return toCallToolResult(
|
||||
ok(
|
||||
session.listUnknowns({
|
||||
...(filters.status === undefined ? {} : { status: filters.status }),
|
||||
...(filters.severity === undefined
|
||||
? {}
|
||||
: { severity: filters.severity }),
|
||||
...(filters.domain === undefined ? {} : { domain: filters.domain }),
|
||||
}),
|
||||
),
|
||||
ok({
|
||||
items,
|
||||
offset: filters.offset,
|
||||
limit: filters.limit,
|
||||
total: all.length,
|
||||
next_offset: nextOffset,
|
||||
has_more: nextOffset !== null,
|
||||
}),
|
||||
listContract,
|
||||
{
|
||||
resourceLinks: items.map((unknown) => ({
|
||||
uri: unknown.uri,
|
||||
name: unknown.unknown.unknown_id,
|
||||
description: `Current ${unknown.unknown.status} residual unknown revision`,
|
||||
})),
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -216,8 +278,17 @@ export const registerUnknownTools = ({
|
||||
);
|
||||
if (!parsed.ok) return toCallToolResult(parsed, recordContract);
|
||||
const result = session.recordUnknown(parsed.value);
|
||||
if (result.ok) server.sendResourceListChanged();
|
||||
return toCallToolResult(result, recordContract);
|
||||
return result.ok
|
||||
? toCallToolResult(result, recordContract, {
|
||||
resourceLinks: [
|
||||
{
|
||||
uri: `rea://unknown/${result.value.unknown_id}`,
|
||||
name: result.value.unknown_id,
|
||||
description: "Current residual unknown head",
|
||||
},
|
||||
],
|
||||
})
|
||||
: toCallToolResult(result, recordContract);
|
||||
},
|
||||
);
|
||||
server.registerTool(
|
||||
@@ -231,8 +302,23 @@ export const registerUnknownTools = ({
|
||||
);
|
||||
if (!parsed.ok) return toCallToolResult(parsed, updateContract);
|
||||
const result = session.updateUnknown(parsed.value);
|
||||
if (result.ok) server.sendResourceListChanged();
|
||||
return toCallToolResult(result, updateContract);
|
||||
if (result.ok)
|
||||
void server.server
|
||||
.sendResourceUpdated({
|
||||
uri: `rea://unknown/${result.value.unknown_id}`,
|
||||
})
|
||||
.catch(() => undefined);
|
||||
return result.ok
|
||||
? toCallToolResult(result, updateContract, {
|
||||
resourceLinks: [
|
||||
{
|
||||
uri: `rea://unknown/${result.value.unknown_id}`,
|
||||
name: result.value.unknown_id,
|
||||
description: "Updated residual unknown head",
|
||||
},
|
||||
],
|
||||
})
|
||||
: toCallToolResult(result, updateContract);
|
||||
},
|
||||
);
|
||||
server.registerTool(
|
||||
|
||||
@@ -2,7 +2,11 @@ import type { McpServer } from "@modelcontextprotocol/server";
|
||||
import { z } from "zod";
|
||||
|
||||
import type { BinarySessionPort } from "../application/BinarySession.js";
|
||||
import { SESSION_TOOL_CONTRACTS } from "../contracts/toolContracts.js";
|
||||
import {
|
||||
addressContextInputSchema,
|
||||
navigationContextInputSchema,
|
||||
SESSION_TOOL_CONTRACTS,
|
||||
} from "../contracts/toolContracts.js";
|
||||
import { toCallToolResult } from "./toolResult.js";
|
||||
import type { Logger } from "../logger.js";
|
||||
import { logToolExecution } from "./toolLogging.js";
|
||||
@@ -52,6 +56,10 @@ import {
|
||||
} from "./sessionAvailabilityPolicy.js";
|
||||
import { permissionFailure } from "./permissionFailure.js";
|
||||
import { registerCloseLifecycleTool } from "./registerCloseLifecycleTool.js";
|
||||
import {
|
||||
getNavigationContext,
|
||||
inspectAddressContext,
|
||||
} from "../application/AnalysisContextQueries.js";
|
||||
|
||||
const recordProcessResidualUnknowns = (
|
||||
session: BinarySessionPort,
|
||||
@@ -317,6 +325,54 @@ const registerRecordAndReplayTools = (
|
||||
registerReplayMachineTool(server, logger);
|
||||
};
|
||||
|
||||
const registerContextTools = (
|
||||
server: McpServer,
|
||||
session: BinarySessionPort,
|
||||
): void => {
|
||||
const navigationContract = SESSION_TOOL_CONTRACTS[20];
|
||||
const addressContract = SESSION_TOOL_CONTRACTS[21];
|
||||
server.registerTool(
|
||||
navigationContract.name,
|
||||
toolRegistrationOptions(navigationContract),
|
||||
async (input, context) => {
|
||||
const parsed = safeParseToolInput(
|
||||
navigationContextInputSchema,
|
||||
input,
|
||||
navigationContract.name,
|
||||
);
|
||||
if (!parsed.ok) return toCallToolResult(parsed, navigationContract);
|
||||
return toCallToolResult(
|
||||
await getNavigationContext(
|
||||
session,
|
||||
parsed.value,
|
||||
context.mcpReq.signal,
|
||||
),
|
||||
navigationContract,
|
||||
);
|
||||
},
|
||||
);
|
||||
server.registerTool(
|
||||
addressContract.name,
|
||||
toolRegistrationOptions(addressContract),
|
||||
async (input, context) => {
|
||||
const parsed = safeParseToolInput(
|
||||
addressContextInputSchema,
|
||||
input,
|
||||
addressContract.name,
|
||||
);
|
||||
if (!parsed.ok) return toCallToolResult(parsed, addressContract);
|
||||
return toCallToolResult(
|
||||
await inspectAddressContext(
|
||||
session,
|
||||
parsed.value,
|
||||
context.mcpReq.signal,
|
||||
),
|
||||
addressContract,
|
||||
);
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export const registerSessionTools = (
|
||||
server: McpServer,
|
||||
session: BinarySessionPort,
|
||||
@@ -344,6 +400,7 @@ export const registerSessionTools = (
|
||||
staticRuntimeContract,
|
||||
reconstructionContract,
|
||||
] = SESSION_TOOL_CONTRACTS;
|
||||
const snapshotContract = SESSION_TOOL_CONTRACTS[19];
|
||||
registerLifecycleTools({
|
||||
server,
|
||||
session,
|
||||
@@ -365,6 +422,7 @@ export const registerSessionTools = (
|
||||
session,
|
||||
exportContract,
|
||||
importContract,
|
||||
snapshotContract,
|
||||
filePolicy: evidenceFilePolicy,
|
||||
...(options.permissionAuthority === undefined
|
||||
? {}
|
||||
@@ -411,4 +469,5 @@ export const registerSessionTools = (
|
||||
: { permissionAuthority: options.permissionAuthority }),
|
||||
});
|
||||
registerRecordAndReplayTools(server, session, logger);
|
||||
registerContextTools(server, session);
|
||||
};
|
||||
@@ -89,6 +89,14 @@ const successResult = (
|
||||
options.evidenceTextProjection),
|
||||
),
|
||||
},
|
||||
...(hasResourceLinks(value, contract, options)
|
||||
? [
|
||||
{
|
||||
type: "text" as const,
|
||||
text: "Full detail is available through MCP resources/read. Copy the opaque URI exactly. In Codex, call read_mcp_resource.",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...evidenceResourceLinks(
|
||||
value,
|
||||
contract.kind === "session" ||
|
||||
@@ -104,6 +112,19 @@ const successResult = (
|
||||
};
|
||||
};
|
||||
|
||||
const hasResourceLinks = (
|
||||
value: JsonValue,
|
||||
contract: ToolContract,
|
||||
options: ToolResultOptions,
|
||||
): boolean =>
|
||||
options.resourceLinks !== undefined && options.resourceLinks.length > 0
|
||||
? true
|
||||
: evidenceResourceLinks(
|
||||
value,
|
||||
contract.kind === "session" ||
|
||||
options.evidenceResourcesAvailable === true,
|
||||
).length > 0;
|
||||
|
||||
const compactEvidence = (
|
||||
value: JsonValue,
|
||||
resultProjection: JsonValue | undefined,
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
getNavigationContext,
|
||||
inspectAddressContext,
|
||||
} from "../src/application/AnalysisContextQueries.js";
|
||||
import {
|
||||
createAnalysisExecution,
|
||||
type AnalysisOperationPort,
|
||||
} from "../src/application/AnalysisProvider.js";
|
||||
import {
|
||||
AnalysisCancelledError,
|
||||
AnalysisProtocolError,
|
||||
} from "../src/domain/errors.js";
|
||||
import { err, ok } from "../src/domain/result.js";
|
||||
|
||||
const provider = { id: "fixture", name: "Fixture", version: "1" };
|
||||
|
||||
describe("analysis context queries", () => {
|
||||
it("aggregates volatile navigation and treats a missing procedure as null", async () => {
|
||||
const calls: {
|
||||
operation: string;
|
||||
parameters: Readonly<Record<string, unknown>>;
|
||||
}[] = [];
|
||||
const analysis: AnalysisOperationPort = {
|
||||
execute: (operation, parameters) => {
|
||||
calls.push({ operation, parameters });
|
||||
if (operation === "current_document")
|
||||
return Promise.resolve(
|
||||
ok(createAnalysisExecution("fixture", provider)),
|
||||
);
|
||||
if (operation === "current_address")
|
||||
return Promise.resolve(
|
||||
ok(createAnalysisExecution("0x401000", provider)),
|
||||
);
|
||||
if (operation === "resolve_containing_procedure")
|
||||
return Promise.resolve(
|
||||
ok(
|
||||
createAnalysisExecution(
|
||||
{
|
||||
query_address: "0x401000",
|
||||
found: false,
|
||||
procedure: null,
|
||||
reason: "not_in_procedure",
|
||||
},
|
||||
provider,
|
||||
),
|
||||
),
|
||||
);
|
||||
return Promise.resolve(
|
||||
err(new AnalysisProtocolError(`unexpected ${operation}`)),
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
await expect(getNavigationContext(analysis, {})).resolves.toEqual(
|
||||
ok({
|
||||
document: "fixture",
|
||||
address: "0x401000",
|
||||
procedure: null,
|
||||
}),
|
||||
);
|
||||
expect(calls).toContainEqual({
|
||||
operation: "current_document",
|
||||
parameters: {},
|
||||
});
|
||||
expect(calls).toContainEqual({
|
||||
operation: "resolve_containing_procedure",
|
||||
parameters: { document: "fixture", address: "0x401000" },
|
||||
});
|
||||
});
|
||||
|
||||
it("uses an explicit document without passing it to current_document and preserves procedure failures", async () => {
|
||||
const calls: string[] = [];
|
||||
const failure = new AnalysisProtocolError("provider response malformed");
|
||||
const analysis: AnalysisOperationPort = {
|
||||
execute: (operation) => {
|
||||
calls.push(operation);
|
||||
if (operation === "current_address")
|
||||
return Promise.resolve(
|
||||
ok(createAnalysisExecution("0x401000", provider)),
|
||||
);
|
||||
return Promise.resolve(err(failure));
|
||||
},
|
||||
};
|
||||
|
||||
await expect(
|
||||
getNavigationContext(analysis, { document: "fixture" }),
|
||||
).resolves.toEqual(err(failure));
|
||||
expect(calls).toEqual(["current_address", "resolve_containing_procedure"]);
|
||||
});
|
||||
|
||||
it("keeps unsupported address facets local and filters bookmarks", async () => {
|
||||
const bookmarkInputs: Readonly<Record<string, unknown>>[] = [];
|
||||
const analysis: AnalysisOperationPort = {
|
||||
execute: (operation, input) => {
|
||||
if (operation === "current_document")
|
||||
return Promise.resolve(
|
||||
ok(createAnalysisExecution("fixture", provider)),
|
||||
);
|
||||
if (operation === "comment")
|
||||
return Promise.resolve(
|
||||
err(new AnalysisProtocolError("comments unavailable")),
|
||||
);
|
||||
switch (operation) {
|
||||
case "address_name":
|
||||
return Promise.resolve(
|
||||
ok(createAnalysisExecution("entry", provider)),
|
||||
);
|
||||
case "resolve_containing_procedure":
|
||||
return Promise.resolve(
|
||||
ok(
|
||||
createAnalysisExecution(
|
||||
{
|
||||
query_address: "0x401000",
|
||||
found: true,
|
||||
procedure: { address: "0x401000", name: "main" },
|
||||
},
|
||||
provider,
|
||||
),
|
||||
),
|
||||
);
|
||||
case "inline_comment":
|
||||
return Promise.resolve(ok(createAnalysisExecution(null, provider)));
|
||||
case "list_bookmarks":
|
||||
bookmarkInputs.push(input);
|
||||
return Promise.resolve(
|
||||
ok(
|
||||
createAnalysisExecution(
|
||||
[
|
||||
{ address: "0x401000", value: "review" },
|
||||
{ address: "0x402000", value: "other" },
|
||||
],
|
||||
provider,
|
||||
),
|
||||
),
|
||||
);
|
||||
default:
|
||||
return Promise.resolve(
|
||||
err(new AnalysisProtocolError(`unexpected ${operation}`)),
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const result = await inspectAddressContext(analysis, {
|
||||
address: "401000",
|
||||
});
|
||||
expect(result).toMatchObject({
|
||||
ok: true,
|
||||
value: {
|
||||
document: "fixture",
|
||||
name: { state: "available", value: "entry" },
|
||||
comment: {
|
||||
state: "unavailable",
|
||||
reason: "comments unavailable",
|
||||
},
|
||||
bookmarks: {
|
||||
state: "available",
|
||||
value: [{ address: "0x401000", value: "review" }],
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(bookmarkInputs).toEqual([{ document: "fixture" }]);
|
||||
});
|
||||
|
||||
it("propagates cancellation instead of degrading it to an unavailable facet", async () => {
|
||||
const cancelled = new AnalysisCancelledError("comment");
|
||||
const analysis: AnalysisOperationPort = {
|
||||
execute: (operation) =>
|
||||
Promise.resolve(
|
||||
operation === "comment"
|
||||
? err(cancelled)
|
||||
: ok(createAnalysisExecution(null, provider)),
|
||||
),
|
||||
};
|
||||
|
||||
await expect(
|
||||
inspectAddressContext(analysis, {
|
||||
address: "0x401000",
|
||||
document: "fixture",
|
||||
}),
|
||||
).resolves.toEqual(err(cancelled));
|
||||
});
|
||||
});
|
||||
@@ -430,17 +430,31 @@ describe("artifact graph MCP integration", () => {
|
||||
arguments: { domain: "artifact-comparison" },
|
||||
});
|
||||
expect(unknowns.structuredContent).toMatchObject({
|
||||
result: [expect.objectContaining({ domain: "artifact-comparison" })],
|
||||
result: {
|
||||
items: [
|
||||
expect.objectContaining({
|
||||
unknown: expect.objectContaining({
|
||||
domain: "artifact-comparison",
|
||||
}),
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
const exported = await client.callTool({
|
||||
name: "export_evidence_bundle",
|
||||
const snapshotted = await client.callTool({
|
||||
name: "snapshot_evidence_bundle",
|
||||
arguments: {},
|
||||
});
|
||||
const envelope = z
|
||||
.object({ result: evidenceBundleSchema })
|
||||
.parse(exported.structuredContent);
|
||||
expect(envelope.result.records).toHaveLength(6);
|
||||
expect(envelope.result.artifacts).toContainEqual({
|
||||
const bundleUri = z
|
||||
.object({ result: z.object({ bundle_uri: z.string() }) })
|
||||
.parse(snapshotted.structuredContent).result.bundle_uri;
|
||||
const bundleResource = await client.readResource({ uri: bundleUri });
|
||||
const bundle = evidenceBundleSchema.parse(
|
||||
JSON.parse(
|
||||
z.object({ text: z.string() }).parse(bundleResource.contents[0]).text,
|
||||
),
|
||||
);
|
||||
expect(bundle.records).toHaveLength(6);
|
||||
expect(bundle.artifacts).toContainEqual({
|
||||
digest: { sha256: evidence.subject?.digest.sha256 },
|
||||
format: "ipa",
|
||||
architecture: null,
|
||||
|
||||
@@ -295,6 +295,11 @@ describe("binary session", () => {
|
||||
).toBe(true);
|
||||
expect(replayCalls).toEqual(["health", "set_address_name", "address_name"]);
|
||||
expect(replay.exportAnalysisSnapshot().ok).toBe(false);
|
||||
expect((await replay.open(first)).ok).toBe(true);
|
||||
expect(replay.exportAnalysisSnapshot()).toMatchObject({
|
||||
ok: true,
|
||||
value: { entries: [{ operation: "address_name" }] },
|
||||
});
|
||||
await replay.close();
|
||||
|
||||
const mismatch = new BinarySession(cacheProvider([]));
|
||||
|
||||
@@ -194,4 +194,36 @@ describe("capability inventory", () => {
|
||||
missing_optional: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("advertises navigation context only when every cursor operation is available", () => {
|
||||
const capabilities = [
|
||||
"current_document",
|
||||
"current_address",
|
||||
"resolve_containing_procedure",
|
||||
].map((operation) => ({
|
||||
operation,
|
||||
available: true,
|
||||
reason: null,
|
||||
}));
|
||||
expect(
|
||||
entry(
|
||||
"get_navigation_context",
|
||||
status({ open: true, kind: "executable", capabilities }),
|
||||
),
|
||||
).toMatchObject({ available: true, reason: "available" });
|
||||
expect(
|
||||
entry(
|
||||
"get_navigation_context",
|
||||
status({
|
||||
open: true,
|
||||
kind: "executable",
|
||||
capabilities: capabilities.slice(0, 2),
|
||||
}),
|
||||
),
|
||||
).toMatchObject({
|
||||
available: false,
|
||||
reason: "provider_missing",
|
||||
remediation: expect.stringContaining("resolve_containing_procedure"),
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -167,7 +167,7 @@ describe("tool contract surface", () => {
|
||||
expect(Object.keys(TOOL_EFFECTS).sort()).toEqual(names.sort());
|
||||
});
|
||||
|
||||
it("keeps exactly nineteen additive session contracts", () => {
|
||||
it("keeps exactly twenty-two additive session contracts", () => {
|
||||
expect(
|
||||
SESSION_TOOL_CONTRACTS.map(({ name, kind }) => ({ name, kind })),
|
||||
).toEqual([
|
||||
@@ -190,6 +190,9 @@ describe("tool contract surface", () => {
|
||||
{ name: "update_unknown", kind: "session" },
|
||||
{ name: "verify_unknown_resolution", kind: "session" },
|
||||
{ name: "run_replay_machine", kind: "session" },
|
||||
{ name: "snapshot_evidence_bundle", kind: "session" },
|
||||
{ name: "get_navigation_context", kind: "session" },
|
||||
{ name: "inspect_address_context", kind: "session" },
|
||||
]);
|
||||
expect(
|
||||
SESSION_TOOL_CONTRACTS.find(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { z } from "zod";
|
||||
|
||||
import { BinarySession } from "../src/application/BinarySession.js";
|
||||
import { createEvidence } from "../src/domain/evidence.js";
|
||||
@@ -11,6 +12,87 @@ import { createInvestigationWorkspace } from "../src/domain/investigationWorkspa
|
||||
const provider = { id: "fixture", name: "Fixture", version: "1" };
|
||||
|
||||
describe("evidence MCP resources", () => {
|
||||
it("publishes mutable snapshot changes for every Evidence mutation", () => {
|
||||
const session = new BinarySession(() => ({
|
||||
health: () => Promise.resolve(),
|
||||
execute: () => Promise.resolve(observed(null)),
|
||||
close: () => Promise.resolve(),
|
||||
}));
|
||||
let updates = 0;
|
||||
session.onAnalysisSnapshotChanged(() => {
|
||||
updates += 1;
|
||||
});
|
||||
const first = createEvidence(undefined, provider, {
|
||||
operation: "notification_probe",
|
||||
parameters: {},
|
||||
result: 1,
|
||||
});
|
||||
expect(session.recordEvidence(first)).toEqual({ ok: true, value: "added" });
|
||||
expect(session.recordEvidence(first)).toEqual({
|
||||
ok: true,
|
||||
value: "duplicate",
|
||||
});
|
||||
const imported = createEvidence(undefined, provider, {
|
||||
operation: "import_notification_probe",
|
||||
parameters: {},
|
||||
result: 2,
|
||||
});
|
||||
expect(
|
||||
session.importEvidenceBundle(createEvidenceBundle([imported])),
|
||||
).toEqual({ ok: true, value: 1 });
|
||||
expect(
|
||||
session.recordUnknown({
|
||||
approved: true,
|
||||
question: "Does mutation publish a snapshot update?",
|
||||
severity: "low",
|
||||
domain: "notifications",
|
||||
supporting_evidence_ids: [first.evidence_id],
|
||||
contradicting_evidence_ids: [],
|
||||
required_authority: "controlled-replay",
|
||||
required_confidence: "observed",
|
||||
required_environment: null,
|
||||
recommended_probes: [],
|
||||
relationships: [],
|
||||
}).ok,
|
||||
).toBe(true);
|
||||
expect(updates).toBe(3);
|
||||
});
|
||||
|
||||
it("retains bundles without eviction and rejects a seventeenth digest", () => {
|
||||
const session = new BinarySession(() => ({
|
||||
health: () => Promise.resolve(),
|
||||
execute: () => Promise.resolve(observed(null)),
|
||||
close: () => Promise.resolve(),
|
||||
}));
|
||||
const first = session.snapshotEvidenceBundle();
|
||||
expect(first.ok).toBe(true);
|
||||
for (let index = 0; index < 15; index += 1) {
|
||||
session.recordEvidence(
|
||||
createEvidence(undefined, provider, {
|
||||
operation: `retention_probe_${String(index)}`,
|
||||
parameters: {},
|
||||
result: index,
|
||||
}),
|
||||
);
|
||||
expect(session.snapshotEvidenceBundle().ok).toBe(true);
|
||||
}
|
||||
session.recordEvidence(
|
||||
createEvidence(undefined, provider, {
|
||||
operation: "retention_probe_overflow",
|
||||
parameters: {},
|
||||
result: true,
|
||||
}),
|
||||
);
|
||||
expect(session.snapshotEvidenceBundle()).toMatchObject({
|
||||
ok: false,
|
||||
error: { _tag: "EvidenceLimitError", limit: "records", maximum: 16 },
|
||||
});
|
||||
if (first.ok)
|
||||
expect(session.retainedEvidenceBundle(first.value.bundleDigest)).toEqual(
|
||||
expect.any(String),
|
||||
);
|
||||
});
|
||||
|
||||
it("lists and reads only evidence owned by the current session", async () => {
|
||||
const session = new BinarySession(() => ({
|
||||
health: () => Promise.resolve(),
|
||||
@@ -71,7 +153,7 @@ describe("evidence MCP resources", () => {
|
||||
expect.arrayContaining([
|
||||
"rea://artifact/{manifestId}/{collection}",
|
||||
"rea://function/{targetSha256}/{address}",
|
||||
"rea://snapshot/{snapshotDigest}",
|
||||
"rea://evidence-bundle/{bundleDigest}",
|
||||
"rea://workspace/{workspaceId}/revision/{revision}",
|
||||
]),
|
||||
);
|
||||
@@ -82,6 +164,11 @@ describe("evidence MCP resources", () => {
|
||||
);
|
||||
const listed = await client.listResources();
|
||||
expect(listed.resources).toContainEqual(
|
||||
expect.objectContaining({
|
||||
uri: "rea://snapshot/current",
|
||||
}),
|
||||
);
|
||||
expect(listed.resources).not.toContainEqual(
|
||||
expect.objectContaining({
|
||||
uri: `rea://evidence/${evidence.evidence_id}`,
|
||||
}),
|
||||
@@ -96,6 +183,33 @@ describe("evidence MCP resources", () => {
|
||||
text: expect.stringContaining(evidence.evidence_id),
|
||||
}),
|
||||
]);
|
||||
const firstSnapshot = await client.callTool({
|
||||
name: "snapshot_evidence_bundle",
|
||||
arguments: {},
|
||||
});
|
||||
const secondSnapshot = await client.callTool({
|
||||
name: "snapshot_evidence_bundle",
|
||||
arguments: {},
|
||||
});
|
||||
expect(secondSnapshot.structuredContent).toEqual(
|
||||
firstSnapshot.structuredContent,
|
||||
);
|
||||
expect(firstSnapshot.content).toContainEqual(
|
||||
expect.objectContaining({
|
||||
type: "text",
|
||||
text: expect.stringContaining("Copy the opaque URI exactly"),
|
||||
}),
|
||||
);
|
||||
const bundleUri = z
|
||||
.object({ result: z.object({ bundle_uri: z.string() }) })
|
||||
.parse(firstSnapshot.structuredContent).result.bundle_uri;
|
||||
const retainedBundle = await client.readResource({ uri: bundleUri });
|
||||
expect(retainedBundle.contents[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
uri: bundleUri,
|
||||
text: expect.stringContaining(evidence.evidence_id),
|
||||
}),
|
||||
);
|
||||
const terminal = await client.readResource({
|
||||
uri: `rea://evidence/${capture.evidence_id}/section/terminal`,
|
||||
});
|
||||
@@ -119,14 +233,6 @@ describe("evidence MCP resources", () => {
|
||||
text: expect.stringContaining(unknown.value.revision_digest),
|
||||
}),
|
||||
);
|
||||
const active = await client.readResource({
|
||||
uri: "rea://unknowns/active",
|
||||
});
|
||||
expect(active.contents[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
text: expect.stringContaining(unknown.value.unknown_id),
|
||||
}),
|
||||
);
|
||||
}
|
||||
await expect(
|
||||
client.readResource({ uri: `rea://evidence/ev_${"0".repeat(64)}` }),
|
||||
|
||||
@@ -49,17 +49,21 @@ describe("function comparison MCP integration", () => {
|
||||
arguments: { domain: "function-comparison" },
|
||||
});
|
||||
expect(unknowns.structuredContent).toMatchObject({
|
||||
result: [
|
||||
expect.objectContaining({
|
||||
domain: "function-comparison",
|
||||
recommended_probes: [
|
||||
expect.objectContaining({
|
||||
rationale:
|
||||
"Capture complete dossiers for both functions under the same target context and analysis limits.",
|
||||
result: {
|
||||
items: [
|
||||
expect.objectContaining({
|
||||
unknown: expect.objectContaining({
|
||||
domain: "function-comparison",
|
||||
recommended_probes: [
|
||||
expect.objectContaining({
|
||||
rationale:
|
||||
"Capture complete dossiers for both functions under the same target context and analysis limits.",
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
await Promise.allSettled([
|
||||
|
||||
+20
-16
@@ -265,7 +265,7 @@ describe("Ghidra MCP and shared CLI composition", () => {
|
||||
arguments: {},
|
||||
})
|
||||
).structuredContent,
|
||||
).toMatchObject({ result: [] });
|
||||
).toMatchObject({ result: { items: [] } });
|
||||
|
||||
const approvedInspection = sessionEvidence(
|
||||
session,
|
||||
@@ -294,22 +294,26 @@ describe("Ghidra MCP and shared CLI composition", () => {
|
||||
})
|
||||
).structuredContent,
|
||||
).toMatchObject({
|
||||
result: [
|
||||
{
|
||||
status: "open",
|
||||
domain: "native-api",
|
||||
question: expect.stringContaining(
|
||||
"additional data sources or targets",
|
||||
),
|
||||
supporting_evidence_ids: [approvedInspection.evidence_id],
|
||||
recommended_probes: [
|
||||
{
|
||||
operation: "inspect_native_api",
|
||||
rationale: expect.stringContaining("ABI probe"),
|
||||
result: {
|
||||
items: [
|
||||
{
|
||||
unknown: {
|
||||
status: "open",
|
||||
domain: "native-api",
|
||||
question: expect.stringContaining(
|
||||
"additional data sources or targets",
|
||||
),
|
||||
supporting_evidence_ids: [approvedInspection.evidence_id],
|
||||
recommended_probes: [
|
||||
{
|
||||
operation: "inspect_native_api",
|
||||
rationale: expect.stringContaining("ABI probe"),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
await harness.close();
|
||||
|
||||
@@ -352,13 +352,17 @@ describe("full MCP integration with multi-tool sequences", () => {
|
||||
await client.callTool({ name: "list_unknowns", arguments: {} }),
|
||||
);
|
||||
expect(listed).toMatchObject({
|
||||
result: [
|
||||
{
|
||||
status: "open",
|
||||
domain: "control-flow",
|
||||
question: "Investigation reached the operation budget.",
|
||||
},
|
||||
],
|
||||
result: {
|
||||
items: [
|
||||
{
|
||||
unknown: {
|
||||
status: "open",
|
||||
domain: "control-flow",
|
||||
question: "Investigation reached the operation budget.",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -412,19 +416,23 @@ describe("full MCP integration with multi-tool sequences", () => {
|
||||
await client.callTool({ name: "list_unknowns", arguments: {} }),
|
||||
),
|
||||
).toMatchObject({
|
||||
result: [
|
||||
{
|
||||
domain: "analysis-capability",
|
||||
question:
|
||||
"The requested analysis is unavailable for the current target.",
|
||||
recommended_probes: [
|
||||
{
|
||||
rationale:
|
||||
"Choose another analysis or target that can answer this question.",
|
||||
result: {
|
||||
items: [
|
||||
{
|
||||
unknown: {
|
||||
domain: "analysis-capability",
|
||||
question:
|
||||
"The requested analysis is unavailable for the current target.",
|
||||
recommended_probes: [
|
||||
{
|
||||
rationale:
|
||||
"Choose another analysis or target that can answer this question.",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -510,14 +518,18 @@ describe("full MCP integration with multi-tool sequences", () => {
|
||||
await client.callTool({ name: "list_unknowns", arguments: {} }),
|
||||
),
|
||||
).toMatchObject({
|
||||
result: [
|
||||
{
|
||||
status: "contradicted",
|
||||
domain: "process-comparison",
|
||||
question: "Process captures disagree across: interaction, shim",
|
||||
contradicting_evidence_ids: [rightEvidence.evidence_id],
|
||||
},
|
||||
],
|
||||
result: {
|
||||
items: [
|
||||
{
|
||||
unknown: {
|
||||
status: "contradicted",
|
||||
domain: "process-comparison",
|
||||
question: "Process captures disagree across: interaction, shim",
|
||||
contradicting_evidence_ids: [rightEvidence.evidence_id],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -59,15 +59,21 @@ const availabilityProvider = (): AnalysisProvider => {
|
||||
describe("server and catalog identity", () => {
|
||||
it("derives package and SDK versions from canonical package metadata", async () => {
|
||||
const packageJson = JSON.parse(await readFile("package.json", "utf8"));
|
||||
const packageLock = JSON.parse(await readFile("package-lock.json", "utf8"));
|
||||
expect(PACKAGE_METADATA).toMatchObject({
|
||||
name: packageJson.name,
|
||||
version: packageJson.version,
|
||||
serverSdkVersion: "2.0.0-beta.4",
|
||||
clientSdkVersion: "2.0.0-beta.4",
|
||||
coreSdkVersion: "2.0.0-beta.4",
|
||||
serverSdkVersion:
|
||||
packageJson.dependencies["@modelcontextprotocol/server"],
|
||||
clientSdkVersion:
|
||||
packageJson.dependencies["@modelcontextprotocol/client"],
|
||||
coreSdkVersion:
|
||||
packageLock.packages["node_modules/@modelcontextprotocol/core"].version,
|
||||
});
|
||||
expect(PRODUCT_IDENTITY.packageVersion).toBe(packageJson.version);
|
||||
expect(SDK_IDENTITY.server).toBe("2.0.0-beta.4");
|
||||
expect(SDK_IDENTITY.server).toBe(
|
||||
packageJson.dependencies["@modelcontextprotocol/server"],
|
||||
);
|
||||
expect(CLI_COMMAND_NAMES).toHaveLength(67);
|
||||
expect(new Set(CLI_COMMAND_NAMES).size).toBe(67);
|
||||
expect(CATALOG_IDENTITY.counts).toEqual({
|
||||
@@ -213,7 +219,10 @@ describe("server and catalog identity", () => {
|
||||
expect(live).toMatchObject({
|
||||
package: { version: PRODUCT_IDENTITY.packageVersion },
|
||||
server: { version: PRODUCT_IDENTITY.packageVersion },
|
||||
sdk: { server: "2.0.0-beta.4", client_test: "2.0.0-beta.4" },
|
||||
sdk: {
|
||||
server: SDK_IDENTITY.server,
|
||||
client_test: PACKAGE_METADATA.clientSdkVersion,
|
||||
},
|
||||
client: { name: "identity-test", version: "9" },
|
||||
alignment: { state: "unknown" },
|
||||
});
|
||||
|
||||
+52
-21
@@ -136,9 +136,14 @@ describe("target-free MCP lifecycle", () => {
|
||||
});
|
||||
const mcp = new Client({ name: "session-test", version: "1.0.0" });
|
||||
let resourceListChanges = 0;
|
||||
let unknownResourceUpdates = 0;
|
||||
mcp.setNotificationHandler("notifications/resources/list_changed", () => {
|
||||
resourceListChanges += 1;
|
||||
});
|
||||
mcp.setNotificationHandler("notifications/resources/updated", (notice) => {
|
||||
if (notice.params.uri.startsWith("rea://unknown/"))
|
||||
unknownResourceUpdates += 1;
|
||||
});
|
||||
const [clientTransport, serverTransport] =
|
||||
InMemoryTransport.createLinkedPair();
|
||||
resources.push(mcp, server);
|
||||
@@ -175,11 +180,21 @@ describe("target-free MCP lifecycle", () => {
|
||||
expect(
|
||||
text(await mcp.callTool({ name: "current_document", arguments: {} })),
|
||||
).toContain("first.hop");
|
||||
const exported = await mcp.callTool({
|
||||
name: "export_evidence_bundle",
|
||||
const snapshotted = await mcp.callTool({
|
||||
name: "snapshot_evidence_bundle",
|
||||
arguments: {},
|
||||
});
|
||||
const bundle = evidenceBundleSchema.parse(structured(exported).result);
|
||||
const snapshotResult = z
|
||||
.object({ result: z.object({ bundle_uri: z.string() }) })
|
||||
.parse(structured(snapshotted)).result;
|
||||
const bundleResource = await mcp.readResource({
|
||||
uri: snapshotResult.bundle_uri,
|
||||
});
|
||||
const bundle = evidenceBundleSchema.parse(
|
||||
JSON.parse(
|
||||
z.object({ text: z.string() }).parse(bundleResource.contents[0]).text,
|
||||
),
|
||||
);
|
||||
expect(bundle.records).toHaveLength(1);
|
||||
const evidencePath = join(directory, "evidence.json");
|
||||
expect(
|
||||
@@ -221,7 +236,7 @@ describe("target-free MCP lifecycle", () => {
|
||||
).result,
|
||||
).toEqual({ imported: 1, total: 2 });
|
||||
await new Promise<void>((resolve) => setImmediate(resolve));
|
||||
expect(resourceListChanges).toBe(changesBeforeMutation + 1);
|
||||
expect(resourceListChanges).toBe(changesBeforeMutation);
|
||||
changesBeforeMutation = resourceListChanges;
|
||||
const recordedUnknown = z
|
||||
.object({
|
||||
@@ -250,15 +265,18 @@ describe("target-free MCP lifecycle", () => {
|
||||
).result;
|
||||
expect(recordedUnknown.revision).toBe(1);
|
||||
await new Promise<void>((resolve) => setImmediate(resolve));
|
||||
expect(resourceListChanges).toBe(changesBeforeMutation + 1);
|
||||
expect(resourceListChanges).toBe(changesBeforeMutation);
|
||||
const listedUnknowns = await mcp.callTool({
|
||||
name: "list_unknowns",
|
||||
arguments: {},
|
||||
});
|
||||
expect(listedUnknowns.isError, text(listedUnknowns)).not.toBe(true);
|
||||
expect(
|
||||
z
|
||||
.object({ result: z.array(z.unknown()) })
|
||||
.parse(
|
||||
structured(
|
||||
await mcp.callTool({ name: "list_unknowns", arguments: {} }),
|
||||
),
|
||||
).result,
|
||||
.object({
|
||||
result: z.object({ items: z.array(z.unknown()) }),
|
||||
})
|
||||
.parse(structured(listedUnknowns)).result.items,
|
||||
).toHaveLength(1);
|
||||
const resolved = await mcp.callTool({
|
||||
name: "update_unknown",
|
||||
@@ -284,7 +302,8 @@ describe("target-free MCP lifecycle", () => {
|
||||
});
|
||||
expect(resolved.isError).not.toBe(true);
|
||||
await new Promise<void>((resolve) => setImmediate(resolve));
|
||||
expect(resourceListChanges).toBe(changesBeforeMutation + 2);
|
||||
expect(unknownResourceUpdates).toBe(1);
|
||||
expect(resourceListChanges).toBe(changesBeforeMutation);
|
||||
expect(
|
||||
structured(
|
||||
await mcp.callTool({
|
||||
@@ -324,7 +343,7 @@ describe("target-free MCP lifecycle", () => {
|
||||
expect(
|
||||
structured(await mcp.callTool({ name: "list_unknowns", arguments: {} }))
|
||||
.result,
|
||||
).toEqual([]);
|
||||
).toMatchObject({ items: [] });
|
||||
expect(
|
||||
(
|
||||
await mcp.callTool({
|
||||
@@ -368,18 +387,30 @@ describe("target-free MCP lifecycle", () => {
|
||||
},
|
||||
});
|
||||
expect(captured.isError, text(captured)).not.toBe(true);
|
||||
const listedUnknowns = await mcp.callTool({
|
||||
name: "list_unknowns",
|
||||
arguments: {},
|
||||
});
|
||||
expect(listedUnknowns.isError, text(listedUnknowns)).not.toBe(true);
|
||||
const listed = z
|
||||
.object({
|
||||
result: z.array(z.object({ question: z.string(), domain: z.string() })),
|
||||
result: z.object({
|
||||
items: z.array(
|
||||
z.object({
|
||||
unknown: z.object({
|
||||
question: z.string(),
|
||||
domain: z.string(),
|
||||
}),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
})
|
||||
.parse(
|
||||
structured(
|
||||
await mcp.callTool({ name: "list_unknowns", arguments: {} }),
|
||||
),
|
||||
).result;
|
||||
.parse(structured(listedUnknowns)).result.items;
|
||||
expect(listed).toContainEqual({
|
||||
question: "Was network behavior fully observed during capture?",
|
||||
domain: "process-network",
|
||||
unknown: expect.objectContaining({
|
||||
question: "Was network behavior fully observed during capture?",
|
||||
domain: "process-network",
|
||||
}),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -86,6 +86,9 @@ describe("tool contract inventory", () => {
|
||||
"update_unknown",
|
||||
"verify_unknown_resolution",
|
||||
"run_replay_machine",
|
||||
"snapshot_evidence_bundle",
|
||||
"get_navigation_context",
|
||||
"inspect_address_context",
|
||||
]);
|
||||
expect(new Set(TOOL_CONTRACTS.map(({ name }) => name)).size).toBe(
|
||||
TOOL_CONTRACTS.length,
|
||||
|
||||
Reference in new issue
Block a user