mirror of
https://github.com/morluto/rea.git
synced 2026-11-03 05:16:16 +00:00
Give ordinary Evidence creation an explicit unknown-payload input contract while retaining the strong JSON contract for producer helpers and cooperative immutable creation. Keep schema admission, input snapshotting and identity hashing with the Evidence owner. Remove 32 generic JSON preparses that only fed ordinary Evidence construction. Preserve semantic format validation, selected-source bindings, standalone JSON boundaries, typed executions and scenario digest contracts. Invalid JSON now reports its Evidence parameters/normalized_result/raw_result prefix. Validation: fresh runtime/catalog build, 560 focused and README tests passed, 4 platform-specific cases skipped, full static/docs checks, test discovery, duplication and diff checks. New regressions cover omitted semantic optionals, raw-result defaults and nested undefined refusal without input mutation. Installed providers, real browsers/Frida/JADX and Linux-only recovery remain unverified; boundary providers are authored fixtures.
194 lines
6.5 KiB
TypeScript
194 lines
6.5 KiB
TypeScript
import { z } from "zod";
|
|
import type { ExecutionOptions } from "./AnalysisProvider.js";
|
|
import type { WebRuntimePort } from "./WebRuntimePort.js";
|
|
import type { AnalysisError } from "../domain/analysisErrorBase.js";
|
|
import {
|
|
AnalysisCancelledError,
|
|
AnalysisOutputError,
|
|
} from "../domain/analysisErrorCore.js";
|
|
import { createEvidence, type Evidence } from "../domain/evidence.js";
|
|
import { analysisInputErrorFromIssues } from "../domain/inputIssueProjection.js";
|
|
import { err, ok, type Result } from "../domain/result.js";
|
|
import {
|
|
observeWebExecutionInputSchema,
|
|
webExecutionSchema,
|
|
webExecutionObservationSchema,
|
|
} from "../domain/webExecution.js";
|
|
import {
|
|
inspectWebEventListenersInputSchema,
|
|
webEventListenersSchema,
|
|
webEventListenersObservationSchema,
|
|
} from "../domain/webEventListeners.js";
|
|
import {
|
|
completeProviderEvidence,
|
|
keepIncompleteObservation,
|
|
} from "./ProviderOperationExecution.js";
|
|
|
|
/** Shared public workflows keep instrumentation and listener inspection independently selectable. */
|
|
export class WebRuntimeService {
|
|
constructor(readonly provider: WebRuntimePort) {}
|
|
|
|
/** Observe externally triggered execution; selection authorizes the declared instrumentation. */
|
|
async observe(
|
|
rawInput: unknown,
|
|
options?: ExecutionOptions,
|
|
): Promise<Result<Evidence, AnalysisError>> {
|
|
const operation = "observe_web_execution";
|
|
const parsed = parseInput(
|
|
observeWebExecutionInputSchema,
|
|
rawInput,
|
|
operation,
|
|
options,
|
|
);
|
|
if (!parsed.ok) return parsed;
|
|
const result = await this.provider.observeExecution(parsed.value, options);
|
|
if (!result.ok)
|
|
return this.retainRuntimePartial(
|
|
result.error,
|
|
operation,
|
|
"Provider retained a non-JSON incomplete execution observation.",
|
|
(partial) => {
|
|
if (!("instrumentation" in partial) || !("window" in partial))
|
|
return undefined;
|
|
const verified = webExecutionObservationSchema.safeParse(partial);
|
|
if (!verified.success) return undefined;
|
|
return this.evidence(operation, parsed.value, verified.data);
|
|
},
|
|
);
|
|
const verified = webExecutionSchema.safeParse(result.value);
|
|
if (!verified.success)
|
|
return err(new AnalysisOutputError(operation, verified.error.message));
|
|
return this.evidence(operation, parsed.value, verified.data, options);
|
|
}
|
|
|
|
/** Inspect one selected node's listener declarations without dispatching an event. */
|
|
async inspect(
|
|
rawInput: unknown,
|
|
options?: ExecutionOptions,
|
|
): Promise<Result<Evidence, AnalysisError>> {
|
|
const operation = "inspect_web_event_listeners";
|
|
const parsed = parseInput(
|
|
inspectWebEventListenersInputSchema,
|
|
rawInput,
|
|
operation,
|
|
options,
|
|
);
|
|
if (!parsed.ok) return parsed;
|
|
const result = await this.provider.inspectEventListeners(
|
|
parsed.value,
|
|
options,
|
|
);
|
|
if (!result.ok)
|
|
return this.retainRuntimePartial(
|
|
result.error,
|
|
operation,
|
|
"Provider retained a non-JSON incomplete event-listener observation.",
|
|
(partial) => {
|
|
if (!("selected_node" in partial) || !("listeners" in partial))
|
|
return undefined;
|
|
const verified =
|
|
webEventListenersObservationSchema.safeParse(partial);
|
|
if (!verified.success) return undefined;
|
|
return this.evidence(operation, parsed.value, verified.data);
|
|
},
|
|
);
|
|
const verified = webEventListenersSchema.safeParse(result.value);
|
|
if (!verified.success)
|
|
return err(new AnalysisOutputError(operation, verified.error.message));
|
|
return this.evidence(operation, parsed.value, verified.data, options);
|
|
}
|
|
|
|
/** Promote a schema-valid partial, or keep a JSON snapshot of an incomplete one. */
|
|
private retainRuntimePartial(
|
|
error: AnalysisError,
|
|
operation: string,
|
|
nonJsonReason: string,
|
|
promote: (
|
|
partial: NonNullable<AnalysisError["partialObservation"]>,
|
|
) => Result<Evidence, AnalysisError> | undefined,
|
|
): Result<Evidence, AnalysisError> {
|
|
const partial = error.partialObservation;
|
|
if (partial !== undefined && !("evidence_id" in partial)) {
|
|
const evidence = promote(partial);
|
|
if (evidence !== undefined) {
|
|
if (evidence.ok) {
|
|
error.partialObservation = evidence.value;
|
|
return err(error);
|
|
}
|
|
}
|
|
}
|
|
return err(keepIncompleteObservation(error, operation, nonJsonReason));
|
|
}
|
|
|
|
private evidence(
|
|
operation: string,
|
|
input:
|
|
| z.infer<typeof observeWebExecutionInputSchema>
|
|
| z.infer<typeof inspectWebEventListenersInputSchema>,
|
|
result:
|
|
| z.infer<typeof webExecutionObservationSchema>
|
|
| z.infer<typeof webEventListenersObservationSchema>,
|
|
options?: ExecutionOptions,
|
|
): Result<Evidence, AnalysisError> {
|
|
if (
|
|
result.target.target_id !== input.target_id ||
|
|
("observation_ms" in input &&
|
|
(!("window" in result) ||
|
|
result.window.requested_ms !== input.observation_ms)) ||
|
|
("selector" in input &&
|
|
(!("selected_node" in result) ||
|
|
result.selected_node.selector !== input.selector))
|
|
)
|
|
return err(
|
|
new AnalysisOutputError(
|
|
operation,
|
|
"observation_ms" in input
|
|
? "Provider changed the selected target or requested window."
|
|
: "Provider changed the selected target or node selector.",
|
|
),
|
|
);
|
|
if (
|
|
input.allowed_origins.length > 0 &&
|
|
!input.allowed_origins.includes(result.target.origin)
|
|
)
|
|
return err(
|
|
new AnalysisOutputError(
|
|
operation,
|
|
"Provider returned a document outside the selected origin scope.",
|
|
),
|
|
);
|
|
return completeProviderEvidence(
|
|
createEvidence(undefined, this.provider.identity(), {
|
|
predicateType: `rea.${operation.replaceAll("_", "-")}`,
|
|
operation,
|
|
parameters: input,
|
|
result,
|
|
confidence: "observed",
|
|
authority: "external-service",
|
|
limitations: result.limitations,
|
|
}),
|
|
options?.signal,
|
|
);
|
|
}
|
|
}
|
|
|
|
const parseInput = <T>(
|
|
schema: z.ZodType<T>,
|
|
rawInput: unknown,
|
|
operation: string,
|
|
options: ExecutionOptions | undefined,
|
|
): Result<T, AnalysisError> => {
|
|
if (options?.signal?.aborted)
|
|
return err(
|
|
new AnalysisCancelledError(operation, { cause: options.signal.reason }),
|
|
);
|
|
const parsed = schema.safeParse(rawInput);
|
|
return parsed.success
|
|
? ok(parsed.data)
|
|
: err(
|
|
analysisInputErrorFromIssues(operation, parsed.error.issues, rawInput, {
|
|
cause: parsed.error,
|
|
}),
|
|
);
|
|
};
|