refactor(tools): remove redundant limits and acknowledgements

This commit is contained in:
morluto 2026-09-29 10:53:45 +08:00
parent 7ba3580c86
commit bd0e341572
158 changed files with 599 additions and 1527 deletions

View File

@ -13,7 +13,6 @@ import socket
from typing import Any, Optional, Protocol, Sequence
MAX_LINE_BYTES = 10 * 1024 * 1024
MAX_READ_BYTES = 4096
BAD_ADDRESSES = (-1, 0xFFFFFFFFFFFFFFFF, None)
_selected_document = None
_rea_session_document = None
@ -647,8 +646,8 @@ def _dispatch(method, params):
length = params.get("length", 256)
if isinstance(length, bool) or not isinstance(length, int):
raise ValueError("Byte-read length must be an integer")
if length < 1 or length > MAX_READ_BYTES:
raise ValueError("Byte-read length must be between 1 and 4096")
if length < 1:
raise ValueError("Byte-read length must be at least 1")
reader = getattr(document, "readBytes", None)
if not callable(reader):
raise CapabilityUnavailableError(

View File

@ -226,7 +226,7 @@ The plan must disclose at least:
projection; and
- expected Evidence and cleanup behavior.
Execution requires both `approved: true` and the exact `plan_digest`. REA
Execution requires the exact `plan_digest`. REA
rebuilds the plan immediately before admission. Any changed file, runtime,
sandbox executable, policy, stub, input, limit, or environment commitment
returns `plan_stale` before application code starts. Approval is not transferable

View File

@ -51,7 +51,7 @@ export REA_BROWSER_ALLOWED_ORIGINS_JSON='["http://127.0.0.1:3000"]'
`REA_BROWSER_ALLOWED_ORIGINS_JSON` accepts at most 32 exact HTTP(S) origins. Paths, credentials, queries, fragments, and wildcards are rejected. An input must stay within both this administrator ceiling and any active project/session/once grant.
Configuration is reloadable through the existing `SIGHUP` permission-policy path. A request still requires `approved: true`; configuration alone does not make a tool call implicit.
Configuration is reloadable through the existing `SIGHUP` permission-policy path. Each request is checked against the configured endpoint, origin, and permission grants.
Inspect the exact effective scope without contacting the browser:
@ -68,9 +68,8 @@ rea policy explain browser_observe \
Configured origins are used by default. `--allowed-origins` can request a narrower set that still fits the configured ceiling.
```bash
rea list-browser-targets http://127.0.0.1:9222 --approved --json
rea list-browser-targets http://127.0.0.1:9222 --json
rea inspect-web-page http://127.0.0.1:9222 TARGET_ID \
--approved \
--observation-ms 1000 \
--json
```
@ -79,34 +78,33 @@ Static bundle analysis and a user-driven observation window are separate operati
```bash
rea analyze-web-bundle http://127.0.0.1:9222 TARGET_ID \
--approved --source-capture-approved --json
--json
rea observe-web-session http://127.0.0.1:9222 TARGET_ID \
--approved --observation-ms 10000 --json
--observation-ms 10000 --json
rea discover-webmcp-tools http://127.0.0.1:9222 TARGET_ID \
--approved --json
--json
```
Accessibility names/descriptions, console primitive text, JSON body shapes, WebSocket shapes, script content, and storage key names require separate opt-ins. Console/body/WebSocket capture additionally requires its matching approval flag:
Accessibility names/descriptions, console primitive text, JSON body shapes, WebSocket shapes, script content, and storage key names are selected independently:
```bash
rea inspect-web-page http://127.0.0.1:9222 TARGET_ID \
--approved \
--include-accessibility-text \
--include-console-text --console-text-approved \
--include-json-body-shapes --json-body-schema-approved \
--include-websocket-shapes --websocket-shape-approved \
--include-console-text \
--include-json-body-shapes \
--include-websocket-shapes \
--include-script-sources \
--include-storage-keys --include-storage-fingerprints \
--json
```
Accessibility text and script content may contain application secrets and become part of the returned Evidence. Accessibility and console text have independent per-field and aggregate UTF-8 byte limits; credential-shaped console substrings are redacted. JSON and WebSocket captures retain paths, types, counts, and truncation only, never values or examples. Storage values remain redacted even when key-name capture is approved. `--include-storage-fingerprints` separately approves SHA-256 identity/value fingerprints, which support comparison only when cookies, DOM storage, IndexedDB, and Cache Storage were all captured completely.
Accessibility text and script content may contain application secrets and become part of the returned Evidence. Accessibility and console text have independent per-field and aggregate UTF-8 byte limits; credential-shaped console substrings are redacted. JSON and WebSocket captures retain paths, types, counts, and truncation only, never values or examples. Storage values remain redacted even when key-name capture is requested. `--include-storage-fingerprints` requests SHA-256 identity/value fingerprints, which support comparison only when cookies, DOM storage, IndexedDB, and Cache Storage were all captured completely.
Screenshot pixels require both `--approved` and `--screenshot-approved`:
Screenshot capture returns the visible viewport as image pixels:
```bash
rea capture-web-screenshot http://127.0.0.1:9222 TARGET_ID \
--approved --screenshot-approved --json
--json
```
## MCP input
@ -116,15 +114,11 @@ rea capture-web-screenshot http://127.0.0.1:9222 TARGET_ID \
"cdp_endpoint": "http://127.0.0.1:9222",
"allowed_origins": ["http://127.0.0.1:3000"],
"target_id": "TARGET_ID_FROM_LIST_BROWSER_TARGETS",
"approved": true,
"observation_ms": 500,
"include_accessibility_text": false,
"include_console_text": false,
"console_text_approved": false,
"include_json_body_shapes": false,
"json_body_schema_approved": false,
"include_websocket_shapes": false,
"websocket_shape_approved": false,
"include_script_sources": false,
"include_storage_keys": false,
"include_storage_fingerprints": false
@ -143,10 +137,10 @@ REA removes sensitive values before normalized event data is retained:
- DOM snapshots retain node types, node names, value lengths, and attribute names, but not text or attribute values.
- Accessibility structure and roles are retained by default, but names and descriptions require `include_accessibility_text: true` and remain byte-bounded.
- Network observations retain method, status, MIME type, size, type, initiator stack location, and redacted URLs. Headers are discarded after an allowlisted projection of length/encoding, structured CSP/Link/policy fields, and untrusted agent hints. Cookies and authorization headers are never retained.
- Request/response bodies are not requested or parsed by default. With independent approval, only allowed-origin JSON media types are read within per-body and aggregate budgets, converted immediately to value-free property paths/types, and discarded. `Network.getResponseBody` is never sent without that approval.
- Console observations with an approved stack source retain call type, argument types, timestamp, and redacted source location. With independent approval, only already-delivered primitive values are retained after credential redaction and byte bounds; objects, getters, and remote properties are never expanded.
- WebSocket observations retain direction, opcode, and payload byte length. With independent approval, bounded text frames are classified as text or value-free JSON shape; binary bytes, hashes, prefixes, and raw frames are never retained.
- Storage observations always redact values. Key names, IndexedDB names, and cache names require `include_storage_keys`; stable content fingerprints require the additional `include_storage_fingerprints` approval. Cache bodies above 64 KiB and partial IndexedDB remote objects make fingerprint coverage incomplete, so identical observations remain `unknown`.
- Request/response bodies are not requested or parsed by default. When selected, only allowed-origin JSON media types are read within per-body and aggregate budgets, converted immediately to value-free property paths/types, and discarded. `Network.getResponseBody` is never sent unless JSON body shape capture is selected.
- Console observations with a stack source retain call type, argument types, timestamp, and redacted source location. When console text capture is selected, only already-delivered primitive values are retained after credential redaction and byte bounds; objects, getters, and remote properties are never expanded.
- WebSocket observations retain direction, opcode, and payload byte length. When shape capture is selected, bounded text frames are classified as text or value-free JSON shape; binary bytes, hashes, prefixes, and raw frames are never retained.
- Storage observations always redact values. Key names, IndexedDB names, and cache names require `include_storage_keys`; stable content fingerprints require the additional `include_storage_fingerprints` selection. Cache bodies above 64 KiB and partial IndexedDB remote objects make fingerprint coverage incomplete, so identical observations remain `unknown`.
- Script metadata is included only when CDP supplies a URL on an allowed origin. Stable keys exclude transient CDP script IDs, and exact transient raw URLs are used only during script/resource reconciliation. URL-less scripts are excluded because their origin cannot be established. When CDP supplies an execution-context association, the accepted script retains its authorized frame ID for later attribution. Source content is omitted unless explicitly requested and becomes a self-verifying `rea://web-content/sha256/...` artifact subject to per-script and aggregate byte limits.
Cross-origin frames, resources, scripts, events, and workers are excluded unless their exact origins are also approved. Excluded target details are counted without being exposed.

View File

@ -79,8 +79,8 @@ Each left/right run is retained as its own `observed` source Evidence. A
differential envelope is `derived` and links those source Evidence IDs, so the
comparison never erases the underlying observations.
Optional `reproducer_export` is committed by the plan and requires both
`approved: true` inside that object and separate `evidence_write` authority.
Optional `reproducer_export` is committed by the plan and requires separate
`evidence_write` authority.
The owner-only manifest is written only after complete sandbox cleanup. Source
bytes are excluded unless `include_sources: true` was explicitly approved.
An export failure is retained in the result and does not erase a completed

View File

@ -29,18 +29,16 @@ inline array. CDP response-size limits remain internal and oversized discovery
responses fail explicitly.
```bash
rea list-electron-targets http://127.0.0.1:9223 --approved --json
rea list-electron-targets http://127.0.0.1:9223 --json
rea inspect-electron-page http://127.0.0.1:9223 TARGET_ID \
--approved --observation-ms 100 --json
--observation-ms 100 --json
```
Script content is excluded by default. Capturing it requires both flags and remains subject to per-script and aggregate byte budgets:
Script content is excluded by default. Requesting it remains subject to per-script and aggregate byte budgets:
```bash
rea inspect-electron-page http://127.0.0.1:9223 TARGET_ID \
--approved \
--include-script-sources \
--source-capture-approved \
--json
```

View File

@ -157,16 +157,13 @@ stability. Edge IDs use the same derivation with `relationship-exact`
stability. Node, observation, edge, and graph IDs are recomputed during parsing,
so callers cannot preserve a stale ID after changing semantic content.
## Bounds
## Structure
The graph boundary admits at most 1,000 roots, 100,000 nodes, 200,000 edges, and 64
observations per node. Each properties object is valid bounded JSON with at most
64 keys, depth 6, 512 structural nodes, and 4,096 characters per string. Arrays
of evidence links, limits, and limitations have independent caps.
These are admission limits, not completeness claims. When an extractor reaches
a limit, it must record partial coverage and the exact named limit. It must not
return a bounded prefix labeled complete.
The graph does not impose aggregate caps on roots, nodes, edges, or observations.
Each properties object is valid JSON with at most 64 keys, depth 6, 512
structural nodes, and 4,096 characters per string. Field-specific validation
still applies to paths, identifiers, and evidence values. Extractors report
actual omissions in coverage instead of silently dropping a prefix.
## Domain API

View File

@ -82,12 +82,15 @@ persistent identities. Duplicate candidates remain ambiguous. Source-map and
structural matches are high/medium-confidence inferences rather than exact
facts. Each item is `unchanged`, `added`, `removed`, `changed`, or `unknown`,
and the result includes its basis, candidates, changed dimensions, Evidence
links, limitations, and a bounded `changed_from` graph.
links, limitations, and a `changed_from` graph containing all compared nodes,
their observations, and relationships.
One-sided absence is `added` or `removed` only when the opposite input graph has
complete coverage. With partial, unavailable, or truncated input, the same
condition is `unknown`. Output truncation separately reports omitted comparison
items, candidate references, graph nodes, edges, and observations. With
condition is `unknown`. The comparison retains every classified item and
ambiguity candidate. Coverage carries omitted counts and limits from each input
graph; comparison adds no caller-selected item, candidate, graph node, or edge
budget. With
`unknown_registry_approved: true`, unresolved comparison items can be retained
as a residual unknown in a live session.

View File

@ -18,9 +18,9 @@ export REA_V8_INSPECTOR_FILE_ROOTS_JSON='["/absolute/path/to/app"]'
export REA_V8_INSPECTOR_ALLOWED_ORIGINS_JSON='[]'
rea list-javascript-runtime-targets http://127.0.0.1:9229 \
--approved --json
--json
rea observe-javascript-runtime http://127.0.0.1:9229 TARGET_ID \
--runtime-kind node --approved --json
--runtime-kind node --json
```
At least one canonical file root or exact HTTP(S) origin is required.

View File

@ -156,14 +156,12 @@ silently equated with the generated bytes observed by CDP.
## Bounds and determinism
Inputs allow up to eight static layers and 32 passive captures. Default output
bounds retain 10,000 runtime entities, 20,000 reconciliation records, and
20,000 static load states; each caller-visible maximum is 50,000. At least one
target node per capture must fit. The combined graph also preserves the JAG v1
caps of 1,000 roots, 100,000 nodes, 200,000 edges, and 64 observations per
node. Reaching a limit reports exact omitted counts for runtime entities,
reconciliation items, static load states, and merged graph items, plus
`coverage.status: truncated`.
Inputs allow up to eight static layers and 32 passive captures. Runtime entity,
reconciliation item, and static load-state projections have caller-visible
limits and report omitted counts when reached. The merged application graph
retains all selected roots, nodes, edges, and observations from those inputs;
it adds no second aggregate prefix cap. Reaching an input projection limit
reports exact omitted counts and `coverage.status: truncated`.
Layers, captures, nodes, edges, matches, and evidence links are canonically
ordered. Repeating the same verified Evidence, mappings, and limits produces

View File

@ -53,9 +53,8 @@ request state and validates the continuation envelope, while REA validates the
untrusted input response before creating a connection-owned grant. The older
push-style `elicitation/create` path is not used for this protocol revision.
Every scenario must also contain `"approved": true`. Executables, working
directories, filesystem roots, and requested environment variables are checked
against operator policy before launch. Process Capture is an observation tool,
Executables, working directories, filesystem roots, and requested environment
variables are checked against operator policy before launch. Process Capture is an observation tool,
not a security sandbox: the target runs with the current user's permissions.
## Capture a scenario
@ -71,7 +70,6 @@ A representative scenario is:
```json
{
"approved": true,
"executable": "/absolute/path/to/node",
"arguments": ["./signup.mjs"],
"working_directory": "/absolute/project",

View File

@ -502,9 +502,9 @@
"mcp_prompts": 6
},
"digests": {
"tools_sha256": "82f634088db343feeabf4ea210c08ed83f29f237c4c21026c5a8fce1c59d7250",
"prompts_sha256": "00b328caadcfac801f42c29caf174fb04b899937390f7ba8c7d40851b63f712b",
"combined_sha256": "6a20b6b76be7cf9ced6e6b71ca435e3ec6eb6f910694f695f2a2627def628036",
"tools_sha256": "c980cd929d66af569d165b5a5fc63d467bedd452fcea3554804e6e8302bd6c46",
"prompts_sha256": "2e46c679689e3fb53fec10d91edf895abd5b1fe7105b7aedbad66ac86e2d9670",
"combined_sha256": "8e1126057d24fa22f0d4425e2e05b01dfde624292363b1f581a2bcc884a99f17",
"providers_sha256": "7247eece8fad09c8023af8d5c682d223d7ed7ebd68eac79e9154962a7e03b66b"
}
}

View File

@ -14,7 +14,7 @@ packaged Java bridge, doctor/setup projection, bounded client lifecycle, and
multi-target real Linux verifier, curated Windows CI, and controlled real
Windows verifier are also implemented. Program identity,
procedure/string/symbol inventory, memory blocks, address/name and
containing-procedure resolution, bounded search, function metadata,
containing-procedure resolution, complete inventory search, function metadata,
decompilation, assembly, resolved calls, typed references, xrefs, CFG, and
function dossiers are admitted. A provider is not a drop-in replacement for
Hopper: every capability is mapped explicitly, with truthful `unavailable`,
@ -41,7 +41,7 @@ follow.
xref, CFG, and decompilation claims require separately admitted operation
contracts and real-provider conformance.
- Implement read-only inventories, assembly, decompilation, function metadata,
calls, references, containment, and bounded search first.
calls, references, containment, and complete inventory search first.
- Report GUI cursor/navigation and persistent mutation operations as unavailable
until their semantics and project ownership are explicitly designed.
- Verify real claims on Linux with at least two distinct source-owned binaries.
@ -83,17 +83,17 @@ capability from a successful import.
## Admitted inventory semantics
| Concern | Ghidra contract |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Program identity | One `analyzeHeadless` import produces exactly one Program; `list_documents` therefore returns exactly one name. |
| Addresses | Default memory uses lowercase `0x` hexadecimal. Non-default and external spaces use `<percent-encoded-space>:0x<hex>` and remain round-trippable. The handshake commits image base and default address-space name. |
| Symbols | `list_names` includes address-bearing memory and external symbols, including dynamic symbols, while excluding variable and no-address namespace records. Each item reports primary, dynamic, external, symbol type, and source facts. |
| Procedures | Both non-external and external functions are listed. A local thunk remains distinct from its resolved target; exact and qualified name lookup fails on ambiguity rather than guessing. |
| Strings | Only Ghidra-defined string `Data` is observed. Items report charset, byte length, and whether a required null terminator is missing. The API cannot distinguish a present terminator from fixed/Pascal layouts when no terminator is missing, so that state is named `present_or_not_required`. |
| Memory | Memory-block end addresses are exclusive. Read/write/execute, initialization, overlay, address space, and image base are direct Ghidra observations. |
| Inventory | Procedure, symbol, and string listings plus searches return the complete matching collection in one response; callers do not provide offsets or limits. The bridge retains a 64 MiB internal process-line safety bound for exceptional responses. |
| Search | Literal search scans the immutable inventory with a 1,000,000-unit cumulative work budget. Regex uses a conservative finite Java-regex subset with 10,000 static paths, 4,096 UTF-16 code units per candidate, and the same cumulative budget. Exceeding a budget fails explicitly; returned values identify truncation. |
| Analysis state | The socket is exposed only after default auto-analysis. A 300-second analysis timeout fails target opening; ordinary established requests have a 10-second client deadline. The internal response-line guard is sized for complete analysis output. |
| Concern | Ghidra contract |
| ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Program identity | One `analyzeHeadless` import produces exactly one Program; `list_documents` therefore returns exactly one name. |
| Addresses | Default memory uses lowercase `0x` hexadecimal. Non-default and external spaces use `<percent-encoded-space>:0x<hex>` and remain round-trippable. The handshake commits image base and default address-space name. |
| Symbols | `list_names` includes address-bearing memory and external symbols, including dynamic symbols, while excluding variable and no-address namespace records. Each item reports primary, dynamic, external, symbol type, and source facts. |
| Procedures | Both non-external and external functions are listed. A local thunk remains distinct from its resolved target; exact and qualified name lookup fails on ambiguity rather than guessing. |
| Strings | Only Ghidra-defined string `Data` is observed. Items report charset, byte length, and whether a required null terminator is missing. The API cannot distinguish a present terminator from fixed/Pascal layouts when no terminator is missing, so that state is named `present_or_not_required`. |
| Memory | Memory-block end addresses are exclusive. Read/write/execute, initialization, overlay, address space, and image base are direct Ghidra observations. |
| Inventory | Procedure, symbol, and string listings plus searches return the complete matching collection in one response; callers do not provide offsets or limits. The bridge retains a 64 MiB internal process-line safety bound for exceptional responses. |
| Search | Literal and Java-regex searches scan the complete immutable inventory and return all matching entries inline. Search uses the established-request deadline; there are no caller-supplied offsets or result-count limits. |
| Analysis state | The socket is exposed only after default auto-analysis. A 300-second analysis timeout fails target opening; ordinary established requests have a 10-second client deadline. The internal response-line guard is sized for complete analysis output. |
## Admitted function-analysis semantics

