From 593a8e12ceac1cfe350493cfe6b0d1360eb85f7c Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Tue, 29 Sep 2026 08:20:18 +0800 Subject: [PATCH] fix(runtime): make observed target role optional --- .../JavaScriptRuntimeObservationEvidence.ts | 4 ++- src/browser/V8InspectorCaptureProjection.ts | 7 +++-- src/browser/V8InspectorProvider.ts | 3 +- src/cliJavaScriptRuntimeCommands.ts | 8 +++-- ...vascriptRuntimeObservationToolContracts.ts | 1 - src/domain/javascriptRuntimeObservation.ts | 12 ++++++-- .../javascriptRuntimeReconciliationParsing.ts | 5 ++-- .../javascriptRuntimeObservation.test.ts | 29 +++++++++++++++++++ 8 files changed, 56 insertions(+), 13 deletions(-) diff --git a/src/application/JavaScriptRuntimeObservationEvidence.ts b/src/application/JavaScriptRuntimeObservationEvidence.ts index 9e829f4578..b3a16fd7b9 100644 --- a/src/application/JavaScriptRuntimeObservationEvidence.ts +++ b/src/application/JavaScriptRuntimeObservationEvidence.ts @@ -52,7 +52,9 @@ const parameters = ( ...("target_id" in input ? { target_id: input.target_id, - runtime_kind: input.runtime_kind, + ...(input.runtime_kind === undefined + ? {} + : { runtime_kind: input.runtime_kind }), observation_ms: input.observation_ms, limits: input.limits, } diff --git a/src/browser/V8InspectorCaptureProjection.ts b/src/browser/V8InspectorCaptureProjection.ts index 2ec33bdb87..4aea6cb9ce 100644 --- a/src/browser/V8InspectorCaptureProjection.ts +++ b/src/browser/V8InspectorCaptureProjection.ts @@ -96,8 +96,11 @@ export const finalizeInspectorCapture = async ({ protocol_type: target.type, attached: target.attached, location: target.location, - runtime_kind: input.runtime_kind, - runtime_kind_authority: "caller-declared-unverified", + runtime_kind: input.runtime_kind ?? "unknown", + runtime_kind_authority: + input.runtime_kind === undefined + ? "not-declared" + : "caller-declared-unverified", }, capture: { observation_ms: input.observation_ms, diff --git a/src/browser/V8InspectorProvider.ts b/src/browser/V8InspectorProvider.ts index d8c700846a..7e6fe4f61e 100644 --- a/src/browser/V8InspectorProvider.ts +++ b/src/browser/V8InspectorProvider.ts @@ -140,7 +140,8 @@ export class V8InspectorProvider implements JavaScriptRuntimeObservationPort { options.signal, ); const target = await authorizedTarget(discovery.targets, input, roots); - assertRuntimeKind(target, input.runtime_kind); + if (input.runtime_kind !== undefined) + assertRuntimeKind(target, input.runtime_kind); connection = await CdpConnection.connect( target.webSocketUrl, "observe_javascript_runtime", diff --git a/src/cliJavaScriptRuntimeCommands.ts b/src/cliJavaScriptRuntimeCommands.ts index bbe3b997f2..06fcfde11b 100644 --- a/src/cliJavaScriptRuntimeCommands.ts +++ b/src/cliJavaScriptRuntimeCommands.ts @@ -41,9 +41,11 @@ const listOptionsSchema = z.object({ ...scopeOptions }); const observeOptionsSchema = z.object({ ...scopeOptions, - runtimeKind: javascriptRuntimeKindSchema.describe( - "Declared target role; Inspector cannot authenticate the Electron role", - ), + runtimeKind: javascriptRuntimeKindSchema + .optional() + .describe( + "Declared target role; Inspector cannot authenticate the Electron role", + ), observationMs: z .number() .int() diff --git a/src/contracts/javascriptRuntimeObservationToolContracts.ts b/src/contracts/javascriptRuntimeObservationToolContracts.ts index eb78ab512c..0669ea3bbe 100644 --- a/src/contracts/javascriptRuntimeObservationToolContracts.ts +++ b/src/contracts/javascriptRuntimeObservationToolContracts.ts @@ -49,7 +49,6 @@ export const JAVASCRIPT_RUNTIME_OBSERVATION_TOOL_CONTRACTS = [ allowed_file_roots: [root], allowed_origins: [], target_id: "TARGET_ID_FROM_LIST_JAVASCRIPT_RUNTIME_TARGETS", - runtime_kind: "node", approved: true, observation_ms: 100, limits: { diff --git a/src/domain/javascriptRuntimeObservation.ts b/src/domain/javascriptRuntimeObservation.ts index d2504af60e..d4ec988a6d 100644 --- a/src/domain/javascriptRuntimeObservation.ts +++ b/src/domain/javascriptRuntimeObservation.ts @@ -69,6 +69,9 @@ export const javascriptRuntimeKindSchema = z.enum([ "electron-preload", "electron-renderer", ]); +const observedJavaScriptRuntimeKindSchema = javascriptRuntimeKindSchema.or( + z.literal("unknown"), +); export const javascriptRuntimeObservationLimitsSchema = z.strictObject({ max_events: z.number().int().min(1).max(50_000).default(10_000), @@ -88,7 +91,7 @@ export const observeJavaScriptRuntimeInputSchema = z .strictObject({ ...approvedRuntimeScope, target_id: z.string().trim().min(1).max(256), - runtime_kind: javascriptRuntimeKindSchema, + runtime_kind: javascriptRuntimeKindSchema.optional(), observation_ms: z.number().int().min(0).max(10_000).default(100), limits: javascriptRuntimeObservationLimitsSchema.default({ max_events: 10_000, @@ -173,8 +176,11 @@ const javascriptRuntimeContextSchema = z.strictObject({ export const javascriptRuntimeObservationSchema = z.strictObject({ runtime: javascriptRuntimeVersionSchema, target: javascriptRuntimeTargetSchema.extend({ - runtime_kind: javascriptRuntimeKindSchema, - runtime_kind_authority: z.literal("caller-declared-unverified"), + runtime_kind: observedJavaScriptRuntimeKindSchema, + runtime_kind_authority: z.enum([ + "caller-declared-unverified", + "not-declared", + ]), }), capture: z.strictObject({ observation_ms: z.number().int().min(0), diff --git a/src/domain/javascriptRuntimeReconciliationParsing.ts b/src/domain/javascriptRuntimeReconciliationParsing.ts index 4f37ee5f92..546e9b505c 100644 --- a/src/domain/javascriptRuntimeReconciliationParsing.ts +++ b/src/domain/javascriptRuntimeReconciliationParsing.ts @@ -284,13 +284,14 @@ const assertV8RuntimeParameters = ( .max(32), allowed_origins: z.array(browserOriginSchema).max(32), target_id: z.string().trim().min(1).max(256), - runtime_kind: javascriptRuntimeKindSchema, + runtime_kind: javascriptRuntimeKindSchema.optional(), }) .passthrough() .parse(evidence.parameters); if ( parameters.target_id !== result.target.target_id || - parameters.runtime_kind !== result.target.runtime_kind + (parameters.runtime_kind !== undefined && + parameters.runtime_kind !== result.target.runtime_kind) ) throw new TypeError( "Runtime Evidence target or declared role disagrees with its result", diff --git a/tests/boundary/browser/javascriptRuntimeObservation.test.ts b/tests/boundary/browser/javascriptRuntimeObservation.test.ts index eb6b897de2..83a92e8317 100644 --- a/tests/boundary/browser/javascriptRuntimeObservation.test.ts +++ b/tests/boundary/browser/javascriptRuntimeObservation.test.ts @@ -172,6 +172,35 @@ describe("passive V8 Inspector provider", () => { ); }); +describe("undeclared V8 runtime role", () => { + test("observes a target without inventing a runtime role", async () => { + const fixture = await runtimeFixture(); + const fake = await startFakeV8Inspector({ + targetUrl: pathToFileURL(fixture.entry).href, + targetType: "page", + }); + try { + const input = observeInput( + fake.endpoint, + fake.targetId, + fixture.root, + "node", + ); + delete input.runtime_kind; + const result = await new V8InspectorProvider().observe(input); + expect(result.ok).toBe(true); + if (!result.ok) return; + expect(result.value.target).toMatchObject({ + protocol_type: "page", + runtime_kind: "unknown", + runtime_kind_authority: "not-declared", + }); + } finally { + await fake.close(); + } + }); +}); + describe("passive V8 Inspector evidence", () => { test("marks capture truncation instead of claiming complete coverage", async () => { const fixture = await runtimeFixture();