From c7f6b8bad1a34d5efb30ff2a017672dea1fcdabb Mon Sep 17 00:00:00 2001 From: Ysopwnable <161784452+N0zoM1z0@users.noreply.github.com> Date: Fri, 16 Oct 2026 01:08:49 +0800 Subject: [PATCH] fix(javascript): bind native handoffs to canonical byte identity Require the native node owner's content digest instead of borrowing an observation artifact digest, which can identify the containing ASAR. Keep unavailable nodes and their path/provenance/partial facts in the trace, without a false actionable target or native Evidence link. Remove the old fallback and allocate the query index only for eligible native targets. Verify real public CLI/stdio MCP workflows, known/missing companion inputs and shipped Joplin controls. Preserve reproducible scripts, source pins and compact correctness/resource/token records without archiving applications. Fixes #3926. Part of #4027. Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com> --- docs/javascript-application-workflows.md | 8 + ...pt-native-handoff-identity-2026-10-15.json | 1052 +++++++++++++++++ ...ript-native-handoff-identity-2026-10-15.md | 179 +++ package.json | 1 + scripts/verify/javascript/native-handoffs.mjs | 348 ++++++ .../javascript/javascriptNativeHandoff.ts | 22 +- .../javascriptNativeHandoffIdentity.test.ts | 223 ++++ 7 files changed, 1820 insertions(+), 13 deletions(-) create mode 100644 docs/verification/javascript-native-handoff-identity-2026-10-15.json create mode 100644 docs/verification/javascript-native-handoff-identity-2026-10-15.md create mode 100644 scripts/verify/javascript/native-handoffs.mjs create mode 100644 tests/boundary/mcp/javascriptNativeHandoffIdentity.test.ts diff --git a/docs/javascript-application-workflows.md b/docs/javascript-application-workflows.md index 6134a4c3f..e4a419bc9 100644 --- a/docs/javascript-application-workflows.md +++ b/docs/javascript-application-workflows.md @@ -95,6 +95,14 @@ matches exactly. Otherwise the result recommends provider-neutral follow-up tools and reports `requires-provider-analysis`; it never starts or switches a provider implicitly. +A handoff requires the add-on's exact content identity. Unavailable unpacked +companions remain visible in the trace graph with their canonical path, +availability and container-backed Evidence. They yield no actionable handoff +until their bytes are available and the application is reanalyzed; the archive +digest does not identify the missing add-on. An empty handoff list does not prove +that the trace contains no native components. Source partial coverage remains +visible independently of handoff eligibility. + ## Semantic relation tracing `trace_javascript_semantics` queries the authenticated companion graph without diff --git a/docs/verification/javascript-native-handoff-identity-2026-10-15.json b/docs/verification/javascript-native-handoff-identity-2026-10-15.json new file mode 100644 index 000000000..9a58a7ddc --- /dev/null +++ b/docs/verification/javascript-native-handoff-identity-2026-10-15.json @@ -0,0 +1,1052 @@ +{ + "base_revision": "9f8659e0536e4e50a438f5b9d5661f3c9624f6ac", + "scope": "Canonical native byte identity for actionable feature-trace handoffs; static shipped Joplin, not native provider/application behavior", + "runtime": { + "platform": "Linux x64", + "node": "24.18.0", + "npm": "11.16.0", + "tiktoken": "0.12.0", + "tokenizer": "cl100k_base" + }, + "acquisition": { + "repository": "laurent22/joplin", + "tag": "v3.7.21", + "published_at": "2026-09-25T21:31:12Z", + "url": "https://github.com/laurent22/joplin/releases/download/v3.7.21/Joplin-3.7.21.AppImage", + "asset_bytes": 201462481, + "asset_sha256": "5d202dcaca31e14206826aa3f04125e7c218dbc3dc3d98479d811efbd6b856c1", + "extraction": "7z 26.02 read-only selected extraction; no application/provider executed; AppImage SquashFS at offset 944632; one trailing-data warning, exit 0", + "files": [ + { + "path": "app.asar", + "bytes": 233575014, + "sha256": "767df409b18effe60421fbefcca873de237c7e813eac744ecdeebbc5e825a33a" + }, + { + "path": "app.asar.unpacked/node_modules/keytar/build/Release/keytar.node", + "bytes": 76960, + "sha256": "0700016bcd761cbe02d275f2046c7cc42f39f730a50d66e3d3307aa9d413373a" + }, + { + "path": "app.asar.unpacked/node_modules/onnxruntime-node/bin/napi-v6/linux/x64/onnxruntime_binding.node", + "bytes": 384040, + "sha256": "a7191fbfe8045d4d5d312e5ecd8f5dd6970e804dd340c98be8a0af8daf7908cc" + }, + { + "path": "app.asar.unpacked/node_modules/sqlite3/lib/binding/napi-v6-linux-glibc-x64/node_sqlite3.node", + "bytes": 2158008, + "sha256": "1f8cc72dfb50f0c54b04dd7a709ef5ef89b2b1f3b063270646bd12c81173b2e8" + } + ] + }, + "inventory_header_observation": { + "files": 6324, + "unpacked_entries": 46, + "unpacked_native_addons": 3, + "selected_static_sources": ["package.json"], + "native_companions_staged_for_available_control": 3, + "other_unpacked_entries_not_staged": 43, + "application_execution": false, + "native_provider_execution": false + }, + "baseline_source_sha256": { + "package.json": "56ecf79edad7f36f425288d80dee14247c7f24e3c7eeec40f95d9b8a62b0880d", + "src/domain/javascript/javascriptNativeHandoff.ts": "ce46a23c936940f1b57cf3c79082438ac7ba95652d671ff1befecbc24a6a2d7a", + "src/domain/javascript/javascriptFeatureTrace.ts": "b2122e4ae026e176dca775351101ad2ce876e3bf804f39554a2d9344d8fe1fd1", + "src/application/javascript/JavaScriptArtifactGraphStructure.ts": "1afdeda88a998133eb2de2cb26cbcffdd51c85f302c2797158e2944874326449", + "src/domain/javascript/javascriptApplicationEvidenceSchemas.ts": "b5229e409aa0b2a9e1470254ca93ab987ca26f1bc9e9f94ab44e89c749df59ca", + "src/domain/evidence.ts": "81e434981572df8c5af1dd5add04bcafc1fbfbf7b9a8dcc5a4b18bbd121cb3c4" + }, + "baseline_verifier_and_regression_source": { + "base_revision": "9f8659e0536e4e50a438f5b9d5661f3c9624f6ac", + "source_sha256": { + "scripts/verify/javascript/native-handoffs.mjs": "9950693500d94aa28b01b2e7e8b8e3e799064f3fc53eef10a65d129f4c6c1f54", + "tests/boundary/mcp/javascriptNativeHandoffIdentity.test.ts": "ff247a8fb2189bf40cab604af23fc792eee939ff93f6c0f8e153a28405b3bdd9", + "src/domain/javascript/javascriptNativeHandoff.ts": "ce46a23c936940f1b57cf3c79082438ac7ba95652d671ff1befecbc24a6a2d7a", + "src/domain/javascript/javascriptFeatureTrace.ts": "b2122e4ae026e176dca775351101ad2ce876e3bf804f39554a2d9344d8fe1fd1", + "src/application/javascript/JavaScriptArtifactGraphStructure.ts": "1afdeda88a998133eb2de2cb26cbcffdd51c85f302c2797158e2944874326449" + }, + "verifier_optional_architecture_correction": { + "current_sha256": "bfdc235cc38a519a39e8b2e772d55259f371ccb15058e6fd241ee5ac05dfef82", + "explanation": "Omitted the internal factory optional architecture field instead of passing null. All four real-case normalized simulated Evidence records proved byte-identical; runtime tool requests/results unchanged.", + "normalized_input_equivalence": [ + { + "mode": "available", + "kind": "container", + "evidence_id": "ev_a67558e076894c3b7d339257765f22d66987cbcef411bf92d1dc50db2f4521ca", + "normalized_bytes_identical": true + }, + { + "mode": "available", + "kind": "native", + "evidence_id": "ev_c1bf000c14a18445db1f834b1c6019ae04725dd948951d5b5079184b4524e8d7", + "normalized_bytes_identical": true + }, + { + "mode": "missing", + "kind": "container", + "evidence_id": "ev_a67558e076894c3b7d339257765f22d66987cbcef411bf92d1dc50db2f4521ca", + "normalized_bytes_identical": true + }, + { + "mode": "missing", + "kind": "native", + "evidence_id": "ev_c1bf000c14a18445db1f834b1c6019ae04725dd948951d5b5079184b4524e8d7", + "normalized_bytes_identical": true + } + ] + } + }, + "final_source_sha256": { + "package.json": "cdb2fb0fd3a88790d921aadb560cac382fa360c821a2c3f6960bf75eff292ee8", + "src/domain/javascript/javascriptNativeHandoff.ts": "6a0c6277dcbd7a5b79bf926d2191afe69af79993bb30c8823c833ccfd946c68c", + "src/domain/javascript/javascriptFeatureTrace.ts": "b2122e4ae026e176dca775351101ad2ce876e3bf804f39554a2d9344d8fe1fd1", + "src/application/javascript/JavaScriptArtifactGraphStructure.ts": "1afdeda88a998133eb2de2cb26cbcffdd51c85f302c2797158e2944874326449", + "src/domain/javascript/javascriptApplicationEvidenceSchemas.ts": "b5229e409aa0b2a9e1470254ca93ab987ca26f1bc9e9f94ab44e89c749df59ca", + "src/domain/evidence.ts": "81e434981572df8c5af1dd5add04bcafc1fbfbf7b9a8dcc5a4b18bbd121cb3c4", + "scripts/verify/javascript/native-handoffs.mjs": "bfdc235cc38a519a39e8b2e772d55259f371ccb15058e6fd241ee5ac05dfef82", + "tests/boundary/mcp/javascriptNativeHandoffIdentity.test.ts": "fe216875d7bc080ddcc72a3ae3da38d0120f918ad1d6d5caa2b41edbc4796ddb" + }, + "cohorts": { + "baseline": { + "available": { + "result": { + "artifact": { + "path": "/tmp/rea-native-handoff-20261015/cases/available/app.asar", + "sha256": "767df409b18effe60421fbefcca873de237c7e813eac744ecdeebbc5e825a33a" + }, + "addon_path": "node_modules/keytar/build/Release/keytar.node", + "companion_sha256_observed": "0700016bcd761cbe02d275f2046c7cc42f39f730a50d66e3d3307aa9d413373a", + "prior_native_sha256_supplied": "0700016bcd761cbe02d275f2046c7cc42f39f730a50d66e3d3307aa9d413373a", + "source_paths": ["package.json"], + "parent_evidence_id": "ev_7d6754f5db07c44889b44ba2a14d235db79b94e9967c8ad36fe6b759615d0b7b", + "trace_evidence_id": "ev_2b1e20e64bf4aef770a556032dd72d70339baba21dc4f4ef8b34601b5ab55051", + "preparation_ms": 38140.725152, + "samples": [ + { + "round": 0, + "elapsed_ms": 150.19293800000014, + "structured_reply_bytes": 12372, + "full_tool_reply_bytes": 25845 + }, + { + "round": 1, + "elapsed_ms": 108.26803500000096, + "structured_reply_bytes": 12372, + "full_tool_reply_bytes": 25845 + }, + { + "round": 2, + "elapsed_ms": 93.04797500000132, + "structured_reply_bytes": 12372, + "full_tool_reply_bytes": 25845 + } + ], + "safe_identity": true, + "node": { + "node_id": "jag_node_98ccdef0e85d67cba9cc843193c5ba877f0a73ba3fa7d3581f6db0396a707b2d", + "identity": { + "strategy": "content-digest", + "stability": "global-exact", + "sha256": "0700016bcd761cbe02d275f2046c7cc42f39f730a50d66e3d3307aa9d413373a" + }, + "unavailable_observed": false, + "basis": [ + { + "authority": "artifact-bytes", + "state": "observed", + "confidence": "exact", + "artifact": { + "available": true, + "artifact_id": "art_0700016bcd761cbe02d275f2046c7cc42f39f730a50d66e3d3307aa9d413373a", + "sha256": "0700016bcd761cbe02d275f2046c7cc42f39f730a50d66e3d3307aa9d413373a" + }, + "location": { + "available": true, + "value": { + "kind": "artifact-path", + "path": "node_modules/keytar/build/Release/keytar.node" + } + } + } + ] + }, + "native_handoffs": [ + { + "native_node_id": "jag_node_98ccdef0e85d67cba9cc843193c5ba877f0a73ba3fa7d3581f6db0396a707b2d", + "artifact_sha256": "0700016bcd761cbe02d275f2046c7cc42f39f730a50d66e3d3307aa9d413373a", + "artifact_path": "node_modules/keytar/build/Release/keytar.node", + "export_node_ids": [], + "requested_exports": [], + "status": "evidence-linked", + "providers": [ + { + "id": "ghidra", + "name": "Ghidra", + "version": "simulated" + } + ], + "evidence_ids": [ + "ev_c1bf000c14a18445db1f834b1c6019ae04725dd948951d5b5079184b4524e8d7" + ], + "recommended_tools": [ + "open_binary", + "binary_overview", + "search_procedures", + "analyze_function", + "xrefs" + ] + } + ], + "evidence_links": [ + "ev_7d6754f5db07c44889b44ba2a14d235db79b94e9967c8ad36fe6b759615d0b7b", + "ev_c1bf000c14a18445db1f834b1c6019ae04725dd948951d5b5079184b4524e8d7" + ], + "source_coverage": { + "status": "partial", + "source_graph_status": "partial", + "total_seed_matches": 3 + }, + "graph_coverage": { + "status": "partial", + "truncated": false, + "omitted_count": null, + "limits": [] + }, + "facts_sha256": "539049c79671407c82040fd9ce443141c78fca5403daadbaf5ca82678fd0c933", + "cli_parity": { + "exact_cli_mcp_trace_parity": true, + "evidence_id": "ev_2b1e20e64bf4aef770a556032dd72d70339baba21dc4f4ef8b34601b5ab55051" + }, + "token_costs": { + "tokenizer": "cl100k_base", + "tiktoken_version": "0.12.0", + "selected_catalog": 5570, + "full_catalog": 195522, + "requests": 601, + "structured_replies": 6169, + "full_tool_replies": 12735, + "selected_catalog_structured_task": 12340, + "full_catalog_structured_task": 202292, + "selected_catalog_full_reply_task": 18906, + "full_catalog_full_reply_task": 208858 + } + }, + "resources": { + "elapsed_seconds": 60.369, + "cpu_seconds": 59.80667, + "peak_cgroup_bytes": 2662629376, + "payload_cleanup_verified": true, + "events_at_payload_exit": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + } + } + }, + "missing": { + "result": { + "artifact": { + "path": "/tmp/rea-native-handoff-20261015/cases/missing/app.asar", + "sha256": "767df409b18effe60421fbefcca873de237c7e813eac744ecdeebbc5e825a33a" + }, + "addon_path": "node_modules/keytar/build/Release/keytar.node", + "companion_sha256_observed": null, + "prior_native_sha256_supplied": "0700016bcd761cbe02d275f2046c7cc42f39f730a50d66e3d3307aa9d413373a", + "source_paths": ["package.json"], + "parent_evidence_id": "ev_ac00ab67200361a181e08d358b591983616044f39c972ee81e65e49a0d8a0536", + "trace_evidence_id": "ev_7bc771bc1fc4b3eb1a666107bd9a49daddcfd7c90f048414f11862a3ca5f3f84", + "preparation_ms": 36053.453958, + "samples": [ + { + "round": 0, + "elapsed_ms": 145.345276, + "structured_reply_bytes": 10008, + "full_tool_reply_bytes": 20875 + }, + { + "round": 1, + "elapsed_ms": 99.91117400000076, + "structured_reply_bytes": 10008, + "full_tool_reply_bytes": 20875 + }, + { + "round": 2, + "elapsed_ms": 89.61581300000398, + "structured_reply_bytes": 10008, + "full_tool_reply_bytes": 20875 + } + ], + "safe_identity": false, + "node": { + "node_id": "jag_node_dedeb2467dfe8cf75eee6588ce33416450e2c02cd67fb8ddff784449a2ec67f0", + "identity": { + "strategy": "canonical-path", + "stability": "artifact-version", + "artifact_sha256": "767df409b18effe60421fbefcca873de237c7e813eac744ecdeebbc5e825a33a", + "path": "node_modules/keytar/build/Release/keytar.node" + }, + "unavailable_observed": true, + "basis": [ + { + "authority": "artifact-bytes", + "state": "observed", + "confidence": "exact", + "artifact": { + "available": true, + "artifact_id": "art_767df409b18effe60421fbefcca873de237c7e813eac744ecdeebbc5e825a33a", + "sha256": "767df409b18effe60421fbefcca873de237c7e813eac744ecdeebbc5e825a33a" + }, + "location": { + "available": true, + "value": { + "kind": "artifact-path", + "path": "node_modules/keytar/build/Release/keytar.node" + } + } + } + ] + }, + "native_handoffs": [ + { + "native_node_id": "jag_node_dedeb2467dfe8cf75eee6588ce33416450e2c02cd67fb8ddff784449a2ec67f0", + "artifact_sha256": "767df409b18effe60421fbefcca873de237c7e813eac744ecdeebbc5e825a33a", + "artifact_path": "node_modules/keytar/build/Release/keytar.node", + "export_node_ids": [], + "requested_exports": [], + "status": "evidence-linked", + "providers": [ + { + "id": "ghidra", + "name": "Ghidra", + "version": "simulated" + } + ], + "evidence_ids": [ + "ev_a67558e076894c3b7d339257765f22d66987cbcef411bf92d1dc50db2f4521ca" + ], + "recommended_tools": [ + "open_binary", + "binary_overview", + "search_procedures", + "analyze_function", + "xrefs" + ] + } + ], + "evidence_links": [ + "ev_a67558e076894c3b7d339257765f22d66987cbcef411bf92d1dc50db2f4521ca", + "ev_ac00ab67200361a181e08d358b591983616044f39c972ee81e65e49a0d8a0536" + ], + "source_coverage": { + "status": "partial", + "source_graph_status": "partial", + "total_seed_matches": 2 + }, + "graph_coverage": { + "status": "partial", + "truncated": false, + "omitted_count": null, + "limits": [] + }, + "facts_sha256": "992f22f9f4979b07bd17ca5ef6c000d31ab6bab8dcbd52d330214bc92b8456b4", + "cli_parity": { + "exact_cli_mcp_trace_parity": true, + "evidence_id": "ev_7bc771bc1fc4b3eb1a666107bd9a49daddcfd7c90f048414f11862a3ca5f3f84" + }, + "token_costs": { + "tokenizer": "cl100k_base", + "tiktoken_version": "0.12.0", + "selected_catalog": 5570, + "full_catalog": 195522, + "requests": 594, + "structured_replies": 5208, + "full_tool_replies": 10789, + "selected_catalog_structured_task": 11372, + "full_catalog_structured_task": 201324, + "selected_catalog_full_reply_task": 16953, + "full_catalog_full_reply_task": 206905 + } + }, + "resources": { + "elapsed_seconds": 57.93, + "cpu_seconds": 57.479109, + "peak_cgroup_bytes": 2719457280, + "payload_cleanup_verified": true, + "events_at_payload_exit": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + } + } + } + }, + "final": { + "available": { + "result": { + "artifact": { + "path": "/tmp/rea-native-handoff-20261015/cases/available/app.asar", + "sha256": "767df409b18effe60421fbefcca873de237c7e813eac744ecdeebbc5e825a33a" + }, + "addon_path": "node_modules/keytar/build/Release/keytar.node", + "companion_sha256_observed": "0700016bcd761cbe02d275f2046c7cc42f39f730a50d66e3d3307aa9d413373a", + "prior_native_sha256_supplied": "0700016bcd761cbe02d275f2046c7cc42f39f730a50d66e3d3307aa9d413373a", + "source_paths": ["package.json"], + "parent_evidence_id": "ev_7d6754f5db07c44889b44ba2a14d235db79b94e9967c8ad36fe6b759615d0b7b", + "trace_evidence_id": "ev_2b1e20e64bf4aef770a556032dd72d70339baba21dc4f4ef8b34601b5ab55051", + "preparation_ms": 116767.19654199999, + "samples": [ + { + "round": 0, + "elapsed_ms": 1120.7472280000075, + "structured_reply_bytes": 12372, + "full_tool_reply_bytes": 25845 + }, + { + "round": 1, + "elapsed_ms": 1173.7802859999938, + "structured_reply_bytes": 12372, + "full_tool_reply_bytes": 25845 + }, + { + "round": 2, + "elapsed_ms": 1782.2942279999988, + "structured_reply_bytes": 12372, + "full_tool_reply_bytes": 25845 + } + ], + "safe_identity": true, + "node": { + "node_id": "jag_node_98ccdef0e85d67cba9cc843193c5ba877f0a73ba3fa7d3581f6db0396a707b2d", + "identity": { + "strategy": "content-digest", + "stability": "global-exact", + "sha256": "0700016bcd761cbe02d275f2046c7cc42f39f730a50d66e3d3307aa9d413373a" + }, + "unavailable_observed": false, + "basis": [ + { + "authority": "artifact-bytes", + "state": "observed", + "confidence": "exact", + "artifact": { + "available": true, + "artifact_id": "art_0700016bcd761cbe02d275f2046c7cc42f39f730a50d66e3d3307aa9d413373a", + "sha256": "0700016bcd761cbe02d275f2046c7cc42f39f730a50d66e3d3307aa9d413373a" + }, + "location": { + "available": true, + "value": { + "kind": "artifact-path", + "path": "node_modules/keytar/build/Release/keytar.node" + } + } + } + ] + }, + "native_handoffs": [ + { + "native_node_id": "jag_node_98ccdef0e85d67cba9cc843193c5ba877f0a73ba3fa7d3581f6db0396a707b2d", + "artifact_sha256": "0700016bcd761cbe02d275f2046c7cc42f39f730a50d66e3d3307aa9d413373a", + "artifact_path": "node_modules/keytar/build/Release/keytar.node", + "export_node_ids": [], + "requested_exports": [], + "status": "evidence-linked", + "providers": [ + { + "id": "ghidra", + "name": "Ghidra", + "version": "simulated" + } + ], + "evidence_ids": [ + "ev_c1bf000c14a18445db1f834b1c6019ae04725dd948951d5b5079184b4524e8d7" + ], + "recommended_tools": [ + "open_binary", + "binary_overview", + "search_procedures", + "analyze_function", + "xrefs" + ] + } + ], + "evidence_links": [ + "ev_7d6754f5db07c44889b44ba2a14d235db79b94e9967c8ad36fe6b759615d0b7b", + "ev_c1bf000c14a18445db1f834b1c6019ae04725dd948951d5b5079184b4524e8d7" + ], + "source_coverage": { + "status": "partial", + "source_graph_status": "partial", + "total_seed_matches": 3 + }, + "graph_coverage": { + "status": "partial", + "truncated": false, + "omitted_count": null, + "limits": [] + }, + "facts_sha256": "539049c79671407c82040fd9ce443141c78fca5403daadbaf5ca82678fd0c933", + "cli_parity": { + "exact_cli_mcp_trace_parity": true, + "evidence_id": "ev_2b1e20e64bf4aef770a556032dd72d70339baba21dc4f4ef8b34601b5ab55051" + }, + "token_costs": { + "tokenizer": "cl100k_base", + "tiktoken_version": "0.12.0", + "selected_catalog": 5570, + "full_catalog": 195522, + "requests": 601, + "structured_replies": 6169, + "full_tool_replies": 12735, + "selected_catalog_structured_task": 12340, + "full_catalog_structured_task": 202292, + "selected_catalog_full_reply_task": 18906, + "full_catalog_full_reply_task": 208858 + } + }, + "resources": { + "elapsed_seconds": 258.885, + "cpu_seconds": 106.221003, + "peak_cgroup_bytes": 2678456320, + "payload_cleanup_verified": true, + "events_at_payload_exit": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + } + } + }, + "missing": { + "result": { + "artifact": { + "path": "/tmp/rea-native-handoff-20261015/cases/missing/app.asar", + "sha256": "767df409b18effe60421fbefcca873de237c7e813eac744ecdeebbc5e825a33a" + }, + "addon_path": "node_modules/keytar/build/Release/keytar.node", + "companion_sha256_observed": null, + "prior_native_sha256_supplied": "0700016bcd761cbe02d275f2046c7cc42f39f730a50d66e3d3307aa9d413373a", + "source_paths": ["package.json"], + "parent_evidence_id": "ev_ac00ab67200361a181e08d358b591983616044f39c972ee81e65e49a0d8a0536", + "trace_evidence_id": "ev_3931e0874b0cffffa10cfe0124d218f09e6b7edbdda789591a7593060afd6450", + "preparation_ms": 36650.457516, + "samples": [ + { + "round": 0, + "elapsed_ms": 156.684454000002, + "structured_reply_bytes": 9298, + "full_tool_reply_bytes": 19401 + }, + { + "round": 1, + "elapsed_ms": 100.968246000004, + "structured_reply_bytes": 9298, + "full_tool_reply_bytes": 19401 + }, + { + "round": 2, + "elapsed_ms": 90.89984700000059, + "structured_reply_bytes": 9298, + "full_tool_reply_bytes": 19401 + } + ], + "safe_identity": true, + "node": { + "node_id": "jag_node_dedeb2467dfe8cf75eee6588ce33416450e2c02cd67fb8ddff784449a2ec67f0", + "identity": { + "strategy": "canonical-path", + "stability": "artifact-version", + "artifact_sha256": "767df409b18effe60421fbefcca873de237c7e813eac744ecdeebbc5e825a33a", + "path": "node_modules/keytar/build/Release/keytar.node" + }, + "unavailable_observed": true, + "basis": [ + { + "authority": "artifact-bytes", + "state": "observed", + "confidence": "exact", + "artifact": { + "available": true, + "artifact_id": "art_767df409b18effe60421fbefcca873de237c7e813eac744ecdeebbc5e825a33a", + "sha256": "767df409b18effe60421fbefcca873de237c7e813eac744ecdeebbc5e825a33a" + }, + "location": { + "available": true, + "value": { + "kind": "artifact-path", + "path": "node_modules/keytar/build/Release/keytar.node" + } + } + } + ] + }, + "native_handoffs": [], + "evidence_links": [ + "ev_ac00ab67200361a181e08d358b591983616044f39c972ee81e65e49a0d8a0536" + ], + "source_coverage": { + "status": "partial", + "source_graph_status": "partial", + "total_seed_matches": 2 + }, + "graph_coverage": { + "status": "partial", + "truncated": false, + "omitted_count": null, + "limits": [] + }, + "facts_sha256": "aae3c060eb63eb75816c4fb6e552eac6055c8195ae4d1e5e9d4c5125618ce2a0", + "cli_parity": { + "exact_cli_mcp_trace_parity": true, + "evidence_id": "ev_3931e0874b0cffffa10cfe0124d218f09e6b7edbdda789591a7593060afd6450" + }, + "token_costs": { + "tokenizer": "cl100k_base", + "tiktoken_version": "0.12.0", + "selected_catalog": 5570, + "full_catalog": 195522, + "requests": 594, + "structured_replies": 4939, + "full_tool_replies": 10237, + "selected_catalog_structured_task": 11103, + "full_catalog_structured_task": 201055, + "selected_catalog_full_reply_task": 16401, + "full_catalog_full_reply_task": 206353 + } + }, + "resources": { + "elapsed_seconds": 59.738, + "cpu_seconds": 59.254776, + "peak_cgroup_bytes": 2571235328, + "payload_cleanup_verified": true, + "events_at_payload_exit": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + } + } + } + } + }, + "resource_policy": { + "hard_bytes": 8589934592, + "early_stop_bytes": 6442450944, + "cpu_quota_percent": 100, + "cpu_affinity": [15], + "memory_accounting": "Cgroup family peak includes charged file cache; not summed RSS. Counters at payload exit while observer remains alive, not final whole-scope counters.", + "cache_conditions": "Input is stream-hashed before analysis, warming its file cache. Builds and OS caches uncontrolled; fresh MCP process is not an OS-cold run.", + "performance_scope": "Whole verifier includes input hashing, preparation, repeated queries, export, CLI parity and tokenization. Per-query samples are wall times only; isolated query CPU/memory and hosted-model costs unmeasured. Final available control258.885s wall/106.221003s CPU is substantially slower than baseline60.369s/59.806670s; final missing control59.738s/59.254776s and prior postfix controls are separately preserved. Timing/resource variability is unresolved, not attributed to this native consumer change. No speedup or performance-equivalence claim." + }, + "observer_sources_sha256": { + "resource-run.py": "7728862171415e7dee2c11af2ead2b9a54265fccaf82a7113956d36b29065e33", + "resource_payload.py": "6f089e645459eba80c5d73e39c21606c78173a44daf49fabc3b88c4631ce51c6" + }, + "validation": { + "harness_attempt": "Initial CLI trace input incorrectly used the MCP inline selector wrapper; both cases rejected before identity assertions. Corrected to the CLI complete-Evidence contract. Not a product reproduction.", + "baseline_public": "Missing-companion failed / available-byte control passed. Both adapters and both native-observation selections executed before identity assertions.", + "initial_focused": "24 passed in four files including public CLI/stdio MCP, workflow/graph, missing companion imports and erased TypeScript native bindings.", + "first_static": "Other static gates passed, typecheck rejected architecture:null in test factory. Omitted optional field; all four real-case normalized simulated Evidence inputs proved byte-identical. No product behavior change.", + "final_public": "2/2 public CLI/stdio MCP cases passed after named summary parsing and optional architecture correction; overlaps initial 24-case selection.", + "final_static_docs": "npm run check && npm run docs:check passed on integration base62bf098 plus this final change; typecheck, lint, format, knip, metadata, architecture/module boundaries and docs checks passed, including5 consistency tests. Prior static attempt/failure retained separately.", + "synced_cli_workflows": "34 passed in tests/acceptance/applications/applicationWorkflowCli.test.ts after sync to62bf098; adds a fifth file and34 distinct cases to initial24 (58 total distinct focused cases)." + }, + "interpretation": "Unavailable case previously returned a successful transport response with incorrect identity/linkage; this is not a successful equivalent analyst task and earns no efficiency credit. Available case is the valid completed-equivalence control. Reduced missing-case bytes/tokens reflect removal of unsupported claims, not an independent efficiency gain.", + "verifier_conditions": "Fresh MCP session, one selected analysis followed by repeated identical retained traces. Filesystem/build caches uncontrolled. Task token counts include one analysis summary and one trace with name/description/inputSchema catalog projections; structured and complete-tool-reply projections are alternatives, not billing. Repeated rounds, export, CLI parity and tokenizer are validation overhead outside that one-task token count but inside total resource receipts. A wrong-identity baseline is not a successful equivalent task or efficiency credit.", + "verifier_scope": "Real shipped-ASAR native identity through public CLI/stdio MCP; simulated native Evidence only, no provider/application executed", + "integration_base_revision": "62bf09809ef7bb224028916424f8f07652a20f4f", + "upstream_sync": "Fast-forwarded to reviewed main62bf098. Six upstream commits change CLI acceptance plus Apple, browser type-import parsing, reference JSON resolution and Kuna caching; no owned/key native source files overlap. Native source hashes unchanged. Updated public workflow acceptance34/34 passed; both real controls repeated on this integration base. Final static/docs gates recorded separately.", + "prior_postfix_controls": { + "available": { + "result": { + "artifact": { + "path": "/tmp/rea-native-handoff-20261015/cases/available/app.asar", + "sha256": "767df409b18effe60421fbefcca873de237c7e813eac744ecdeebbc5e825a33a" + }, + "addon_path": "node_modules/keytar/build/Release/keytar.node", + "companion_sha256_observed": "0700016bcd761cbe02d275f2046c7cc42f39f730a50d66e3d3307aa9d413373a", + "prior_native_sha256_supplied": "0700016bcd761cbe02d275f2046c7cc42f39f730a50d66e3d3307aa9d413373a", + "source_paths": ["package.json"], + "parent_evidence_id": "ev_7d6754f5db07c44889b44ba2a14d235db79b94e9967c8ad36fe6b759615d0b7b", + "trace_evidence_id": "ev_2b1e20e64bf4aef770a556032dd72d70339baba21dc4f4ef8b34601b5ab55051", + "preparation_ms": 37729.472507, + "samples": [ + { + "round": 0, + "elapsed_ms": 157.96848200000386, + "structured_reply_bytes": 12372, + "full_tool_reply_bytes": 25845 + }, + { + "round": 1, + "elapsed_ms": 114.56066299999657, + "structured_reply_bytes": 12372, + "full_tool_reply_bytes": 25845 + }, + { + "round": 2, + "elapsed_ms": 99.90600200000335, + "structured_reply_bytes": 12372, + "full_tool_reply_bytes": 25845 + } + ], + "safe_identity": true, + "node": { + "node_id": "jag_node_98ccdef0e85d67cba9cc843193c5ba877f0a73ba3fa7d3581f6db0396a707b2d", + "identity": { + "strategy": "content-digest", + "stability": "global-exact", + "sha256": "0700016bcd761cbe02d275f2046c7cc42f39f730a50d66e3d3307aa9d413373a" + }, + "unavailable_observed": false, + "basis": [ + { + "authority": "artifact-bytes", + "state": "observed", + "confidence": "exact", + "artifact": { + "available": true, + "artifact_id": "art_0700016bcd761cbe02d275f2046c7cc42f39f730a50d66e3d3307aa9d413373a", + "sha256": "0700016bcd761cbe02d275f2046c7cc42f39f730a50d66e3d3307aa9d413373a" + }, + "location": { + "available": true, + "value": { + "kind": "artifact-path", + "path": "node_modules/keytar/build/Release/keytar.node" + } + } + } + ] + }, + "native_handoffs": [ + { + "native_node_id": "jag_node_98ccdef0e85d67cba9cc843193c5ba877f0a73ba3fa7d3581f6db0396a707b2d", + "artifact_sha256": "0700016bcd761cbe02d275f2046c7cc42f39f730a50d66e3d3307aa9d413373a", + "artifact_path": "node_modules/keytar/build/Release/keytar.node", + "export_node_ids": [], + "requested_exports": [], + "status": "evidence-linked", + "providers": [ + { + "id": "ghidra", + "name": "Ghidra", + "version": "simulated" + } + ], + "evidence_ids": [ + "ev_c1bf000c14a18445db1f834b1c6019ae04725dd948951d5b5079184b4524e8d7" + ], + "recommended_tools": [ + "open_binary", + "binary_overview", + "search_procedures", + "analyze_function", + "xrefs" + ] + } + ], + "evidence_links": [ + "ev_7d6754f5db07c44889b44ba2a14d235db79b94e9967c8ad36fe6b759615d0b7b", + "ev_c1bf000c14a18445db1f834b1c6019ae04725dd948951d5b5079184b4524e8d7" + ], + "source_coverage": { + "status": "partial", + "source_graph_status": "partial", + "total_seed_matches": 3 + }, + "graph_coverage": { + "status": "partial", + "truncated": false, + "omitted_count": null, + "limits": [] + }, + "facts_sha256": "539049c79671407c82040fd9ce443141c78fca5403daadbaf5ca82678fd0c933", + "cli_parity": { + "exact_cli_mcp_trace_parity": true, + "evidence_id": "ev_2b1e20e64bf4aef770a556032dd72d70339baba21dc4f4ef8b34601b5ab55051" + }, + "token_costs": { + "tokenizer": "cl100k_base", + "tiktoken_version": "0.12.0", + "selected_catalog": 5570, + "full_catalog": 195522, + "requests": 601, + "structured_replies": 6169, + "full_tool_replies": 12735, + "selected_catalog_structured_task": 12340, + "full_catalog_structured_task": 202292, + "selected_catalog_full_reply_task": 18906, + "full_catalog_full_reply_task": 208858 + } + }, + "resources": { + "elapsed_seconds": 61.657, + "cpu_seconds": 61.135962, + "peak_cgroup_bytes": 3146776576, + "payload_cleanup_verified": true, + "events_at_payload_exit": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + } + } + }, + "missing": { + "result": { + "artifact": { + "path": "/tmp/rea-native-handoff-20261015/cases/missing/app.asar", + "sha256": "767df409b18effe60421fbefcca873de237c7e813eac744ecdeebbc5e825a33a" + }, + "addon_path": "node_modules/keytar/build/Release/keytar.node", + "companion_sha256_observed": null, + "prior_native_sha256_supplied": "0700016bcd761cbe02d275f2046c7cc42f39f730a50d66e3d3307aa9d413373a", + "source_paths": ["package.json"], + "parent_evidence_id": "ev_ac00ab67200361a181e08d358b591983616044f39c972ee81e65e49a0d8a0536", + "trace_evidence_id": "ev_3931e0874b0cffffa10cfe0124d218f09e6b7edbdda789591a7593060afd6450", + "preparation_ms": 51944.249193999996, + "samples": [ + { + "round": 0, + "elapsed_ms": 140.67567200000485, + "structured_reply_bytes": 9298, + "full_tool_reply_bytes": 19401 + }, + { + "round": 1, + "elapsed_ms": 105.40526200000022, + "structured_reply_bytes": 9298, + "full_tool_reply_bytes": 19401 + }, + { + "round": 2, + "elapsed_ms": 88.52443199999834, + "structured_reply_bytes": 9298, + "full_tool_reply_bytes": 19401 + } + ], + "safe_identity": true, + "node": { + "node_id": "jag_node_dedeb2467dfe8cf75eee6588ce33416450e2c02cd67fb8ddff784449a2ec67f0", + "identity": { + "strategy": "canonical-path", + "stability": "artifact-version", + "artifact_sha256": "767df409b18effe60421fbefcca873de237c7e813eac744ecdeebbc5e825a33a", + "path": "node_modules/keytar/build/Release/keytar.node" + }, + "unavailable_observed": true, + "basis": [ + { + "authority": "artifact-bytes", + "state": "observed", + "confidence": "exact", + "artifact": { + "available": true, + "artifact_id": "art_767df409b18effe60421fbefcca873de237c7e813eac744ecdeebbc5e825a33a", + "sha256": "767df409b18effe60421fbefcca873de237c7e813eac744ecdeebbc5e825a33a" + }, + "location": { + "available": true, + "value": { + "kind": "artifact-path", + "path": "node_modules/keytar/build/Release/keytar.node" + } + } + } + ] + }, + "native_handoffs": [], + "evidence_links": [ + "ev_ac00ab67200361a181e08d358b591983616044f39c972ee81e65e49a0d8a0536" + ], + "source_coverage": { + "status": "partial", + "source_graph_status": "partial", + "total_seed_matches": 2 + }, + "graph_coverage": { + "status": "partial", + "truncated": false, + "omitted_count": null, + "limits": [] + }, + "facts_sha256": "aae3c060eb63eb75816c4fb6e552eac6055c8195ae4d1e5e9d4c5125618ce2a0", + "cli_parity": { + "exact_cli_mcp_trace_parity": true, + "evidence_id": "ev_3931e0874b0cffffa10cfe0124d218f09e6b7edbdda789591a7593060afd6450" + }, + "token_costs": { + "tokenizer": "cl100k_base", + "tiktoken_version": "0.12.0", + "selected_catalog": 5570, + "full_catalog": 195522, + "requests": 594, + "structured_replies": 4939, + "full_tool_replies": 10237, + "selected_catalog_structured_task": 11103, + "full_catalog_structured_task": 201055, + "selected_catalog_full_reply_task": 16401, + "full_catalog_full_reply_task": 206353 + } + }, + "resources": { + "elapsed_seconds": 74.956, + "cpu_seconds": 60.352428, + "peak_cgroup_bytes": 3124801536, + "payload_cleanup_verified": true, + "events_at_payload_exit": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + } + } + } + }, + "prior_postfix_controls_note": "Executed before upstream sync, on base9f8659e plus this native fix. Retained, not asserted as a final-integration measurement.", + "validation_resources": { + "synced_cli_34": { + "elapsed_seconds": 240.873, + "cpu_seconds": 141.092204, + "peak_cgroup_bytes": 3220983808, + "payload_cleanup_verified": true, + "events_at_payload_exit": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + } + }, + "initial_focused_24": { + "elapsed_seconds": 86.733, + "cpu_seconds": 85.48165, + "peak_cgroup_bytes": 3216478208, + "payload_cleanup_verified": true, + "events_at_payload_exit": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + } + }, + "final_public_2": { + "elapsed_seconds": 78.183, + "cpu_seconds": 32.274966, + "peak_cgroup_bytes": 1365196800, + "payload_cleanup_verified": true, + "events_at_payload_exit": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + } + }, + "prior_static_docs": { + "elapsed_seconds": 169.015, + "cpu_seconds": 119.713, + "peak_cgroup_bytes": 4265230336, + "payload_cleanup_verified": true, + "events_at_payload_exit": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + } + }, + "final_integration_static_docs": { + "elapsed_seconds": 128.732, + "cpu_seconds": 115.51892, + "peak_cgroup_bytes": 4301684736, + "payload_cleanup_verified": true, + "events_at_payload_exit": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + } + } + }, + "integration_equivalence": "Final controls on62bf098 preserve all recorded available-case artifact, identity, facts, Evidence, coverage and token fields from baseline; missing-case parent/node/provenance/coverage unchanged, handoff/container link removed. Both exactly equal the earlier postfix controls in all recorded non-timing contract fields.", + "boundary_audit": "Unavailable native producer owns canonical-path identity and archive-backed observations; native handoff derivation now consumes only its content-digest identity. Feature trace is the only production caller. Application-version matching/content comparison already consume canonical identity rather than a supporting observation digest; no duplicate fallback found there. CLI/MCP use the same application workflow." +} diff --git a/docs/verification/javascript-native-handoff-identity-2026-10-15.md b/docs/verification/javascript-native-handoff-identity-2026-10-15.md new file mode 100644 index 000000000..c3bd79e91 --- /dev/null +++ b/docs/verification/javascript-native-handoff-identity-2026-10-15.md @@ -0,0 +1,179 @@ +# Native handoff byte-identity verification + +Tracks #3926 under #4027. Baseline: `9f8659e0536e4e50a438f5b9d5661f3c9624f6ac`. +Final integration base: `62bf09809ef7bb224028916424f8f07652a20f4f`. + +The analyst already knows an ASAR-relative native path and supplies prior native +Evidence. The question is which Evidence can be linked to that component's exact +bytes. Discovery and native-provider execution are outside this task. + +## Observed behavior and correction + +The native-node producer distinguishes a content digest from a canonical path +whose bytes are unavailable. Its unavailable observation legitimately cites the +containing archive. The handoff consumer previously treated that supporting +artifact digest as the native component's digest. + +Public CLI/stdio MCP regression: the missing-companion case failed and the +available-byte control passed. Real Joplin reproduced the same result: missing +keytar bytes produced an `evidence-linked` handoff with the ASAR digest and the +simulated container observation. CLI/MCP agreed on that incorrect answer. + +The consumer now uses only the node owner's content-digest identity. The fallback +observation scan is removed. Unavailable native nodes retain their canonical +path, availability, container-backed provenance and partial coverage in the +inline graph; they yield no false actionable handoff or native Evidence link. +An empty handoff list describes eligibility, not absence of native components. +The per-query node index is allocated only after an eligible target exists; no +persistent cache, schema, tool or target authority is added. + +Final real checks passed both cases with exact CLI/MCP trace parity. The available +control preserved the complete parent/trace Evidence IDs, native facts, links, +coverage and all token projections. The missing control preserved its parent, +native node, source provenance and coverage, while removing the wrong handoff +and container Evidence link. + +## Real input and scope + +[Official Joplin v3.7.21 Linux AppImage](https://github.com/laurent22/joplin/releases/download/v3.7.21/Joplin-3.7.21.AppImage), +published 2026-09-25, 201,462,481 bytes, SHA-256 +`5d202dcaca31e14206826aa3f04125e7c218dbc3dc3d98479d811efbd6b856c1`. + +Read-only 7z 26.02 inspection found 6,324 ASAR files, 46 unpacked entries and +three `.node` components. Extract only the ASAR and those three binaries: + +```sh +input_root="$(mktemp -d -t rea-native-handoff-XXXXXX)" +curl --fail --location --output "$input_root/Joplin-3.7.21.AppImage" \ + https://github.com/laurent22/joplin/releases/download/v3.7.21/Joplin-3.7.21.AppImage +sha256sum "$input_root/Joplin-3.7.21.AppImage" +7z x -mmt=1 "-o$input_root/extracted" "$input_root/Joplin-3.7.21.AppImage" \ + resources/app.asar \ + resources/app.asar.unpacked/node_modules/keytar/build/Release/keytar.node \ + resources/app.asar.unpacked/node_modules/sqlite3/lib/binding/napi-v6-linux-glibc-x64/node_sqlite3.node \ + resources/app.asar.unpacked/node_modules/onnxruntime-node/bin/napi-v6/linux/x64/onnxruntime_binding.node +python3 - "$input_root" <<'PY' +import os, sys +from pathlib import Path +root = Path(sys.argv[1]) +source = root / 'extracted/resources' +for mode in ('available', 'missing'): + case = root / 'cases' / mode + case.mkdir(parents=True) + os.link(source / 'app.asar', case / 'app.asar') + if mode == 'available': + for native in (source / 'app.asar.unpacked').rglob('*.node'): + target = case / native.relative_to(source) + target.parent.mkdir(parents=True, exist_ok=True) + os.link(native, target) +PY +``` + +Verify the asset digest above before extraction. The ASAR is 233,575,014 bytes, +SHA-256 `767df409b18effe60421fbefcca873de237c7e813eac744ecdeebbc5e825a33a`. +Keytar is 76,960 bytes, SHA-256 +`0700016bcd761cbe02d275f2046c7cc42f39f730a50d66e3d3307aa9d413373a`. +All extracted component hashes are in the JSON record. 7z reported one +trailing-data warning for the AppImage's embedded SquashFS and exited 0. + +Both cases select only `package.json` for static source analysis while retaining +the inventory. The available case stages all three native companions; the other +43 unpacked entries are not staged. The missing case stages none. Whole-application +coverage is therefore partial in both cases, independently of the selected +native component's known or unknown byte identity. Neither the application nor +a native analysis provider is executed. Native Evidence is explicitly simulated +to test exact subject linkage. Live Electron, real Ghidra/Hopper/IDA behavior and +Windows/macOS verification are **NOT_ENOUGH_INFO**. + +## Reproduce the public workflow + +Use pinned Node 24.18.0/npm 11.16.0, `npm ci`, then `npm run build:cached`. +The verifier returns compact records and deletes its private exported parent and +CLI input after owned cleanup. No downloadable application or large Evidence is +archived. + +```sh +for mode in available missing; do + node scripts/verify/javascript/native-handoffs.mjs \ + --artifact "$input_root/cases/$mode/app.asar" \ + --addon-path node_modules/keytar/build/Release/keytar.node \ + --native-sha256 0700016bcd761cbe02d275f2046c7cc42f39f730a50d66e3d3307aa9d413373a \ + --source-path package.json --rounds 3 --cli-parity --require-safe +done +``` + +Add `--tokenizer-python /absolute/path/to/python-with-tiktoken-0.12.0` for the +reported BPE projections. The npm alias `verify:javascript:native-handoffs` +builds first; recorded costs use the direct Node invocation above after building. +For a pre-fix observation, run the saved verifier on the baseline build without +`--require-safe`; `safe_identity: false` is an incorrect answer, not a passing +analyst task. Paths affect serialized requests and some Evidence identities. + +The exact resource runs additionally used the preserved +[8 GiB observer](https://github.com/morluto/rea/blob/b3bdabab694af1f9801eff7caf75001b92314a58/experiments/analysis-efficiency/resource-run.py) +and adjacent `resource_payload.py`; both hashes are pinned in the JSON. + +## Completed workflow costs and limits + +| Case | Revision | Wall s | CPU s | Peak cgroup bytes | Identity oracle | +| ---------------------- | -------- | ------: | ---------: | ----------------: | -------------------------- | +| Native bytes available | Before | 60.369 | 59.806670 | 2,662,629,376 | pass | +| Native bytes available | After | 258.885 | 106.221003 | 2,678,456,320 | pass, exact baseline facts | +| Native bytes missing | Before | 57.930 | 57.479109 | 2,719,457,280 | wrong identity/linkage | +| Native bytes missing | After | 59.738 | 59.254776 | 2,571,235,328 | pass, unknown retained | + +Whole-verifier resources include input hashing, analysis, three retained trace +queries, export, CLI parity and tokenization. One CPU, 8 GiB hard cap, 6 GiB early +stop, no swap; cleanup verified and no OOM. Memory includes charged file cache, +not summed RSS. Counters are at payload exit while the observer remains alive. +The available control's final wall and CPU times are substantially higher than +baseline. Earlier post-fix controls on the original base took 61.657/74.956 s wall +and 61.135962/60.352428 s CPU for available/missing cases; their complete receipts +remain separately recorded. The final missing case took 59.738 s wall. This +variability is unresolved and cannot be attributed to the native consumer change. +These runs establish neither speedup nor performance equivalence. Input hashing warms the ASAR +cache, build/OS caches and shared-host scheduling are uncontrolled. Fresh MCP +sessions do not establish OS-cold conditions. First post-analysis and repeated +warm-query samples are preserved separately; query CPU/memory are unmeasured. + +One defined task is one analysis summary plus one retained trace, with the path +and prior Evidence already supplied. Static `cl100k_base`/tiktoken 0.12.0 counts +include both requests, replies and name/description/inputSchema contracts: + +| Available-byte task, unchanged before/after | BPE tokens | +| ----------------------------------------------- | ---------: | +| Selected catalog + structured replies | 12,340 | +| Selected catalog + complete tool replies | 18,906 | +| Full catalog projection + structured replies | 202,292 | +| Full catalog projection + complete tool replies | 208,858 | + +These are alternative reply/catalog projections. The selected catalog alone is +5,570 BPE; the full projection is 195,522. Output schemas, annotations, host +rendering/replay, reasoning, final answers and model billing are unmeasured. +Repeated queries/export/CLI/tokenization are validation overhead outside this +one-task token count, but inside resource receipts. Missing-case structured-task +counts change 11,372 → 11,103 by removing unsupported claims; the incorrect +baseline earns no efficiency credit. + +## Regression and verification record + +The initial four-file focused selection passed 24 tests. The two public cases +were rerun after adding named summary-result parsing and correcting an optional +architecture factory input. Omitting that field instead of passing null produced +byte-identical normalized simulated Evidence in all four real-case inputs. +The initial CLI-selector harness error and first typecheck failure are retained +as harness/validation attempts, not product reproductions. + +After syncing the reviewed upstream changes, the updated application workflow +CLI acceptance file passed 34 tests, bringing the focused selection to 58 distinct +cases across five files. Both Joplin controls were then repeated on the final +integration base. Canonical byte identity is also used by the related version +matching/content comparison consumers; no equivalent observation-digest fallback +was found there. + +Final `npm run check && npm run docs:check` passed on the integration base, +including five documentation consistency tests: 128.732 s wall, 115.518920 s CPU, +4,301,684,736 B peak cgroup memory, clean owned exit and no OOM. + +Final public and static/docs outcomes are recorded in the +[compact measurements and source pins](javascript-native-handoff-identity-2026-10-15.json). diff --git a/package.json b/package.json index 904a3806f..cf1a4defa 100644 --- a/package.json +++ b/package.json @@ -214,6 +214,7 @@ "verify:javascript:digests": "npm run build:cached && node scripts/verify/javascript/digests.mjs", "verify:javascript:output": "npm run build:cached && node scripts/verify/javascript/output.mjs", "verify:javascript:module-references": "npm run build:cached && node scripts/verify/javascript/module-references.mjs", + "verify:javascript:native-handoffs": "npm run build:cached && node scripts/verify/javascript/native-handoffs.mjs", "verify:application-workflows": "npm run build:cached && node scripts/verify/javascript/application-workflows.mjs", "verify:managed": "npm run build:cached && node scripts/verify/managed/conformance.mjs", "verify:firmware": "npm run build:cached && node scripts/verify/firmware/analysis.mjs", diff --git a/scripts/verify/javascript/native-handoffs.mjs b/scripts/verify/javascript/native-handoffs.mjs new file mode 100644 index 000000000..4ee67aa7c --- /dev/null +++ b/scripts/verify/javascript/native-handoffs.mjs @@ -0,0 +1,348 @@ +#!/usr/bin/env node + +import assert from "node:assert/strict"; +import { createHash, randomUUID } from "node:crypto"; +import { createReadStream } from "node:fs"; +import { readFile, writeFile } from "node:fs/promises"; +import { isAbsolute, join } from "node:path"; +import { performance } from "node:perf_hooks"; +import { fileURLToPath } from "node:url"; +import { parseArgs } from "node:util"; +import { Client } from "@modelcontextprotocol/client"; +import { StdioClientTransport } from "@modelcontextprotocol/client/stdio"; +import { createEvidence } from "../../../dist/domain/evidence.js"; +import { PrivateRuntimeRoot } from "../../../dist/process/PrivateRuntimeRoot.js"; +import { + OwnedCommandFailure, + runOwnedCommand, +} from "../../../dist/process/OwnedCommand.js"; +import { closeVerifierResources } from "../../lib/verifier-cleanup.mjs"; + +const { values } = parseArgs({ + options: { + artifact: { type: "string" }, + "addon-path": { type: "string" }, + "native-sha256": { type: "string" }, + "source-path": { type: "string", multiple: true }, + rounds: { type: "string", default: "3" }, + "cli-parity": { type: "boolean", default: false }, + "tokenizer-python": { type: "string" }, + "require-safe": { type: "boolean", default: false }, + }, + strict: true, +}); +assert.ok( + values.artifact && isAbsolute(values.artifact), + "--artifact must be absolute", +); +assert.ok( + values["addon-path"] && + !isAbsolute(values["addon-path"]) && + values["addon-path"] + .split("/") + .every((part) => part !== "" && part !== "." && part !== ".."), + "--addon-path must be a normalized archive-relative path", +); +assert.match( + values["native-sha256"] ?? "", + /^[a-f0-9]{64}$/, + "--native-sha256 pins prior native bytes; it does not prove availability", +); +const rounds = Number(values.rounds); +assert.ok(Number.isSafeInteger(rounds) && rounds > 0); +const environment = { + PATH: process.env.PATH ?? "/usr/bin:/bin", + REA_LOG_LEVEL: "silent", + REA_ANALYSIS_PROVIDER: "auto", + HOPPER_LAUNCHER_PATH: "/rea-unconfigured-deep-provider/hopper", +}; +const transport = new StdioClientTransport({ + command: process.execPath, + args: [fileURLToPath(new URL("../../rea.mjs", import.meta.url)), "mcp"], + env: environment, + stderr: "pipe", +}); +const client = new Client({ + name: "javascript-native-handoff-verifier", + version: "1", +}); +const serialize = (value) => JSON.stringify(value); +const digest = (value) => + createHash("sha256").update(serialize(value)).digest("hex"); +const fileDigest = async (path) => { + const hash = createHash("sha256"); + for await (const chunk of createReadStream(path)) hash.update(chunk); + return hash.digest("hex"); +}; +const simulatedNativeEvidence = (path, sha256) => + createEvidence( + { path, sha256, format: "raw-binary" }, + { id: "ghidra", name: "Ghidra", version: "simulated" }, + { + operation: "binary_overview", + parameters: {}, + result: { + summary: + "Simulated subject-linkage input; no native provider executed.", + }, + limitations: ["No native provider behavior is established."], + }, + ); +let temporary; +let primaryError; +let retainTemporary = false; +let report; +const ownedCommand = async (command, arguments_, timeoutMs = 60_000) => { + try { + return await runOwnedCommand( + { + command, + arguments: arguments_, + cwd: temporary.path, + env: environment, + runId: `rea-native-handoff-${randomUUID()}`, + expectedCommand: command === process.execPath ? process.execPath : null, + sweepTokenOwnedProcesses: true, + }, + { timeoutMs, diagnosticBytes: 16 * 1024 * 1024 }, + ); + } catch (cause) { + retainTemporary ||= + cause instanceof OwnedCommandFailure && cause.cleanupOwner !== undefined; + throw cause; + } +}; +try { + const archiveSha256 = await fileDigest(values.artifact); + const companion = join(`${values.artifact}.unpacked`, values["addon-path"]); + let companionSha256 = null; + try { + companionSha256 = await fileDigest(companion); + } catch (cause) { + if (cause?.code !== "ENOENT") throw cause; + } + if (companionSha256 !== null) + assert.equal(companionSha256, values["native-sha256"]); + const native = simulatedNativeEvidence(companion, values["native-sha256"]); + const container = simulatedNativeEvidence(values.artifact, archiveSha256); + await client.connect(transport); + transport.stderr?.on("data", () => undefined); + const tools = (await client.listTools()).tools; + const catalog = tools.map(({ name, description, inputSchema }) => ({ + name, + description, + inputSchema, + })); + const selected = catalog.filter(({ name }) => + ["analyze_javascript_application", "trace_application_feature"].includes( + name, + ), + ); + assert.equal(selected.length, 2); + const call = async (name, arguments_) => { + const response = await client.callTool( + { name, arguments: arguments_ }, + { timeout: 360_000 }, + ); + assert.notEqual(response.isError, true, serialize(response)); + assert.ok(response.structuredContent?.evidence_id); + return { + request: { name, arguments: arguments_ }, + response: response.structuredContent, + wire_response: response, + }; + }; + const prepStart = performance.now(); + const preparation = await call("analyze_javascript_application", { + input_path: values.artifact, + detail: "summary", + ...(values["source-path"] === undefined + ? {} + : { source_paths: values["source-path"] }), + }); + const preparationMs = performance.now() - prepStart; + assert.equal(preparation.response.subject.digest.sha256, archiveSha256); + const sourceId = preparation.response.normalized_result.parent_evidence_id; + assert.match(sourceId, /^ev_[a-f0-9]{64}$/); + const query = { + application: { kind: "retained-evidence", evidence_id: sourceId }, + native_observations: [container, native], + seed: { + kind: "string", + value: values["addon-path"], + match: "exact", + case_sensitive: true, + }, + direction: "outgoing", + }; + const samples = []; + let first; + for (let round = 0; round < rounds; round++) { + const start = performance.now(); + const result = await call("trace_application_feature", query); + const elapsedMs = performance.now() - start; + if (first === undefined) first = result; + else assert.equal(result.response.evidence_id, first.response.evidence_id); + samples.push({ + round, + elapsed_ms: elapsedMs, + structured_reply_bytes: Buffer.byteLength(serialize(result.response)), + full_tool_reply_bytes: Buffer.byteLength(serialize(result.wire_response)), + }); + } + const trace = first.response.normalized_result; + const addons = trace.graph.nodes.filter( + ({ kind }) => kind === "native-addon", + ); + assert.equal( + addons.length, + 1, + "The selected native add-on must remain visible", + ); + const addon = addons[0]; + const unavailable = addon.observations.some( + ({ properties }) => properties.availability === "unavailable", + ); + const expectedLinks = [ + sourceId, + ...(companionSha256 === null ? [] : [native.evidence_id]), + ].sort(); + const safeIdentity = + serialize(trace.evidence_links) === serialize(expectedLinks) && + (companionSha256 === null + ? unavailable && + addon.identity.strategy !== "content-digest" && + trace.coverage.status === "partial" && + trace.native_handoffs.length === 0 + : addon.identity.strategy === "content-digest" && + addon.identity.sha256 === companionSha256 && + trace.native_handoffs.length === 1 && + trace.native_handoffs[0].artifact_sha256 === companionSha256 && + trace.native_handoffs[0].status === "evidence-linked" && + serialize(trace.native_handoffs[0].evidence_ids) === + serialize([native.evidence_id])); + let cliParity = null; + if (values["cli-parity"]) { + temporary = await PrivateRuntimeRoot.create({ + prefix: "rea-native-handoff-verifier-", + }); + const bundlePath = join(temporary.path, "bundle.json"); + const exported = await client.callTool({ + name: "export_evidence_bundle", + arguments: { path: bundlePath }, + }); + assert.notEqual(exported.isError, true, serialize(exported)); + const bundle = JSON.parse(await readFile(bundlePath, "utf8")); + const parent = bundle.records.find( + ({ evidence_id }) => evidence_id === sourceId, + ); + assert.ok(parent, "Export omitted the retained parent"); + const request = join(temporary.path, "trace.json"); + // The CLI's portable file contract takes complete Evidence; MCP resolves a + // same-session reference into the same shared application workflow. + await writeFile(request, serialize({ ...query, application: parent })); + const result = await ownedCommand( + process.execPath, + [ + fileURLToPath(new URL("../../rea.mjs", import.meta.url)), + "trace-application-feature", + request, + "--json", + ], + 120_000, + ); + const evidence = JSON.parse(result.stdout.text); + assert.deepEqual(evidence.normalized_result, trace); + assert.equal(evidence.evidence_id, first.response.evidence_id); + cliParity = { + exact_cli_mcp_trace_parity: true, + evidence_id: evidence.evidence_id, + }; + } + let tokenCosts = null; + if (values["tokenizer-python"]) { + temporary ??= await PrivateRuntimeRoot.create({ + prefix: "rea-native-handoff-verifier-", + }); + const input = join(temporary.path, "token-input.json"); + await writeFile( + input, + serialize({ catalog, selected, preparation, query: first }), + ); + const result = await ownedCommand(values["tokenizer-python"], [ + "-c", + `import json,sys,tiktoken +with open(sys.argv[1],encoding='utf-8') as f:d=json.load(f) +enc=tiktoken.get_encoding('cl100k_base') +def n(v):return len(enc.encode(json.dumps(v,ensure_ascii=False,separators=(',',':')),disallowed_special=())) +requests=n(d['preparation']['request'])+n(d['query']['request']) +structured=n(d['preparation']['response'])+n(d['query']['response']) +full_reply=n(d['preparation']['wire_response'])+n(d['query']['wire_response']) +s=n(d['selected']);a=n(d['catalog']) +print(json.dumps(dict(tokenizer='cl100k_base',tiktoken_version=tiktoken.__version__,selected_catalog=s,full_catalog=a,requests=requests,structured_replies=structured,full_tool_replies=full_reply,selected_catalog_structured_task=s+requests+structured,full_catalog_structured_task=a+requests+structured,selected_catalog_full_reply_task=s+requests+full_reply,full_catalog_full_reply_task=a+requests+full_reply)))`, + input, + ]); + tokenCosts = JSON.parse(result.stdout.text); + } + report = { + scope: + "Real shipped-ASAR native identity through public CLI/stdio MCP; simulated native Evidence only, no provider/application executed", + artifact: { path: values.artifact, sha256: archiveSha256 }, + addon_path: values["addon-path"], + companion_sha256_observed: companionSha256, + prior_native_sha256_supplied: values["native-sha256"], + source_paths: values["source-path"] ?? null, + parent_evidence_id: sourceId, + trace_evidence_id: first.response.evidence_id, + preparation_ms: preparationMs, + samples, + safe_identity: safeIdentity, + node: { + node_id: addon.node_id, + identity: addon.identity, + unavailable_observed: unavailable, + basis: addon.observations.map(({ evidence }) => ({ + authority: evidence.authority, + state: evidence.state, + confidence: evidence.confidence, + artifact: evidence.artifact, + location: evidence.location, + })), + }, + native_handoffs: trace.native_handoffs, + evidence_links: trace.evidence_links, + source_coverage: trace.coverage, + graph_coverage: trace.graph.coverage, + facts_sha256: digest({ + node: addon, + native_handoffs: trace.native_handoffs, + evidence_links: trace.evidence_links, + coverage: trace.coverage, + }), + cli_parity: cliParity, + token_costs: tokenCosts, + conditions: + "Fresh MCP session, one selected analysis followed by repeated identical retained traces. Filesystem/build caches uncontrolled. Task token counts include one analysis summary and one trace with name/description/inputSchema catalog projections; structured and complete-tool-reply projections are alternatives, not billing. Repeated rounds, export, CLI parity and tokenizer are validation overhead outside that one-task token count but inside total resource receipts. A wrong-identity baseline is not a successful equivalent task or efficiency credit.", + }; + if (values["require-safe"]) + assert.ok(safeIdentity, "Native identity/linkage oracle failed"); +} catch (cause) { + primaryError = cause; + throw cause; +} finally { + await closeVerifierResources( + [ + () => client.close(), + () => transport.close(), + async () => { + if (retainTemporary) + throw new Error( + `Command cleanup incomplete; retaining owned inputs at ${temporary?.path}.`, + ); + await temporary?.close(); + }, + ], + primaryError, + ); +} +console.log(serialize(report)); diff --git a/src/domain/javascript/javascriptNativeHandoff.ts b/src/domain/javascript/javascriptNativeHandoff.ts index 7d2f7dfb0..00b7c699e 100644 --- a/src/domain/javascript/javascriptNativeHandoff.ts +++ b/src/domain/javascript/javascriptNativeHandoff.ts @@ -14,15 +14,21 @@ export const buildJavaScriptNativeHandoffs = ( edges: readonly ApplicationEdge[], nativeEvidence: readonly Evidence[], ): NativeHandoff[] => { - const nodeById = new Map(nodes.map((node) => [node.node_id, node])); + let nodeById: Map | undefined; return nodes .filter(({ kind }) => kind === "native-addon") .flatMap((node) => { - const digest = artifactDigest(node); - if (digest === null) return []; + // Observations may describe only a containing archive. A native target + // requires the node owner's exact byte identity, independently of its + // path, availability labels or the artifact supporting an observation. + if (node.identity.strategy !== "content-digest") return []; + const digest = node.identity.sha256; const linked = nativeEvidence.filter( ({ subject }) => subject?.digest.sha256 === digest, ); + nodeById ??= new Map( + nodes.map((candidate) => [candidate.node_id, candidate]), + ); const exportNodes = nativeExportNodes(node, edges, nodeById); return [ { @@ -54,16 +60,6 @@ export const buildJavaScriptNativeHandoffs = ( ); }; -const artifactDigest = (node: ApplicationNode): string | null => { - if (node.identity.strategy === "content-digest") return node.identity.sha256; - const observation = node.observations.find( - ({ evidence }) => evidence.artifact.available, - ); - return observation?.evidence.artifact.available === true - ? observation.evidence.artifact.sha256 - : null; -}; - const artifactPath = (node: ApplicationNode): string | null => { for (const { evidence } of node.observations) if ( diff --git a/tests/boundary/mcp/javascriptNativeHandoffIdentity.test.ts b/tests/boundary/mcp/javascriptNativeHandoffIdentity.test.ts new file mode 100644 index 000000000..851a28397 --- /dev/null +++ b/tests/boundary/mcp/javascriptNativeHandoffIdentity.test.ts @@ -0,0 +1,223 @@ +import { Client } from "@modelcontextprotocol/client"; +import { StdioClientTransport } from "@modelcontextprotocol/client/stdio"; +import { createHash } from "node:crypto"; +import { readFile, rm, writeFile } from "node:fs/promises"; +import { join, resolve } from "node:path"; +import { afterAll, beforeAll, expect } from "vitest"; + +import { toolContract } from "../../../src/contracts/toolContracts.js"; +import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; +import { createEvidence } from "../../../src/domain/evidence.js"; +import { + applicationFeatureTraceResultSchema, + type ApplicationFeatureTraceResult, +} from "../../../src/domain/javascript/javascriptFeatureTraceSchemas.js"; +import { createStrippedAsarAddon } from "../../fixtures/strippedAsarAddon.js"; +import { cliTest } from "../../support/cli/cliFixture.js"; + +const client = new Client({ name: "native-handoff-identity", version: "1" }); +const transport = new StdioClientTransport({ + command: process.execPath, + args: [resolve("scripts/rea.mjs"), "mcp"], + cwd: process.cwd(), + env: { PATH: process.env.PATH ?? "" }, + stderr: "pipe", +}); +beforeAll(async () => { + await client.connect(transport); + transport.stderr?.on("data", () => undefined); +}); +afterAll(async () => { + await Promise.all([client.close(), transport.close()]); +}); + +const sha256 = (value: Uint8Array) => + createHash("sha256").update(value).digest("hex"); +const nativeObservation = (path: string, sha256: string) => + createEvidence( + { path, sha256, format: "raw-binary" }, + { id: "ghidra", name: "Ghidra", version: "simulated" }, + { + operation: "binary_overview", + parameters: {}, + result: { + summary: "Simulated native Evidence for subject identity only.", + }, + limitations: ["No native provider executed."], + }, + ); + +interface ExpectedIdentity { + readonly available: boolean; + readonly addon: string; + readonly archiveDigest: string; + readonly nativeDigest: string; + readonly parentId: string; + readonly nativeId: string; + readonly linked: boolean; +} + +const expectHandoffIdentity = ( + trace: ApplicationFeatureTraceResult, + expected: ExpectedIdentity, +) => { + const node = trace.graph?.nodes.find(({ kind }) => kind === "native-addon"); + expect(node).toBeDefined(); + expect(trace.evidence_links).toEqual( + [ + expected.parentId, + ...(expected.available && expected.linked ? [expected.nativeId] : []), + ].sort(), + ); + if (expected.available) { + expect(node?.identity).toMatchObject({ + strategy: "content-digest", + sha256: expected.nativeDigest, + }); + expect(trace.native_handoffs).toHaveLength(1); + expect(trace.native_handoffs[0]).toMatchObject({ + native_node_id: node?.node_id, + artifact_sha256: expected.nativeDigest, + artifact_path: expected.addon, + status: expected.linked + ? "evidence-linked" + : "requires-provider-analysis", + evidence_ids: expected.linked ? [expected.nativeId] : [], + }); + } else { + expect(node?.identity).toMatchObject({ + strategy: "canonical-path", + artifact_sha256: expected.archiveDigest, + path: expected.addon, + }); + expect(node?.observations).toContainEqual( + expect.objectContaining({ + properties: expect.objectContaining({ + availability: "unavailable", + hash_status: "unavailable", + inventory_artifact_id: null, + }), + }), + ); + expect(trace.coverage.status).toBe("partial"); + expect(trace.graph?.coverage.status).toBe("partial"); + expect(trace.native_handoffs).toEqual([]); + expect(trace.summary.native_handoffs).toBe(0); + } +}; + +for (const available of [false, true]) { + cliTest( + `keeps native handoff byte identity ${available ? "exact when available" : "unknown for a missing ASAR companion"}`, + async ({ cli }) => { + const { root, archive, addon, original } = + await createStrippedAsarAddon(); + const companion = join(`${archive}.unpacked`, addon); + if (available) await writeFile(companion, original); + else await rm(companion); + const archiveDigest = sha256(await readFile(archive)); + const nativeDigest = sha256(original); + const containerEvidence = nativeObservation(archive, archiveDigest); + const nativeEvidence = nativeObservation(companion, nativeDigest); + const analysis = await cli.run({ + arguments: ["analyze-javascript-application", archive, "--json"], + }); + expect(analysis.exitCode, analysis.stderr).toBe(0); + const parent = toolContract( + "analyze_javascript_application", + ).outputSchema.parse(analysis.json); + const summary = await client.callTool({ + name: "analyze_javascript_application", + arguments: { input_path: archive, detail: "summary" }, + }); + expect(summary.isError).not.toBe(true); + const summaryEvidence = toolContract( + "analyze_javascript_application", + ).outputSchema.parse(summary.structuredContent); + expect( + analysisViewResultSchema.parse(summaryEvidence.normalized_result) + .parent_evidence_id, + ).toBe(parent.evidence_id); + const observed: { + trace: ApplicationFeatureTraceResult; + linked: boolean; + }[] = []; + for (const native_observations of [ + [containerEvidence, nativeEvidence], + [], + ]) { + const query = { + seed: { + kind: "string", + value: addon, + match: "exact", + case_sensitive: true, + }, + direction: "outgoing", + native_observations, + }; + const request = join( + root, + `trace-${String(native_observations.length)}.json`, + ); + await writeFile( + request, + JSON.stringify({ + ...query, + application: parent, + }), + ); + const cliTrace = await cli.run({ + arguments: ["trace-application-feature", request, "--json"], + }); + expect( + cliTrace.exitCode, + cliTrace.exitCode === 0 + ? undefined + : JSON.stringify(cliTrace.json ?? cliTrace.stderr), + ).toBe(0); + const cliEvidence = toolContract( + "trace_application_feature", + ).outputSchema.parse(cliTrace.json); + const response = await client.callTool({ + name: "trace_application_feature", + arguments: { + ...query, + application: { + kind: "retained-evidence", + evidence_id: parent.evidence_id, + }, + }, + }); + expect(response.isError).not.toBe(true); + const evidence = toolContract( + "trace_application_feature", + ).outputSchema.parse(response.structuredContent); + expect(evidence.evidence_id).toBe(cliEvidence.evidence_id); + for (const value of [cliEvidence, evidence]) + observed.push({ + trace: applicationFeatureTraceResultSchema.parse( + value.normalized_result, + ), + linked: native_observations.length > 0, + }); + } + // Exercise both provider-evidence selections and adapters before checking + // the missing-byte outcome; a failed baseline still observes both paths. + for (const { trace, linked } of observed) + expectHandoffIdentity(trace, { + available, + addon, + archiveDigest, + nativeDigest, + parentId: parent.evidence_id, + nativeId: nativeEvidence.evidence_id, + linked, + }); + expect(sha256(await readFile(archive))).toBe(archiveDigest); + if (available) + expect(sha256(await readFile(companion))).toBe(nativeDigest); + await client.ping(); + }, + ); +}