Merge pull request #294 from morluto/codex/agent-tool-guidance

fix(contracts): make agent-facing schemas self-describing
This commit is contained in:
morluto authored and GitHub committed 2026-07-17 10:55:25 +08:00
commit 0df11dd4d1
15 files changed
+989 -316

No files matched your search

+3 -1
View File
@@ -102,7 +102,9 @@ Use ESM TypeScript, two-space indentation, and Prettier defaults. Keep compiler
Name tests `*.test.ts`. Use Vitest and production seams (`tests/fixtures/`) rather than module mocks. Domain tests assert pure behavior; adapter tests use fake launcher/socket seams; MCP tests connect with the client SDK version pinned in `package.json`. Preserve the canonical tool inventory defined by `TOOL_CONTRACTS` and verified through `CATALOG_IDENTITY` and generated product metadata. Cover malformed input, cancellation, timeouts, process exit, concurrency, limits, and clean shutdown. Real Hopper, Ghidra, browser, JavaScript replay, managed conformance, and any real managed-tool claims cannot be replaced by mocks; use the corresponding `verify:*` command.
MCP tool catalogs must remain complete and self-describing. Prefer capability- and session-scoped tool advertisement over schema truncation. Do not impose arbitrary serialized-byte ceilings on tool schemas or `tools/list`; assess model-context cost only against a named client's actual model-facing projection and tokenizer.
MCP tools are agent-computer interfaces. Optimize them first for successful agent use: distinct purposes, clear names and parameters, relevant result context, actionable errors, and realistic multi-step workflows. Validate tool-design changes with representative agent tasks and transcripts when possible; schema validity and transport success alone do not prove that a tool is easy for an agent to use.
MCP tool catalogs must remain complete and self-describing. Prefer capability- and session-scoped tool advertisement over schema truncation. Never use serialized byte counts as a proxy for model-context cost, token use, tool usability, or agent performance. Measure bytes only when evaluating a named transport, storage, or protocol constraint. Any model-context or token-efficiency claim must identify the client and its actual model-facing projection and tokenizer, since clients may omit, transform, dereference, or otherwise reproject MCP schemas. Do not weaken names, descriptions, schemas, examples, errors, or result context to improve a byte-based metric.
## Commit & Pull Request Guidelines
@@ -24,7 +24,7 @@
<li><span><span class="tsd-kind-parameter">value</span>: <span class="tsd-signature-type">Readonly</span><span class="tsd-signature-symbol">&lt;</span><span class="tsd-signature-type">Record</span><span class="tsd-signature-symbol">&lt;</span><span class="tsd-signature-type">string</span><span class="tsd-signature-symbol">,</span> <span class="tsd-signature-type">unknown</span><span class="tsd-signature-symbol">&gt;</span><span class="tsd-signature-symbol">&gt;</span></span></li></ul></div>
<h4 class="tsd-returns-title">Returns <span class="tsd-signature-type">Record</span><span class="tsd-signature-symbol">&lt;</span><span class="tsd-signature-type">string</span><span class="tsd-signature-symbol">,</span> <span class="tsd-signature-type">unknown</span><span class="tsd-signature-symbol">&gt;</span></h4><aside class="tsd-sources">
<ul>
<li>Defined in <a href="https://github.com/morluto/rea/blob/main/src/server/toolRegistrationOptions.ts#L62">src/server/toolRegistrationOptions.ts:62</a></li></ul></aside></div></li></ul></section></div>
<li>Defined in <a href="https://github.com/morluto/rea/blob/main/src/server/toolRegistrationOptions.ts#L67">src/server/toolRegistrationOptions.ts:67</a></li></ul></aside></div></li></ul></section></div>
<div class="col-sidebar">
<div class="page-menu">
<div class="tsd-navigation settings">
+1
View File
@@ -57,6 +57,7 @@ export function cliCommandOptionNames(
cli: unknown,
name: string,
): readonly string[];
export function cliCommandDescriptionIssues(cli: unknown): readonly string[];
export function serializeProductCatalog(
catalog: ProductCatalog,
): Promise<string>;
+25
View File
@@ -79,6 +79,31 @@ export const cliCommandOptionNames = (cli, name) => {
);
};
/** Report primary CLI fields whose generated JSON Schema lacks help text. */
export const cliCommandDescriptionIssues = (cli) => {
const commands = Cli.toCommands.get(cli);
if (commands === undefined)
throw new Error("Incur did not expose the registered REA CLI commands");
const missing = [];
for (const [commandName, command] of commands) {
if ("_alias" in command) continue;
for (const groupName of ["args", "options"]) {
if (!(groupName in command) || command[groupName] === undefined) continue;
const properties = z.toJSONSchema(command[groupName]).properties ?? {};
for (const [propertyName, property] of Object.entries(properties)) {
if (
typeof property !== "object" ||
property === null ||
typeof property.description !== "string" ||
property.description.trim() === ""
)
missing.push(`${commandName}.${groupName}.${propertyName}`);
}
}
}
return missing;
};
const assertSameNames = (label, actual, expected) => {
const actualSet = new Set(actual);
const expectedSet = new Set(expected);
+287 -130
View File
@@ -106,8 +106,17 @@ const registerCoreCommands = (
logger: Logger,
): void => {
const overviewOptions = z.object({
detail: z.enum(["concise", "detailed"]).default("concise"),
limit: z.number().int().min(1).max(50).default(10),
detail: z
.enum(["concise", "detailed"])
.default("concise")
.describe("Overview detail level"),
limit: z
.number()
.int()
.min(1)
.max(50)
.default(10)
.describe("Maximum overview items to return"),
snapshot: z
.string()
.min(1)
@@ -154,7 +163,11 @@ const registerCoreCommands = (
address: z.string().describe("Procedure address"),
}),
options: z.object({
snapshot: z.string().min(1).optional(),
snapshot: z
.string()
.min(1)
.optional()
.describe("Load and update a local analysis snapshot"),
provider: providerSelectionOption,
}),
run: ({ args, options }) =>
@@ -269,7 +282,11 @@ const registerXrefsCommand = (
address: z.string().describe("Hexadecimal address"),
}),
options: z.object({
snapshot: z.string().min(1).optional(),
snapshot: z
.string()
.min(1)
.optional()
.describe("Load and update a local analysis snapshot"),
provider: providerSelectionOption,
}),
run: ({ args, options }) =>
@@ -295,10 +312,29 @@ const registerTraceCommand = (
query: z.string().min(1).describe("Literal feature query"),
}),
options: z.object({
caseSensitive: z.boolean().default(false),
limit: z.number().int().min(1).max(100).default(20),
maxOperations: z.number().int().min(1).max(100).default(20),
snapshot: z.string().min(1).optional(),
caseSensitive: z
.boolean()
.default(false)
.describe("Match the query with exact letter case"),
limit: z
.number()
.int()
.min(1)
.max(100)
.default(20)
.describe("Maximum matching roots to examine"),
maxOperations: z
.number()
.int()
.min(1)
.max(100)
.default(20)
.describe("Maximum analysis operations in the trace"),
snapshot: z
.string()
.min(1)
.optional()
.describe("Load and update a local analysis snapshot"),
provider: providerSelectionOption,
}),
alias: {
@@ -351,11 +387,36 @@ const registerFunctionCommand = (
address: z.string().describe("Procedure name or address"),
}),
options: z.object({
includeAssembly: z.boolean().default(false),
limit: z.number().int().min(1).max(500).default(100),
maxPseudocodeChars: z.number().int().min(1).max(100_000).default(20_000),
maxInstructions: z.number().int().min(1).max(5_000).default(500),
snapshot: z.string().min(1).optional(),
includeAssembly: z
.boolean()
.default(false)
.describe("Include bounded assembly instructions"),
limit: z
.number()
.int()
.min(1)
.max(500)
.default(100)
.describe("Maximum referenced items to return"),
maxPseudocodeChars: z
.number()
.int()
.min(1)
.max(100_000)
.default(20_000)
.describe("Maximum pseudocode characters to return"),
maxInstructions: z
.number()
.int()
.min(1)
.max(5_000)
.default(500)
.describe("Maximum assembly instructions to return"),
snapshot: z
.string()
.min(1)
.optional()
.describe("Load and update a local analysis snapshot"),
provider: providerSelectionOption,
}),
alias: {
@@ -392,12 +453,36 @@ const registerSearchCommand = (
pattern: z.string().min(1).describe("Literal text or regex pattern"),
}),
options: z.object({
kind: z.enum(["strings", "procedures"]).default("strings"),
mode: z.enum(["literal", "regex"]).default("literal"),
caseSensitive: z.boolean().default(false),
offset: z.number().int().min(0).default(0),
limit: z.number().int().min(1).max(100).default(100),
snapshot: z.string().min(1).optional(),
kind: z
.enum(["strings", "procedures"])
.default("strings")
.describe("Analyzed item kind to search"),
mode: z
.enum(["literal", "regex"])
.default("literal")
.describe("Interpret the pattern as literal text or a regex"),
caseSensitive: z
.boolean()
.default(false)
.describe("Match the pattern with exact letter case"),
offset: z
.number()
.int()
.min(0)
.default(0)
.describe("Zero-based matching-item offset"),
limit: z
.number()
.int()
.min(1)
.max(100)
.default(100)
.describe("Maximum matching items to return"),
snapshot: z
.string()
.min(1)
.optional()
.describe("Load and update a local analysis snapshot"),
provider: providerSelectionOption,
}),
alias: { caseSensitive: "case-sensitive" },
@@ -495,12 +580,38 @@ const registerArtifactCommands = (
path: z.string().describe("Application or package path"),
}),
options: z.object({
offset: z.number().int().min(0).default(0),
limit: z.number().int().min(1).max(500).default(100),
integrityPolicy: z.enum(["fail", "record-and-continue"]).default("fail"),
integrityContinueApproved: z.boolean().default(false),
maxIntegrityMismatches: z.number().int().min(1).max(100).default(10),
nativeMountApproved: z.boolean().default(false),
offset: z
.number()
.int()
.min(0)
.default(0)
.describe("Zero-based offset for graph nodes, occurrences, and edges"),
limit: z
.number()
.int()
.min(1)
.max(500)
.default(100)
.describe("Maximum nodes, occurrences, and edges per result section"),
integrityPolicy: z
.enum(["fail", "record-and-continue"])
.default("fail")
.describe("Behavior when declared artifact integrity does not match"),
integrityContinueApproved: z
.boolean()
.default(false)
.describe("Approve continuing after recorded integrity mismatches"),
maxIntegrityMismatches: z
.number()
.int()
.min(1)
.max(100)
.default(10)
.describe("Maximum integrity mismatches to record before stopping"),
nativeMountApproved: z
.boolean()
.default(false)
.describe("Approve read-only native mounting when required"),
}),
alias: {
integrityPolicy: "integrity-policy",
@@ -537,7 +648,8 @@ const registerArtifactCommands = (
occurrenceIds: z
.array(z.string().regex(/^occ_[a-f0-9]{64}$/u))
.min(1)
.max(500),
.max(500)
.describe("Exact artifact occurrence IDs selected for extraction"),
}),
alias: { outputRoot: "output-root", occurrenceIds: "occurrence-ids" },
run: ({ args }) =>
@@ -556,6 +668,49 @@ const registerArtifactCommands = (
});
};
const managedOffset = (subject: string) =>
z.number().int().min(0).default(0).describe(`Zero-based ${subject} offset`);
const managedLimit = (subject: string, maximum: number, fallback: number) =>
z
.number()
.int()
.min(1)
.max(maximum)
.default(fallback)
.describe(`Maximum ${subject} to return`);
const managedInspectionLimits = {
maxFileBytes: z
.number()
.int()
.min(4_096)
.max(1_073_741_824)
.default(268_435_456)
.describe("Maximum managed artifact file size accepted for inspection"),
maxMetadataBytes: z
.number()
.int()
.min(256)
.max(268_435_456)
.default(67_108_864)
.describe("Maximum CLI metadata region size accepted for inspection"),
maxTableRows: z
.number()
.int()
.min(1)
.max(1_000_000)
.default(100_000)
.describe("Maximum rows accepted in any CLI metadata table"),
maxHeapItemBytes: z
.number()
.int()
.min(1)
.max(16_777_216)
.default(1_048_576)
.describe("Maximum bytes accepted for one CLI metadata heap item"),
};
const registerManagedCommands = (
cli: ReturnType<typeof Cli.create>,
logger: Logger,
@@ -566,31 +721,46 @@ const registerManagedCommands = (
path: z.string().describe("Managed PE executable or assembly path"),
}),
options: z.object({
referenceOffset: z.number().int().min(0).default(0),
referenceLimit: z.number().int().min(1).max(500).default(100),
resourceOffset: z.number().int().min(0).default(0),
resourceLimit: z.number().int().min(1).max(500).default(100),
attributeOffset: z.number().int().min(0).default(0),
attributeLimit: z.number().int().min(1).max(500).default(100),
maxFileBytes: z
referenceOffset: z
.number()
.int()
.min(4_096)
.max(1_073_741_824)
.default(268_435_456),
maxMetadataBytes: z
.number()
.int()
.min(256)
.max(268_435_456)
.default(67_108_864),
maxTableRows: z.number().int().min(1).max(1_000_000).default(100_000),
maxHeapItemBytes: z
.min(0)
.default(0)
.describe("Zero-based assembly-reference offset"),
referenceLimit: z
.number()
.int()
.min(1)
.max(16_777_216)
.default(1_048_576),
.max(500)
.default(100)
.describe("Maximum assembly references to return"),
resourceOffset: z
.number()
.int()
.min(0)
.default(0)
.describe("Zero-based manifest-resource offset"),
resourceLimit: z
.number()
.int()
.min(1)
.max(500)
.default(100)
.describe("Maximum manifest resources to return"),
attributeOffset: z
.number()
.int()
.min(0)
.default(0)
.describe("Zero-based assembly-attribute offset"),
attributeLimit: z
.number()
.int()
.min(1)
.max(500)
.default(100)
.describe("Maximum assembly attributes to return"),
...managedInspectionLimits,
}),
alias: {
referenceOffset: "reference-offset",
@@ -632,48 +802,38 @@ const registerManagedCommands = (
path: z.string().describe("Managed PE executable or assembly path"),
}),
options: z.object({
typeOffset: z.number().int().min(0).default(0),
typeLimit: z.number().int().min(1).max(500).default(100),
methodOffset: z.number().int().min(0).default(0),
methodLimit: z.number().int().min(1).max(500).default(100),
fieldOffset: z.number().int().min(0).default(0),
fieldLimit: z.number().int().min(1).max(500).default(100),
memberRefOffset: z.number().int().min(0).default(0),
memberRefLimit: z.number().int().min(1).max(500).default(100),
edgeOffset: z.number().int().min(0).default(0),
edgeLimit: z.number().int().min(1).max(1_000).default(250),
instructionAnchorLimit: z.number().int().min(0).max(500).default(100),
maxFileBytes: z
typeOffset: managedOffset("type definition"),
typeLimit: managedLimit("type definitions", 500, 100),
methodOffset: managedOffset("method definition"),
methodLimit: managedLimit("method definitions", 500, 100),
fieldOffset: managedOffset("field definition"),
fieldLimit: managedLimit("field definitions", 500, 100),
memberRefOffset: managedOffset("member reference"),
memberRefLimit: managedLimit("member references", 500, 100),
edgeOffset: managedOffset("member edge"),
edgeLimit: managedLimit("member edges", 1_000, 250),
instructionAnchorLimit: z
.number()
.int()
.min(4_096)
.max(1_073_741_824)
.default(268_435_456),
maxMetadataBytes: z
.number()
.int()
.min(256)
.max(268_435_456)
.default(67_108_864),
maxTableRows: z.number().int().min(1).max(1_000_000).default(100_000),
maxHeapItemBytes: z
.number()
.int()
.min(1)
.max(16_777_216)
.default(1_048_576),
.min(0)
.max(500)
.default(100)
.describe("Maximum CIL instruction anchors per method"),
...managedInspectionLimits,
maxMethodBodyBytes: z
.number()
.int()
.min(1)
.max(16_777_216)
.default(1_048_576),
.default(1_048_576)
.describe("Maximum CIL method body size accepted for inspection"),
maxMethodInstructions: z
.number()
.int()
.min(1)
.max(100_000)
.default(10_000),
.default(10_000)
.describe("Maximum decoded CIL instructions per method"),
}),
alias: {
typeOffset: "type-offset",
@@ -729,31 +889,13 @@ const registerManagedCommands = (
path: z.string().describe("Managed PE executable or assembly path"),
}),
options: z.object({
moduleRefOffset: z.number().int().min(0).default(0),
moduleRefLimit: z.number().int().min(1).max(500).default(100),
importOffset: z.number().int().min(0).default(0),
importLimit: z.number().int().min(1).max(500).default(100),
implementationOffset: z.number().int().min(0).default(0),
implementationLimit: z.number().int().min(1).max(500).default(100),
maxFileBytes: z
.number()
.int()
.min(4_096)
.max(1_073_741_824)
.default(268_435_456),
maxMetadataBytes: z
.number()
.int()
.min(256)
.max(268_435_456)
.default(67_108_864),
maxTableRows: z.number().int().min(1).max(1_000_000).default(100_000),
maxHeapItemBytes: z
.number()
.int()
.min(1)
.max(16_777_216)
.default(1_048_576),
moduleRefOffset: managedOffset("module reference"),
moduleRefLimit: managedLimit("module references", 500, 100),
importOffset: managedOffset("platform import"),
importLimit: managedLimit("platform imports", 500, 100),
implementationOffset: managedOffset("native implementation"),
implementationLimit: managedLimit("native implementations", 500, 100),
...managedInspectionLimits,
}),
alias: {
moduleRefOffset: "module-ref-offset",
@@ -800,46 +942,54 @@ const registerManagedCommands = (
.describe("Candidate managed PE executable or assembly"),
}),
options: z.object({
maxMethodMatches: z.number().int().min(1).max(50_000).default(10_000),
maxFieldMatches: z.number().int().min(0).max(50_000).default(5_000),
maxCandidates: z.number().int().min(1).max(500).default(50),
typeLimit: z.number().int().min(1).max(500).default(500),
methodLimit: z.number().int().min(1).max(500).default(500),
fieldLimit: z.number().int().min(1).max(500).default(500),
memberRefLimit: z.number().int().min(1).max(500).default(500),
edgeLimit: z.number().int().min(1).max(1_000).default(1_000),
instructionAnchorLimit: z.number().int().min(0).max(500).default(500),
maxFileBytes: z
.number()
.int()
.min(4_096)
.max(1_073_741_824)
.default(268_435_456),
maxMetadataBytes: z
.number()
.int()
.min(256)
.max(268_435_456)
.default(67_108_864),
maxTableRows: z.number().int().min(1).max(1_000_000).default(100_000),
maxHeapItemBytes: z
maxMethodMatches: z
.number()
.int()
.min(1)
.max(16_777_216)
.default(1_048_576),
.max(50_000)
.default(10_000)
.describe("Maximum exact method matches to return"),
maxFieldMatches: z
.number()
.int()
.min(0)
.max(50_000)
.default(5_000)
.describe("Maximum exact field matches to return"),
maxCandidates: z
.number()
.int()
.min(1)
.max(500)
.default(50)
.describe("Maximum ambiguous candidates per unmatched member"),
typeLimit: managedLimit("type definitions per artifact", 500, 500),
methodLimit: managedLimit("method definitions per artifact", 500, 500),
fieldLimit: managedLimit("field definitions per artifact", 500, 500),
memberRefLimit: managedLimit("member references per artifact", 500, 500),
edgeLimit: managedLimit("member edges per artifact", 1_000, 1_000),
instructionAnchorLimit: z
.number()
.int()
.min(0)
.max(500)
.default(500)
.describe("Maximum CIL instruction anchors per compared method"),
...managedInspectionLimits,
maxMethodBodyBytes: z
.number()
.int()
.min(1)
.max(16_777_216)
.default(1_048_576),
.default(1_048_576)
.describe("Maximum CIL method body size accepted for comparison"),
maxMethodInstructions: z
.number()
.int()
.min(1)
.max(100_000)
.default(100_000),
.default(100_000)
.describe("Maximum decoded CIL instructions per compared method"),
}),
alias: {
maxMethodMatches: "max-method-matches",
@@ -1006,7 +1156,10 @@ const registerNativeCommands = (
description: "Parse app plist metadata without launching Hopper",
args: z.object({ path: z.string().describe("App or Mach-O path") }),
options: z.object({
relativePath: z.string().default("Contents/Info.plist"),
relativePath: z
.string()
.default("Contents/Info.plist")
.describe("Plist path relative to the app root"),
}),
alias: { relativePath: "relative-path" },
run: ({ args, options }) =>
@@ -1023,7 +1176,11 @@ const registerNativeCommands = (
description: "Demangle a bounded Swift symbol batch without Hopper",
args: z.object({
path: z.string().describe("Artifact path used for evidence identity"),
symbols: z.array(z.string().min(1)).min(1).max(500),
symbols: z
.array(z.string().min(1))
.min(1)
.max(500)
.describe("Swift mangled symbols to demangle"),
}),
run: ({ args }) =>
logCliCommand(logger, "demangle-swift", () =>
+75 -16
View File
@@ -22,8 +22,13 @@ import type { Logger } from "./logger.js";
import { CLI_COMMANDS } from "./cliCommandNames.js";
const scopeOptions = {
allowedOrigins: z.array(z.string().min(1)).optional(),
approved: z.boolean().default(false),
allowedOrigins: z
.array(z.string().min(1))
.optional()
.describe(
"Exact origins to observe; defaults to REA_BROWSER_ALLOWED_ORIGINS_JSON",
),
approved: z.boolean().default(false).describe("Approve passive observation"),
};
/** Register WebMCP, capture-diff, and screenshot CLI equivalents. */
@@ -43,19 +48,47 @@ const registerWebMcp = (
): void => {
cli.command(CLI_COMMANDS.discoverWebMcpTools, {
description: "Passively discover page-declared WebMCP tools",
args: z.object({ endpoint: z.string(), targetId: z.string() }),
args: z.object({
endpoint: z.string().describe("Configured loopback CDP HTTP endpoint"),
targetId: z.string().describe("Target ID from list-browser-targets"),
}),
options: z.object({
...scopeOptions,
observationMs: z.number().int().min(0).max(10_000).default(100),
maxTools: z.number().int().min(1).max(5_000).default(500),
observationMs: z
.number()
.int()
.min(0)
.max(10_000)
.default(100)
.describe("Observation duration in milliseconds"),
maxTools: z
.number()
.int()
.min(1)
.max(5_000)
.default(500)
.describe("Maximum page-declared WebMCP tools to return"),
maxSchemaBytes: z
.number()
.int()
.min(1)
.max(1_024 * 1_024)
.default(256 * 1_024),
maxSchemaNodes: z.number().int().min(1).max(100_000).default(5_000),
maxSchemaDepth: z.number().int().min(1).max(100).default(20),
.default(256 * 1_024)
.describe("Maximum serialized size of one declared tool schema"),
maxSchemaNodes: z
.number()
.int()
.min(1)
.max(100_000)
.default(5_000)
.describe("Maximum nodes in one declared tool schema"),
maxSchemaDepth: z
.number()
.int()
.min(1)
.max(100)
.default(20)
.describe("Maximum nesting depth in one declared tool schema"),
}),
run: ({ args, options }) =>
logCliCommand(logger, "discover-webmcp-tools", async () => {
@@ -90,9 +123,18 @@ const registerCaptureDiff = (
): void => {
cli.command(CLI_COMMANDS.compareWebCaptures, {
description: "Compare two normalized web capture JSON values",
args: z.object({ beforeJson: z.string(), afterJson: z.string() }),
args: z.object({
beforeJson: z.string().describe("Earlier normalized web capture JSON"),
afterJson: z.string().describe("Later normalized web capture JSON"),
}),
options: z.object({
maxChanges: z.number().int().min(1).max(20_000).default(2_000),
maxChanges: z
.number()
.int()
.min(1)
.max(20_000)
.default(2_000)
.describe("Maximum normalized changes to return"),
}),
run: ({ args, options }) =>
logCliCommand(logger, "compare-web-captures", async () => {
@@ -119,16 +161,23 @@ const registerScreenshot = (
): void => {
cli.command(CLI_COMMANDS.captureWebScreenshot, {
description: "Capture an explicitly approved visible page viewport",
args: z.object({ endpoint: z.string(), targetId: z.string() }),
args: z.object({
endpoint: z.string().describe("Configured loopback CDP HTTP endpoint"),
targetId: z.string().describe("Target ID from list-browser-targets"),
}),
options: z.object({
...scopeOptions,
screenshotApproved: z.boolean().default(false),
screenshotApproved: z
.boolean()
.default(false)
.describe("Approve capturing the visible page viewport"),
maximumImageBytes: z
.number()
.int()
.min(1)
.max(8 * 1_024 * 1_024)
.default(4 * 1_024 * 1_024),
.default(4 * 1_024 * 1_024)
.describe("Maximum captured PNG size"),
}),
run: ({ args, options }) =>
logCliCommand(logger, "capture-web-screenshot", async () => {
@@ -160,15 +209,25 @@ const registerScreenshotDiff = (
): void => {
cli.command(CLI_COMMANDS.compareWebScreenshots, {
description: "Compare two self-verifying PNG artifact JSON values",
args: z.object({ beforeJson: z.string(), afterJson: z.string() }),
args: z.object({
beforeJson: z.string().describe("Earlier screenshot artifact JSON"),
afterJson: z.string().describe("Later screenshot artifact JSON"),
}),
options: z.object({
channelThreshold: z.number().int().min(0).max(255).default(0),
channelThreshold: z
.number()
.int()
.min(0)
.max(255)
.default(0)
.describe("Per-channel difference threshold for changed pixels"),
maximumPixels: z
.number()
.int()
.min(1)
.max(32_000_000)
.default(16_000_000),
.default(16_000_000)
.describe("Maximum decoded pixels per screenshot"),
}),
run: ({ args, options }) =>
logCliCommand(logger, "compare-web-screenshots", async () => {
+177 -112
View File
@@ -31,6 +31,23 @@ const browserScopeOptions = {
approved: z.boolean().default(false).describe("Approve passive observation"),
};
const boundedCount = (
subject: string,
maximum: number,
fallback: number,
minimum = 1,
) =>
z
.number()
.int()
.min(minimum)
.max(maximum)
.default(fallback)
.describe(`Maximum ${subject}`);
const boundedBytes = (subject: string, maximum: number, fallback: number) =>
boundedCount(`${subject} in bytes`, maximum, fallback);
/** Register CLI equivalents of the passive browser MCP tools. */
export const registerBrowserCommands = (
cli: ReturnType<typeof Cli.create>,
@@ -53,8 +70,13 @@ const registerTargetList = (
}),
options: z.object({
...browserScopeOptions,
offset: z.number().int().min(0).default(0),
limit: z.number().int().min(1).max(200).default(100),
offset: z
.number()
.int()
.min(0)
.default(0)
.describe("Zero-based browser-target offset"),
limit: boundedCount("browser targets to return", 200, 100),
}),
run: ({ args, options }) =>
logCliCommand(logger, "list-browser-targets", async () => {
@@ -92,88 +114,114 @@ const registerPageInspection = (
}),
options: z.object({
...browserScopeOptions,
observationMs: z.number().int().min(0).max(10_000).default(500),
includeAccessibilityText: z.boolean().default(false),
includeConsoleText: z.boolean().default(false),
consoleTextApproved: z.boolean().default(false),
includeJsonBodyShapes: z.boolean().default(false),
jsonBodySchemaApproved: z.boolean().default(false),
includeWebsocketShapes: z.boolean().default(false),
websocketShapeApproved: z.boolean().default(false),
includeScriptSources: z.boolean().default(false),
includeStorageKeys: z.boolean().default(false),
maxFrames: z.number().int().min(1).max(1_000).default(200),
maxDomNodes: z.number().int().min(1).max(10_000).default(2_000),
maxAxNodes: z.number().int().min(1).max(10_000).default(2_000),
maxAxTextFieldBytes: z
.number()
.int()
.min(1)
.max(16 * 1_024)
.default(1_024),
maxTotalAxTextBytes: z
.number()
.int()
.min(1)
.max(1_024 * 1_024)
.default(64 * 1_024),
maxScripts: z.number().int().min(1).max(1_000).default(200),
maxResources: z.number().int().min(1).max(10_000).default(2_000),
maxWorkers: z.number().int().min(1).max(5_000).default(500),
maxStorageKeys: z.number().int().min(1).max(10_000).default(1_000),
maxScriptSourceBytes: z
.number()
.int()
.min(1)
.max(4 * 1_024 * 1_024)
.default(1_024 * 1_024),
maxTotalScriptSourceBytes: z
.number()
.int()
.min(1)
.max(16 * 1_024 * 1_024)
.default(4 * 1_024 * 1_024),
maxNetworkEvents: z.number().int().min(1).max(10_000).default(1_000),
maxConsoleEvents: z.number().int().min(1).max(2_000).default(200),
maxConsoleTextFieldBytes: z
.number()
.int()
.min(1)
.max(16 * 1_024)
.default(1_024),
maxTotalConsoleTextBytes: z
.number()
.int()
.min(1)
.max(1_024 * 1_024)
.default(64 * 1_024),
maxJsonBodyBytes: z
.number()
.int()
.min(1)
.max(4 * 1_024 * 1_024)
.default(1_024 * 1_024),
maxTotalJsonBodyBytes: z
.number()
.int()
.min(1)
.max(16 * 1_024 * 1_024)
.default(4 * 1_024 * 1_024),
maxJsonShapeNodes: z.number().int().min(1).max(100_000).default(5_000),
maxJsonShapeDepth: z.number().int().min(1).max(100).default(20),
maxWebsocketEvents: z.number().int().min(1).max(5_000).default(500),
maxWebsocketShapeBytes: z
.number()
.int()
.min(1)
.max(1_024 * 1_024)
.default(64 * 1_024),
maxTotalWebsocketShapeBytes: z
.number()
.int()
.min(1)
.max(16 * 1_024 * 1_024)
.default(1_024 * 1_024),
observationMs: boundedCount(
"observation duration in milliseconds",
10_000,
500,
0,
),
includeAccessibilityText: z
.boolean()
.default(false)
.describe("Include bounded accessibility text"),
includeConsoleText: z
.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"),
includeStorageKeys: z
.boolean()
.default(false)
.describe("Include storage key names without values"),
maxFrames: boundedCount("page frames", 1_000, 200),
maxDomNodes: boundedCount("DOM nodes", 10_000, 2_000),
maxAxNodes: boundedCount("accessibility nodes", 10_000, 2_000),
maxAxTextFieldBytes: boundedBytes(
"one accessibility text field",
16 * 1_024,
1_024,
),
maxTotalAxTextBytes: boundedBytes(
"total accessibility text",
1_024 * 1_024,
64 * 1_024,
),
maxScripts: boundedCount("scripts", 1_000, 200),
maxResources: boundedCount("resources", 10_000, 2_000),
maxWorkers: boundedCount("workers", 5_000, 500),
maxStorageKeys: boundedCount("storage keys", 10_000, 1_000),
maxScriptSourceBytes: boundedBytes(
"one script source",
4 * 1_024 * 1_024,
1_024 * 1_024,
),
maxTotalScriptSourceBytes: boundedBytes(
"total script source",
16 * 1_024 * 1_024,
4 * 1_024 * 1_024,
),
maxNetworkEvents: boundedCount("network events", 10_000, 1_000),
maxConsoleEvents: boundedCount("console events", 2_000, 200),
maxConsoleTextFieldBytes: boundedBytes(
"one console text field",
16 * 1_024,
1_024,
),
maxTotalConsoleTextBytes: boundedBytes(
"total console text",
1_024 * 1_024,
64 * 1_024,
),
maxJsonBodyBytes: boundedBytes(
"one JSON response body",
4 * 1_024 * 1_024,
1_024 * 1_024,
),
maxTotalJsonBodyBytes: boundedBytes(
"total JSON response bodies",
16 * 1_024 * 1_024,
4 * 1_024 * 1_024,
),
maxJsonShapeNodes: boundedCount("JSON shape nodes", 100_000, 5_000),
maxJsonShapeDepth: boundedCount("JSON shape depth", 100, 20),
maxWebsocketEvents: boundedCount("WebSocket events", 5_000, 500),
maxWebsocketShapeBytes: boundedBytes(
"one WebSocket shape",
1_024 * 1_024,
64 * 1_024,
),
maxTotalWebsocketShapeBytes: boundedBytes(
"total WebSocket shapes",
16 * 1_024 * 1_024,
1_024 * 1_024,
),
}),
run: ({ args, options }) =>
logCliCommand(logger, "inspect-web-page", async () => {
@@ -245,32 +293,45 @@ const registerBundleAnalysis = (
}),
options: z.object({
...browserScopeOptions,
sourceCaptureApproved: z.boolean().default(false),
observationMs: z.number().int().min(0).max(10_000).default(500),
fetchSourceMaps: z.boolean().default(false),
sourceMapFetchApproved: z.boolean().default(false),
maxFindings: z.number().int().min(1).max(10_000).default(1_000),
maxAstNodes: z.number().int().min(1).max(2_000_000).default(250_000),
maxSourceMaps: z.number().int().min(1).max(1_000).default(100),
maxSourceMapBytes: z
.number()
.int()
.min(1)
.max(16 * 1_024 * 1_024)
.default(4 * 1_024 * 1_024),
maxTotalSourceMapBytes: z
.number()
.int()
.min(1)
.max(64 * 1_024 * 1_024)
.default(16 * 1_024 * 1_024),
maxSourceMapMappings: z
.number()
.int()
.min(1)
.max(100_000)
.default(10_000),
maxOriginalSources: z.number().int().min(1).max(20_000).default(2_000),
sourceCaptureApproved: z
.boolean()
.default(false)
.describe("Approve capturing bounded script source text"),
observationMs: boundedCount(
"observation duration in milliseconds",
10_000,
500,
0,
),
fetchSourceMaps: z
.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 () => {
@@ -321,8 +382,12 @@ const registerObservationSession = (
}),
options: z.object({
...browserScopeOptions,
observationMs: z.number().int().min(1).max(60_000).default(10_000),
maxTimelineEvents: z.number().int().min(1).max(20_000).default(2_000),
observationMs: boundedCount(
"observation duration in milliseconds",
60_000,
10_000,
),
maxTimelineEvents: boundedCount("timeline events", 20_000, 2_000),
}),
run: ({ args, options }) =>
logCliCommand(logger, "observe-web-session", async () => {
+162 -32
View File
@@ -26,7 +26,7 @@ const scopeOptions = {
.array(z.string().min(1))
.optional()
.describe("Filesystem roots; defaults to REA_ELECTRON_FILE_ROOTS_JSON"),
approved: z.boolean().default(false),
approved: z.boolean().default(false).describe("Approve passive observation"),
};
/** Register CLI equivalents of the Electron MCP tools. */
@@ -76,11 +76,26 @@ const registerElectronObservationCommands = (
): void => {
cli.command(CLI_COMMANDS.listElectronTargets, {
description: "List root-confined file pages from Electron CDP",
args: z.object({ endpoint: z.string() }),
args: z.object({
endpoint: z
.string()
.describe("Configured loopback Electron CDP endpoint"),
}),
options: z.object({
...scopeOptions,
offset: z.number().int().min(0).default(0),
limit: z.number().int().min(1).max(200).default(100),
offset: z
.number()
.int()
.min(0)
.default(0)
.describe("Zero-based Electron-target offset"),
limit: z
.number()
.int()
.min(1)
.max(200)
.default(100)
.describe("Maximum Electron targets to return"),
}),
run: ({ args, options }) =>
logCliCommand(logger, "list-electron-targets", async () => {
@@ -105,29 +120,78 @@ const registerElectronObservationCommands = (
});
cli.command(CLI_COMMANDS.inspectElectronPage, {
description: "Passively inspect one root-confined Electron file page",
args: z.object({ endpoint: z.string(), targetId: z.string() }),
args: z.object({
endpoint: z
.string()
.describe("Configured loopback Electron CDP endpoint"),
targetId: z.string().describe("Target ID from list-electron-targets"),
}),
options: z.object({
...scopeOptions,
observationMs: z.number().int().min(0).max(10_000).default(100),
includeScriptSources: z.boolean().default(false),
sourceCaptureApproved: z.boolean().default(false),
maxFrames: z.number().int().min(1).max(1_000).default(200),
maxDomNodes: z.number().int().min(1).max(10_000).default(2_000),
maxScripts: z.number().int().min(1).max(2_000).default(500),
maxResources: z.number().int().min(1).max(10_000).default(2_000),
maxWorkers: z.number().int().min(1).max(5_000).default(500),
observationMs: z
.number()
.int()
.min(0)
.max(10_000)
.default(100)
.describe("Observation duration in milliseconds"),
includeScriptSources: z
.boolean()
.default(false)
.describe("Include bounded JavaScript source text"),
sourceCaptureApproved: z
.boolean()
.default(false)
.describe("Approve capturing bounded script source text"),
maxFrames: z
.number()
.int()
.min(1)
.max(1_000)
.default(200)
.describe("Maximum page frames"),
maxDomNodes: z
.number()
.int()
.min(1)
.max(10_000)
.default(2_000)
.describe("Maximum DOM nodes"),
maxScripts: z
.number()
.int()
.min(1)
.max(2_000)
.default(500)
.describe("Maximum scripts"),
maxResources: z
.number()
.int()
.min(1)
.max(10_000)
.default(2_000)
.describe("Maximum resources"),
maxWorkers: z
.number()
.int()
.min(1)
.max(5_000)
.default(500)
.describe("Maximum workers"),
maxScriptSourceBytes: z
.number()
.int()
.min(1)
.max(4 * 1_024 * 1_024)
.default(1_024 * 1_024),
.default(1_024 * 1_024)
.describe("Maximum size of one captured script source"),
maxTotalScriptSourceBytes: z
.number()
.int()
.min(1)
.max(16 * 1_024 * 1_024)
.default(4 * 1_024 * 1_024),
.default(4 * 1_024 * 1_024)
.describe("Maximum total captured script source size"),
}),
run: ({ args, options }) =>
logCliCommand(logger, "inspect-electron-page", async () => {
@@ -174,39 +238,105 @@ const registerJavaScriptApplicationCommand = (
path: z.string().describe("Absolute ASAR or extracted application path"),
}),
options: z.object({
approved: z.boolean().default(false),
format: z.enum(["auto", "asar", "directory"]).default("auto"),
sourceMapReadApproved: z.boolean().default(false),
maxEntries: z.number().int().min(1).default(8_000),
approved: z
.boolean()
.default(false)
.describe("Approve reading the application artifact"),
format: z
.enum(["auto", "asar", "directory"])
.default("auto")
.describe("Application artifact format"),
sourceMapReadApproved: z
.boolean()
.default(false)
.describe(
"Approve reading local source maps referenced by the application",
),
maxEntries: z
.number()
.int()
.min(1)
.default(8_000)
.describe("Maximum artifact entries to inspect"),
maxTotalArtifactBytes: z
.number()
.int()
.min(1)
.default(512 * 1_024 * 1_024),
.default(512 * 1_024 * 1_024)
.describe("Maximum total uncompressed artifact content to inspect"),
maxArtifactEntryBytes: z
.number()
.int()
.min(1)
.default(128 * 1_024 * 1_024),
maxCompressionRatio: z.number().min(1).default(1_000),
maxDepth: z.number().int().min(1).default(64),
maxPathBytes: z.number().int().min(1).default(4_096),
maxTextFiles: z.number().int().min(1).default(5_000),
.default(128 * 1_024 * 1_024)
.describe("Maximum uncompressed content for one artifact entry"),
maxCompressionRatio: z
.number()
.min(1)
.default(1_000)
.describe(
"Maximum accepted compressed-to-uncompressed expansion ratio",
),
maxDepth: z
.number()
.int()
.min(1)
.default(64)
.describe("Maximum artifact directory depth"),
maxPathBytes: z
.number()
.int()
.min(1)
.default(4_096)
.describe("Maximum encoded artifact path length"),
maxTextFiles: z
.number()
.int()
.min(1)
.default(5_000)
.describe("Maximum text files to parse"),
maxTotalTextBytes: z
.number()
.int()
.min(1)
.default(128 * 1_024 * 1_024),
.default(128 * 1_024 * 1_024)
.describe("Maximum total text content to parse"),
maxTextFileBytes: z
.number()
.int()
.min(1)
.default(8 * 1_024 * 1_024),
maxAstNodes: z.number().int().min(1).default(2_000_000),
maxFindings: z.number().int().min(1).default(8_000),
maxModules: z.number().int().min(1).default(20_000),
maxSourceMapSources: z.number().int().min(1).default(5_000),
maxParseMilliseconds: z.number().int().min(1).default(30_000),
.default(8 * 1_024 * 1_024)
.describe("Maximum size of one parsed text file"),
maxAstNodes: z
.number()
.int()
.min(1)
.default(2_000_000)
.describe("Maximum JavaScript AST nodes to parse"),
maxFindings: z
.number()
.int()
.min(1)
.default(8_000)
.describe("Maximum static-analysis findings to return"),
maxModules: z
.number()
.int()
.min(1)
.default(20_000)
.describe("Maximum application modules to project"),
maxSourceMapSources: z
.number()
.int()
.min(1)
.default(5_000)
.describe("Maximum original sources from source maps"),
maxParseMilliseconds: z
.number()
.int()
.min(1)
.default(30_000)
.describe("Maximum JavaScript parsing time in milliseconds"),
}),
run: ({ args, options }) =>
logCliCommand(
+13 -2
View File
@@ -79,8 +79,19 @@ export const registerEvidenceCommands = (
right: z.string().describe("Right Evidence bundle JSON path"),
}),
options: z.object({
offset: z.number().int().min(0).default(0),
limit: z.number().int().min(1).max(500).default(100),
offset: z
.number()
.int()
.min(0)
.default(0)
.describe("Zero-based changed-record offset"),
limit: z
.number()
.int()
.min(1)
.max(500)
.default(100)
.describe("Maximum changed records to return"),
}),
run: ({ args, options }) =>
logCliCommand(logger, "compare", async () => {
+53 -10
View File
@@ -17,31 +17,74 @@ const investigationOptionsSchema = z.object({
.boolean()
.default(false)
.describe("Approve persistent workspace writes"),
workspaceName: z.string().trim().min(1).max(200).default("default"),
expectedRevision: z.number().int().min(1).optional(),
workspaceName: z
.string()
.trim()
.min(1)
.max(200)
.default("default")
.describe("Logical name recorded in the investigation workspace"),
expectedRevision: z
.number()
.int()
.min(1)
.optional()
.describe("Required current workspace revision for an atomic update"),
replayRunId: z
.string()
.regex(/^run_[a-f0-9]{64}$/u)
.optional()
.describe("Replay one verified complete workspace run without rescanning"),
maxEntries: z.number().int().min(1).max(50_000).default(10_000),
maxEntries: z
.number()
.int()
.min(1)
.max(50_000)
.default(10_000)
.describe("Maximum artifact entries to inspect per version"),
maxTotalBytes: z
.number()
.int()
.min(1)
.max(Number.MAX_SAFE_INTEGER)
.default(1_073_741_824),
.default(1_073_741_824)
.describe("Maximum total uncompressed artifact content to inspect"),
maxEntryBytes: z
.number()
.int()
.min(1)
.max(Number.MAX_SAFE_INTEGER)
.default(268_435_456),
pageSize: z.number().int().min(1).max(500).default(500),
changeLimit: z.number().int().min(1).max(500).default(500),
integrityPolicy: z.enum(["fail", "record-and-continue"]).default("fail"),
integrityContinueApproved: z.boolean().default(false),
maxIntegrityMismatches: z.number().int().min(1).max(100).default(10),
.default(268_435_456)
.describe("Maximum uncompressed content to inspect for one entry"),
pageSize: z
.number()
.int()
.min(1)
.max(500)
.default(500)
.describe("Maximum graph items loaded per investigation page"),
changeLimit: z
.number()
.int()
.min(1)
.max(500)
.default(500)
.describe("Maximum version changes to record"),
integrityPolicy: z
.enum(["fail", "record-and-continue"])
.default("fail")
.describe("Behavior when declared artifact integrity does not match"),
integrityContinueApproved: z
.boolean()
.default(false)
.describe("Approve continuing after recorded integrity mismatches"),
maxIntegrityMismatches: z
.number()
.int()
.min(1)
.max(100)
.default(10)
.describe("Maximum integrity mismatches to record before stopping"),
});
/** Register the persistent cross-version CLI workflow. */
+32 -9
View File
@@ -58,17 +58,40 @@ export const registerPolicyCommands = (
cli.command(CLI_COMMANDS.policy, {
description: "Inspect, explain, or revoke unified local permission grants",
args: z.object({
action: z.enum(["status", "list", "explain", "revoke"]),
value: z.string().optional(),
action: z
.enum(["status", "list", "explain", "revoke"])
.describe("Permission-policy operation to perform"),
value: z
.string()
.optional()
.describe("Capability to explain or grant ID to revoke"),
}),
options: z.object({
root: z.string().optional(),
executable: z.string().optional(),
environmentNames: z.array(z.string()).default([]),
origins: z.array(z.string()).default([]),
network: z.enum(["none", "loopback", "external"]).default("none"),
mount: z.boolean().default(false),
write: z.boolean().default(false),
root: z.string().optional().describe("Filesystem root to authorize"),
executable: z
.string()
.optional()
.describe("Executable path to authorize"),
environmentNames: z
.array(z.string())
.default([])
.describe("Environment variable names the operation may read"),
origins: z
.array(z.string())
.default([])
.describe("Exact browser origins the operation may access"),
network: z
.enum(["none", "loopback", "external"])
.default("none")
.describe("Maximum network scope to authorize"),
mount: z
.boolean()
.default(false)
.describe("Authorize mounting an artifact filesystem"),
write: z
.boolean()
.default(false)
.describe("Explain the write form of the requested capability"),
yes: z
.boolean()
.default(false)
+118 -3
View File
@@ -41,9 +41,14 @@ const advertisedOutputSchema = (schema: z.ZodObject) => {
"~standard": {
...standard,
jsonSchema: {
input: () => closeObjectSchemas(converter({ target: "draft-2020-12" })),
input: () =>
describeRootProperties(
closeObjectSchemas(converter({ target: "draft-2020-12" })),
),
output: () =>
closeObjectSchemas(converter({ target: "draft-2020-12" })),
describeRootProperties(
closeObjectSchemas(converter({ target: "draft-2020-12" })),
),
},
},
};
@@ -93,7 +98,7 @@ const describeProperties = (
isObject(schema) && typeof schema.description !== "string"
? {
...describeProperties(schema),
description: property.replaceAll("_", " "),
description: fallbackPropertyDescription(property),
}
: describeValue(schema),
]),
@@ -107,5 +112,115 @@ const describeValue = (value: unknown): unknown => {
return isObject(value) ? describeProperties(value) : value;
};
const describeRootProperties = (
value: Readonly<Record<string, unknown>>,
): Record<string, unknown> => {
const properties = value.properties;
if (!isObject(properties)) return { ...value };
return {
...value,
properties: Object.fromEntries(
Object.entries(properties).map(([property, schema]) => [
property,
isObject(schema) && typeof schema.description !== "string"
? {
...schema,
description: fallbackPropertyDescription(property),
}
: schema,
]),
),
};
};
const PROPERTY_DESCRIPTIONS: Readonly<Record<string, string>> = {
addresses: "Ordered provider-normalized procedure addresses to analyze.",
approved: "Explicit operator approval to perform this operation.",
after: "The later or right-hand observation to compare.",
before: "The earlier or left-hand observation to compare.",
boundary_id: "Exact reconstruction boundary identifier to evaluate.",
case_sensitive:
"Whether text matching distinguishes uppercase and lowercase.",
cdp_endpoint: "Approved literal loopback Chrome DevTools Protocol endpoint.",
comment: "Exact analyst comment text to write.",
comparisons: "Validated comparison Evidence records to aggregate.",
coverage: "Exact reconstruction-coverage commitment to verify.",
detail: "Requested response detail level.",
direction: "Direction in which to traverse or compare relationships.",
document: "Exact provider document or program identity.",
error: "Structured, caller-actionable error when the operation fails.",
evidence: "Evidence v2 record produced by this operation.",
evidence_id: "Stable identifier of the recorded Evidence v2 observation.",
evidence_uri:
"Session resource URI for the recorded Evidence v2 observation.",
executable: "Approved absolute path of the executable to run.",
format: "Declared input artifact format.",
name: "Exact name used by this operation.",
pattern: "Text or bounded pattern used to filter matching results.",
phase: "Current plan or execution phase of the operation.",
plan: "Content-bound execution plan and its approval commitment.",
query: "Non-empty feature or text query to investigate.",
question: "Concrete unresolved question to retain for later investigation.",
result: "Primary structured result returned by this operation.",
left: "Left-hand input used for comparison or differential execution.",
limit: "Maximum number of results to return in this page.",
limits:
"Bounded resource-consumption and result-size limits for this operation.",
mode: "Operation mode that selects the requested behavior.",
offset: "Zero-based index of the first result to return.",
overwrite: "Whether an existing destination may be replaced.",
path: "Local filesystem path used by this operation.",
provider_id:
"Exact deep-analysis provider ID, or automatic selection when omitted.",
right: "Right-hand input used for comparison or differential execution.",
schema_version: "Version of this structured result schema.",
source_evidence:
"Evidence records supporting the prepared source transformation.",
status: "Current lifecycle or verification status.",
summary: "Concise evidence-backed summary of the result.",
symbols: "Ordered bounded Swift symbols to demangle.",
target_id: "Exact authorized CDP target identifier.",
unknown_id: "Exact residual-unknown identifier.",
unknown_registry_approved:
"Explicit approval to record bounded residual uncertainty in the session registry.",
workspace_path:
"Approved local path of the persistent investigation workspace.",
};
const fallbackPropertyDescription = (property: string): string => {
const explicit = PROPERTY_DESCRIPTIONS[property];
if (explicit !== undefined) return explicit;
const words = property.replaceAll("_", " ");
if (property.endsWith("_approved"))
return `Explicit operator approval for ${words.slice(0, -9)}.`;
if (property.endsWith("_offset"))
return `Zero-based index of the first ${words.slice(0, -7)} to return.`;
if (property.endsWith("_limit"))
return `Maximum number of ${words.slice(0, -6)} entries to return.`;
if (property.startsWith("max_"))
return `Maximum permitted ${words.slice(4)} for this operation.`;
if (property.startsWith("include_"))
return `Whether to include ${words.slice(8)} in the result.`;
if (property.startsWith("expected_"))
return `Expected ${words.slice(9)} used to reject stale or mismatched input.`;
if (property.endsWith("_sha256"))
return `Exact SHA-256 digest of ${words.slice(0, -7)}.`;
if (property.endsWith("_evidence_id"))
return `Exact Evidence v2 identifier for the ${words.slice(0, -12)} observation.`;
if (property.endsWith("_evidence_ids"))
return `Ordered Evidence v2 identifiers for the ${words.slice(0, -13)} observations.`;
if (property.endsWith("_path"))
return `Local filesystem path for ${words.slice(0, -5)}.`;
if (property.endsWith("_uri"))
return `Canonical URI for ${words.slice(0, -4)}.`;
if (property.endsWith("_bytes"))
return `Bounded byte count for ${words.slice(0, -6)}.`;
if (property.endsWith("_root") || property.endsWith("_roots"))
return `Approved canonical filesystem ${words}.`;
if (property.startsWith("is_") || property.startsWith("has_"))
return `Whether ${words}.`;
return `Value for ${words}.`;
};
const isObject = (value: unknown): value is Readonly<Record<string, unknown>> =>
typeof value === "object" && value !== null && !Array.isArray(value);
+26
View File
@@ -25,6 +25,7 @@ import {
annotationsFromEffects,
TOOL_EFFECTS,
} from "../src/contracts/toolEffects.js";
import { toolRegistrationOptions } from "../src/server/toolRegistrationOptions.js";
const convertContractJsonSchema = (schema: z.ZodType) =>
z.toJSONSchema(schema, {
@@ -59,6 +60,31 @@ const emptySchemaPaths = (value: unknown, path = "$"): string[] => {
};
describe("tool contract surface", () => {
it("gives fallback parameter guidance operational meaning", () => {
const contract = TOOL_CONTRACTS.find(
({ name }) => name === "analyze_function",
);
expect(contract).toBeDefined();
if (contract === undefined) return;
const advertisedSchema =
toolRegistrationOptions(contract).inputSchema[
"~standard"
].jsonSchema.input();
const properties = Object.fromEntries(
Object.entries(advertisedSchema.properties ?? {}),
);
expect(properties.include_assembly).toMatchObject({
description: "Whether to include assembly in the result.",
});
expect(properties.max_instructions).toMatchObject({
description: "Maximum permitted instructions for this operation.",
});
expect(properties.pseudocode_offset).toMatchObject({
description: "Zero-based index of the first pseudocode to return.",
});
});
it("advertises complete typed schemas and annotations for all analysis tools", () => {
const contracts = [
...OFFICIAL_TOOL_CONTRACTS,
@@ -115,6 +115,16 @@ const assertDescribedStrictObjects = (
assertDescribedStrictObjects(value, root, `${path}[${String(index)}]`);
};
const assertDescribedRootObject = (schema: unknown, path: string): void => {
const object = Object.fromEntries(objectEntries(schema));
expect(object.additionalProperties, path).toBe(false);
for (const [property, propertySchema] of objectEntries(object.properties))
expect(
Object.fromEntries(objectEntries(propertySchema)).description,
`${path}.properties.${property}`,
).toEqual(expect.any(String));
};
describe("full MCP integration with multi-tool sequences", () => {
it("executes a realistic workflow: list methods, decompile selected, get xrefs", async () => {
const client = await connect({
@@ -225,6 +235,7 @@ describe("full MCP integration with multi-tool sequences", () => {
tool.inputSchema,
tool.name,
);
assertDescribedRootObject(tool.outputSchema, `${tool.name}.output`);
}
});
+5
View File
@@ -38,6 +38,7 @@ import {
import { ensureGeneratedFile } from "../scripts/lib/generated-file.mjs";
import {
createCliInventory,
cliCommandDescriptionIssues,
cliCommandOptionNames,
createProductCatalog,
serializeProductCatalog,
@@ -161,6 +162,10 @@ describe("canonical product catalog", () => {
]);
});
it("describes every primary command argument and option", () => {
expect(cliCommandDescriptionIssues(createCli())).toEqual([]);
});
it("exposes the shared provider selector on every deep-analysis command", () => {
const cli = createCli();
for (const name of [