Files
rea/src/domain/javascriptRuntimeReconciliationRuntime.ts
T
N0zoM1z0 84e4361c09 fix(inspector): preserve lossy Node discovery locations (#542)
Keep explicitly selected Node Inspector targets observable when discovery
cannot verify their file location. Resolve literal POSIX discovery paths
without URI decoding, and retain lossy Windows or underscore paths as
unresolved metadata. Validate scriptParsed file URLs independently.

Preserve unknown target identity through runtime reconciliation, regenerate
contracts, and verify real CLI/MCP behavior on Linux and Windows.

Closes #530

Assisted-by: Kogasa (gensokyo-skills:kogasa-surprise-testing)
2026-10-05 16:55:42 +08:00

439 lines
13 KiB
TypeScript

import type { JsonValue } from "./jsonValue.js";
import {
createJavaScriptApplicationEdge,
createJavaScriptApplicationNode,
type ApplicationEdge,
type ApplicationGraphEvidence,
type ApplicationNode,
} from "./javascriptApplicationGraph.js";
import type { ParsedRuntimeCapture } from "./javascriptRuntimeReconciliationParsing.js";
export interface RuntimeReconciliationEntity {
readonly kind: "target" | "frame" | "script" | "worker";
readonly section: "target" | "frames" | "scripts" | "workers";
readonly node: ApplicationNode;
readonly capture: ParsedRuntimeCapture;
readonly runtimeKey: string;
readonly location: {
readonly kind: "file" | "url" | "unresolved";
readonly value: string;
};
readonly frameKey: string | null;
readonly sourceSha256: string | null;
readonly staticKind: "renderer" | "javascript" | "worker" | "service-worker";
}
export interface RuntimeProjection {
readonly entities: readonly RuntimeReconciliationEntity[];
readonly nodes: readonly ApplicationNode[];
readonly edges: readonly ApplicationEdge[];
readonly targetNodeByEvidenceId: ReadonlyMap<string, ApplicationNode>;
readonly omittedEntities: number;
}
/** Project authorized passive capture metadata into capture-scoped JAG nodes. */
export const projectRuntimeCaptures = (
captures: readonly ParsedRuntimeCapture[],
): RuntimeProjection => {
const all = captures.flatMap(projectCapture);
const retained = all;
const retainedIds = new Set(retained.map(({ node }) => node.node_id));
const nodes = retained.map(({ node }) => node);
const edges = captures
.flatMap((capture) => runtimeEdges(capture, all))
.filter(
({ source_node_id: source, target_node_id: target }) =>
retainedIds.has(source) && retainedIds.has(target),
);
const targetNodeByEvidenceId = new Map(
retained
.filter(({ kind }) => kind === "target")
.map(({ capture, node }) => [capture.evidence.evidence_id, node]),
);
return {
entities: retained,
nodes,
edges,
targetNodeByEvidenceId,
omittedEntities: 0,
};
};
const projectCapture = (
capture: ParsedRuntimeCapture,
): RuntimeReconciliationEntity[] => {
const target = targetEntity(capture);
return [
target,
...frameEntities(capture),
...scriptEntities(capture),
...workerEntities(capture),
];
};
const targetEntity = (
capture: ParsedRuntimeCapture,
): RuntimeReconciliationEntity => {
const target = capture.inspection.target;
const location =
"unresolved_location" in target
? ({
kind: "unresolved",
value: target.unresolved_location.reported_url,
} as const)
: "url" in target
? ({ kind: "url", value: target.url } as const)
: ({ kind: "file", value: target.file_path } as const);
return runtimeEntity(capture, {
kind: "target",
nodeKind: "target",
runtimeKey: target.target_id,
targetKey: target.target_id,
frameKey: null,
scriptKey: null,
location,
staticKind: "renderer",
sourceSha256: null,
label: target.title,
properties: {
runtime_type: target.type,
...(location.kind === "unresolved"
? {
reported_url: location.value,
location_kind: "unresolved",
location_reason: "unverifiable-file-location",
}
: { location: location.value }),
attached: target.attached,
},
section: "target",
});
};
const frameEntities = (
capture: ParsedRuntimeCapture,
): RuntimeReconciliationEntity[] =>
capture.inspection.frames.map((frame) => {
const location =
"url" in frame
? ({ kind: "url", value: frame.url } as const)
: ({ kind: "file", value: frame.file_path } as const);
return runtimeEntity(capture, {
kind: "frame",
nodeKind: "frame",
runtimeKey: frame.frame_id,
targetKey: capture.inspection.target.target_id,
frameKey: frame.frame_id,
scriptKey: null,
location,
staticKind: "renderer",
sourceSha256: null,
label: location.value,
properties: {
parent_frame_id: frame.parent_frame_id,
location: location.value,
},
section: "frames",
});
});
const scriptEntities = (
capture: ParsedRuntimeCapture,
): RuntimeReconciliationEntity[] =>
capture.inspection.scripts.items.map((script) => {
const location =
"url" in script
? ({ kind: "url", value: script.url } as const)
: ({ kind: "file", value: script.file_path } as const);
return runtimeEntity(capture, {
kind: "script",
nodeKind: "runtime-script-instance",
runtimeKey: script.script_key,
targetKey: capture.inspection.target.target_id,
frameKey: script.frame_id,
scriptKey: script.script_key,
location,
staticKind: "javascript",
sourceSha256: script.source.included
? script.source.artifact.sha256
: null,
label: location.value,
properties: {
location: location.value,
cdp_hash: script.cdp_hash,
length: script.length,
is_module: script.is_module,
language: script.language,
source_sha256: script.source.included
? script.source.artifact.sha256
: null,
...("source_map_url" in script
? { source_map_url: script.source_map_url }
: {}),
},
section: "scripts",
...(script.source.included
? { artifactSha256: script.source.artifact.sha256 }
: {}),
});
});
const workerEntities = (
capture: ParsedRuntimeCapture,
): RuntimeReconciliationEntity[] =>
capture.inspection.workers.map((worker) => {
const location =
"url" in worker
? ({ kind: "url", value: worker.url } as const)
: ({ kind: "file", value: worker.file_path } as const);
const serviceWorker = worker.type.includes("service");
return runtimeEntity(capture, {
kind: "worker",
nodeKind: serviceWorker ? "service-worker" : "worker",
runtimeKey: worker.target_id,
targetKey: worker.target_id,
frameKey: worker.parent_frame_id,
scriptKey: null,
location,
staticKind: serviceWorker ? "service-worker" : "worker",
sourceSha256: null,
label: location.value,
properties: {
runtime_type: worker.type,
location: location.value,
attached: worker.attached,
opener_target_id: worker.opener_target_id,
parent_frame_id: worker.parent_frame_id,
},
section: "workers",
});
});
interface RuntimeEntityInput {
readonly kind: RuntimeReconciliationEntity["kind"];
readonly nodeKind: ApplicationNode["kind"];
readonly runtimeKey: string;
readonly targetKey: string;
readonly frameKey: string | null;
readonly scriptKey: string | null;
readonly location: RuntimeReconciliationEntity["location"];
readonly staticKind: RuntimeReconciliationEntity["staticKind"];
readonly sourceSha256: string | null;
readonly label: string;
readonly properties: Readonly<Record<string, JsonValue>>;
readonly section: "target" | "frames" | "scripts" | "workers";
readonly artifactSha256?: string;
}
const runtimeEntity = (
capture: ParsedRuntimeCapture,
input: RuntimeEntityInput,
): RuntimeReconciliationEntity => ({
kind: input.kind,
section: input.section,
capture,
runtimeKey: input.runtimeKey,
location: input.location,
frameKey: input.frameKey,
sourceSha256: input.sourceSha256,
staticKind: input.staticKind,
node: createJavaScriptApplicationNode({
kind: input.nodeKind,
identity: {
strategy: "runtime-instance",
stability: "capture-only",
capture_sha256: capture.captureSha256,
runtime_key: input.runtimeKey,
},
observations: [
{
label: input.label || null,
properties: input.properties,
evidence: runtimeEvidence(capture, input),
},
],
}),
});
const runtimeEvidence = (
capture: ParsedRuntimeCapture,
input: RuntimeEntityInput,
): ApplicationGraphEvidence => ({
authority: "passive-cdp-runtime",
state: "observed",
confidence: "exact",
artifact:
input.artifactSha256 === undefined
? {
available: false,
reason:
input.section === "scripts" ? "not-observed" : "not-applicable",
detail:
input.section === "scripts"
? "Script source bytes were not included in this passive capture."
: "This runtime entity has no standalone artifact bytes.",
}
: {
available: true,
artifact_id: `art_${input.artifactSha256}`,
sha256: input.artifactSha256,
},
location: {
available: true,
value: {
kind: "runtime",
capture_sha256: capture.captureSha256,
target_key: input.targetKey,
frame_key: input.frameKey,
script_key: input.scriptKey,
},
},
extractor: {
name: capture.evidence.provider.id,
version:
capture.evidence.provider.version ??
(capture.kind === "browser" ? "2" : "1"),
operation: capture.evidence.operation,
executable_sha256: null,
},
coverage: runtimeSectionCoverage(capture, input.section),
limitations: [
"The entity was observed only within one bounded passive CDP capture.",
...(runtimeSectionComplete(capture, input.section)
? []
: [
`The runtime ${input.section} inventory was incomplete within this capture.`,
]),
],
evidence_ids: [capture.evidence.evidence_id],
});
const runtimeEdges = (
capture: ParsedRuntimeCapture,
entities: readonly RuntimeReconciliationEntity[],
): ApplicationEdge[] => {
const local = entities.filter(
({ capture: candidate }) =>
candidate.evidence.evidence_id === capture.evidence.evidence_id,
);
const target = local.find(({ kind }) => kind === "target");
if (target === undefined) return [];
const frames = new Map(
local
.filter(({ kind }) => kind === "frame")
.map((entity) => [entity.runtimeKey, entity]),
);
return local.flatMap((entity) => {
if (entity === target) return [];
const parent =
(entity.kind === "script" || entity.kind === "worker") &&
entity.frameKey !== null
? (frames.get(entity.frameKey) ?? target)
: target;
return [
createJavaScriptApplicationEdge({
source_node_id: parent.node.node_id,
target_node_id: entity.node.node_id,
relation: entity.kind === "script" ? "loads" : "contains",
properties: { basis: "passive-cdp-observation" },
evidence: runtimeRelationshipEvidence(capture, entity),
}),
];
});
};
const runtimeRelationshipEvidence = (
capture: ParsedRuntimeCapture,
entity: RuntimeReconciliationEntity,
): ApplicationGraphEvidence => ({
authority: "passive-cdp-runtime",
state: "observed",
confidence: "exact",
artifact: {
available: false,
reason: "not-applicable",
detail: "The relationship is runtime metadata, not standalone bytes.",
},
location: {
available: true,
value: {
kind: "runtime",
capture_sha256: capture.captureSha256,
target_key: capture.inspection.target.target_id,
frame_key: entity.frameKey,
script_key: entity.kind === "script" ? entity.runtimeKey : null,
},
},
extractor: {
name: capture.evidence.provider.id,
version:
capture.evidence.provider.version ??
(capture.kind === "browser" ? "2" : "1"),
operation: capture.evidence.operation,
executable_sha256: null,
},
coverage: runtimeSectionCoverage(capture, entity.section),
limitations: [
"The relationship was observed only within one bounded passive CDP capture.",
],
evidence_ids: [capture.evidence.evidence_id],
});
const runtimeSectionComplete = (
capture: ParsedRuntimeCapture,
section: RuntimeEntityInput["section"],
): boolean => {
if (section === "target") return true;
if (section === "scripts") return capture.scriptsCompleteWithinScope;
return (
!capture.inspection.completeness.truncated_sections.includes(section) &&
!capture.inspection.completeness.unavailable_sections.includes(section)
);
};
const runtimeSectionCoverage = (
capture: ParsedRuntimeCapture,
section: RuntimeEntityInput["section"],
): ApplicationGraphEvidence["coverage"] => {
if (runtimeSectionComplete(capture, section))
return {
status: "complete",
truncated: false,
omitted_count: 0,
limits: [],
};
const truncated =
section !== "target" &&
capture.inspection.completeness.truncated_sections.includes(section);
return {
status: "partial",
truncated,
omitted_count: null,
limits: truncated
? [
{
name: `passive-cdp-${section}-limit`,
value: runtimeSectionCount(capture, section),
unit: "items",
},
]
: [],
};
};
const runtimeSectionCount = (
capture: ParsedRuntimeCapture,
section: RuntimeEntityInput["section"],
): number => {
switch (section) {
case "target":
return 1;
case "frames":
return capture.inspection.frames.length;
case "scripts":
return capture.inspection.scripts.items.length;
case "workers":
return capture.inspection.workers.length;
}
};