View File

@ -1,5 +1,5 @@
{
"manifest_id": "ecm_ee1c9fd867e7991298097f591459ae0585ca772ca0ac95ef58be91074fe1979e",
"manifest_id": "ecm_815669f16f8c04304d15f9a549855ac5dca53516d17c6b946009a080af08f4fb",
"verifier": {
"id": "rea-managed-conformance",
"version": "1"
@ -13,7 +13,7 @@
"skill_digests": [
{
"skill_id": "reverse-engineer-anything",
"sha256": "03f0a14abe8ad6394c7ba5597130f86922443d16a796ea6d9032da83b590da44"
"sha256": "d50973cdadae91e4baab27ad5f8d6a2a6f4ca1751701ff8e370091c7e29b229d"
}
],
"claims": [

View File

@ -89,7 +89,6 @@ try {
inspectWebPageInputSchema.parse({
cdp_endpoint: endpoint,
allowed_origins: [site.origin],
approved: true,
target_id: target,
observation_ms: 1_000,
include_storage_keys: true,
@ -109,18 +108,13 @@ try {
inspectWebPageInputSchema.parse({
cdp_endpoint: endpoint,
allowed_origins: [site.origin],
approved: true,
target_id: target,
observation_ms: 1_000,
include_accessibility_text: true,
include_script_sources: true,
source_capture_approved: true,
include_console_text: true,
console_text_approved: true,
include_json_body_shapes: true,
json_body_schema_approved: true,
include_websocket_shapes: true,
websocket_shape_approved: true,
}),
);
if (!withSource.ok) throw withSource.error;
@ -147,12 +141,9 @@ try {
analyzeWebBundleInputSchema.parse({
cdp_endpoint: endpoint,
allowed_origins: [site.origin],
approved: true,
target_id: target,
observation_ms: 200,
source_capture_approved: true,
fetch_source_maps: true,
source_map_fetch_approved: true,
}),
);
if (!bundle.ok) throw bundle.error;
@ -175,8 +166,6 @@ try {
captureWebScreenshotInputSchema.parse({
cdp_endpoint: endpoint,
allowed_origins: [site.origin],
approved: true,
screenshot_approved: true,
target_id: target,
}),
);
@ -204,7 +193,6 @@ try {
observeWebSessionInputSchema.parse({
cdp_endpoint: endpoint,
allowed_origins: [site.origin],
approved: true,
target_id: target,
observation_ms: 1_500,
}),
@ -324,7 +312,6 @@ async function verifyPageScopedTransport(provider, proxy, origin) {
const input = inspectWebPageInputSchema.parse({
cdp_endpoint: proxy.endpoint,
allowed_origins: [origin],
approved: true,
target_id: target,
observation_ms: 100,
});
@ -354,7 +341,6 @@ async function verifyPageScopedTransport(provider, proxy, origin) {
observeWebSessionInputSchema.parse({
cdp_endpoint: proxy.endpoint,
allowed_origins: [origin],
approved: true,
target_id: target,
observation_ms: 5_000,
}),
@ -405,7 +391,6 @@ async function pageTarget(provider, endpoint, origin) {
listBrowserTargetsInputSchema.parse({
cdp_endpoint: endpoint,
allowed_origins: [origin],
approved: true,
}),
);
if (listed.ok && listed.value.targets[0] !== undefined)

View File

@ -164,7 +164,6 @@ if (plan === null || typeof plan.plan_digest !== "string")
const executed = await runControlledReplay(dependencies, {
...input,
mode: "execute",
approved: true,
plan_digest: plan.plan_digest,
});
if (!executed.ok) throw executed.error;
@ -245,7 +244,6 @@ const runSingle = async (path, limits = undefined) => {
const observed = await runControlledReplay(dependencies, {
...manifest,
mode: "execute",
approved: true,
plan_digest: proposed.value.plan.plan_digest,
});
if (!observed.ok || observed.value.evidence === null)
@ -313,7 +311,6 @@ if (!differentialTimeoutPlan.ok || differentialTimeoutPlan.value.plan === null)
const differentialTimeout = await runControlledReplay(dependencies, {
...differentialTimeoutInput,
mode: "execute",
approved: true,
plan_digest: differentialTimeoutPlan.value.plan.plan_digest,
});
if (

View File

@ -28,7 +28,7 @@ try {
REA_V8_INSPECTOR_ALLOWED_ORIGINS_JSON: "[]",
};
const listed = await runCli(
["list-javascript-runtime-targets", endpoint, "--approved", "--json"],
["list-javascript-runtime-targets", endpoint, "--json"],
environment,
);
const target = listed.normalized_result?.targets?.items?.[0];
@ -46,7 +46,6 @@ try {
target.target_id,
"--runtime-kind",
"node",
"--approved",
"--observation-ms",
"100",
"--json",

View File

@ -4,7 +4,7 @@ description: Reverse engineer native, managed, Electron/JavaScript, packaged, an
metadata:
version: "23"
tool_count: 112
catalog_digest: "6a20b6b76be7cf9ced6e6b71ca435e3ec6eb6f910694f695f2a2627def628036"
catalog_digest: "8e1126057d24fa22f0d4425e2e05b01dfde624292363b1f581a2bcc884a99f17"
---
# REA

View File

@ -6,7 +6,6 @@ import {
EvidenceIntegrityError,
NoBinaryOpenError,
type AnalysisError,
type EvidenceLimitError,
} from "../domain/errors.js";
import { err, ok, type Result } from "../domain/result.js";
import {
@ -150,7 +149,7 @@ export class AnalysisSnapshotCache {
| undefined,
mergeEvidence: (
bundle: EvidenceBundle,
) => Result<number, EvidenceIntegrityError | EvidenceLimitError>,
) => Result<number, EvidenceIntegrityError>,
): Result<number, AnalysisError> {
if (
active !== undefined &&

View File

@ -136,7 +136,7 @@ const browserParameters = (
allowed_origins: input.allowed_origins,
};
if (!("target_id" in input)) return scope;
if ("screenshot_approved" in input)
if ("maximum_image_bytes" in input)
return {
...scope,
target_id: input.target_id,
@ -171,13 +171,8 @@ const browserParameters = (
include_storage_keys: input.include_storage_keys,
include_storage_fingerprints: input.include_storage_fingerprints,
limits: input.limits,
source_capture_approved: input.source_capture_approved,
...(input.include_script_sources && "analysis_limits" in input
? {
fetch_source_maps: input.fetch_source_maps,
source_map_fetch_approved: input.source_map_fetch_approved,
analysis_limits: input.analysis_limits,
}
...("fetch_source_maps" in input
? { fetch_source_maps: input.fetch_source_maps }
: {}),
};
};

View File

@ -33,7 +33,6 @@ export const createElectronActiveEvidence = (
const scenarioProjection = (
input: ElectronActiveObservationInput,
): EvidenceObservation["parameters"] => ({
approved: input.approved,
executable_path: input.executable_path,
application_path: input.application_path,
application_root: input.application_root,

View File

@ -246,10 +246,7 @@ const addValidationObservations = (state: IpcGraphState): void => {
for (const analyzed of state.context.analysis.files) {
const { file, javascript } = analyzed;
if (javascript === null) continue;
const coverage = javascriptAnalysisCoverage(
javascript,
state.context.input,
);
const coverage = javascriptAnalysisCoverage(javascript);
for (const validation of javascript.electron.sender_validations) {
const handlerRecord = containingHandler(state.records, file, validation);
const target =
@ -341,5 +338,5 @@ const coverageFor = (
)?.javascript;
return javascript === null || javascript === undefined
? undefined
: javascriptAnalysisCoverage(javascript, context.input);
: javascriptAnalysisCoverage(javascript);
};

View File

@ -28,7 +28,7 @@ export const addElectronNativeBoundaries = (
for (const analyzed of context.analysis.files) {
const { file, javascript } = analyzed;
if (javascript === null) continue;
const coverage = javascriptAnalysisCoverage(javascript, context.input);
const coverage = javascriptAnalysisCoverage(javascript);
for (const value of javascript.electron.native_addon_bindings)
addNativeBinding({ context, file, value, coverage });
}

View File

@ -37,7 +37,7 @@ export const addElectronWindowBoundaries = (
for (const analyzed of context.analysis.files) {
const { file, javascript } = analyzed;
if (javascript === null) continue;
const coverage = javascriptAnalysisCoverage(javascript, context.input);
const coverage = javascriptAnalysisCoverage(javascript);
for (const value of javascript.electron.browser_windows) {
const source = electronFindingSourceNode(context, file, value.module_key);
if (source !== undefined)

View File

@ -53,7 +53,6 @@ const parameters = (
target_id: input.target_id,
observation_ms: input.observation_ms,
include_script_sources: input.include_script_sources,
source_capture_approved: input.source_capture_approved,
limits: input.limits,
}
: {}),

View File

@ -40,5 +40,4 @@ const parameters = (
input: AnalyzeJavaScriptApplicationInput,
): EvidenceObservation["parameters"] => ({
format: input.format,
limits: input.limits,
});

View File

@ -53,7 +53,6 @@ export const analyzeJavaScriptApplicationValidated = async (
{
input_path: input.input_path,
format: input.format,
limits: input.limits,
},
options.signal,
);

View File

@ -119,7 +119,6 @@ export const compareApplicationVersionsEvidenceValidated = (
rootArtifactSha256: right.rootArtifactSha256,
graph: right.graph,
},
limits: input.limits,
leftNativeEvidence: leftNative,
rightNativeEvidence: rightNative,
});
@ -132,7 +131,6 @@ export const compareApplicationVersionsEvidenceValidated = (
right_native_evidence_ids: rightNative.map(
({ evidence_id: id }) => id,
),
limits: input.limits,
},
result,
),

View File

@ -26,7 +26,7 @@ import type {
JavaScriptSourceMapObservation,
JavaScriptSourceMapOriginal,
} from "./JavaScriptArtifactAnalysisTypes.js";
import type { JavaScriptArtifactReconstructionInput } from "./JavaScriptArtifactReconstructionInput.js";
import { JAVASCRIPT_APPLICATION_RESOURCE_LIMITS } from "./JavaScriptArtifactReconstructionInput.js";
import { analyzeJavaScriptJsonModule } from "./JavaScriptJsonModules.js";
interface MutableArtifactAnalysis {
@ -40,60 +40,45 @@ interface MutableArtifactAnalysis {
modules: number;
parseFailures: number;
truncatedScopes: number;
sourceMapSources: number;
}
interface ArtifactAnalysisContext {
readonly state: MutableArtifactAnalysis;
readonly input: JavaScriptArtifactReconstructionInput;
readonly deadline: number;
readonly now: () => number;
}
/** Analyze selected text files under one shared AST, finding, module, and time budget. */
/** Analyze every selected source under shared AST and parser safety bounds. */
export const analyzeJavaScriptArtifactFiles = (
fileSet: JavaScriptArtifactFileSet,
input: JavaScriptArtifactReconstructionInput,
now: () => number,
): JavaScriptArtifactAnalysis => {
const state = emptyArtifactAnalysis();
const context = {
state,
input,
deadline: now() + input.limits.max_parse_milliseconds,
deadline:
now() + JAVASCRIPT_APPLICATION_RESOURCE_LIMITS.maxParseMilliseconds,
now,
};
for (const file of fileSet.files) analyzeArtifactFile(file, context);
return finalizeArtifactAnalysis(state, input);
return finalizeArtifactAnalysis(state);
};
const finalizeArtifactAnalysis = (
state: MutableArtifactAnalysis,
input: JavaScriptArtifactReconstructionInput,
): JavaScriptArtifactAnalysis => {
const omittedHtmlFindings = Math.max(
0,
state.htmlScripts.length + state.findings - input.limits.max_findings,
);
const findings = Math.min(
input.limits.max_findings,
state.findings + state.htmlScripts.length,
);
const sourceMapTruncations = state.sourceMaps.filter(
({ status }) => status === "truncated",
).length;
const truncatedScopes =
state.truncatedScopes +
sourceMapTruncations +
(omittedHtmlFindings > 0 ? 1 : 0);
const truncatedScopes = state.truncatedScopes + sourceMapTruncations;
return {
files: state.files,
packages: state.packages,
json_modules: state.jsonModules,
html_scripts: state.htmlScripts.slice(0, input.limits.max_findings),
html_scripts: state.htmlScripts,
source_maps: state.sourceMaps,
visited_ast_nodes: state.visitedNodes,
findings,
findings: state.findings + state.htmlScripts.length,
modules: state.modules,
parse_failures: state.parseFailures,
truncated_scopes: truncatedScopes,
@ -111,30 +96,18 @@ const analyzeArtifactFile = (
file: JavaScriptArtifactFile,
context: ArtifactAnalysisContext,
): void => {
const { state, input } = context;
const { state } = context;
addStructuredObservations(file, state);
if (file.kind === "html" && file.text.included)
state.htmlScripts.push(
...parseHtmlScripts(
file.path,
file.text.value,
Math.max(0, input.limits.max_findings - state.findings),
),
);
state.htmlScripts.push(...parseHtmlScripts(file.path, file.text.value));
if (file.kind === "source-map") addSourceMap(file, context);
if (file.kind !== "javascript" || !file.text.included) {
state.files.push({ file, javascript: null, semantic: null });
return;
}
const remainingNodes = input.limits.max_ast_nodes - state.visitedNodes;
const remainingFindings = input.limits.max_findings - state.findings;
const remainingModules = input.limits.max_modules - state.modules;
if (
remainingNodes <= 0 ||
remainingFindings <= 0 ||
remainingModules <= 0 ||
context.now() > context.deadline
) {
const remainingNodes =
JAVASCRIPT_APPLICATION_RESOURCE_LIMITS.maxAstNodes - state.visitedNodes;
if (remainingNodes <= 0 || context.now() > context.deadline) {
state.files.push({ file, javascript: null, semantic: null });
state.truncatedScopes += 1;
return;
@ -151,24 +124,20 @@ const analyzeArtifactFile = (
parsed,
{
maxAstNodes: remainingNodes,
maxFindings: remainingFindings,
maxModules: remainingModules,
maxFindings: Number.MAX_SAFE_INTEGER,
maxModules: Number.MAX_SAFE_INTEGER,
deadline: context.deadline,
now: context.now,
},
);
const staticFindings = findingCount(analysis);
const semanticFindingBudget = Math.max(
0,
input.limits.max_findings - state.findings - staticFindings,
);
const semanticLimits: JavaScriptSemanticLimits = {
...DEFAULT_JAVASCRIPT_SEMANTIC_LIMITS,
maxScopes: remainingNodes,
maxBindings: remainingNodes,
maxCallables: remainingNodes,
maxReferences: remainingNodes,
maxModuleLinks: semanticFindingBudget,
maxModuleLinks: remainingNodes,
};
const semantics =
analysis.parse_status === "complete" || analysis.parse_status === "partial"
@ -198,14 +167,8 @@ const addSourceMap = (
const sourceMap = parseSourceMap(file, {
deadline: context.deadline,
now: context.now,
maximumSources: Math.max(
0,
context.input.limits.max_source_map_sources -
context.state.sourceMapSources,
),
});
context.state.sourceMaps.push(sourceMap);
context.state.sourceMapSources += sourceMap.sources.length;
};
const emptyArtifactAnalysis = (): MutableArtifactAnalysis => ({
@ -219,7 +182,6 @@ const emptyArtifactAnalysis = (): MutableArtifactAnalysis => ({
modules: 0,
parseFailures: 0,
truncatedScopes: 0,
sourceMapSources: 0,
});
const addStructuredObservations = (
@ -286,14 +248,13 @@ const unavailablePackage = (
const parseHtmlScripts = (
path: string,
text: string,
maximum: number,
): JavaScriptHtmlScriptObservation[] => {
const scripts: JavaScriptHtmlScriptObservation[] = [];
const baseHref = htmlBaseHref(text);
const pattern = /<script\b[^>]*\bsrc\s*=\s*(["'])([^"']+)\1[^>]*>/giu;
for (const match of text.matchAll(pattern)) {
const script = match[2]?.slice(0, 4_096);
if (script === undefined || scripts.length >= maximum) break;
if (script === undefined) continue;
const start = match.index;
scripts.push({
html_path: path,
@ -315,7 +276,6 @@ const parseSourceMap = (
context: {
readonly deadline: number;
readonly now: () => number;
readonly maximumSources: number;
},
): JavaScriptSourceMapObservation => {
if (!file.text.included) return unavailableSourceMap(file);
@ -349,11 +309,9 @@ const collectSourceMapOriginals = (
context: {
readonly deadline: number;
readonly now: () => number;
readonly maximumSources: number;
},
): JavaScriptSourceMapObservation => {
const sources: JavaScriptSourceMapOriginal[] = [];
let total = 0;
for (const map of maps) {
const names = map.sources;
if (!Array.isArray(names))
@ -363,7 +321,7 @@ const collectSourceMapOriginals = (
: [];
const root = typeof map.sourceRoot === "string" ? map.sourceRoot : "";
for (const [index, raw] of names.entries()) {
if (total % 1_024 === 0 && context.now() > context.deadline)
if (sources.length % 1_024 === 0 && context.now() > context.deadline)
return {
path: file.path,
sha256: file.sha256,
@ -377,8 +335,6 @@ const collectSourceMapOriginals = (
file,
"Source map contains a non-string source name.",
);
total += 1;
if (sources.length >= context.maximumSources) continue;
const content =
typeof contents[index] === "string" ? contents[index] : null;
sources.push({
@ -388,25 +344,14 @@ const collectSourceMapOriginals = (
});
}
}
const omitted = total - sources.length;
return omitted === 0
? {
path: file.path,
sha256: file.sha256,
status: "included",
sources,
omitted_sources: 0,
limitation: null,
}
: {
path: file.path,
sha256: file.sha256,
status: "truncated",
sources,
omitted_sources: omitted,
limitation:
"Original-source inventory reached the approved source-map limit.",
};
return {
path: file.path,
sha256: file.sha256,
status: "included",
sources,
omitted_sources: 0,
limitation: null,
};
};
const unavailableSourceMap = (

View File

@ -14,7 +14,7 @@ import {
} from "../artifacts/ArtifactReader.js";
import { streamChunkToBuffer } from "../artifacts/StreamBytes.js";
import type { ArtifactInventorySnapshot } from "./ArtifactInventory.js";
import type { JavaScriptArtifactReconstructionInput } from "./JavaScriptArtifactReconstructionInput.js";
import { JAVASCRIPT_APPLICATION_RESOURCE_LIMITS } from "./JavaScriptArtifactReconstructionInput.js";
/** Relevant file categories projected from the complete artifact inventory. */
export type JavaScriptArtifactFileKind =
@ -79,7 +79,6 @@ interface Selection {
}
interface ReadContext {
readonly input: JavaScriptArtifactReconstructionInput;
readonly expected: ReadonlyMap<string, ExpectedFile>;
readonly expectedContainers: ReadonlyMap<string, JavaScriptArtifactContainer>;
readonly selections: ReadonlyMap<string, Selection>;
@ -102,14 +101,12 @@ interface ReadTextInput {
export const readJavaScriptArtifactFiles = async (
reader: ArtifactReader,
snapshot: ArtifactInventorySnapshot,
input: JavaScriptArtifactReconstructionInput,
signal?: AbortSignal,
): Promise<JavaScriptArtifactFileSet> => {
const inventory = expectedInventory(snapshot);
const expected = inventory.files;
const selections = selectTextFiles(expected, input);
const selections = selectTextFiles(expected);
const context: ReadContext = {
input,
expected,
expectedContainers: inventory.containers,
selections,
@ -156,8 +153,9 @@ const visitReader = async (
const path = normalizeArtifactPath(
prefix === "" ? entry.path : `${prefix}/${entry.path}`,
{
maxDepth: context.input.limits.max_depth,
maxPathBytes: context.input.limits.max_path_bytes,
maxDepth: JAVASCRIPT_APPLICATION_RESOURCE_LIMITS.artifact.maxDepth,
maxPathBytes:
JAVASCRIPT_APPLICATION_RESOURCE_LIMITS.artifact.maxPathBytes,
},
);
const nestedAsar = isFilesystemAsar(entry, path);
@ -229,7 +227,7 @@ const readTextIfSelected = async (
};
const bytes = await readBounded(
await input.reader.open(input.entry, context.signal),
context.input.limits.max_text_file_bytes,
JAVASCRIPT_APPLICATION_RESOURCE_LIMITS.maxTextFileBytes,
context.signal,
);
const digest = createHash("sha256").update(bytes).digest("hex");
@ -290,7 +288,6 @@ const expectedInventory = (
const selectTextFiles = (
files: ReadonlyMap<string, ExpectedFile>,
input: JavaScriptArtifactReconstructionInput,
): ReadonlyMap<string, Selection> => {
const selected = new Map<string, Selection>();
let fileCount = 0;
@ -302,13 +299,16 @@ const selectTextFiles = (
for (const file of ordered) {
if (file.kind === "native-addon") continue;
if (
fileCount >= input.limits.max_text_files ||
file.bytes > input.limits.max_text_file_bytes
fileCount >= JAVASCRIPT_APPLICATION_RESOURCE_LIMITS.maxTextFiles ||
file.bytes > JAVASCRIPT_APPLICATION_RESOURCE_LIMITS.maxTextFileBytes
) {
selected.set(file.path, { selected: false, reason: "file-limit" });
continue;
}
if (bytes + file.bytes > input.limits.max_total_text_bytes) {
if (
bytes + file.bytes >
JAVASCRIPT_APPLICATION_RESOURCE_LIMITS.maxTotalTextBytes
) {
selected.set(file.path, { selected: false, reason: "byte-limit" });
continue;
}

View File

@ -9,7 +9,6 @@ import {
export class JavaScriptArtifactGraphAccumulator {
readonly #nodes = new Map<string, ApplicationNode>();
readonly #edges = new Map<string, ApplicationEdge>();
#omittedObservations = 0;
/** Create or merge one canonical node. */
addNode(input: unknown): ApplicationNode {
@ -27,9 +26,7 @@ export class JavaScriptArtifactGraphAccumulator {
);
const observations = [...bySemanticId.entries()]
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
.slice(0, 64)
.map(([, observation]) => observation);
this.#omittedObservations += Math.max(0, bySemanticId.size - 64);
const merged = createJavaScriptApplicationNode({
kind: existing.kind,
identity: existing.identity,
@ -55,11 +52,6 @@ export class JavaScriptArtifactGraphAccumulator {
edges(): readonly ApplicationEdge[] {
return [...this.#edges.values()];
}
/** Count distinct node observations omitted by the graph contract bound. */
omittedObservations(): number {
return this.#omittedObservations;
}
}
const observationInput = (

View File

@ -31,7 +31,7 @@ import {
addJavaScriptPackageNodes,
createJavaScriptArtifactRootNode,
} from "./JavaScriptArtifactGraphStructure.js";
import type { JavaScriptArtifactReconstructionInput } from "./JavaScriptArtifactReconstructionInput.js";
import { JAVASCRIPT_APPLICATION_RESOURCE_LIMITS } from "./JavaScriptArtifactReconstructionInput.js";
import { addElectronBoundaries } from "./ElectronBoundaryGraph.js";
import {
classifyElectronIpcPairings,
@ -43,7 +43,6 @@ export const buildJavaScriptArtifactGraph = (
snapshot: ArtifactInventorySnapshot,
fileSet: JavaScriptArtifactFileSet,
analysis: JavaScriptArtifactAnalysis,
input: JavaScriptArtifactReconstructionInput,
): JavaScriptApplicationGraph => {
const accumulator = new JavaScriptArtifactGraphAccumulator();
const root = createJavaScriptArtifactRootNode(accumulator, snapshot);
@ -52,7 +51,6 @@ export const buildJavaScriptArtifactGraph = (
snapshot,
fileSet,
analysis,
input,
root,
filesByPath: new Map(fileSet.files.map((file) => [file.path, file])),
fileNodes: new Map(),
@ -87,9 +85,7 @@ export const buildJavaScriptArtifactGraph = (
};
const graphCoverage = (context: JavaScriptArtifactGraphContext) => {
const exactLimitOmissions =
context.fileSet.limit_omitted_text_files +
context.accumulator.omittedObservations();
const exactLimitOmissions = context.fileSet.limit_omitted_text_files;
const sourceMapPolicyGap = context.analysis.source_maps.some(
({ status }) => status === "invalid",
);
@ -109,20 +105,17 @@ const graphCoverage = (context: JavaScriptArtifactGraphContext) => {
partialJavaScript;
if (context.analysis.truncated_scopes > 0)
return truncatedApplicationCoverage(
reconstructionLimits(context.input),
reconstructionLimits(),
truncationOmittedCount(context),
);
if (exactLimitOmissions > 0)
return truncatedApplicationCoverage(
reconstructionLimits(context.input),
reconstructionLimits(),
exactLimitOmissions,
);
if (unknownGap)
return partialApplicationCoverage(
reconstructionLimits(context.input),
null,
);
return completeApplicationCoverage(reconstructionLimits(context.input));
return partialApplicationCoverage(reconstructionLimits(), null);
return completeApplicationCoverage(reconstructionLimits());
};
const truncationOmittedCount = (
@ -202,11 +195,6 @@ const graphLimitations = (
"Native member names are requested by JavaScript syntax and are not verified binary exports in this workflow.",
]
: []),
...(context.accumulator.omittedObservations() === 0
? []
: [
"Repeated content identities exceeded the per-node observation bound; containment edges still preserve inventoried paths.",
]),
...(coverage === "complete"
? []
: [
@ -215,27 +203,26 @@ const graphLimitations = (
];
};
const reconstructionLimits = (input: JavaScriptArtifactReconstructionInput) => [
const reconstructionLimits = () => [
{
name: "max-entries",
value: input.limits.max_entries,
value: JAVASCRIPT_APPLICATION_RESOURCE_LIMITS.artifact.maxEntries,
unit: "items" as const,
},
{
name: "max-total-artifact-bytes",
value: input.limits.max_total_artifact_bytes,
value: JAVASCRIPT_APPLICATION_RESOURCE_LIMITS.artifact.maxTotalBytes,
unit: "bytes" as const,
},
{
name: "max-text-files",
value: input.limits.max_text_files,
value: JAVASCRIPT_APPLICATION_RESOURCE_LIMITS.maxTextFiles,
unit: "items" as const,
},
{
name: "max-total-text-bytes",
value: input.limits.max_total_text_bytes,
value: JAVASCRIPT_APPLICATION_RESOURCE_LIMITS.maxTotalTextBytes,
unit: "bytes" as const,
},
...javaScriptAnalysisLimits(input),
{ name: "max-observations-per-node", value: 64, unit: "items" as const },
...javaScriptAnalysisLimits(),
];

View File

@ -23,7 +23,7 @@ export const addJavaScriptBundlerNodes = (
const { file, javascript } = analyzed;
const asset = context.assetNodes.get(file.path);
if (javascript === null || asset === undefined) continue;
const coverage = javascriptAnalysisCoverage(javascript, context.input);
const coverage = javascriptAnalysisCoverage(javascript);
const records: BundlerGraphRecord[] = [];
for (const registration of javascript.bundler_registrations) {
const input = {

View File

@ -20,7 +20,6 @@ import {
staticInferenceEvidence,
unavailableAstEvidence,
} from "./JavaScriptArtifactGraphEvidence.js";
import type { JavaScriptArtifactReconstructionInput } from "./JavaScriptArtifactReconstructionInput.js";
import {
resolveArtifactPathByContext,
type ArtifactPathResolution,
@ -32,7 +31,6 @@ export interface JavaScriptArtifactGraphContext {
readonly snapshot: ArtifactInventorySnapshot;
readonly fileSet: JavaScriptArtifactFileSet;
readonly analysis: JavaScriptArtifactAnalysis;
readonly input: JavaScriptArtifactReconstructionInput;
readonly root: ApplicationNode;
readonly filesByPath: ReadonlyMap<string, JavaScriptArtifactFile>;
readonly fileNodes: Map<string, ApplicationNode>;
@ -214,13 +212,12 @@ export const javascriptAnalysisCoverage = (
analysis: NonNullable<
JavaScriptArtifactAnalysis["files"][number]["javascript"]
>,
input: JavaScriptArtifactReconstructionInput,
): JavaScriptArtifactGraphCoverage =>
analysis.parse_status === "complete"
? completeApplicationCoverage(javaScriptAnalysisLimits(input))
? completeApplicationCoverage(javaScriptAnalysisLimits())
: analysis.parse_status === "truncated"
? truncatedApplicationCoverage(javaScriptAnalysisLimits(input), null)
: partialApplicationCoverage(javaScriptAnalysisLimits(input), null);
? truncatedApplicationCoverage(javaScriptAnalysisLimits(), null)
: partialApplicationCoverage(javaScriptAnalysisLimits(), null);
/** Stable identity for a fact scoped to one exact artifact. */
export const artifactLocalIdentity = (
@ -348,28 +345,16 @@ export const sourceNodeFor = (
? context.sourceModuleNodes.get(path)
: context.moduleNodes.get(moduleLookupKey(path, moduleKey));
/** Named AST budgets committed to graph evidence coverage. */
export const javaScriptAnalysisLimits = (
input: JavaScriptArtifactReconstructionInput,
) => [
/** Actual parser safety bounds committed to graph evidence coverage. */
export const javaScriptAnalysisLimits = () => [
{
name: "max-ast-nodes",
value: input.limits.max_ast_nodes,
unit: "items" as const,
},
{
name: "max-findings",
value: input.limits.max_findings,
unit: "items" as const,
},
{
name: "max-modules",
value: input.limits.max_modules,
value: 2_000_000,
unit: "items" as const,
},
{
name: "max-parse-milliseconds",
value: input.limits.max_parse_milliseconds,
value: 30_000,
unit: "milliseconds" as const,
},
];

View File

@ -10,6 +10,7 @@ import {
completeApplicationCoverage,
truncatedApplicationCoverage,
} from "../domain/javascriptApplicationEvidenceSchemas.js";
import { javaScriptAnalysisLimits } from "./JavaScriptArtifactGraphContext.js";
import { resolveArtifactPathByContext } from "./JavaScriptArtifactPathResolution.js";
/** Project HTML renderer entrypoints and their local script assets. */
@ -94,13 +95,7 @@ export const addJavaScriptSourceMapOriginals = (
const coverage =
sourceMap.status === "truncated"
? truncatedApplicationCoverage(
[
{
name: "max-source-map-sources",
value: context.input.limits.max_source_map_sources,
unit: "items",
},
],
javaScriptAnalysisLimits(),
sourceMap.omitted_sources,
)
: completeApplicationCoverage();

View File

@ -47,7 +47,7 @@ export const addJavaScriptStaticFindings = (
const { file, javascript } = analyzed;
const asset = context.assetNodes.get(file.path);
if (javascript === null || asset === undefined) continue;
const coverage = javascriptAnalysisCoverage(javascript, context.input);
const coverage = javascriptAnalysisCoverage(javascript);
for (const value of javascript.references)
addReference(context, { file, asset, value, coverage });
for (const value of javascript.endpoints)

View File

@ -61,7 +61,7 @@ export const reconstructJavaScriptArtifact = async (
const format = await resolveFormat(path, input);
const snapshot = await scanCanonicalArtifactInventory(
path,
artifactLimitsForReconstruction(input),
artifactLimitsForReconstruction(),
{ signal },
);
if (snapshot.manifest.root_format !== format)
@ -71,22 +71,12 @@ export const reconstructJavaScriptArtifact = async (
);
const reader = createReader(path, format);
try {
const files = await readJavaScriptArtifactFiles(
reader,
snapshot,
input,
signal,
);
const analysis = analyzeJavaScriptArtifactFiles(files, input, () =>
const files = await readJavaScriptArtifactFiles(reader, snapshot, signal);
const analysis = analyzeJavaScriptArtifactFiles(files, () =>
performance.now(),
);
abortIfNeeded(signal);
const graph = buildJavaScriptArtifactGraph(
snapshot,
files,
analysis,
input,
);
const graph = buildJavaScriptArtifactGraph(snapshot, files, analysis);
const semanticGraph = buildJavaScriptSemanticGraph({
rootArtifactSha256: snapshot.manifest.root_sha256,
applicationGraph: graph,

View File

@ -1,33 +1,35 @@
import { z } from "zod";
import type { ArtifactLimits } from "../artifacts/ArtifactReader.js";
import {
JAVASCRIPT_APPLICATION_ANALYSIS_DEFAULT_LIMITS,
javascriptApplicationAnalysisLimitsSchema,
} from "../domain/javascriptApplicationAnalysis.js";
/** Resource safeguards for local artifact traversal and static parsing. */
export const JAVASCRIPT_APPLICATION_RESOURCE_LIMITS = {
artifact: {
maxEntries: 8_000,
maxTotalBytes: 512 * 1_024 * 1_024,
maxEntryBytes: 128 * 1_024 * 1_024,
maxCompressionRatio: 1_000,
maxDepth: 64,
maxPathBytes: 4_096,
} satisfies ArtifactLimits,
maxTextFiles: 5_000,
maxTotalTextBytes: 128 * 1_024 * 1_024,
maxTextFileBytes: 8 * 1_024 * 1_024,
maxAstNodes: 2_000_000,
maxParseMilliseconds: 30_000,
} as const;
/** Local ASAR/directory reconstruction request. */
export const javascriptArtifactReconstructionInputSchema = z.strictObject({
input_path: z.string().min(1),
format: z.enum(["auto", "asar", "directory"]).default("auto"),
limits: javascriptApplicationAnalysisLimitsSchema.default(
JAVASCRIPT_APPLICATION_ANALYSIS_DEFAULT_LIMITS,
),
});
/** Parsed bounded reconstruction request. */
/** Parsed local reconstruction request. */
export type JavaScriptArtifactReconstructionInput = z.infer<
typeof javascriptArtifactReconstructionInputSchema
>;
/** Project reconstruction limits into the shared artifact traversal policy. */
export const artifactLimitsForReconstruction = (
input: JavaScriptArtifactReconstructionInput,
): ArtifactLimits => ({
maxEntries: input.limits.max_entries,
maxTotalBytes: input.limits.max_total_artifact_bytes,
maxEntryBytes: input.limits.max_artifact_entry_bytes,
maxCompressionRatio: input.limits.max_compression_ratio,
maxDepth: input.limits.max_depth,
maxPathBytes: input.limits.max_path_bytes,
});
/** Project private resource safeguards into the artifact traversal policy. */
export const artifactLimitsForReconstruction = (): ArtifactLimits =>
JAVASCRIPT_APPLICATION_RESOURCE_LIMITS.artifact;

View File

@ -31,13 +31,13 @@ export const analyzeJavaScriptJsonModule = (
typeof value === "object" && value !== null && !Array.isArray(value)
? Object.keys(value).sort(compareCodePoints)
: [];
const retained = keys.slice(0, 128);
const retained = keys;
return {
path: file.path,
sha256: file.sha256,
status: "included",
top_level_keys: retained,
omitted_top_level_keys: keys.length - retained.length,
omitted_top_level_keys: 0,
limitation: null,
};
};

View File

@ -76,7 +76,7 @@ export const addJavaScriptSourceModules = (
exportedName === null ? [] : [exportedName],
)
.sort(compareCodePoints);
const retainedExports = [...new Set(exports)].slice(0, 128);
const retainedExports = [...new Set(exports)];
const source = context.accumulator.addNode({
kind: "javascript-module",
identity: artifactLocalIdentity(
@ -98,10 +98,7 @@ export const addJavaScriptSourceModules = (
).length,
export_relationships: exports.length,
export_names: retainedExports,
omitted_export_names: Math.max(
0,
exports.length - retainedExports.length,
),
omitted_export_names: 0,
},
evidence: astObservationEvidence({
sha256: file.sha256,

View File

@ -160,7 +160,7 @@ export const prepareReplayPlan = async (
"Perform no host filesystem writes and expose no host or external network",
...(input.reproducer_export === undefined
? []
: ["Write one separately approved reproducer after sandbox cleanup"]),
: ["Write one reproducer after sandbox cleanup"]),
],
reproducer_export:
input.reproducer_export === undefined

View File

@ -49,10 +49,6 @@ import {
addSemanticGraphRelation as addRelation,
constructSemanticGraphNode as semanticNode,
createSemanticGraphProjectionState as emptyState,
MAX_APPLICATION_NODE_IDS_PER_SEMANTIC_NODE,
MAX_SEMANTIC_GRAPH_NODES,
MAX_SEMANTIC_GRAPH_RELATIONS,
MAX_SEMANTIC_GRAPH_UNKNOWNS,
type SemanticGraphProjectionState as BuilderState,
} from "./JavaScriptSemanticGraphConstruction.js";
@ -97,7 +93,7 @@ export const buildJavaScriptSemanticGraph = ({
const semanticTruncated = analysis.files.some(
({ semantic }) => semantic?.ir.coverage.status === "truncated",
);
const truncated = state.limitsReached.size > 0 || semanticTruncated;
const truncated = semanticTruncated;
const unknowns = [...state.unknowns.values()];
return createJavaScriptSemanticGraph({
schema: "JavaScriptSemanticRelationGraph",
@ -113,29 +109,7 @@ export const buildJavaScriptSemanticGraph = ({
truncated,
omitted_nodes: truncated ? null : 0,
omitted_relations: truncated ? null : 0,
limits: [
{
name: "max_nodes",
value: MAX_SEMANTIC_GRAPH_NODES,
unit: "items",
},
{
name: "max_relations",
value: MAX_SEMANTIC_GRAPH_RELATIONS,
unit: "items",
},
{
name: "max_unknowns",
value: MAX_SEMANTIC_GRAPH_UNKNOWNS,
unit: "items",
},
{
name: "max_application_node_ids_per_semantic_node",
value: MAX_APPLICATION_NODE_IDS_PER_SEMANTIC_NODE,
unit: "items",
},
...semanticRecoveryLimits(analysis),
],
limits: [...semanticRecoveryLimits(analysis)],
families: JAVASCRIPT_SEMANTIC_RELATION_FAMILIES.map((family) => ({
family,
status: semanticFamilyStatus(family, analysis, truncated),
@ -161,11 +135,6 @@ export const buildJavaScriptSemanticGraph = ({
"Request extraction covers fetch, WebSocket, node:http/node:https construction, direct option fields, and exact local response consumers.",
"Boundary extraction covers unshadowed JSON/global coercions plus parse and validation method candidates.",
"Resource extraction covers built-in filesystem/network acquisition and exact local close/destroy/end handles.",
...(truncated
? [
"Semantic graph projection reached one or more explicit hard limits.",
]
: []),
],
});
};

View File

@ -18,28 +18,12 @@ import {
unavailableSemanticRootEvidence,
} from "./JavaScriptSemanticGraphEvidence.js";
/** Hard semantic graph node bound enforced during artifact projection. */
export const MAX_SEMANTIC_GRAPH_NODES = 100_000;
/** Hard semantic graph relation bound enforced during artifact projection. */
export const MAX_SEMANTIC_GRAPH_RELATIONS = 200_000;
/** Hard unresolved-frontier bound enforced across all analyzed files. */
export const MAX_SEMANTIC_GRAPH_UNKNOWNS = 100_000;
/** Maximum structural nodes linked to one exact semantic location. */
export const MAX_APPLICATION_NODE_IDS_PER_SEMANTIC_NODE = 64;
type SemanticGraphProjectionLimit =
| "max_application_node_ids_per_semantic_node"
| "max_nodes"
| "max_relations"
| "max_unknowns";
/** Mutable local projection state hidden from graph callers. */
export interface SemanticGraphProjectionState {
readonly nodes: Map<string, JavaScriptSemanticGraphNode>;
readonly relations: Map<string, JavaScriptSemanticGraphRelation>;
readonly unknowns: Map<string, JavaScriptSemanticGraphUnknown>;
readonly roots: Set<string>;
readonly limitsReached: Set<SemanticGraphProjectionLimit>;
readonly applicationNodeIdsByLocation: ReadonlyMap<string, readonly string[]>;
}
@ -73,9 +57,6 @@ export const createSemanticGraphProjectionState = (
relations: new Map(),
unknowns: new Map(),
roots: new Set(),
limitsReached: new Set(
index.truncated ? ["max_application_node_ids_per_semantic_node"] : [],
),
applicationNodeIdsByLocation: index.identifiers,
};
};
@ -101,22 +82,18 @@ export const constructSemanticGraphNode = (
evidence: observedSemanticEvidence(file, input.location),
});
/** Retain one canonical node unless the hard graph bound was reached. */
/** Retain one canonical semantic node. */
export const addSemanticGraphNode = (
state: SemanticGraphProjectionState,
node: JavaScriptSemanticGraphNode,
): JavaScriptSemanticGraphNode | null => {
const existing = state.nodes.get(node.node_id);
if (existing !== undefined) return existing;
if (state.nodes.size >= MAX_SEMANTIC_GRAPH_NODES) {
state.limitsReached.add("max_nodes");
return null;
}
state.nodes.set(node.node_id, node);
return node;
};
/** Retain one non-self semantic relationship unless its hard bound was reached. */
/** Retain one non-self semantic relationship. */
export const addSemanticGraphRelation = (
state: SemanticGraphProjectionState,
input: SemanticRelationConstructionInput,
@ -138,23 +115,15 @@ export const addSemanticGraphRelation = (
evidence: input.evidence ?? inferredSemanticEvidence(input.source),
});
if (state.relations.has(value.relation_id)) return;
if (state.relations.size >= MAX_SEMANTIC_GRAPH_RELATIONS) {
state.limitsReached.add("max_relations");
return;
}
state.relations.set(value.relation_id, value);
};
/** Retain one unresolved frontier unless the global graph bound was reached. */
/** Retain one unresolved frontier. */
export const addSemanticGraphUnknown = (
state: SemanticGraphProjectionState,
unknown: JavaScriptSemanticGraphUnknown,
): void => {
if (state.unknowns.has(unknown.unknown_id)) return;
if (state.unknowns.size >= MAX_SEMANTIC_GRAPH_UNKNOWNS) {
state.limitsReached.add("max_unknowns");
return;
}
state.unknowns.set(unknown.unknown_id, unknown);
};
@ -187,7 +156,6 @@ const indexApplicationNodes = (
nodes: JavaScriptApplicationGraph["nodes"],
): {
readonly identifiers: ReadonlyMap<string, readonly string[]>;
readonly truncated: boolean;
} => {
const identifiers = new Map<string, Set<string>>();
for (const node of nodes) {
@ -213,16 +181,13 @@ const indexApplicationNodes = (
}
}
}
let truncated = false;
const indexed = new Map(
[...identifiers].map(([key, values]) => {
const sorted = [...values].sort();
if (sorted.length > MAX_APPLICATION_NODE_IDS_PER_SEMANTIC_NODE)
truncated = true;
return [key, sorted.slice(0, MAX_APPLICATION_NODE_IDS_PER_SEMANTIC_NODE)];
return [key, sorted];
}),
);
return { identifiers: indexed, truncated };
return { identifiers: indexed };
};
const matchingApplicationNodeIds = (

View File

@ -107,7 +107,6 @@ export const executeNodeCharacterization = async (
{
...parsed.data.preparation.replay,
mode: "execute",
approved: true,
plan_digest: plannedReplay.plan.plan_digest,
},
options,

View File

@ -20,7 +20,6 @@ import {
import { emptyProcessCapture as emptyCapture } from "../domain/processCapture.fixture.js";
const base = {
approved: true as const,
executable: "/bin/sh",
working_directory: "/tmp",
};

View File

@ -6,7 +6,6 @@ import { parseProcessScenario } from "../domain/processCapture.js";
describe("process capture permission request", () => {
it("projects the complete adapter-neutral authority scope", () => {
const scenario = parseProcessScenario({
approved: true,
executable: "/bin/tool",
arguments: ["--probe"],
working_directory: "/workspace",

View File

@ -18,9 +18,7 @@ import { ok } from "../domain/result.js";
const experiment = () =>
processPairedExperimentSchema.parse({
approved: true,
shared_scenario: {
approved: true,
executable: "/bin/template",
arguments: ["shared"],
working_directory: "/tmp",

View File

@ -140,7 +140,6 @@ const baseScenario = () => ({
max_storage_entries: 256,
max_total_metadata_bytes: 4 * 1_024 * 1_024,
},
approved: true,
});
describe("browserScenarioSchema", () => {

View File

@ -11,7 +11,6 @@ describe("CDP execution-context retention", () => {
cdp_endpoint: "http://127.0.0.1:9222",
allowed_origins: ["https://app.example.test"],
target_id: "page-1",
approved: true,
});
const browser = new CdpCaptureEvents(
request,

View File

@ -40,7 +40,6 @@ describe("CDP document capture", () => {
cdp_endpoint: "http://127.0.0.1:9222",
allowed_origins: [origin],
target_id: "page-1",
approved: true,
});
const snapshot = {
strings: [`${origin}/main`, `${origin}/frame`, "#document", "DIV", ""],

View File

@ -99,7 +99,6 @@ const scenario = (
max_storage_entries: 10,
max_total_metadata_bytes: 16_384,
},
approved: true,
});
type SnapshotKind = BrowserScenario["capture"]["after_each_step"][number];

View File

@ -23,7 +23,6 @@ it("parses bounded window, renderer, and deep-link actions for agents", () => {
url: "rea-fixture://open/item",
},
],
approved: true,
});
expect(input.actions).toEqual([
@ -57,7 +56,6 @@ it("rejects non-absolute deep-link values", () => {
url: "not a URL",
},
],
approved: true,
});
expect(result.success).toBe(false);
@ -70,7 +68,6 @@ it("redacts action inputs from Playwright failures", async () => {
application_path: "/opt/app/main.js",
application_root: "/opt/app",
actions: [{ step_id: "click-step", kind: "click", selector }],
approved: true,
});
const page = {
locator: () => ({
@ -111,7 +108,6 @@ it("runs an untargeted deep-link without requiring a BrowserWindow", async () =>
url: "rea-fixture://open/item",
},
],
approved: true,
});
const application = {
windows: () => [],

View File

@ -68,7 +68,6 @@ describe("PlaywrightScenarioEvents", () => {
max_storage_entries: 10,
max_total_metadata_bytes: 1_000_000,
},
approved: true,
});
const secrets = BrowserScenarioSecrets.resolve(scenario, {});
if (secrets === undefined) throw new Error("Expected resolved secrets");

View File

@ -224,12 +224,6 @@ const normalizeSourceMap = (
);
try {
const map = new AnyMap(text, request.fetchUrl);
if (map.sources.length > input.analysis_limits.max_original_sources)
return emptySourceMapItem(
request,
"truncated",
"Source map exceeds the original-source inventory limit.",
);
const resolvedBySource = new Map<string, string>();
const originalSources = map.sources.map((source, index) => {
const content = map.sourcesContent?.[index];
@ -267,28 +261,18 @@ const normalizeSourceMap = (
name: mapping.name?.slice(0, 1_024) ?? null,
});
});
const modules = originalModuleEdges(
originalSources,
input.analysis_limits.max_findings,
);
const modules = originalModuleEdges(originalSources);
const mappingTruncated = totalMappings > mappings.length;
const limitations = [
...(mappingTruncated
? [
`Mappings were truncated from ${String(totalMappings)} to ${String(mappings.length)}.`,
]
: []),
...(modules.truncated
? [
`Original module edges were truncated to ${String(modules.edges.length)}.`,
]
: []),
];
const limitations = mappingTruncated
? [
`Mappings were truncated from ${String(totalMappings)} to ${String(mappings.length)}.`,
]
: [];
const parsed = {
...sourceMapContext(request),
artifact: createWebTextArtifact(text, "application/source-map+json"),
original_sources: originalSources,
original_module_edges: modules.edges,
original_module_edges: modules,
mappings,
};
return limitations.length === 0
@ -309,11 +293,7 @@ const normalizeSourceMap = (
const originalModuleEdges = (
sources: ParsedSourceMapItem["original_sources"],
maximum: number,
): {
readonly edges: ParsedSourceMapItem["original_module_edges"];
readonly truncated: boolean;
} => {
): ParsedSourceMapItem["original_module_edges"] => {
const edges: ParsedSourceMapItem["original_module_edges"] = [];
const seen = new Set<string>();
for (const source of sources) {
@ -325,7 +305,6 @@ const originalModuleEdges = (
const key = `${source.source}\0${detector.kind}\0${specifier}`;
if (seen.has(key)) continue;
seen.add(key);
if (edges.length >= maximum) return { edges, truncated: true };
edges.push({
from_source: source.source,
kind: detector.kind,
@ -335,7 +314,7 @@ const originalModuleEdges = (
}
}
}
return { edges, truncated: false };
return edges;
};
const validSourceMapEnvelope = (text: string): boolean => {

View File

@ -89,7 +89,6 @@ const registerWebMcp = (
allowed_origins:
options.allowedOrigins ?? context.allowedBrowserOrigins,
target_id: args.targetId,
approved: options.approved,
observation_ms: options.observationMs,
max_tools: options.maxTools,
max_schema_bytes: options.maxSchemaBytes,
@ -173,10 +172,6 @@ const registerScreenshot = (
}),
options: z.object({
...browserScopeOptions,
screenshotApproved: z
.boolean()
.default(false)
.describe("Approve capturing the visible page viewport"),
maximumImageBytes: z
.number()
.int()
@ -194,8 +189,6 @@ const registerScreenshot = (
allowed_origins:
options.allowedOrigins ?? context.allowedBrowserOrigins,
target_id: args.targetId,
approved: options.approved,
screenshot_approved: options.screenshotApproved,
maximum_image_bytes: options.maximumImageBytes,
});
if (!parsed.success) return inputError("capture_web_screenshot");

View File

@ -21,7 +21,6 @@ import {
browserPageInspectionOptions,
browserScopeOptions,
boundedCount,
boundedBytes,
} from "./cliObservationOptions.js";
/** Register CLI equivalents of the passive browser MCP tools. */
@ -53,7 +52,6 @@ const registerTargetList = (
cdp_endpoint: args.endpoint,
allowed_origins:
options.allowedOrigins ?? context.allowedBrowserOrigins,
approved: options.approved,
});
if (!parsed.success)
return browserCliError(
@ -88,18 +86,13 @@ const registerPageInspection = (
cdp_endpoint: args.endpoint,
allowed_origins:
options.allowedOrigins ?? context.allowedBrowserOrigins,
approved: options.approved,
target_id: args.targetId,
observation_ms: options.observationMs,
include_accessibility_text: options.includeAccessibilityText,
include_console_text: options.includeConsoleText,
console_text_approved: options.consoleTextApproved,
include_json_body_shapes: options.includeJsonBodyShapes,
json_body_schema_approved: options.jsonBodySchemaApproved,
include_websocket_shapes: options.includeWebsocketShapes,
websocket_shape_approved: options.websocketShapeApproved,
include_script_sources: options.includeScriptSources,
source_capture_approved: options.sourceCaptureApproved,
include_storage_keys: options.includeStorageKeys,
include_storage_fingerprints: options.includeStorageFingerprints,
limits: {
@ -152,10 +145,6 @@ const registerBundleAnalysis = (
}),
options: z.object({
...browserScopeOptions,
sourceCaptureApproved: z
.boolean()
.default(false)
.describe("Approve capturing bounded script source text"),
observationMs: boundedCount(
"observation duration in milliseconds",
10_000,
@ -166,31 +155,6 @@ const registerBundleAnalysis = (
.boolean()
.default(false)
.describe("Fetch source maps referenced by captured scripts"),
sourceMapFetchApproved: z
.boolean()
.default(false)
.describe(
"Approve fetching referenced source maps from allowed origins",
),
maxFindings: boundedCount("static-analysis findings", 10_000, 1_000),
maxAstNodes: boundedCount("JavaScript AST nodes", 2_000_000, 250_000),
maxSourceMaps: boundedCount("source maps", 1_000, 100),
maxSourceMapBytes: boundedBytes(
"one source map",
16 * 1_024 * 1_024,
4 * 1_024 * 1_024,
),
maxTotalSourceMapBytes: boundedBytes(
"total source maps",
64 * 1_024 * 1_024,
16 * 1_024 * 1_024,
),
maxSourceMapMappings: boundedCount(
"decoded source-map mappings",
100_000,
10_000,
),
maxOriginalSources: boundedCount("original source files", 20_000, 2_000),
}),
run: ({ args, options }) =>
logCliCommand(logger, "analyze-web-bundle", async () => {
@ -200,22 +164,10 @@ const registerBundleAnalysis = (
cdp_endpoint: args.endpoint,
allowed_origins:
options.allowedOrigins ?? context.allowedBrowserOrigins,
approved: options.approved,
target_id: args.targetId,
observation_ms: options.observationMs,
include_script_sources: true,
source_capture_approved: options.sourceCaptureApproved,
fetch_source_maps: options.fetchSourceMaps,
source_map_fetch_approved: options.sourceMapFetchApproved,
analysis_limits: {
max_findings: options.maxFindings,
max_ast_nodes: options.maxAstNodes,
max_source_maps: options.maxSourceMaps,
max_source_map_bytes: options.maxSourceMapBytes,
max_total_source_map_bytes: options.maxTotalSourceMapBytes,
max_source_map_mappings: options.maxSourceMapMappings,
max_original_sources: options.maxOriginalSources,
},
});
if (!parsed.success)
return browserCliError(new AnalysisInputError("analyze_web_bundle"));
@ -257,7 +209,6 @@ const registerObservationSession = (
allowed_origins:
options.allowedOrigins ?? context.allowedBrowserOrigins,
target_id: args.targetId,
approved: options.approved,
observation_ms: options.observationMs,
max_timeline_events: options.maxTimelineEvents,
});

View File

@ -140,7 +140,6 @@ const registerElectronTargetList = (
cdp_endpoint: args.endpoint,
allowed_file_roots:
options.allowedFileRoots ?? context.allowedFileRoots,
approved: options.approved,
});
if (!parsed.success) return inputError("list_electron_targets");
const result = await listElectronTargets(
@ -177,10 +176,8 @@ const registerElectronPageInspection = (
allowed_file_roots:
options.allowedFileRoots ?? context.allowedFileRoots,
target_id: args.targetId,
approved: options.approved,
observation_ms: options.observationMs,
include_script_sources: options.includeScriptSources,
source_capture_approved: options.sourceCaptureApproved,
limits: {
max_frames: options.maxFrames,
max_dom_nodes: options.maxDomNodes,
@ -218,22 +215,6 @@ const registerJavaScriptApplicationCommand = (
runCliJavaScriptApplicationAnalysis({
input_path: args.path,
format: options.artifactFormat,
limits: {
max_entries: options.maxEntries,
max_total_artifact_bytes: options.maxTotalArtifactBytes,
max_artifact_entry_bytes: options.maxArtifactEntryBytes,
max_compression_ratio: options.maxCompressionRatio,
max_depth: options.maxDepth,
max_path_bytes: options.maxPathBytes,
max_text_files: options.maxTextFiles,
max_total_text_bytes: options.maxTotalTextBytes,
max_text_file_bytes: options.maxTextFileBytes,
max_ast_nodes: options.maxAstNodes,
max_findings: options.maxFindings,
max_modules: options.maxModules,
max_source_map_sources: options.maxSourceMapSources,
max_parse_milliseconds: options.maxParseMilliseconds,
},
}),
),
});

View File

@ -34,7 +34,6 @@ const scopeOptions = {
.describe(
"Exact origins; defaults to REA_V8_INSPECTOR_ALLOWED_ORIGINS_JSON",
),
approved: z.boolean().default(false).describe("Approve passive attachment"),
};
const listOptionsSchema = z.object({ ...scopeOptions });
@ -115,7 +114,6 @@ export const registerJavaScriptRuntimeObservationCommands = (
allowed_file_roots:
options.allowedFileRoots ?? context.allowedFileRoots,
allowed_origins: options.allowedOrigins ?? context.allowedOrigins,
approved: options.approved,
});
if (!parsed.success)
return inputError("list_javascript_runtime_targets");
@ -149,7 +147,6 @@ export const registerJavaScriptRuntimeObservationCommands = (
allowed_origins: options.allowedOrigins ?? context.allowedOrigins,
target_id: args.targetId,
runtime_kind: options.runtimeKind,
approved: options.approved,
observation_ms: options.observationMs,
limits: {
max_events: options.maxEvents,

View File

@ -22,10 +22,7 @@ export const boundedBytes = (
fallback: number,
) => boundedCount(`${subject} in bytes`, maximum, fallback);
const positiveCount = (subject: string, fallback: number) =>
z.number().int().min(1).default(fallback).describe(`Maximum ${subject}`);
/** Shared passive browser origin and approval options. */
/** Shared passive browser origin options. */
export const browserScopeOptions = {
allowedOrigins: z
.array(z.string().min(1))
@ -33,7 +30,6 @@ export const browserScopeOptions = {
.describe(
"Exact origins to observe; defaults to REA_BROWSER_ALLOWED_ORIGINS_JSON",
),
approved: z.boolean().default(false).describe("Approve passive observation"),
};
export const browserPageInspectionOptions = z.object({
@ -52,34 +48,18 @@ export const browserPageInspectionOptions = z.object({
.boolean()
.default(false)
.describe("Include bounded console message text"),
consoleTextApproved: z
.boolean()
.default(false)
.describe("Approve capturing console message text"),
includeJsonBodyShapes: z
.boolean()
.default(false)
.describe("Include structural shapes of JSON response bodies"),
jsonBodySchemaApproved: z
.boolean()
.default(false)
.describe("Approve inspecting JSON response bodies for structural shapes"),
includeWebsocketShapes: z
.boolean()
.default(false)
.describe("Include structural shapes of WebSocket payloads"),
websocketShapeApproved: z
.boolean()
.default(false)
.describe("Approve inspecting WebSocket payloads for structural shapes"),
includeScriptSources: z
.boolean()
.default(false)
.describe("Include bounded JavaScript source text"),
sourceCaptureApproved: z
.boolean()
.default(false)
.describe("Approve capturing bounded script source text"),
includeStorageKeys: z
.boolean()
.default(false)
@ -152,13 +132,12 @@ export const browserPageInspectionOptions = z.object({
),
});
/** Shared passive Electron root and approval options. */
/** Shared passive Electron root options. */
export const electronScopeOptions = {
allowedFileRoots: z
.array(z.string().min(1))
.optional()
.describe("Filesystem roots; defaults to REA_ELECTRON_FILE_ROOTS_JSON"),
approved: z.boolean().default(false).describe("Approve passive observation"),
};
export const electronPageInspectionOptions = z.object({
@ -173,10 +152,6 @@ export const electronPageInspectionOptions = z.object({
.boolean()
.default(false)
.describe("Include bounded JavaScript source text"),
sourceCaptureApproved: z
.boolean()
.default(false)
.describe("Approve capturing bounded script source text"),
maxFrames: boundedCount("page frames", 1_000, 200),
maxDomNodes: boundedCount("DOM nodes", 10_000, 2_000),
maxScripts: boundedCount("scripts", 2_000, 500),
@ -199,40 +174,4 @@ export const javascriptApplicationOptions = z.object({
.enum(["auto", "asar", "directory"])
.default("auto")
.describe("Application artifact format"),
maxEntries: positiveCount("artifact entries to inspect", 8_000),
maxTotalArtifactBytes: positiveCount(
"total uncompressed artifact content to inspect",
512 * 1_024 * 1_024,
),
maxArtifactEntryBytes: positiveCount(
"uncompressed content for one artifact entry",
128 * 1_024 * 1_024,
),
maxCompressionRatio: z
.number()
.min(1)
.default(1_000)
.describe("Maximum accepted compressed-to-uncompressed expansion ratio"),
maxDepth: positiveCount("artifact directory depth", 64),
maxPathBytes: positiveCount("encoded artifact path length", 4_096),
maxTextFiles: positiveCount("text files to parse", 5_000),
maxTotalTextBytes: positiveCount(
"total text content to parse",
128 * 1_024 * 1_024,
),
maxTextFileBytes: positiveCount(
"size of one parsed text file",
8 * 1_024 * 1_024,
),
maxAstNodes: positiveCount("JavaScript AST nodes to parse", 2_000_000),
maxFindings: positiveCount("static-analysis findings to return", 8_000),
maxModules: positiveCount("application modules to project", 20_000),
maxSourceMapSources: positiveCount(
"original sources from source maps",
5_000,
),
maxParseMilliseconds: positiveCount(
"JavaScript parsing time in milliseconds",
30_000,
),
});

View File

@ -5,7 +5,7 @@ export const analysisSearchInput = {
pattern: z
.string()
.min(1)
.describe("The literal text or bounded regex pattern to search for"),
.describe("The literal text or regex pattern to search for"),
mode: z.enum(["literal", "regex"]).default("literal"),
case_sensitive: z.boolean().default(false).describe("Whether to match case"),
document: z.string().optional().describe("The document name"),

View File

@ -55,7 +55,6 @@ const reconstructionObligationLedgerOutputSchema = evidenceResultOf(
);
const HASH = "0".repeat(64);
const NODE_PREPARATION_EXAMPLE = jsonObjectSchema.parse({
preparation_approved: true,
selected_alias: "bundle",
expected_effect: "pure",
instrumentation: {
@ -130,7 +129,7 @@ export const APPLICATION_TOOL_CONTRACTS = [
name: "compare_application_versions",
...toolContractMetadata("compare_application_versions"),
description:
"Compare two authenticated JavaScript Application Graph versions supplied as full Evidence or Evidence IDs returned earlier in this session. Uses unique-only exact digest, module source digest, source-map identity, structural fingerprint, and non-module semantic-key tiers. Reports added, removed, changed, ambiguous, and unknown entities plus a bounded changed_from graph without fuzzy or module-ordinal pairing.",
"Compare two authenticated JavaScript Application Graph versions supplied as full Evidence or Evidence IDs returned earlier in this session. Uses unique-only exact digest, module source digest, source-map identity, structural fingerprint, and non-module semantic-key tiers. Reports added, removed, changed, ambiguous, and unknown entities plus the complete matching changed_from graph without fuzzy or module-ordinal pairing.",
kind: "application",
inputSchema: compareApplicationVersionsRequestSchema,
outputSchema: comparisonOutputSchema,
@ -176,7 +175,7 @@ export const APPLICATION_TOOL_CONTRACTS = [
name: "run_controlled_replay",
...toolContractMetadata("run_controlled_replay"),
description:
"Plan or execute a content-bound extracted-module JavaScript replay inside the Linux Bubblewrap, seccomp, and cgroup boundary. Execution requires approved: true and the exact plan digest. Supports deterministic boundary cases and optional left/right differential comparison; observations have controlled-replay authority and do not claim real application runtime behavior.",
"Plan or execute a content-bound extracted-module JavaScript replay inside the Linux Bubblewrap, seccomp, and cgroup boundary. Execution requires the exact plan digest. Supports deterministic boundary cases and optional left/right differential comparison; observations have controlled-replay authority and do not claim real application runtime behavior.",
kind: "application",
inputSchema: controlledReplayInputSchema,
outputSchema: controlledReplayOutputSchema,
@ -228,7 +227,7 @@ export const APPLICATION_TOOL_CONTRACTS = [
name: "prepare_node_characterization",
...toolContractMetadata("prepare_node_characterization"),
description:
"Prepare a hash-bound Node/JavaScript characterization plan and deterministic reversible export transformation without executing target code. The exact source, selected byte range, runtime closure, sandbox profile, cases, and limits are committed for separate execution approval.",
"Prepare a hash-bound Node/JavaScript characterization plan and deterministic reversible export transformation without executing target code. The exact source, selected byte range, runtime closure, sandbox profile, cases, and limits are committed for later execution.",
kind: "application",
inputSchema: nodeCharacterizationPreparationInputSchema,
outputSchema: nodeCharacterizationPreparationOutputSchema,
@ -243,15 +242,14 @@ export const APPLICATION_TOOL_CONTRACTS = [
name: "execute_node_characterization",
...toolContractMetadata("execute_node_characterization"),
description:
"Recompute and execute one separately approved exact Node characterization plan in the owned controlled-replay boundary. Returns transformation, replay, cleanup, and provider-neutral characterization Evidence; stale plans fail before execution.",
"Recompute and execute one exact Node characterization plan in the owned controlled-replay boundary. Returns transformation, replay, cleanup, and provider-neutral characterization Evidence; stale plans fail before execution.",
kind: "application",
inputSchema: nodeCharacterizationExecutionInputSchema,
outputSchema: nodeCharacterizationExecutionOutputSchema,
examples: [
{
title: "Execute one approved exact characterization plan",
title: "Execute one exact characterization plan",
input: {
execution_approved: true,
approved_plan_sha256: HASH,
preparation: NODE_PREPARATION_EXAMPLE,
},

View File

@ -51,7 +51,6 @@ const compareApplicationVersionsFacts = {
right_native_observations:
compareApplicationVersionsInputSchema.shape.right_native_observations,
right_native_observation_evidence_ids: z.array(evidenceIdSchema).default([]),
limits: compareApplicationVersionsInputSchema.shape.limits,
unknown_registry_approved:
compareApplicationVersionsInputSchema.shape.unknown_registry_approved,
} as const;

View File

@ -65,7 +65,6 @@ const example: Record<string, JsonValue> = {
max_storage_entries: 256,
max_total_metadata_bytes: 4_194_304,
},
approved: true,
};
/** Controlled browser scenario contract shared by MCP and catalog generation. */

View File

@ -6,7 +6,7 @@ import {
webPageInspectionSchema,
} from "../domain/browserObservation.js";
import {
analyzeWebBundleInputSchema,
analyzeWebBundleToolInputSchema,
webBundleAnalysisSchema,
} from "../domain/webBundleAnalysis.js";
import {
@ -130,7 +130,6 @@ export const BROWSER_TOOL_CONTRACTS = [
input: {
cdp_endpoint: endpoint,
allowed_origins: [origin],
approved: true,
},
},
],
@ -149,16 +148,12 @@ export const BROWSER_TOOL_CONTRACTS = [
input: {
cdp_endpoint: endpoint,
allowed_origins: [origin],
approved: true,
target_id: "TARGET_ID_FROM_LIST_BROWSER_TARGETS",
observation_ms: 500,
include_accessibility_text: false,
include_console_text: false,
console_text_approved: false,
include_json_body_shapes: false,
json_body_schema_approved: false,
include_websocket_shapes: false,
websocket_shape_approved: false,
include_script_sources: false,
include_storage_keys: false,
include_storage_fingerprints: false,
@ -194,9 +189,9 @@ export const BROWSER_TOOL_CONTRACTS = [
name: "analyze_web_bundle",
...toolContractMetadata("analyze_web_bundle"),
description:
"Capture explicitly approved JavaScript source from one approved CDP page and statically derive a bounded chunk graph, route and endpoint candidates, vendor fingerprints, page-declared WebMCP metadata, and optional separately approved source-map evidence. JavaScript is parsed but never executed.",
"Capture JavaScript source from one configured CDP page and statically derive a bounded chunk graph, route and endpoint candidates, vendor fingerprints, page-declared WebMCP metadata, and optionally fetch source maps from allowed origins. JavaScript is parsed but never executed.",
kind: "browser-provider",
inputSchema: analyzeWebBundleInputSchema,
inputSchema: analyzeWebBundleToolInputSchema,
outputSchema: bundleOutputSchema,
examples: [
{
@ -204,24 +199,12 @@ export const BROWSER_TOOL_CONTRACTS = [
input: {
cdp_endpoint: endpoint,
allowed_origins: [origin],
approved: true,
target_id: "TARGET_ID_FROM_LIST_BROWSER_TARGETS",
observation_ms: 500,
include_accessibility_text: false,
include_script_sources: true,
include_storage_keys: false,
source_capture_approved: true,
fetch_source_maps: false,
source_map_fetch_approved: false,
analysis_limits: {
max_findings: 1_000,
max_ast_nodes: 250_000,
max_source_maps: 100,
max_source_map_bytes: 4_194_304,
max_total_source_map_bytes: 16_777_216,
max_source_map_mappings: 10_000,
max_original_sources: 2_000,
},
},
},
],
@ -241,7 +224,6 @@ export const BROWSER_TOOL_CONTRACTS = [
cdp_endpoint: endpoint,
allowed_origins: [origin],
target_id: "TARGET_ID_FROM_LIST_BROWSER_TARGETS",
approved: true,
observation_ms: 10_000,
max_timeline_events: 2_000,
},
@ -263,7 +245,6 @@ export const BROWSER_TOOL_CONTRACTS = [
cdp_endpoint: endpoint,
allowed_origins: [origin],
target_id: "TARGET_ID_FROM_LIST_BROWSER_TARGETS",
approved: true,
observation_ms: 100,
max_tools: 500,
max_schema_bytes: 262_144,
@ -306,7 +287,7 @@ export const BROWSER_TOOL_CONTRACTS = [
name: "capture_web_screenshot",
...toolContractMetadata("capture_web_screenshot"),
description:
"Capture the current visible viewport of one approved page as a bounded, content-addressed PNG artifact. Screenshot capture requires separate explicit approval and never scrolls, navigates, or evaluates page JavaScript.",
"Capture the current visible viewport of one configured page as a bounded, content-addressed PNG artifact. The operation never scrolls, navigates, or evaluates page JavaScript.",
kind: "browser-provider",
inputSchema: captureWebScreenshotInputSchema,
outputSchema: screenshotOutputSchema,
@ -317,8 +298,6 @@ export const BROWSER_TOOL_CONTRACTS = [
cdp_endpoint: endpoint,
allowed_origins: [origin],
target_id: "TARGET_ID_FROM_LIST_BROWSER_TARGETS",
approved: true,
screenshot_approved: true,
maximum_image_bytes: 4_194_304,
},
},

View File

@ -104,7 +104,7 @@ describe("tool contract surface", () => {
for (const contract of contracts) {
const inputSchema = contractJsonSchema(contract.inputSchema);
const outputSchema = contractJsonSchema(contract.outputSchema);
expect(describesObject(inputSchema)).toBe(true);
expect(describesObject(inputSchema), contract.name).toBe(true);
expect(describesObject(outputSchema)).toBe(true);
expect(contract.title.length).toBeGreaterThan(2);
expect(contract.annotations).toEqual(

View File

@ -46,7 +46,6 @@ const activeExample = {
max_processes: 32,
max_windows: 32,
},
approved: true,
};
/** Root-confined Electron file-page discovery and inspection contracts. */
@ -65,7 +64,6 @@ export const ELECTRON_TOOL_CONTRACTS = [
input: {
cdp_endpoint: endpoint,
allowed_file_roots: [root],
approved: true,
},
},
],
@ -85,10 +83,8 @@ export const ELECTRON_TOOL_CONTRACTS = [
cdp_endpoint: endpoint,
allowed_file_roots: [root],
target_id: "TARGET_ID_FROM_LIST_ELECTRON_TARGETS",
approved: true,
observation_ms: 100,
include_script_sources: false,
source_capture_approved: false,
limits: {
max_frames: 200,
max_dom_nodes: 2_000,

View File

@ -7,15 +7,15 @@ const address = z
"A provider-normalized address; default memory uses 0x-prefixed hexadecimal",
);
/** Bounded Hopper memory and file-mapping contracts. */
/** Hopper memory and file-mapping contracts. */
export const HOPPER_MEMORY_TOOL_DEFINITIONS = [
{
name: "read_bytes",
description:
"Read at most 4,096 analyzed bytes from one provider-normalized virtual address. The hexadecimal payload reports the exact returned length; incomplete reads remain explicit and unsupported provider APIs return typed capability unavailability.",
"Read analyzed bytes from one provider-normalized virtual address. The hexadecimal payload reports the exact returned length; incomplete reads remain explicit and unsupported provider APIs return typed capability unavailability.",
inputSchema: z.object({
address,
length: z.number().int().min(1).max(4_096).default(256),
length: z.number().int().min(1).default(256),
document,
}),
},

View File

@ -28,7 +28,6 @@ export const JAVASCRIPT_RUNTIME_OBSERVATION_TOOL_CONTRACTS = [
inspector_endpoint: endpoint,
allowed_file_roots: [root],
allowed_origins: [],
approved: true,
},
},
],
@ -49,7 +48,6 @@ export const JAVASCRIPT_RUNTIME_OBSERVATION_TOOL_CONTRACTS = [
allowed_file_roots: [root],
allowed_origins: [],
target_id: "TARGET_ID_FROM_LIST_JAVASCRIPT_RUNTIME_TARGETS",
approved: true,
observation_ms: 100,
limits: {
max_events: 10_000,

View File

@ -192,7 +192,6 @@ const runtimeEvidence = createEvidence(
allowed_file_roots: [inputPath],
target_id: "example-target",
include_script_sources: true,
source_capture_approved: true,
},
result: {
browser: {

View File

@ -406,7 +406,7 @@ export const PROMPT_CONTRACTS = [
{
tools: ["capture_process_scenario"],
instruction:
"Present exact executable and working roots, filesystem roots, environment names without secret values, host-network effects, scripted events, replay peers, byte/time/process limits, and cleanup expectations. Run only with operator policy plus approved: true.",
"Present exact executable and working roots, filesystem roots, environment names without secret values, host-network effects, scripted events, replay peers, byte/time/process limits, and cleanup expectations. Run only with operator policy authorization.",
},
{
tools: ["compare_process_captures"],

View File

@ -44,7 +44,6 @@ export const TOOL_EXAMPLE_OVERRIDES: Readonly<
inspect_address_context: { address: "0x1000" },
import_evidence_bundle: { path: "evidence.json" },
capture_process_scenario: {
approved: true,
executable: "/usr/bin/true",
working_directory: "/tmp",
},

View File

@ -397,7 +397,7 @@ export const SESSION_TOOL_CONTRACTS = [
),
session(
"capture_process_scenario",
"Run one bounded process under a PTY using operator-approved executable and working roots. Produces process capture Evidence. Requires approved: true; unknown_registry_approved: true separately records capture residuals. Captures raw and xterm-rendered terminal frames, scripted interactions, lifecycle filesystem checkpoints, process ownership, declarative command shims, and loopback replay. Disabled unless operator policy enables it; not a security sandbox.",
"Run one bounded process under a PTY using operator-approved executable and working roots. Produces process capture Evidence; unknown_registry_approved: true separately records capture residuals. Captures raw and xterm-rendered terminal frames, scripted interactions, lifecycle filesystem checkpoints, process ownership, declarative command shims, and loopback replay. Disabled unless operator policy enables it; not a security sandbox.",
processScenarioSchema,
),
session(
@ -447,12 +447,12 @@ export const SESSION_TOOL_CONTRACTS = [
),
session(
"record_unknown",
"Create one deterministic residual unknown and immutable mutation evidence. Requires approved: true, validates all evidence and relationship references, and rejects duplicate stable identity.",
"Create one deterministic residual unknown and immutable mutation evidence. Validates all evidence and relationship references, and rejects duplicate stable identity.",
recordUnknownInputSchema,
),
session(
"update_unknown",
"Append one immutable full-state revision and mutation evidence. Requires approved: true and exact expected_revision; stale concurrent writers fail instead of overwriting newer analysis.",
"Append one immutable full-state revision and mutation evidence. Requires exact expected_revision; stale concurrent writers fail instead of overwriting newer analysis.",
updateUnknownInputSchema,
),
session(

View File

@ -102,8 +102,8 @@ export const officialOutputSchemas: Readonly<Record<string, z.ZodObject>> = {
read_bytes: resultOf(
z.object({
address: z.string(),
requested_bytes: z.number().int().min(1).max(4_096),
returned_bytes: z.number().int().min(0).max(4_096),
requested_bytes: z.number().int().min(1),
returned_bytes: z.number().int().min(0),
bytes_hex: z.string().regex(/^(?:[a-f0-9]{2})*$/u),
complete: z.boolean(),
}),

View File

@ -4,7 +4,6 @@ import type { JsonValue } from "../domain/jsonValue.js";
export const UNKNOWN_CONTRACT_EXAMPLES = {
list_unknowns: {},
record_unknown: {
approved: true,
question: "Does this branch require an unavailable external service?",
severity: "medium",
domain: "protocol",
@ -17,7 +16,6 @@ export const UNKNOWN_CONTRACT_EXAMPLES = {
relationships: [],
},
update_unknown: {
approved: true,
unknown_id: `unk_${"0".repeat(64)}`,
expected_revision: 1,
status: "investigating",

View File

@ -57,29 +57,23 @@ describe("browser observation contracts", () => {
listBrowserTargetsInputSchema.parse({
cdp_endpoint: "http://127.0.0.1:9222",
allowed_origins: ["https://app.example.test"],
approved: true,
}),
).toEqual({
cdp_endpoint: "http://127.0.0.1:9222",
allowed_origins: ["https://app.example.test"],
approved: true,
});
expect(
inspectWebPageInputSchema.parse({
cdp_endpoint: "http://127.0.0.1:9222",
allowed_origins: ["https://app.example.test"],
target_id: "page-1",
approved: true,
}),
).toMatchObject({
observation_ms: 500,
include_accessibility_text: false,
include_console_text: false,
console_text_approved: false,
include_json_body_shapes: false,
json_body_schema_approved: false,
include_websocket_shapes: false,
websocket_shape_approved: false,
include_script_sources: false,
include_storage_keys: false,
limits: {
@ -111,40 +105,35 @@ describe("browser observation contracts", () => {
});
describe("browser observation sensitive surfaces and retention", () => {
it("requires independent approval for each sensitive capture surface", () => {
it("accepts selected sensitive capture surfaces without extra flags", () => {
const base = {
cdp_endpoint: "http://127.0.0.1:9222",
allowed_origins: ["https://app.example.test"],
target_id: "page-1",
approved: true,
};
for (const input of [
{ include_console_text: true },
{ include_json_body_shapes: true },
{ include_websocket_shapes: true },
{ include_storage_fingerprints: true },
])
expect(
inspectWebPageInputSchema.safeParse({ ...base, ...input }).success,
).toBe(false);
).toBe(true);
expect(
inspectWebPageInputSchema.safeParse({
...base,
include_storage_fingerprints: true,
}).success,
).toBe(false);
});
it("requires an independent literal approval for screenshot pixels", () => {
it("accepts screenshot capture without an extra approval flag", () => {
const input = {
cdp_endpoint: "http://127.0.0.1:9222",
allowed_origins: ["https://app.example.test"],
target_id: "page-1",
approved: true,
};
expect(captureWebScreenshotInputSchema.safeParse(input).success).toBe(
false,
);
expect(
captureWebScreenshotInputSchema.safeParse({
...input,
screenshot_approved: true,
}).success,
).toBe(true);
expect(captureWebScreenshotInputSchema.safeParse(input).success).toBe(true);
});
it("removes credentials, query values, and fragments from observed URLs", () => {

View File

@ -92,17 +92,14 @@ export const browserAllowedOriginsSchema = z
.max(32)
.transform((origins) => [...new Set(origins)].sort());
const approvedBrowserInput = {
const browserInput = {
cdp_endpoint: browserEndpointSchema,
allowed_origins: browserAllowedOriginsSchema,
approved: z
.literal(true)
.describe("Explicit approval for browser observation"),
};
/** Public input for complete discovery of allowed page targets. */
export const listBrowserTargetsInputSchema = z.object({
...approvedBrowserInput,
...browserInput,
});
const browserInspectionLimitsSchema = z.object({
@ -206,16 +203,13 @@ const DEFAULT_BROWSER_INSPECTION_LIMITS = {
} as const;
const inspectWebPageInputFacts = {
...approvedBrowserInput,
...browserInput,
target_id: z.string().trim().min(1).max(256),
observation_ms: z.number().int().min(0).max(10_000).default(500),
include_accessibility_text: z.boolean().default(false),
include_console_text: z.boolean().default(false),
console_text_approved: z.boolean().default(false),
include_json_body_shapes: z.boolean().default(false),
json_body_schema_approved: z.boolean().default(false),
include_websocket_shapes: z.boolean().default(false),
websocket_shape_approved: z.boolean().default(false),
include_storage_keys: z.boolean().default(false),
include_storage_fingerprints: z.boolean().default(false),
limits: browserInspectionLimitsSchema.default(
@ -226,13 +220,11 @@ const inspectWebPageInputFacts = {
const inspectWebPageWithoutSourceSchema = z.strictObject({
...inspectWebPageInputFacts,
include_script_sources: z.literal(false).default(false),
source_capture_approved: z.literal(false).default(false),
});
const inspectWebPageWithSourceShapeSchema = z.strictObject({
...inspectWebPageInputFacts,
include_script_sources: z.literal(true).default(true),
source_capture_approved: z.literal(true),
});
type InspectWebPageShape =
@ -258,28 +250,6 @@ const refineInspectWebPageInput = (
path: ["limits", "max_script_source_bytes"],
message: "Per-script source limit cannot exceed the total source limit",
});
for (const [include, approved, path, message] of [
[
input.include_console_text,
input.console_text_approved,
"console_text_approved",
"Console text capture requires separate approval",
],
[
input.include_json_body_shapes,
input.json_body_schema_approved,
"json_body_schema_approved",
"JSON body schema capture requires separate approval",
],
[
input.include_websocket_shapes,
input.websocket_shape_approved,
"websocket_shape_approved",
"WebSocket shape capture requires separate approval",
],
] as const)
if (include && !approved)
context.addIssue({ code: "custom", path: [path], message });
if (
input.limits.max_console_text_field_bytes >
input.limits.max_total_console_text_bytes

View File

@ -46,7 +46,6 @@ const scenarioShapeSchema = z.strictObject({
redaction: browserScenarioRedactionSchema,
capture: browserScenarioCaptureSchema,
limits: browserScenarioCaptureLimitsSchema,
approved: z.literal(true),
});
type ScenarioShape = z.infer<typeof scenarioShapeSchema>;

View File

@ -152,7 +152,6 @@ const locatorSchema = z.discriminatedUnion("kind", [
z.strictObject({
kind: z.literal("css"),
selector: z.string().min(1).max(4_096),
approved: z.literal(true),
}),
]);

View File

@ -11,7 +11,6 @@ export const observeWebSessionInputSchema = z.object({
cdp_endpoint: browserEndpointSchema,
allowed_origins: browserAllowedOriginsSchema,
target_id: z.string().trim().min(1).max(256),
approved: z.literal(true),
observation_ms: z.number().int().min(1).max(60_000).default(10_000),
max_timeline_events: z.number().int().min(1).max(20_000).default(2_000),
});

View File

@ -126,7 +126,6 @@ describe("bundle comparison history", () => {
const mutationOne = evidence("mutation-one");
const initial = createResidualUnknown(
recordUnknownInputSchema.parse({
approved: true,
question: "Which branch remains unexplained?",
severity: "high",
domain: "comparison",
@ -145,7 +144,6 @@ describe("bundle comparison history", () => {
const advanced = updateResidualUnknown(
initial,
updateUnknownInputSchema.parse({
approved: true,
unknown_id: initial.unknown_id,
expected_revision: 1,
status: "investigating",

View File

@ -80,7 +80,6 @@ export const electronActiveObservationInputSchema = z
max_processes: 32,
max_windows: 32,
}),
approved: z.literal(true),
})
.superRefine((input, context) => {
if (input.actions.length > input.limits.max_actions)

View File

@ -22,7 +22,6 @@ export const electronFileRootsSchema = z
const approvedElectronInput = {
cdp_endpoint: browserEndpointSchema,
allowed_file_roots: electronFileRootsSchema,
approved: z.literal(true),
};
/** Input for listing root-confined file:// page targets from Electron CDP. */
@ -73,12 +72,10 @@ export const inspectElectronPageInputSchema = z
z.object({
...inspectElectronPageFacts,
include_script_sources: z.literal(false).default(false),
source_capture_approved: z.literal(false).default(false),
}),
z.object({
...inspectElectronPageFacts,
include_script_sources: z.literal(true),
source_capture_approved: z.literal(true),
}),
])
.superRefine((input, context) => {

View File

@ -8,58 +8,6 @@ import { javaScriptSemanticGraphSchema } from "./javascriptSemanticGraph.js";
const countSchema = z.number().int().min(0);
const digestSchema = z.string().regex(/^[a-f0-9]{64}$/u);
/** Defaults for one local JavaScript application analysis. */
export const JAVASCRIPT_APPLICATION_ANALYSIS_DEFAULT_LIMITS = {
max_entries: 8_000,
max_total_artifact_bytes: 512 * 1_024 * 1_024,
max_artifact_entry_bytes: 128 * 1_024 * 1_024,
max_compression_ratio: 1_000,
max_depth: 64,
max_path_bytes: 4_096,
max_text_files: 5_000,
max_total_text_bytes: 128 * 1_024 * 1_024,
max_text_file_bytes: 8 * 1_024 * 1_024,
max_ast_nodes: 2_000_000,
max_findings: 8_000,
max_modules: 20_000,
max_source_map_sources: 5_000,
max_parse_milliseconds: 30_000,
} as const;
/** Combined artifact, text, AST, and graph projection bounds. */
export const javascriptApplicationAnalysisLimitsSchema = z.strictObject({
max_entries: z.number().int().min(1).default(8_000),
max_total_artifact_bytes: z
.number()
.int()
.min(1)
.default(512 * 1_024 * 1_024),
max_artifact_entry_bytes: z
.number()
.int()
.min(1)
.default(128 * 1_024 * 1_024),
max_compression_ratio: z.number().min(1).default(1_000),
max_depth: z.number().int().min(1).default(64),
max_path_bytes: z.number().int().min(1).default(4_096),
max_text_files: z.number().int().min(1).default(5_000),
max_total_text_bytes: z
.number()
.int()
.min(1)
.default(128 * 1_024 * 1_024),
max_text_file_bytes: z
.number()
.int()
.min(1)
.default(8 * 1_024 * 1_024),
max_ast_nodes: z.number().int().min(1).default(2_000_000),
max_findings: z.number().int().min(1).default(8_000),
max_modules: z.number().int().min(1).default(20_000),
max_source_map_sources: z.number().int().min(1).default(5_000),
max_parse_milliseconds: z.number().int().min(1).default(30_000),
});
/** Public target-free request for static JavaScript application analysis. */
export const analyzeJavaScriptApplicationInputSchema = z.strictObject({
input_path: z
@ -67,9 +15,6 @@ export const analyzeJavaScriptApplicationInputSchema = z.strictObject({
.min(1)
.refine(isAbsolute, "JavaScript application input path must be absolute"),
format: z.enum(["auto", "asar", "directory"]).default("auto"),
limits: javascriptApplicationAnalysisLimitsSchema.default(
JAVASCRIPT_APPLICATION_ANALYSIS_DEFAULT_LIMITS,
),
});
/** Compact counts for the high-level Electron architecture/security surface. */

View File

@ -8,10 +8,7 @@ import {
type ApplicationNode,
type JavaScriptApplicationGraph,
} from "./javascriptApplicationGraph.js";
import type {
ApplicationVersionComparisonItem,
CompareApplicationVersionsInput,
} from "./javascriptApplicationVersionComparisonSchemas.js";
import type { ApplicationVersionComparisonItem } from "./javascriptApplicationVersionComparisonSchemas.js";
interface ChangeGraphInput {
readonly left: JavaScriptApplicationGraph;
@ -19,16 +16,11 @@ interface ChangeGraphInput {
readonly leftEvidenceId: string;
readonly rightEvidenceId: string;
readonly items: readonly ApplicationVersionComparisonItem[];
readonly omittedComparisonItems: number;
readonly limits: CompareApplicationVersionsInput["limits"];
}
/** Bounded cross-version graph and exact projection omission counts. */
/** Complete cross-version graph projection. */
export interface ApplicationChangeGraphProjection {
readonly graph: JavaScriptApplicationGraph;
readonly omittedNodes: number;
readonly omittedEdges: number;
readonly omittedObservations: number;
}
/** Merge compared nodes and add inferred right-to-left changed_from edges. */
@ -47,10 +39,9 @@ export const buildJavaScriptApplicationChangeGraph = (
...(left === null ? [] : [left]),
]),
]);
const retainedIds = orderedNodeIds.slice(0, input.limits.max_graph_nodes);
const retained = new Set(retainedIds);
const merged = mergeNodes(
retainedIds.flatMap((nodeId) => allNodes.get(nodeId) ?? []),
const retained = new Set(orderedNodeIds);
const mergedNodes = mergeNodes(
orderedNodeIds.flatMap((nodeId) => allNodes.get(nodeId) ?? []),
);
const sourceEdges = uniqueEdges([
...input.left.edges,
@ -63,15 +54,8 @@ export const buildJavaScriptApplicationChangeGraph = (
changedFromEdge(item, input, retained),
);
const candidateEdges = uniqueEdges([...sourceEdges, ...comparisonEdges]);
const edges = candidateEdges.slice(0, input.limits.max_graph_edges);
const omittedNodes = Math.max(0, orderedNodeIds.length - retainedIds.length);
const omittedEdges = Math.max(0, candidateEdges.length - edges.length);
const graphOmissions =
omittedNodes + omittedEdges + merged.omittedObservations;
const rootNodeIds = preferredRoots
.filter((nodeId) => retained.has(nodeId))
.slice(0, 1_000);
const fallbackRoot = merged.nodes[0]?.node_id;
const rootNodeIds = preferredRoots.filter((nodeId) => retained.has(nodeId));
const fallbackRoot = mergedNodes[0]?.node_id;
const graph = createJavaScriptApplicationGraph({
schema: "JavaScriptApplicationGraph",
root_node_ids:
@ -80,25 +64,17 @@ export const buildJavaScriptApplicationChangeGraph = (
: fallbackRoot === undefined
? []
: [fallbackRoot],
nodes: merged.nodes,
edges,
coverage: changeGraphCoverage(input, graphOmissions),
nodes: mergedNodes,
edges: candidateEdges,
coverage: changeGraphCoverage(input),
limitations: uniqueSorted([
...input.left.limitations.map((value) => `Left: ${value}`),
...input.right.limitations.map((value) => `Right: ${value}`),
"changed_from edges are cross-version inferences and never promote structural or semantic matches to exact identity.",
"The change graph contains compared entities and their retained relationships; it is not an executable application.",
...(graphOmissions === 0
? []
: ["The change graph omitted content at explicit caller bounds."]),
]),
});
return {
graph,
omittedNodes,
omittedEdges,
omittedObservations: merged.omittedObservations,
};
return { graph };
};
const nodeCandidates = (
@ -168,8 +144,7 @@ const comparisonEvidence = (
right?.observations[0]?.evidence ?? left?.observations[0]?.evidence;
const complete =
input.left.coverage.status === "complete" &&
input.right.coverage.status === "complete" &&
input.omittedComparisonItems === 0;
input.right.coverage.status === "complete";
return {
authority: "cross-version-comparison",
state: "inferred",
@ -194,21 +169,13 @@ const comparisonEvidence = (
? { status: "complete", truncated: false, omitted_count: 0, limits: [] }
: {
status: "partial",
truncated: input.omittedComparisonItems > 0,
omitted_count:
input.omittedComparisonItems > 0
? input.omittedComparisonItems
: (source?.coverage.omitted_count ?? null),
limits:
input.omittedComparisonItems > 0
? [
{
name: "max-comparison-items",
value: input.limits.max_comparison_items,
unit: "items",
},
]
: [],
truncated:
input.left.coverage.truncated || input.right.coverage.truncated,
omitted_count: combineOmittedCounts(
input.left.coverage.omitted_count,
input.right.coverage.omitted_count,
),
limits: sourceCoverageLimits(input),
},
limitations: [
"Cross-version pairing is inferred from the stated match basis; changed_from does not prove runtime reachability.",
@ -220,32 +187,7 @@ const comparisonEvidence = (
const changeGraphCoverage = (
input: ChangeGraphInput,
graphOmissions: number,
): JavaScriptApplicationGraph["coverage"] => {
const outputOmissions = graphOmissions + input.omittedComparisonItems;
if (outputOmissions > 0)
return {
status: "partial",
truncated: true,
omitted_count: outputOmissions,
limits: [
{
name: "max-comparison-items",
value: input.limits.max_comparison_items,
unit: "items",
},
{
name: "max-graph-nodes",
value: input.limits.max_graph_nodes,
unit: "items",
},
{
name: "max-graph-edges",
value: input.limits.max_graph_edges,
unit: "items",
},
],
};
if (
input.left.coverage.status === "complete" &&
input.right.coverage.status === "complete"
@ -256,30 +198,33 @@ const changeGraphCoverage = (
omitted_count: 0,
limits: [],
};
const counts = [
input.left.coverage.omitted_count,
input.right.coverage.omitted_count,
];
return {
status: "partial",
truncated: false,
omitted_count: counts.every((value) => value !== null)
? counts.reduce<number>((total, value) => total + (value ?? 0), 0)
: null,
limits: [],
truncated: input.left.coverage.truncated || input.right.coverage.truncated,
omitted_count: combineOmittedCounts(
input.left.coverage.omitted_count,
input.right.coverage.omitted_count,
),
limits: sourceCoverageLimits(input),
};
};
const mergeNodes = (
nodes: readonly ApplicationNode[],
): {
readonly nodes: ApplicationNode[];
readonly omittedObservations: number;
} => {
const sourceCoverageLimits = (input: ChangeGraphInput) =>
[
...new Map(
[...input.left.coverage.limits, ...input.right.coverage.limits].map(
(limit) => [`${limit.name}\0${limit.value}\0${limit.unit}`, limit],
),
).values(),
].sort((left, right) => compareCodePoints(left.name, right.name));
const combineOmittedCounts = (left: number | null, right: number | null) =>
left === null || right === null ? null : left + right;
const mergeNodes = (nodes: readonly ApplicationNode[]): ApplicationNode[] => {
const groups = new Map<string, ApplicationNode[]>();
for (const node of nodes)
groups.set(node.node_id, [...(groups.get(node.node_id) ?? []), node]);
let omittedObservations = 0;
const merged = [...groups.values()].map((group) => {
const first = group[0];
if (first === undefined)
@ -293,24 +238,18 @@ const mergeNodes = (
].sort((left, right) =>
compareCodePoints(left.observation_id, right.observation_id),
);
omittedObservations += Math.max(0, observations.length - 64);
return createJavaScriptApplicationNode({
kind: first.kind,
identity: first.identity,
observations: observations
.slice(0, 64)
.map(
({ observation_id: _id, identifier_strategy: _strategy, ...value }) =>
value,
),
observations: observations.map(
({ observation_id: _id, identifier_strategy: _strategy, ...value }) =>
value,
),
});
});
return {
nodes: merged.sort((left, right) =>
compareCodePoints(left.node_id, right.node_id),
),
omittedObservations,
};
return merged.sort((left, right) =>
compareCodePoints(left.node_id, right.node_id),
);
};
const uniqueEdges = (edges: readonly ApplicationEdge[]): ApplicationEdge[] =>

View File

@ -112,7 +112,7 @@ export const applicationNodeObservationSchema =
export const applicationNodeInputSchema = z.strictObject({
kind: applicationNodeKindSchema,
identity: applicationNodeIdentitySchema,
observations: z.array(applicationNodeObservationInputSchema).min(1).max(64),
observations: z.array(applicationNodeObservationInputSchema).min(1),
});
/** One stable application entity with one or more bounded observations. */
@ -120,7 +120,7 @@ export const applicationNodeSchema = z.strictObject({
node_id: z.string().regex(/^jag_node_[a-f0-9]{64}$/u),
kind: applicationNodeKindSchema,
identity: applicationNodeIdentitySchema,
observations: z.array(applicationNodeObservationSchema).min(1).max(64),
observations: z.array(applicationNodeObservationSchema).min(1),
});
/** One directed relationship before its semantic identifier is derived. */
@ -148,7 +148,7 @@ export const javascriptApplicationGraphInputSchema = z.strictObject({
nodes: z.array(applicationNodeSchema).min(1),
edges: z.array(applicationEdgeSchema),
coverage: applicationCoverageSchema,
limitations: z.array(boundedTextSchema).max(1_000),
limitations: z.array(boundedTextSchema),
});
/** Strict stored shape for a JavaScript Application Graph. */

View File

@ -7,7 +7,6 @@ import { buildJavaScriptApplicationChangeGraph } from "./javascriptApplicationCh
import {
applicationVersionComparisonResultSchema,
type ApplicationVersionComparisonResult,
type CompareApplicationVersionsInput,
} from "./javascriptApplicationVersionComparisonSchemas.js";
import type { JavaScriptApplicationGraph } from "./javascriptApplicationGraph.js";
import { compareCodePoints } from "./javascriptApplicationGraph.js";
@ -28,7 +27,6 @@ export interface ApplicationVersionComparisonProjectionInput {
};
readonly leftNativeEvidence: readonly Evidence[];
readonly rightNativeEvidence: readonly Evidence[];
readonly limits: CompareApplicationVersionsInput["limits"];
}
/** Compare application entities with ordered unique-only identity tiers. */
@ -46,28 +44,20 @@ export const compareJavaScriptApplicationVersions = (
rightEvidenceId: input.right.evidenceId,
leftNativeEvidence: input.leftNativeEvidence,
rightNativeEvidence: input.rightNativeEvidence,
maxCandidates: input.limits.max_candidate_nodes,
});
const items = projection.items.slice(0, input.limits.max_comparison_items);
const omittedComparisonItems = projection.items.length - items.length;
const changeGraph = buildJavaScriptApplicationChangeGraph({
left: input.left.graph,
right: input.right.graph,
leftEvidenceId: input.left.evidenceId,
rightEvidenceId: input.right.evidenceId,
items,
omittedComparisonItems,
limits: input.limits,
items: projection.items,
});
const evidenceLinks = uniqueSorted(
projection.items.flatMap(({ evidence_links: links }) => links),
);
const omissions = {
omitted_comparison_items: omittedComparisonItems,
omitted_candidate_references: projection.omittedCandidateReferences,
omitted_graph_nodes: changeGraph.omittedNodes,
omitted_graph_edges: changeGraph.omittedEdges,
omitted_graph_observations: changeGraph.omittedObservations,
left_graph_omitted_count: input.left.graph.coverage.omitted_count,
right_graph_omitted_count: input.right.graph.coverage.omitted_count,
};
const coverage = comparisonCoverage(input, omissions);
const semantic = {
@ -83,11 +73,11 @@ export const compareJavaScriptApplicationVersions = (
},
summary: summary(projection.items),
matching: matchingSummary(projection.items),
items,
items: projection.items,
graph: changeGraph.graph,
coverage,
evidence_links: evidenceLinks,
limitations: comparisonLimitations(input, omissions),
limitations: comparisonLimitations(input),
};
return applicationVersionComparisonResultSchema.parse({
...semantic,
@ -131,11 +121,7 @@ const countBasis = (
type ComparisonOmissions = ApplicationVersionComparisonResult["coverage"];
type ComparisonOmissionCounts = Pick<
ComparisonOmissions,
| "omitted_comparison_items"
| "omitted_candidate_references"
| "omitted_graph_nodes"
| "omitted_graph_edges"
| "omitted_graph_observations"
"left_graph_omitted_count" | "right_graph_omitted_count"
>;
const comparisonCoverage = (
@ -146,35 +132,24 @@ const comparisonCoverage = (
left_graph_status: input.left.graph.coverage.status,
right_graph_status: input.right.graph.coverage.status,
};
if (Object.values(omissions).some((value) => value > 0))
return { ...context, ...omissions, status: "truncated" };
if (
const sourceGraphsComplete =
input.left.graph.coverage.status === "complete" &&
input.right.graph.coverage.status === "complete"
)
return {
...context,
status: "complete-within-inputs",
omitted_comparison_items: 0,
omitted_candidate_references: 0,
omitted_graph_nodes: 0,
omitted_graph_edges: 0,
omitted_graph_observations: 0,
};
input.right.graph.coverage.status === "complete";
const sourceGraphsTruncated =
input.left.graph.coverage.truncated || input.right.graph.coverage.truncated;
return {
...context,
status: "partial",
omitted_comparison_items: 0,
omitted_candidate_references: 0,
omitted_graph_nodes: 0,
omitted_graph_edges: 0,
omitted_graph_observations: 0,
...omissions,
status: sourceGraphsComplete
? "complete-within-inputs"
: sourceGraphsTruncated
? "truncated"
: "partial",
};
};
const comparisonLimitations = (
input: ApplicationVersionComparisonProjectionInput,
omissions: ComparisonOmissionCounts,
): string[] =>
uniqueSorted([
"Application comparison never uses bundler module ordinals as persistent identity and performs no fuzzy pairing.",
@ -188,9 +163,6 @@ const comparisonLimitations = (
: [
"At least one source graph is incomplete; unmatched entities on the opposite side remain unknown rather than added or removed.",
]),
...(Object.values(omissions).some((value) => value > 0)
? ["Comparison output was truncated at explicit caller limits."]
: []),
]);
const digestCanonical = (value: unknown): string => {

View File

@ -8,26 +8,13 @@ const evidenceIdSchema = z.string().regex(/^ev_[a-f0-9]{64}$/u);
const nodeIdSchema = z.string().regex(/^jag_node_[a-f0-9]{64}$/u);
const boundedTextSchema = z.string().min(1).max(4_096);
const comparisonLimitsSchema = z.strictObject({
max_comparison_items: z.number().int().min(1).max(50_000).default(20_000),
max_candidate_nodes: z.number().int().min(1).max(1_000).default(100),
max_graph_nodes: z.number().int().min(1).max(100_000).default(20_000),
max_graph_edges: z.number().int().min(1).max(200_000).default(40_000),
});
/** Two authenticated application versions and deterministic output bounds. */
/** Two authenticated application versions and their native observations. */
export const compareApplicationVersionsInputSchema = z
.strictObject({
left: evidenceSchema,
right: evidenceSchema,
left_native_observations: z.array(evidenceSchema).max(64).default([]),
right_native_observations: z.array(evidenceSchema).max(64).default([]),
limits: comparisonLimitsSchema.default({
max_comparison_items: 20_000,
max_candidate_nodes: 100,
max_graph_nodes: 20_000,
max_graph_edges: 40_000,
}),
left_native_observations: z.array(evidenceSchema).default([]),
right_native_observations: z.array(evidenceSchema).default([]),
unknown_registry_approved: z.literal(true).optional(),
})
.superRefine((input, context) => {
@ -52,10 +39,7 @@ export const compareApplicationVersionsInputSchema = z
});
const emptyCandidateNodesSchema = z.tuple([]);
const candidateNodesSchema = z
.tuple([nodeIdSchema])
.rest(nodeIdSchema)
.check(z.maxLength(1_000));
const candidateNodesSchema = z.tuple([nodeIdSchema]).rest(nodeIdSchema);
const exactMatchSchema = z.strictObject({
status: z.literal("matched"),
basis: z.enum([
@ -123,8 +107,8 @@ const comparisonItemContextShape = {
]),
)
.max(6),
evidence_links: z.array(evidenceIdSchema).min(2).max(130),
limitations: z.array(boundedTextSchema).max(100),
evidence_links: z.array(evidenceIdSchema).min(2),
limitations: z.array(boundedTextSchema),
};
const comparisonItemSchema = z.union([
@ -165,55 +149,15 @@ const comparisonItemSchema = z.union([
}),
]);
const comparisonCoverageContextShape = {
const comparisonCoverageSchema = z.strictObject({
left_graph_status: z.enum(["complete", "partial", "unknown", "unavailable"]),
right_graph_status: z.enum(["complete", "partial", "unknown", "unavailable"]),
};
const zeroComparisonOmissionsShape = {
omitted_comparison_items: z.literal(0),
omitted_candidate_references: z.literal(0),
omitted_graph_nodes: z.literal(0),
omitted_graph_edges: z.literal(0),
omitted_graph_observations: z.literal(0),
};
const comparisonOmissionsShape = {
omitted_comparison_items: z.number().int().min(0),
omitted_candidate_references: z.number().int().min(0),
omitted_graph_nodes: z.number().int().min(0),
omitted_graph_edges: z.number().int().min(0),
omitted_graph_observations: z.number().int().min(0),
};
const comparisonCoverageSchema = z.union([
z.strictObject({
...comparisonCoverageContextShape,
...zeroComparisonOmissionsShape,
status: z.literal("complete-within-inputs"),
}),
z.strictObject({
...comparisonCoverageContextShape,
...zeroComparisonOmissionsShape,
status: z.literal("partial"),
}),
z
.strictObject({
...comparisonCoverageContextShape,
...comparisonOmissionsShape,
status: z.literal("truncated"),
})
.refine(
(coverage) =>
[
coverage.omitted_comparison_items,
coverage.omitted_candidate_references,
coverage.omitted_graph_nodes,
coverage.omitted_graph_edges,
coverage.omitted_graph_observations,
].some((value) => value > 0),
{ message: "Truncated comparison coverage must name an omission" },
),
]);
left_graph_omitted_count: z.number().int().min(0).nullable(),
right_graph_omitted_count: z.number().int().min(0).nullable(),
status: z.enum(["complete-within-inputs", "partial", "truncated"]),
});
/** Tiered module/entity matching plus a bounded cross-version change graph. */
/** Tiered module/entity matching plus its complete cross-version change graph. */
export const applicationVersionComparisonResultSchema = z
.strictObject({
comparison_id: z.string().regex(/^javc_[a-f0-9]{64}$/u),
@ -244,20 +188,19 @@ export const applicationVersionComparisonResultSchema = z
ambiguous: z.number().int().min(0),
unmatched: z.number().int().min(0),
}),
items: z.array(comparisonItemSchema).max(50_000),
items: z.array(comparisonItemSchema),
graph: javascriptApplicationGraphSchema,
coverage: comparisonCoverageSchema,
evidence_links: z.array(evidenceIdSchema).min(2).max(130),
limitations: z.array(boundedTextSchema).max(1_000),
evidence_links: z.array(evidenceIdSchema).min(2),
limitations: z.array(boundedTextSchema),
})
.superRefine((result, context) => {
const sourceGraphsComplete =
result.coverage.left_graph_status === "complete" &&
result.coverage.right_graph_status === "complete";
if (
result.coverage.status !== "truncated" &&
(result.coverage.status === "complete-within-inputs") !==
sourceGraphsComplete
sourceGraphsComplete
)
context.addIssue({
code: "custom",

View File

@ -9,19 +9,15 @@ import {
type ApplicationNode,
type JavaScriptApplicationGraph,
} from "./javascriptApplicationGraph.js";
import type {
ApplicationVersionComparisonItem,
CompareApplicationVersionsInput,
} from "./javascriptApplicationVersionComparisonSchemas.js";
import type { ApplicationVersionComparisonItem } from "./javascriptApplicationVersionComparisonSchemas.js";
import type {
ApplicationVersionMatchingProjection,
ApplicationVersionNodePair,
} from "./javascriptApplicationVersionKeys.js";
/** All classified entities before the caller's item bound is applied. */
/** All classified entities and their complete ambiguity candidate sets. */
export interface ApplicationVersionItemProjection {
readonly items: ApplicationVersionComparisonItem[];
readonly omittedCandidateReferences: number;
}
interface ItemContext {
@ -33,7 +29,6 @@ interface ItemContext {
readonly rightNativeEvidence: readonly Evidence[];
readonly pairByLeft: ReadonlyMap<string, string>;
readonly pairByRight: ReadonlyMap<string, string>;
readonly maxCandidates: number;
}
type OneSidedNode =
@ -60,9 +55,7 @@ type ComparisonItemSemantic<
/** Classify matched, unmatched, and ambiguous nodes without inventing absence. */
export const classifyJavaScriptApplicationVersions = (
matching: ApplicationVersionMatchingProjection,
context: Omit<ItemContext, "pairByLeft" | "pairByRight" | "maxCandidates"> & {
readonly maxCandidates: CompareApplicationVersionsInput["limits"]["max_candidate_nodes"];
},
context: Omit<ItemContext, "pairByLeft" | "pairByRight">,
): ApplicationVersionItemProjection => {
const pairByLeft = new Map(
matching.pairs.map(({ left, right }) => [
@ -77,31 +70,22 @@ export const classifyJavaScriptApplicationVersions = (
]),
);
const fullContext: ItemContext = { ...context, pairByLeft, pairByRight };
let omittedCandidateReferences = 0;
const leftItems = matching.unmatchedLeft.map((node) => {
const candidates = matching.candidatesForLeft.get(node.node_id) ?? [];
omittedCandidateReferences += Math.max(
0,
candidates.length - context.maxCandidates,
);
return unmatchedItem(
{
left: node,
candidateRightNodeIds: candidates.slice(0, context.maxCandidates),
candidateRightNodeIds: candidates,
},
fullContext,
);
});
const rightItems = matching.unmatchedRight.map((node) => {
const candidates = matching.candidatesForRight.get(node.node_id) ?? [];
omittedCandidateReferences += Math.max(
0,
candidates.length - context.maxCandidates,
);
return unmatchedItem(
{
right: node,
candidateLeftNodeIds: candidates.slice(0, context.maxCandidates),
candidateLeftNodeIds: candidates,
},
fullContext,
);
@ -112,7 +96,6 @@ export const classifyJavaScriptApplicationVersions = (
...leftItems,
...rightItems,
].sort((left, right) => compareCodePoints(left.item_id, right.item_id)),
omittedCandidateReferences,
};
};

View File

@ -143,7 +143,6 @@ const controlledReplayInputShape = {
reproducer_export: z
.strictObject({
path: absolutePathSchema,
approved: z.literal(true),
include_sources: z.boolean().default(false),
})
.optional(),
@ -155,11 +154,10 @@ export const controlledReplayPlanInputSchema = z.strictObject({
mode: z.literal("plan"),
});
/** Parse a replay request carrying literal approval for one exact plan. */
/** Parse a replay request carrying the digest for one exact plan. */
export const controlledReplayExecutionInputSchema = z.strictObject({
...controlledReplayInputShape,
mode: z.literal("execute"),
approved: z.literal(true),
plan_digest: digestSchema,
});

View File

@ -28,11 +28,10 @@ const runtimeOriginsSchema = z
.overwrite((origins) => [...new Set(origins)].sort())
.default([]);
const approvedRuntimeScope = {
const runtimeScope = {
inspector_endpoint: browserEndpointSchema,
allowed_file_roots: runtimeFileRootsSchema,
allowed_origins: runtimeOriginsSchema,
approved: z.literal(true),
};
const requireRuntimeScope = (
@ -56,7 +55,7 @@ const requireRuntimeScope = (
/** Input for listing approved Node/Electron V8 Inspector targets. */
export const listJavaScriptRuntimeTargetsInputSchema = z
.strictObject({
...approvedRuntimeScope,
...runtimeScope,
})
.superRefine(requireRuntimeScope);
export type ListJavaScriptRuntimeTargetsInput = z.infer<
@ -89,7 +88,7 @@ export const javascriptRuntimeObservationLimitsSchema = z.strictObject({
/** Input for one bounded, attach-only V8 Inspector observation. */
export const observeJavaScriptRuntimeInputSchema = z
.strictObject({
...approvedRuntimeScope,
...runtimeScope,
target_id: z.string().trim().min(1).max(256),
runtime_kind: javascriptRuntimeKindSchema.optional(),
observation_ms: z.number().int().min(0).max(10_000).default(100),

View File

@ -26,7 +26,6 @@ export const parseActiveElectronCapture = (
);
const parameters = z
.object({
approved: z.literal(true),
application_path: absolutePathSchema,
application_root: absolutePathSchema,
})
@ -40,7 +39,7 @@ export const parseActiveElectronCapture = (
)
)
throw new TypeError(
"Active Electron Evidence application path disagrees with its approved root",
"Active Electron Evidence application path disagrees with its configured root",
);
return {
kind: "electron-active",

View File

@ -432,7 +432,6 @@ const assertRuntimeParameters = (
.object({
...common,
allowed_file_roots: electronFileRootsSchema,
source_capture_approved: z.boolean(),
})
.passthrough()
.parse(evidence.parameters);
@ -440,13 +439,9 @@ const assertRuntimeParameters = (
throw new TypeError(
"Runtime Evidence target disagrees with its captured result",
);
if (
expected.sourceIncluded &&
(!parameters.include_script_sources ||
parameters.source_capture_approved !== true)
)
if (expected.sourceIncluded && !parameters.include_script_sources)
throw new TypeError(
"Runtime Evidence contains source without source-capture approval",
"Runtime Evidence contains source without source-capture selection",
);
};

View File

@ -224,9 +224,7 @@ export const javaScriptSemanticNodeInputSchema = z.strictObject({
kind: z.enum(JAVASCRIPT_SEMANTIC_NODE_KINDS),
identity: semanticNodeIdentitySchema,
function_node_id: semanticNodeIdSchema.nullable(),
application_node_ids: z
.array(z.string().regex(/^jag_node_[a-f0-9]{64}$/u))
.max(64),
application_node_ids: z.array(z.string().regex(/^jag_node_[a-f0-9]{64}$/u)),
label: z.string().min(1).max(1_024).nullable(),
properties: semanticPropertiesSchema,
evidence: applicationGraphEvidenceSchema,
@ -281,7 +279,7 @@ export const javaScriptSemanticUnknownInputSchema = z.strictObject({
relation_kinds: z.array(z.enum(JAVASCRIPT_SEMANTIC_RELATIONS)).min(1).max(45),
reason: z.enum(JAVASCRIPT_SEMANTIC_UNKNOWN_REASONS),
detail: boundedTextSchema,
candidate_node_ids: z.array(semanticNodeIdSchema).max(1_000),
candidate_node_ids: z.array(semanticNodeIdSchema),
evidence: applicationGraphEvidenceSchema,
});
@ -334,7 +332,7 @@ const familyCoverageSchema = z.strictObject({
status: z.enum(["complete", "partial", "unknown", "unsupported"]),
retained_relations: z.number().int().min(0),
omitted_relations: z.number().int().min(0).nullable(),
unknown_ids: z.array(semanticUnknownIdSchema).max(100_000),
unknown_ids: z.array(semanticUnknownIdSchema),
});
const graphCoverageSchema = z.strictObject({
@ -359,11 +357,11 @@ export const javaScriptSemanticGraphInputSchema = z.strictObject({
schema: z.literal("JavaScriptSemanticRelationGraph"),
root_artifact_sha256: digestSchema,
application_graph_id: z.string().regex(/^jag_[a-f0-9]{64}$/u),
root_node_ids: z.array(semanticNodeIdSchema).min(1).max(10_000),
nodes: z.array(javaScriptSemanticNodeSchema).min(1).max(100_000),
relations: z.array(javaScriptSemanticRelationSchema).max(200_000),
fingerprints: z.array(javaScriptSemanticFingerprintSchema).max(20_000),
unknowns: z.array(javaScriptSemanticUnknownSchema).max(100_000),
root_node_ids: z.array(semanticNodeIdSchema).min(1),
nodes: z.array(javaScriptSemanticNodeSchema).min(1),
relations: z.array(javaScriptSemanticRelationSchema),
fingerprints: z.array(javaScriptSemanticFingerprintSchema),
unknowns: z.array(javaScriptSemanticUnknownSchema),
coverage: graphCoverageSchema,
limitations: z.array(boundedTextSchema).max(1_000),
});

View File

@ -307,3 +307,36 @@ it("excludes candidate edges unless the caller explicitly opts in", () => {
{ relation: "supplies-request-field", resolution: "candidate" },
]);
});
it("retains every structural application node linked to one semantic node", () => {
const original = node("module", "module");
const linked = createJavaScriptSemanticGraphNode({
kind: original.kind,
identity: original.identity,
function_node_id: original.function_node_id,
application_node_ids: Array.from(
{ length: 65 },
(_, index) => `jag_node_${index.toString(16).padStart(64, "0")}`,
),
label: original.label,
properties: original.properties,
evidence: original.evidence,
});
expect(linked.application_node_ids).toHaveLength(65);
});
it("retains all explicit unresolved candidate node identifiers", () => {
const unknown = createJavaScriptSemanticGraphUnknown({
node_id: null,
family: "call-flow",
relation_kinds: ["calls"],
reason: "ambiguous-target",
detail: "All candidate targets are retained.",
candidate_node_ids: Array.from(
{ length: 1_001 },
(_, index) => `jsrg_node_${index.toString(16).padStart(64, "0")}`,
),
evidence: unknownEvidence(),
});
expect(unknown.candidate_node_ids).toHaveLength(1_001);
});

View File

@ -24,7 +24,6 @@ const nodeRuntimeCharacterizationPlanSchema =
export const nodeCharacterizationPreparationInputSchema = z
.strictObject({
preparation_approved: z.literal(true),
selected_alias: z.string().min(1).max(200),
expected_effect: nodeCharacterizationExpectedEffectSchema,
instrumentation: javascriptExportInstrumentationInputSchema,
@ -63,7 +62,6 @@ export const nodeCharacterizationPreparationInputSchema = z
});
export const nodeCharacterizationExecutionInputSchema = z.strictObject({
execution_approved: z.literal(true),
approved_plan_sha256: digestSchema,
preparation: nodeCharacterizationPreparationInputSchema,
});

View File

@ -31,7 +31,6 @@ const sideBindingSchema = z
/** Boundary contract for one bounded authority/candidate process experiment. */
export const processPairedExperimentSchema = z
.object({
approved: z.literal(true),
shared_scenario: processScenarioSchema,
authority: sideBindingSchema,
candidate: sideBindingSchema,

View File

@ -114,16 +114,11 @@ export const processComparisonContract = (
});
/**
* Boundary schema for one explicitly approved, bounded process experiment.
* Boundary schema for one bounded process experiment.
* Defaults are part of the evidence contract and must remain deterministic.
*/
export const processScenarioSchema = z
.object({
approved: z
.literal(true)
.describe(
"Explicit per-call acknowledgement that this operation launches the target",
),
unknown_registry_approved: z
.literal(true)
.optional()

View File

@ -383,7 +383,6 @@ describe("reconstruction verification integrity", () => {
const unknowns = mutations.map((mutation, index) =>
createResidualUnknown(
{
approved: true,
question: `Unresolved replay ${index}`,
severity: "high",
domain: "reconstruction-verification",

View File

@ -167,7 +167,6 @@ export const recordUnknownInputSchema = residualUnknownObjectSchema
.extend({
supporting_evidence_ids: z.array(evidenceIdSchema).max(100).default([]),
contradicting_evidence_ids: z.array(evidenceIdSchema).max(100).default([]),
approved: z.literal(true),
});
export type RecordUnknownInput = z.infer<typeof recordUnknownInputSchema>;
@ -176,7 +175,6 @@ export type RecordUnknownInput = z.infer<typeof recordUnknownInputSchema>;
export const updateUnknownInputSchema = z.object({
unknown_id: unknownIdSchema,
expected_revision: z.number().int().min(1),
approved: z.literal(true),
status: residualUnknownObjectSchema.shape.status,
severity: residualUnknownObjectSchema.shape.severity,
supporting_evidence_ids: z.array(evidenceIdSchema).max(100),

View File

@ -3,63 +3,31 @@ import { z } from "zod";
import { inspectWebPageWithSourceInputSchema } from "./browserObservation.js";
import { webTextArtifactSchema } from "./webContentArtifact.js";
const webBundleLimitsSchema = z.object({
max_findings: z.number().int().min(1).max(10_000).default(1_000),
max_ast_nodes: z.number().int().min(1).max(2_000_000).default(250_000),
max_source_maps: z.number().int().min(1).max(1_000).default(100),
max_source_map_bytes: z
.number()
.int()
.min(1)
.max(16 * 1_024 * 1_024)
.default(4 * 1_024 * 1_024),
max_total_source_map_bytes: z
.number()
.int()
.min(1)
.max(64 * 1_024 * 1_024)
.default(16 * 1_024 * 1_024),
max_source_map_mappings: z.number().int().min(1).max(100_000).default(10_000),
max_original_sources: z.number().int().min(1).max(20_000).default(2_000),
});
/** Provider-owned parser and network safety bounds, never caller settings. */
export const WEB_BUNDLE_ANALYSIS_LIMITS = {
max_ast_nodes: 250_000,
max_source_maps: 100,
max_source_map_bytes: 4 * 1_024 * 1_024,
max_total_source_map_bytes: 16 * 1_024 * 1_024,
max_source_map_mappings: 10_000,
} as const;
/** Capture-and-analyze input with separate source and source-map approvals. */
const analyzeWebBundleFactsSchema =
/** Capture-and-analyze input with optional source-map fetching. */
export const analyzeWebBundleToolInputSchema =
inspectWebPageWithSourceInputSchema.safeExtend({
analysis_limits: webBundleLimitsSchema.default({
max_findings: 1_000,
max_ast_nodes: 250_000,
max_source_maps: 100,
max_source_map_bytes: 4 * 1_024 * 1_024,
max_total_source_map_bytes: 16 * 1_024 * 1_024,
max_source_map_mappings: 10_000,
max_original_sources: 2_000,
}),
fetch_source_maps: z.boolean().default(false),
});
export const analyzeWebBundleInputSchema = z
.union([
analyzeWebBundleFactsSchema.safeExtend({
fetch_source_maps: z.literal(false).default(false),
source_map_fetch_approved: z.literal(false).default(false),
}),
analyzeWebBundleFactsSchema.safeExtend({
fetch_source_maps: z.literal(true),
source_map_fetch_approved: z.literal(true),
}),
])
.superRefine((input, context) => {
if (
input.analysis_limits.max_source_map_bytes >
input.analysis_limits.max_total_source_map_bytes
)
context.addIssue({
code: "custom",
path: ["analysis_limits", "max_source_map_bytes"],
message:
"Per-map source-map byte limit cannot exceed the aggregate limit",
});
});
export type AnalyzeWebBundleInput = z.infer<typeof analyzeWebBundleInputSchema>;
export const analyzeWebBundleInputSchema =
analyzeWebBundleToolInputSchema.transform((input) => ({
...input,
analysis_limits: WEB_BUNDLE_ANALYSIS_LIMITS,
})) as z.ZodType<AnalyzeWebBundleInput>;
export type AnalyzeWebBundleInput = z.infer<
typeof inspectWebPageWithSourceInputSchema
> & {
readonly fetch_source_maps: boolean;
readonly analysis_limits: typeof WEB_BUNDLE_ANALYSIS_LIMITS;
};
const sourceLocationSchema = z.object({
script_key: z.string(),
@ -258,7 +226,6 @@ export const webBundleAnalysisSchema = z.object({
parsed_scripts: z.number().int().min(0),
parse_failures: z.number().int().min(0),
visited_ast_nodes: z.number().int().min(0),
dropped_findings: z.number().int().min(0),
}),
limitations: z.array(z.string()),
});

View File

@ -73,7 +73,7 @@ describe("web bundle analyzer", () => {
expect(result.completeness.status).toBe("complete_within_limits");
});
it("reports parser gaps and finding truncation instead of claiming completeness", () => {
it("reports parser gaps without imposing a finding cap", () => {
const malformed = inspection("function broken( {");
const failed = analyzeCapturedWebBundle(malformed, input());
expect(failed.completeness).toMatchObject({
@ -82,15 +82,12 @@ describe("web bundle analyzer", () => {
});
expect(failed.unknowns[0]?.dimension).toBe("javascript_ast");
const bounded = analyzeCapturedWebBundle(
const analyzed = analyzeCapturedWebBundle(
inspection("fetch('/one'); fetch('/two'); fetch('/three');"),
input({ analysis_limits: { max_findings: 1 } }),
input(),
);
expect(bounded.observations.endpoints).toHaveLength(1);
expect(bounded.completeness).toMatchObject({
status: "truncated",
dropped_findings: 2,
});
expect(analyzed.observations.endpoints).toHaveLength(3);
expect(analyzed.completeness.status).toBe("complete_within_limits");
});
it("propagates source-map truncation into completeness and unknowns", () => {
@ -121,7 +118,7 @@ describe("web bundle analyzer", () => {
expect(analysis.completeness.status).toBe("truncated");
expect(analysis.unknowns).toContainEqual({
dimension: "source_maps",
reason: "Source-map evidence was truncated by approved limits",
reason: "Source-map evidence was truncated by provider safety limits",
affected_script_keys: [`scr_${"1".repeat(64)}`, `scr_${"2".repeat(64)}`],
});
});
@ -149,9 +146,7 @@ const input = (overrides: Record<string, unknown> = {}) =>
cdp_endpoint: "http://127.0.0.1:9222",
allowed_origins: [origin],
target_id: "page-1",
approved: true,
include_script_sources: true,
source_capture_approved: true,
}),
...overrides,
});

Some files were not shown because too many files have changed in this diff Show More