mirror of
https://github.com/morluto/rea.git
synced 2026-11-15 19:23:24 +00:00
refactor(cli): project view-delivery inspectors from their contracts (#10600)
* refactor(cli): project view-delivery inspectors from their contracts * refactor(cli): project APK signature, desktop application and EVM inspectors from contracts (#10603)
This commit is contained in:
1 parent
254bb21857
commit
fa53f7070a
15 files changed
+622
-605
No files matched your search
@@ -608,8 +608,11 @@ and rendered for the CLI framework by `registerContractCommand` in
|
||||
The projection names only what the CLI spells differently: positionals,
|
||||
caller-relative path fields, option renames and short flags, CLI help wording,
|
||||
omitted optional fields with a reason, Evidence delivery options, and whether the contract's cross-field
|
||||
rules refuse at argv parsing or at the service's contract parse. Field schemas
|
||||
and decimal argv tokens are derived, and
|
||||
rules refuse at argv parsing or at the service's contract parse. A delivery
|
||||
`view` is JSON text decoded when the command runs; the contract input then
|
||||
admits the view with its `detail` before any target is opened, including a
|
||||
`detail`-discriminated union input. Field schemas and decimal argv tokens are
|
||||
derived, and
|
||||
`tests/boundary/cli/cliContractCommandParity.test.ts` checks that each projected
|
||||
command admits what its contract admits.
|
||||
|
||||
|
||||
@@ -1,20 +1,12 @@
|
||||
import { parseCliJsonOption } from "./cliJsonOption.js";
|
||||
import { z } from "incur";
|
||||
import type { Logger } from "pino";
|
||||
|
||||
import { APK_SIGNATURE_PROVIDER_IDENTITY } from "../apkSignature/ApkSignatureRelease.js";
|
||||
import { apkSignatureModule } from "../composition/apkSignature.js";
|
||||
import { inspectApkSignatureInputSchema } from "../domain/apkSignature/apkSignatureQuery.js";
|
||||
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
||||
import { analysisInputErrorFromIssues } from "../domain/inputIssueProjection.js";
|
||||
import { logCliCommand } from "../cliLogging.js";
|
||||
import { localPathStringSchema } from "../domain/localPath.js";
|
||||
import { absoluteCallerPath } from "../filesystem/callerPath.js";
|
||||
import type { CliInstance } from "./types.js";
|
||||
import { withCommandCancellation } from "./commandCancellation.js";
|
||||
import {
|
||||
analysisEvidenceDeliveryOptions,
|
||||
deliverCliAnalysisEvidence,
|
||||
} from "./analysisEvidenceDelivery.js";
|
||||
deliverWithService,
|
||||
registerContractCommand,
|
||||
} from "./contractCommand.js";
|
||||
import type { CliInstance } from "./types.js";
|
||||
|
||||
/** CLI shares selected delivery and complete-Evidence publication with other artifact tools. */
|
||||
export const registerApkSignatureCommands = (
|
||||
@@ -22,65 +14,35 @@ export const registerApkSignatureCommands = (
|
||||
logger: Logger,
|
||||
environment: Readonly<NodeJS.ProcessEnv>,
|
||||
): void => {
|
||||
cli.command(apkSignatureModule.cliCommand, {
|
||||
description:
|
||||
"Inspect declared APK signing schemes, signers and content digests",
|
||||
args: z.object({
|
||||
path: localPathStringSchema.describe("Selected local APK/ZIP/AAB"),
|
||||
}),
|
||||
options: z.object({
|
||||
...analysisEvidenceDeliveryOptions,
|
||||
view: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
"JSON signer/digest/lineage selection; requires --detail summary and --evidence_output",
|
||||
),
|
||||
}),
|
||||
run: ({ args, options }) =>
|
||||
withCommandCancellation((signal) =>
|
||||
logCliCommand(logger, apkSignatureModule.cliCommand, async () => {
|
||||
const decoded = parseCliJsonOption(
|
||||
"inspect_apk_signature",
|
||||
"view",
|
||||
options.view,
|
||||
"view must be a JSON APK signature selection",
|
||||
);
|
||||
if (!decoded.ok) return projectAnalysisError(decoded.error);
|
||||
const input = inspectApkSignatureInputSchema.safeParse({
|
||||
path: absoluteCallerPath(args.path),
|
||||
detail: options.detail,
|
||||
...(decoded.value === undefined ? {} : { view: decoded.value }),
|
||||
});
|
||||
if (!input.success)
|
||||
return projectAnalysisError(
|
||||
analysisInputErrorFromIssues(
|
||||
"inspect_apk_signature",
|
||||
input.error.issues,
|
||||
{ view: decoded.value },
|
||||
),
|
||||
);
|
||||
const service = apkSignatureModule.create(environment);
|
||||
return deliverCliAnalysisEvidence({
|
||||
callerInputPath: args.path,
|
||||
run: () =>
|
||||
service.inspect(
|
||||
{ path: input.data.path, detail: "complete" },
|
||||
{ signal },
|
||||
),
|
||||
service,
|
||||
providerId: APK_SIGNATURE_PROVIDER_IDENTITY.id,
|
||||
operation: apkSignatureModule.contract.name,
|
||||
signal,
|
||||
detail: options.detail,
|
||||
evidenceOutput: options.evidence_output,
|
||||
...(input.data.view === undefined
|
||||
? {}
|
||||
: {
|
||||
view: { kind: "apk-signature" as const, ...input.data.view },
|
||||
}),
|
||||
});
|
||||
}),
|
||||
),
|
||||
});
|
||||
const deliver = deliverWithService(
|
||||
() => apkSignatureModule.create(environment),
|
||||
APK_SIGNATURE_PROVIDER_IDENTITY.id,
|
||||
);
|
||||
registerContractCommand(
|
||||
cli,
|
||||
logger,
|
||||
apkSignatureModule.contract,
|
||||
{
|
||||
command: apkSignatureModule.cliCommand,
|
||||
description:
|
||||
"Inspect declared APK signing schemes, signers and content digests",
|
||||
positionals: ["path"],
|
||||
callerPaths: ["path"],
|
||||
evidenceDelivery: true,
|
||||
viewDelivery: {
|
||||
invalidJson: "view must be a JSON APK signature selection",
|
||||
kind: "apk-signature",
|
||||
},
|
||||
fields: {
|
||||
path: { description: "Selected local APK/ZIP/AAB" },
|
||||
view: {
|
||||
description:
|
||||
"JSON signer/digest/lineage selection; requires --detail summary and --evidence_output",
|
||||
},
|
||||
},
|
||||
},
|
||||
// The CLI delivers the summary itself, so the service returns complete
|
||||
// Evidence; the contract's own default would retain a summary instead.
|
||||
(input, context) => deliver({ ...input, detail: "complete" }, context),
|
||||
);
|
||||
};
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
inspectWithService,
|
||||
registerContractCommand,
|
||||
} from "./contractCommand.js";
|
||||
import { PRODUCER_LIMITS_HELP } from "./producerLimitsOption.js";
|
||||
|
||||
type BinaryDiagnosticsOperation =
|
||||
(typeof BINARY_DIAGNOSTICS_TOOL_CONTRACTS)[number]["name"];
|
||||
@@ -309,8 +310,6 @@ export const registerBinaryDiagnosticsCommands = (
|
||||
},
|
||||
deliverWithService(createNativeExportsService, "rea-native-exports"),
|
||||
);
|
||||
const producerLimitsHelp =
|
||||
"JSON producer budget overrides. Keys and units follow this operation's producer_limits contract; changes apply on this call without restart and preserve tighter inherited OS limits.";
|
||||
const layout = createBinaryLayoutService(environment);
|
||||
registerContractCommand(
|
||||
cli,
|
||||
@@ -325,7 +324,7 @@ export const registerBinaryDiagnosticsCommands = (
|
||||
jsonText: ["producer_limits"],
|
||||
fields: {
|
||||
path: { description: "Local ELF binary or relocatable object" },
|
||||
producer_limits: { description: producerLimitsHelp },
|
||||
producer_limits: { description: PRODUCER_LIMITS_HELP },
|
||||
},
|
||||
omit: {
|
||||
detail:
|
||||
@@ -355,7 +354,7 @@ export const registerBinaryDiagnosticsCommands = (
|
||||
path: {
|
||||
description: "Local Linux ELF64 amd64 or AArch64 little-endian core",
|
||||
},
|
||||
producer_limits: { description: producerLimitsHelp },
|
||||
producer_limits: { description: PRODUCER_LIMITS_HELP },
|
||||
include_debugger_context: {
|
||||
name: "debuggerContext",
|
||||
description: "Add BYO core-only GDB/pwndbg mapping candidates",
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import type { AnalysisInputError } from "../domain/analysisErrorCore.js";
|
||||
import type { AnalysisViewRequest } from "../domain/analysisView/analysisViewRequest.js";
|
||||
import { analysisInputErrorFromIssues } from "../domain/inputIssueProjection.js";
|
||||
import { localPathStringSchema } from "../domain/localPath.js";
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
import { absoluteCallerPath } from "../filesystem/callerPath.js";
|
||||
import { analysisEvidenceDeliveryOptions } from "./analysisEvidenceDelivery.js";
|
||||
import {
|
||||
analysisEvidenceDeliveryOptions,
|
||||
cliEvidenceDeliveryInputError,
|
||||
} from "./analysisEvidenceDelivery.js";
|
||||
import { parseCliJsonOption } from "./cliJsonOption.js";
|
||||
import { safeParseJson } from "../domain/safeJson.js";
|
||||
import { cliDecimalShape } from "./options.js";
|
||||
|
||||
@@ -13,10 +21,16 @@ import { cliDecimalShape } from "./options.js";
|
||||
* framework adapter (today `contractCommand.ts` for Incur) only renders it.
|
||||
*/
|
||||
|
||||
/** The parts of a tool contract that a CLI projection reads. */
|
||||
/**
|
||||
* The parts of a tool contract that a CLI projection reads. A union input
|
||||
* shares every field except those the CLI delivery owns, such as a
|
||||
* `detail`-discriminated delivery.
|
||||
*/
|
||||
export interface ProjectedContract<Name extends string = string> {
|
||||
readonly name: Name;
|
||||
readonly inputSchema: z.ZodObject;
|
||||
readonly inputSchema:
|
||||
| z.ZodObject
|
||||
| z.ZodDiscriminatedUnion<readonly z.ZodObject[]>;
|
||||
}
|
||||
|
||||
/** A contract input field name. */
|
||||
@@ -74,6 +88,26 @@ export interface CliContractProjection<Field extends string> {
|
||||
* field selects that delivery and is not passed to the analysis.
|
||||
*/
|
||||
readonly evidenceDelivery?: true;
|
||||
/**
|
||||
* With `evidenceDelivery`, the contract `view` field selects the delivered
|
||||
* page. `--view` takes JSON text that is decoded when the command runs, and
|
||||
* the delivery is then checked against the contract input before any
|
||||
* target is opened.
|
||||
*/
|
||||
readonly viewDelivery?: CliViewDelivery;
|
||||
}
|
||||
|
||||
/** How a contract command takes its `view` selection. */
|
||||
export interface CliViewDelivery {
|
||||
/** The refusal when `--view` is not JSON. */
|
||||
readonly invalidJson: string;
|
||||
/** Added to the selection when the contract's view does not name its kind. */
|
||||
readonly kind?: AnalysisViewRequest["kind"];
|
||||
/**
|
||||
* `--detail` has no default: a view selects summary delivery, and complete
|
||||
* applies otherwise, as in the contract.
|
||||
*/
|
||||
readonly viewSelectsSummary?: true;
|
||||
}
|
||||
|
||||
/** A CLI option with no contract field, such as `--evidence_output`. */
|
||||
@@ -87,6 +121,8 @@ export interface CliOnlyOption {
|
||||
export interface CliEvidenceDeliveryChoice {
|
||||
readonly detail: "complete" | "summary";
|
||||
readonly evidenceOutput: string | undefined;
|
||||
/** The admitted view, for a projection with `viewDelivery`. */
|
||||
readonly view?: AnalysisViewRequest;
|
||||
}
|
||||
|
||||
/** One positional argument or option of a contract command. */
|
||||
@@ -115,6 +151,11 @@ export interface ContractCliParameter {
|
||||
readonly jsonText: boolean;
|
||||
/** Selects CLI Evidence delivery and is not analysis input. */
|
||||
readonly delivery: boolean;
|
||||
/**
|
||||
* A delivery view as JSON text, decoded when the command runs and admitted
|
||||
* with the delivery by the contract input.
|
||||
*/
|
||||
readonly viewSelection: boolean;
|
||||
}
|
||||
|
||||
/** One cross-field refusal, naming CLI keys. */
|
||||
@@ -129,6 +170,11 @@ export interface ContractCliDescription {
|
||||
readonly command: string;
|
||||
readonly description: string;
|
||||
readonly contract: ProjectedContract;
|
||||
/**
|
||||
* Each contract input field's own schema. In a union input, a field the
|
||||
* delivery owns admits the value of any branch.
|
||||
*/
|
||||
readonly fields: Readonly<Record<string, z.ZodType>>;
|
||||
readonly parameters: readonly ContractCliParameter[];
|
||||
/** Options after the contract parameters that no contract field owns. */
|
||||
readonly cliOnlyOptions: readonly CliOnlyOption[];
|
||||
@@ -153,10 +199,15 @@ export interface ContractCliDescription {
|
||||
readonly callerInputs: (
|
||||
values: Readonly<Record<string, unknown>>,
|
||||
) => Readonly<Record<string, string>>;
|
||||
/** The selected delivery, for a projection with `evidenceDelivery`. */
|
||||
readonly delivery: (
|
||||
values: Readonly<Record<string, unknown>>,
|
||||
) => CliEvidenceDeliveryChoice;
|
||||
/**
|
||||
* The selected delivery, for a projection with `evidenceDelivery`, refused
|
||||
* before any target is opened when it is not admitted.
|
||||
*/
|
||||
readonly delivery:
|
||||
| ((
|
||||
values: Readonly<Record<string, unknown>>,
|
||||
) => Result<CliEvidenceDeliveryChoice, AnalysisInputError>)
|
||||
| undefined;
|
||||
}
|
||||
|
||||
/** One contract from a contract group, keeping its exact schema types. */
|
||||
@@ -310,16 +361,24 @@ const renameConstraint = (
|
||||
);
|
||||
};
|
||||
|
||||
/** The union keyword naming a root discriminator, checked with the delivery. */
|
||||
const ROOT_DISCRIMINATOR = "x-rea-root-discriminator";
|
||||
|
||||
const projectObjectConstraints = (
|
||||
contract: ProjectedContract,
|
||||
rename: (field: string) => string,
|
||||
deliveryFields: ReadonlySet<string>,
|
||||
): Record<string, unknown> | undefined => {
|
||||
const metadata = contract.inputSchema.meta();
|
||||
if (metadata === undefined) return undefined;
|
||||
for (const keyword of Object.keys(metadata))
|
||||
if (
|
||||
!ROOT_ANNOTATIONS.has(keyword) &&
|
||||
!(OBJECT_CONSTRAINTS as readonly string[]).includes(keyword)
|
||||
!(OBJECT_CONSTRAINTS as readonly string[]).includes(keyword) &&
|
||||
!(
|
||||
keyword === ROOT_DISCRIMINATOR &&
|
||||
deliveryFields.has(String(metadata[keyword]))
|
||||
)
|
||||
)
|
||||
throw new Error(
|
||||
`${contract.name} input keyword ${keyword} has no CLI projection`,
|
||||
@@ -340,12 +399,59 @@ const projectObjectConstraints = (
|
||||
return Object.keys(projected).length === 0 ? undefined : projected;
|
||||
};
|
||||
|
||||
/**
|
||||
* Each input field's schema. Branches of a union input must share each field
|
||||
* the CLI takes as an argument; only delivery-owned fields may differ, and
|
||||
* they admit any branch's value here. The delivery is then checked against
|
||||
* the whole union.
|
||||
*/
|
||||
const inputFields = (
|
||||
contract: ProjectedContract,
|
||||
deliveryFields: ReadonlySet<string>,
|
||||
): Record<string, z.ZodType> => {
|
||||
const schema = contract.inputSchema;
|
||||
if (schema instanceof z.ZodObject)
|
||||
return schema.shape as Record<string, z.ZodType>;
|
||||
const shapes = schema.options.map(
|
||||
(option) => option.shape as Record<string, z.ZodType>,
|
||||
);
|
||||
const names = new Set(shapes.flatMap((shape) => Object.keys(shape)));
|
||||
return Object.fromEntries(
|
||||
[...names].map((field) => {
|
||||
const branches = shapes.map((shape) => shape[field]);
|
||||
const [first] = branches;
|
||||
if (branches.every((branch) => branch === first)) return [field, first!];
|
||||
if (
|
||||
!deliveryFields.has(field) ||
|
||||
branches.some((branch) => branch === undefined)
|
||||
)
|
||||
throw new Error(
|
||||
`${contract.name}.${field} differs between input branches and has no argv projection`,
|
||||
);
|
||||
return [field, z.union(branches as z.ZodType[])];
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
/** Derive the CLI parameters of one contract from its input schema and projection. */
|
||||
export const describeContractCommand = <Contract extends ProjectedContract>(
|
||||
contract: Contract,
|
||||
projection: CliContractProjection<ContractField<Contract>>,
|
||||
): ContractCliDescription => {
|
||||
const shape = contract.inputSchema.shape as Record<string, z.ZodType>;
|
||||
const evidenceDelivery = projection.evidenceDelivery === true;
|
||||
const { viewDelivery } = projection;
|
||||
if (viewDelivery !== undefined && !evidenceDelivery)
|
||||
throw new Error(`${contract.name} view delivery requires evidenceDelivery`);
|
||||
const deliveryFields: ReadonlySet<string> = new Set(
|
||||
evidenceDelivery
|
||||
? viewDelivery === undefined
|
||||
? ["detail"]
|
||||
: ["detail", "view"]
|
||||
: [],
|
||||
);
|
||||
const shape = inputFields(contract, deliveryFields);
|
||||
if (viewDelivery !== undefined && !admitsOmission(shape.view ?? z.never()))
|
||||
throw new Error(`${contract.name} has no optional input field view`);
|
||||
const positionals: readonly string[] = projection.positionals ?? [];
|
||||
const callerPaths: ReadonlySet<string> = new Set(
|
||||
projection.callerPaths ?? [],
|
||||
@@ -374,16 +480,21 @@ export const describeContractCommand = <Contract extends ProjectedContract>(
|
||||
if (!admitsOmission(shape[field]!))
|
||||
throw new Error(`${contract.name}.${field} is required on the CLI`);
|
||||
|
||||
const evidenceDelivery = projection.evidenceDelivery === true;
|
||||
const parameter = (field: string): ContractCliParameter => {
|
||||
const declared = shape[field]!;
|
||||
const projected = fieldProjections[field];
|
||||
const callerPath = callerPaths.has(field);
|
||||
const delivery = evidenceDelivery && field === "detail";
|
||||
const delivery = deliveryFields.has(field);
|
||||
const viewSelection = delivery && field === "view";
|
||||
const jsonText = jsonTextFields.has(field);
|
||||
const key = projected?.name ?? camelCase(field);
|
||||
let schema: z.ZodType;
|
||||
if (delivery) schema = analysisEvidenceDeliveryOptions.detail;
|
||||
if (viewSelection) schema = z.string().optional();
|
||||
else if (delivery)
|
||||
schema =
|
||||
viewDelivery?.viewSelectsSummary === true
|
||||
? analysisEvidenceDeliveryOptions.detail.unwrap().optional()
|
||||
: analysisEvidenceDeliveryOptions.detail;
|
||||
else if (jsonText) schema = jsonTextField(declared, kebabCase(key));
|
||||
else if (callerPath) {
|
||||
schema = admitsOmission(declared)
|
||||
@@ -412,6 +523,7 @@ export const describeContractCommand = <Contract extends ProjectedContract>(
|
||||
callerPath,
|
||||
delivery,
|
||||
jsonText,
|
||||
viewSelection,
|
||||
};
|
||||
};
|
||||
const parameters = [
|
||||
@@ -494,7 +606,12 @@ export const describeContractCommand = <Contract extends ProjectedContract>(
|
||||
parameters,
|
||||
cliOnlyOptions,
|
||||
omitted,
|
||||
optionConstraints: projectObjectConstraints(contract, rename),
|
||||
fields: shape,
|
||||
optionConstraints: projectObjectConstraints(
|
||||
contract,
|
||||
rename,
|
||||
deliveryFields,
|
||||
),
|
||||
checkOptions,
|
||||
toContractInput,
|
||||
callerInputs: (values) =>
|
||||
@@ -506,12 +623,70 @@ export const describeContractCommand = <Contract extends ProjectedContract>(
|
||||
: [];
|
||||
}),
|
||||
),
|
||||
delivery: (values) => ({
|
||||
detail: values.detail === "summary" ? "summary" : "complete",
|
||||
evidenceOutput:
|
||||
typeof values.evidence_output === "string"
|
||||
? values.evidence_output
|
||||
: undefined,
|
||||
}),
|
||||
delivery: evidenceDelivery
|
||||
? (values) => {
|
||||
const selected = selectDelivery(
|
||||
contract,
|
||||
viewDelivery,
|
||||
values,
|
||||
toContractInput,
|
||||
rename,
|
||||
);
|
||||
if (!selected.ok) return selected;
|
||||
const refused = cliEvidenceDeliveryInputError({
|
||||
...selected.value,
|
||||
operation: contract.name,
|
||||
});
|
||||
return refused === undefined ? selected : err(refused);
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* The delivery the parsed values select. A view is decoded from its JSON
|
||||
* text and admitted with the delivery and the other inputs by the contract
|
||||
* input schema, so the contract's own delivery rules and messages apply.
|
||||
*/
|
||||
const selectDelivery = (
|
||||
contract: ProjectedContract,
|
||||
viewDelivery: CliViewDelivery | undefined,
|
||||
values: Readonly<Record<string, unknown>>,
|
||||
toContractInput: ContractCliDescription["toContractInput"],
|
||||
rename: (field: string) => string,
|
||||
): Result<CliEvidenceDeliveryChoice, AnalysisInputError> => {
|
||||
const detail = values[rename("detail")] as "complete" | "summary" | undefined;
|
||||
const evidenceOutput = values.evidence_output as string | undefined;
|
||||
if (viewDelivery === undefined)
|
||||
return ok({ detail: detail ?? "complete", evidenceOutput });
|
||||
const viewKey = rename("view");
|
||||
const decoded = parseCliJsonOption(
|
||||
contract.name,
|
||||
viewKey,
|
||||
values[viewKey] as string | undefined,
|
||||
viewDelivery.invalidJson,
|
||||
);
|
||||
if (!decoded.ok) return decoded;
|
||||
const input = {
|
||||
...toContractInput(values),
|
||||
...(detail === undefined ? {} : { detail }),
|
||||
...(decoded.value === undefined ? {} : { view: decoded.value }),
|
||||
};
|
||||
const admitted = contract.inputSchema.safeParse(input);
|
||||
if (!admitted.success)
|
||||
return err(
|
||||
analysisInputErrorFromIssues(contract.name, admitted.error.issues, input),
|
||||
);
|
||||
const { view } = admitted.data as { readonly view?: object };
|
||||
return ok({
|
||||
detail: detail ?? (view === undefined ? "complete" : "summary"),
|
||||
evidenceOutput,
|
||||
...(view === undefined
|
||||
? {}
|
||||
: {
|
||||
view: (viewDelivery.kind === undefined
|
||||
? view
|
||||
: { kind: viewDelivery.kind, ...view }) as AnalysisViewRequest,
|
||||
}),
|
||||
});
|
||||
};
|
||||
@@ -3,6 +3,7 @@ import type { Logger } from "pino";
|
||||
|
||||
import { logCliCommand } from "../cliLogging.js";
|
||||
import type { AnalysisError } from "../domain/analysisErrorBase.js";
|
||||
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
||||
import type { Evidence } from "../domain/evidence.js";
|
||||
import type { Result } from "../domain/result.js";
|
||||
import { deliverCliAnalysisEvidence } from "./analysisEvidenceDelivery.js";
|
||||
@@ -32,8 +33,8 @@ export interface ContractCommandContext {
|
||||
readonly operation: string;
|
||||
/** Each caller-path field as the caller spelled it, before resolution. */
|
||||
readonly callerInputs: Readonly<Record<string, string>>;
|
||||
/** The selected delivery, for a projection with `evidenceDelivery`. */
|
||||
readonly delivery: CliEvidenceDeliveryChoice;
|
||||
/** The admitted delivery, for a projection with `evidenceDelivery`. */
|
||||
readonly delivery: CliEvidenceDeliveryChoice | undefined;
|
||||
}
|
||||
|
||||
/** A closeable one-shot service that inspects one contract input. */
|
||||
@@ -82,6 +83,9 @@ export const deliverWithService =
|
||||
(service: () => EvidenceInspectionService, providerId: string) =>
|
||||
(input: unknown, context: ContractCommandContext) => {
|
||||
const callerInputPath = primaryCallerPath(context);
|
||||
const { delivery } = context;
|
||||
if (delivery === undefined)
|
||||
throw new Error(`${context.operation} does not declare evidenceDelivery`);
|
||||
const owned = service();
|
||||
return deliverCliAnalysisEvidence({
|
||||
callerInputPath,
|
||||
@@ -90,8 +94,9 @@ export const deliverWithService =
|
||||
providerId,
|
||||
operation: context.operation,
|
||||
signal: context.signal,
|
||||
detail: context.delivery.detail,
|
||||
evidenceOutput: context.delivery.evidenceOutput,
|
||||
detail: delivery.detail,
|
||||
evidenceOutput: delivery.evidenceOutput,
|
||||
...(delivery.view === undefined ? {} : { view: delivery.view }),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -174,8 +179,11 @@ export const registerContractCommand = <
|
||||
}) => {
|
||||
const values = { ...options, ...args };
|
||||
return withCommandCancellation((signal) =>
|
||||
logCliCommand(logger, description.command, () =>
|
||||
run(
|
||||
logCliCommand(logger, description.command, async () => {
|
||||
const delivery = description.delivery?.(values);
|
||||
if (delivery?.ok === false)
|
||||
return projectAnalysisError(delivery.error);
|
||||
return run(
|
||||
description.toContractInput(values) as z.input<
|
||||
Contract["inputSchema"]
|
||||
>,
|
||||
@@ -183,10 +191,10 @@ export const registerContractCommand = <
|
||||
signal,
|
||||
operation: contract.name,
|
||||
callerInputs: description.callerInputs(values),
|
||||
delivery: description.delivery(values),
|
||||
delivery: delivery?.value,
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
},
|
||||
} as never);
|
||||
|
||||
@@ -1,67 +1,47 @@
|
||||
import { localPathStringSchema } from "../domain/localPath.js";
|
||||
import { inspectDesktopApplicationInputSchema } from "../domain/desktopApplication.js";
|
||||
import {
|
||||
analysisEvidenceDeliveryOptions,
|
||||
deliverCliAnalysisEvidence,
|
||||
} from "./analysisEvidenceDelivery.js";
|
||||
import { z } from "incur";
|
||||
import type { Logger } from "pino";
|
||||
import type { CliInstance } from "./types.js";
|
||||
import { createDesktopApplicationService } from "../composition/desktop.js";
|
||||
import { absoluteCallerPath } from "../filesystem/callerPath.js";
|
||||
|
||||
import { CLI_COMMANDS } from "../cliCommandNames.js";
|
||||
import { logCliCommand } from "../cliLogging.js";
|
||||
import { withCommandCancellation } from "./commandCancellation.js";
|
||||
import { cliDecimalField } from "./options.js";
|
||||
import { createDesktopApplicationService } from "../composition/desktop.js";
|
||||
import { DESKTOP_APPLICATION_TOOL_CONTRACTS } from "../contracts/desktopApplicationToolContracts.js";
|
||||
import { contractNamed } from "./contractCliDescription.js";
|
||||
import {
|
||||
deliverWithService,
|
||||
registerContractCommand,
|
||||
} from "./contractCommand.js";
|
||||
import type { CliInstance } from "./types.js";
|
||||
|
||||
/** One-shot complete Evidence can be saved and queried with inspect-analysis-view. */
|
||||
export const registerDesktopApplicationCommands = (
|
||||
cli: CliInstance,
|
||||
logger: Logger,
|
||||
): void => {
|
||||
cli.command(CLI_COMMANDS.inspectDesktopApplication, {
|
||||
description:
|
||||
"Identify desktop runtime candidates from bounded directory and header observations",
|
||||
args: z.object({
|
||||
path: localPathStringSchema.describe("Local installation directory"),
|
||||
}),
|
||||
options: z.object({
|
||||
// Decimal tokens only: numeric coercion would read "0x1" as 1 and
|
||||
// "1e1" as 10. The shared fields keep their bounds and defaults.
|
||||
max_entries: cliDecimalField(
|
||||
inspectDesktopApplicationInputSchema.shape.max_entries,
|
||||
).describe(
|
||||
"Maximum directory entries to examine; exhaustion leaves partial coverage",
|
||||
),
|
||||
...analysisEvidenceDeliveryOptions,
|
||||
max_probe_bytes: cliDecimalField(
|
||||
inspectDesktopApplicationInputSchema.shape.max_probe_bytes,
|
||||
).describe("Maximum total bytes read from selected file ranges"),
|
||||
}),
|
||||
run: ({ args, options }) =>
|
||||
withCommandCancellation((signal) =>
|
||||
logCliCommand(
|
||||
logger,
|
||||
CLI_COMMANDS.inspectDesktopApplication,
|
||||
async () => {
|
||||
const { detail, evidence_output, ...limits } = options;
|
||||
const service = createDesktopApplicationService();
|
||||
return deliverCliAnalysisEvidence({
|
||||
callerInputPath: args.path,
|
||||
run: () =>
|
||||
service.inspect(
|
||||
{ path: absoluteCallerPath(args.path), ...limits },
|
||||
{ signal },
|
||||
),
|
||||
service,
|
||||
providerId: "rea-artifact-graph",
|
||||
operation: "inspect_desktop_application",
|
||||
signal,
|
||||
detail,
|
||||
evidenceOutput: evidence_output,
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
});
|
||||
registerContractCommand(
|
||||
cli,
|
||||
logger,
|
||||
contractNamed(
|
||||
DESKTOP_APPLICATION_TOOL_CONTRACTS,
|
||||
"inspect_desktop_application",
|
||||
),
|
||||
{
|
||||
command: CLI_COMMANDS.inspectDesktopApplication,
|
||||
description:
|
||||
"Identify desktop runtime candidates from bounded directory and header observations",
|
||||
positionals: ["path"],
|
||||
callerPaths: ["path"],
|
||||
evidenceDelivery: true,
|
||||
fields: {
|
||||
path: { description: "Local installation directory" },
|
||||
max_entries: {
|
||||
name: "max_entries",
|
||||
description:
|
||||
"Maximum directory entries to examine; exhaustion leaves partial coverage",
|
||||
},
|
||||
max_probe_bytes: {
|
||||
name: "max_probe_bytes",
|
||||
description: "Maximum total bytes read from selected file ranges",
|
||||
},
|
||||
},
|
||||
},
|
||||
deliverWithService(createDesktopApplicationService, "rea-artifact-graph"),
|
||||
);
|
||||
};
|
||||
+31
-72
@@ -1,81 +1,40 @@
|
||||
import { z } from "incur";
|
||||
import { localPathStringSchema } from "../domain/localPath.js";
|
||||
import { absoluteCallerPath } from "../filesystem/callerPath.js";
|
||||
import { createBeamService } from "../composition/erlang.js";
|
||||
import { CLI_COMMANDS } from "../cliCommandNames.js";
|
||||
import { logCliCommand } from "../cliLogging.js";
|
||||
import type { Logger } from "pino";
|
||||
import type { CliInstance } from "./types.js";
|
||||
import { withCommandCancellation } from "./commandCancellation.js";
|
||||
import { inspectErlangBeamInputSchema } from "../domain/erlang/beam.js";
|
||||
|
||||
import { CLI_COMMANDS } from "../cliCommandNames.js";
|
||||
import { createBeamService } from "../composition/erlang.js";
|
||||
import { ERLANG_BEAM_TOOL_CONTRACTS } from "../contracts/erlang/beamToolContracts.js";
|
||||
import { contractNamed } from "./contractCliDescription.js";
|
||||
import {
|
||||
analysisEvidenceDeliveryOptions,
|
||||
deliverCliAnalysisEvidence,
|
||||
parseCliAnalysisEvidenceDelivery,
|
||||
} from "./analysisEvidenceDelivery.js";
|
||||
import { erlangBeamDeliverySchema } from "../domain/analysisView/erlangBeamView.js";
|
||||
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
||||
import { cliDecimalField } from "./options.js";
|
||||
deliverWithService,
|
||||
registerContractCommand,
|
||||
} from "./contractCommand.js";
|
||||
import type { CliInstance } from "./types.js";
|
||||
|
||||
/** Inspect operator-selected BEAM files through the same shared workflow as MCP. */
|
||||
export const registerErlangCommands = (
|
||||
cli: CliInstance,
|
||||
logger: Logger,
|
||||
): void => {
|
||||
cli.command(CLI_COMMANDS.inspectErlangBeam, {
|
||||
description:
|
||||
"Inspect Erlang BEAM atoms, exports, imports and Code header without executing bytecode",
|
||||
args: z.object({
|
||||
path: localPathStringSchema.describe("Local BEAM file"),
|
||||
}),
|
||||
options: z.object({
|
||||
// Decimal tokens only, as for inspect-python-pyc.
|
||||
maxFileBytes: cliDecimalField(
|
||||
inspectErlangBeamInputSchema.shape.max_file_bytes,
|
||||
),
|
||||
maxChunks: cliDecimalField(inspectErlangBeamInputSchema.shape.max_chunks),
|
||||
maxAtoms: cliDecimalField(inspectErlangBeamInputSchema.shape.max_atoms),
|
||||
...analysisEvidenceDeliveryOptions,
|
||||
view: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
"JSON beam view; requires --detail summary and --evidence_output",
|
||||
),
|
||||
}),
|
||||
run: ({ args, options }) =>
|
||||
withCommandCancellation((signal) =>
|
||||
logCliCommand(logger, CLI_COMMANDS.inspectErlangBeam, async () => {
|
||||
const delivery = parseCliAnalysisEvidenceDelivery(
|
||||
"inspect_erlang_beam",
|
||||
options,
|
||||
erlangBeamDeliverySchema,
|
||||
);
|
||||
if (!delivery.ok) return projectAnalysisError(delivery.error);
|
||||
const service = createBeamService();
|
||||
return deliverCliAnalysisEvidence({
|
||||
callerInputPath: args.path,
|
||||
run: () =>
|
||||
service.inspect(
|
||||
{
|
||||
path: absoluteCallerPath(args.path),
|
||||
max_file_bytes: options.maxFileBytes,
|
||||
max_chunks: options.maxChunks,
|
||||
max_atoms: options.maxAtoms,
|
||||
},
|
||||
{ signal },
|
||||
),
|
||||
service,
|
||||
providerId: "rea-erlang-beam",
|
||||
operation: "inspect_erlang_beam",
|
||||
signal,
|
||||
detail: delivery.value.detail,
|
||||
evidenceOutput: delivery.value.evidenceOutput,
|
||||
...(delivery.value.view === undefined
|
||||
? {}
|
||||
: { view: delivery.value.view }),
|
||||
});
|
||||
}),
|
||||
),
|
||||
});
|
||||
registerContractCommand(
|
||||
cli,
|
||||
logger,
|
||||
contractNamed(ERLANG_BEAM_TOOL_CONTRACTS, "inspect_erlang_beam"),
|
||||
{
|
||||
command: CLI_COMMANDS.inspectErlangBeam,
|
||||
description:
|
||||
"Inspect Erlang BEAM atoms, exports, imports and Code header without executing bytecode",
|
||||
positionals: ["path"],
|
||||
callerPaths: ["path"],
|
||||
evidenceDelivery: true,
|
||||
viewDelivery: { invalidJson: "view must be valid JSON" },
|
||||
fields: {
|
||||
path: { description: "Local BEAM file" },
|
||||
view: {
|
||||
description:
|
||||
"JSON beam view; requires --detail summary and --evidence_output",
|
||||
},
|
||||
},
|
||||
},
|
||||
deliverWithService(createBeamService, "rea-erlang-beam"),
|
||||
);
|
||||
};
|
||||
+30
-55
@@ -1,19 +1,12 @@
|
||||
import { projectCliAnalysisResult } from "./projectCliAnalysisResult.js";
|
||||
import { z } from "incur";
|
||||
import { evmInterfaceModule } from "../composition/evm.js";
|
||||
import { logCliCommand } from "../cliLogging.js";
|
||||
import type { Logger } from "pino";
|
||||
import type { CliInstance } from "./types.js";
|
||||
import { withCommandCancellation } from "./commandCancellation.js";
|
||||
import { localPathStringSchema } from "../domain/localPath.js";
|
||||
import { absoluteCallerPath } from "../filesystem/callerPath.js";
|
||||
import { finishCliEvidenceCommand } from "./cliEvidenceCallerPath.js";
|
||||
import { cliDecimalField } from "./options.js";
|
||||
|
||||
import { evmInterfaceModule } from "../composition/evm.js";
|
||||
import {
|
||||
inspectEvmInterfaceInputSchema,
|
||||
evmProducerLimitsSchema,
|
||||
} from "../domain/evm/evmInterface.js";
|
||||
import { producerLimitsOption } from "./producerLimitsOption.js";
|
||||
inspectWithService,
|
||||
registerContractCommand,
|
||||
} from "./contractCommand.js";
|
||||
import { PRODUCER_LIMITS_HELP } from "./producerLimitsOption.js";
|
||||
import type { CliInstance } from "./types.js";
|
||||
|
||||
/** Resolve operator paths and preserve explicit encoding through the shared workflow. */
|
||||
export const registerEvmCommands = (
|
||||
@@ -22,45 +15,27 @@ export const registerEvmCommands = (
|
||||
environment: Readonly<NodeJS.ProcessEnv>,
|
||||
): void => {
|
||||
const service = evmInterfaceModule.create(environment);
|
||||
cli.command(evmInterfaceModule.cliCommand, {
|
||||
description: "Infer interface candidates from explicit local EVM bytecode",
|
||||
args: z.object({
|
||||
path: localPathStringSchema.describe("Local bytecode carrier"),
|
||||
encoding: z
|
||||
.enum(["raw", "hex"])
|
||||
.describe("Selected carrier representation"),
|
||||
}),
|
||||
options: z.object({
|
||||
producerLimits: producerLimitsOption(evmProducerLimitsSchema),
|
||||
timeoutMs: cliDecimalField(
|
||||
inspectEvmInterfaceInputSchema.shape.timeout_ms,
|
||||
),
|
||||
}),
|
||||
run: ({ args, options }) =>
|
||||
withCommandCancellation((signal) =>
|
||||
logCliCommand(logger, evmInterfaceModule.cliCommand, () =>
|
||||
finishCliEvidenceCommand(
|
||||
args.path,
|
||||
() =>
|
||||
service.inspect(
|
||||
{
|
||||
path: absoluteCallerPath(args.path),
|
||||
encoding: args.encoding,
|
||||
...(options.producerLimits === undefined
|
||||
? {}
|
||||
: { producer_limits: options.producerLimits }),
|
||||
...(options.timeoutMs === undefined
|
||||
? {}
|
||||
: { timeout_ms: options.timeoutMs }),
|
||||
},
|
||||
{ signal },
|
||||
),
|
||||
service,
|
||||
"evmole-interface",
|
||||
"inspect_evm_interface",
|
||||
signal,
|
||||
).then(projectCliAnalysisResult),
|
||||
),
|
||||
),
|
||||
});
|
||||
registerContractCommand(
|
||||
cli,
|
||||
logger,
|
||||
evmInterfaceModule.contract,
|
||||
{
|
||||
command: evmInterfaceModule.cliCommand,
|
||||
description:
|
||||
"Infer interface candidates from explicit local EVM bytecode",
|
||||
positionals: ["path", "encoding"],
|
||||
callerPaths: ["path"],
|
||||
jsonText: ["producer_limits"],
|
||||
fields: {
|
||||
path: { description: "Local bytecode carrier" },
|
||||
encoding: { description: "Selected carrier representation" },
|
||||
producer_limits: { description: PRODUCER_LIMITS_HELP },
|
||||
},
|
||||
omit: {
|
||||
detail:
|
||||
"One-shot output is complete Evidence; this command has no summary delivery",
|
||||
},
|
||||
},
|
||||
inspectWithService(() => service, "evmole-interface"),
|
||||
);
|
||||
};
|
||||
+42
-92
@@ -1,102 +1,52 @@
|
||||
import { parseCliJsonOption } from "./cliJsonOption.js";
|
||||
import { inspectModelArtifactInputSchema } from "../domain/model/modelArtifactQuery.js";
|
||||
import { analysisInputErrorFromIssues } from "../domain/inputIssueProjection.js";
|
||||
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
||||
import { z } from "incur";
|
||||
import type { Logger } from "pino";
|
||||
import type { CliInstance } from "./types.js";
|
||||
import { localPathStringSchema } from "../domain/localPath.js";
|
||||
import { absoluteCallerPath } from "../filesystem/callerPath.js";
|
||||
import { createModelArtifactService } from "../composition/model.js";
|
||||
|
||||
import { CLI_COMMANDS } from "../cliCommandNames.js";
|
||||
import { logCliCommand } from "../cliLogging.js";
|
||||
import { withCommandCancellation } from "./commandCancellation.js";
|
||||
import { createModelArtifactService } from "../composition/model.js";
|
||||
import { INSPECT_MODEL_ARTIFACT_CONTRACT } from "../contracts/model/modelToolContracts.js";
|
||||
import {
|
||||
analysisEvidenceDeliveryOptions,
|
||||
deliverCliAnalysisEvidence,
|
||||
} from "./analysisEvidenceDelivery.js";
|
||||
deliverWithService,
|
||||
registerContractCommand,
|
||||
} from "./contractCommand.js";
|
||||
import type { CliInstance } from "./types.js";
|
||||
|
||||
/** One-shot summaries export the same complete observations used by MCP retained pages. */
|
||||
export const registerModelCommands = (
|
||||
cli: CliInstance,
|
||||
logger: Logger,
|
||||
): void => {
|
||||
cli.command(CLI_COMMANDS.inspectModelArtifact, {
|
||||
description:
|
||||
"Inspect GGUF or safetensors headers without loading model weights",
|
||||
args: z.object({
|
||||
path: localPathStringSchema.describe(
|
||||
"Local model file, detected by content",
|
||||
),
|
||||
}),
|
||||
options: z.object({
|
||||
...analysisEvidenceDeliveryOptions,
|
||||
view: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
"JSON model page selection; requires --detail summary and --evidence_output",
|
||||
),
|
||||
hash: z
|
||||
.enum(["header", "full"])
|
||||
.default("header")
|
||||
.describe(
|
||||
"header reads declarations only; full additionally streams all bytes for SHA-256",
|
||||
),
|
||||
}),
|
||||
run: ({ args, options }) =>
|
||||
withCommandCancellation((signal) =>
|
||||
logCliCommand(logger, CLI_COMMANDS.inspectModelArtifact, async () => {
|
||||
const decoded = parseCliJsonOption(
|
||||
"inspect_model_artifact",
|
||||
"view",
|
||||
options.view,
|
||||
"view must be a JSON model page selection",
|
||||
);
|
||||
if (!decoded.ok) return projectAnalysisError(decoded.error);
|
||||
const view = decoded.value;
|
||||
const selected = inspectModelArtifactInputSchema.safeParse({
|
||||
path: absoluteCallerPath(args.path),
|
||||
hash: options.hash,
|
||||
detail: options.detail,
|
||||
...(view === undefined ? {} : { view }),
|
||||
});
|
||||
if (!selected.success)
|
||||
return projectAnalysisError(
|
||||
analysisInputErrorFromIssues(
|
||||
"inspect_model_artifact",
|
||||
selected.error.issues,
|
||||
{ view },
|
||||
),
|
||||
);
|
||||
const service = createModelArtifactService();
|
||||
return deliverCliAnalysisEvidence({
|
||||
callerInputPath: args.path,
|
||||
run: () =>
|
||||
service.inspect(
|
||||
{
|
||||
path: selected.data.path,
|
||||
hash: selected.data.hash,
|
||||
detail: "complete",
|
||||
},
|
||||
{ signal },
|
||||
),
|
||||
service,
|
||||
providerId: "model-artifact",
|
||||
operation: "inspect_model_artifact",
|
||||
signal,
|
||||
detail: options.detail,
|
||||
evidenceOutput: options.evidence_output,
|
||||
...(selected.data.view === undefined
|
||||
? {}
|
||||
: {
|
||||
view: {
|
||||
kind: "model-artifact" as const,
|
||||
...selected.data.view,
|
||||
},
|
||||
}),
|
||||
});
|
||||
}),
|
||||
),
|
||||
});
|
||||
const deliver = deliverWithService(
|
||||
createModelArtifactService,
|
||||
"model-artifact",
|
||||
);
|
||||
registerContractCommand(
|
||||
cli,
|
||||
logger,
|
||||
INSPECT_MODEL_ARTIFACT_CONTRACT,
|
||||
{
|
||||
command: CLI_COMMANDS.inspectModelArtifact,
|
||||
description:
|
||||
"Inspect GGUF or safetensors headers without loading model weights",
|
||||
positionals: ["path"],
|
||||
callerPaths: ["path"],
|
||||
evidenceDelivery: true,
|
||||
viewDelivery: {
|
||||
invalidJson: "view must be a JSON model page selection",
|
||||
kind: "model-artifact",
|
||||
},
|
||||
fields: {
|
||||
path: { description: "Local model file, detected by content" },
|
||||
hash: {
|
||||
description:
|
||||
"header reads declarations only; full additionally streams all bytes for SHA-256",
|
||||
},
|
||||
view: {
|
||||
description:
|
||||
"JSON model page selection; requires --detail summary and --evidence_output",
|
||||
},
|
||||
},
|
||||
},
|
||||
// The CLI delivers the summary itself, so the service returns complete
|
||||
// Evidence; the contract's own default would retain a summary instead.
|
||||
(input, context) => deliver({ ...input, detail: "complete" }, context),
|
||||
);
|
||||
};
|
||||
@@ -1,6 +1,10 @@
|
||||
import { z } from "incur";
|
||||
import { safeParseJson } from "../domain/safeJson.js";
|
||||
|
||||
/** CLI help for a producer's `producer_limits` JSON text. */
|
||||
export const PRODUCER_LIMITS_HELP =
|
||||
"JSON producer budget overrides. Keys and units follow this operation's producer_limits contract; changes apply on this call without restart and preserve tighter inherited OS limits.";
|
||||
|
||||
/** Incur passes object flags as text; validate the selected producer's exact policy. */
|
||||
export const producerLimitsOption = <Schema extends z.ZodType>(
|
||||
schema: Schema,
|
||||
@@ -21,6 +25,4 @@ export const producerLimitsOption = <Schema extends z.ZodType>(
|
||||
})
|
||||
.pipe(schema)
|
||||
.optional()
|
||||
.describe(
|
||||
"JSON producer budget overrides. Keys and units follow this operation's producer_limits contract; changes apply on this call without restart and preserve tighter inherited OS limits.",
|
||||
);
|
||||
.describe(PRODUCER_LIMITS_HELP);
|
||||
@@ -1,88 +1,40 @@
|
||||
import { z } from "incur";
|
||||
import { localPathStringSchema } from "../domain/localPath.js";
|
||||
import { absoluteCallerPath } from "../filesystem/callerPath.js";
|
||||
import { createPythonPycService } from "../composition/pythonPyc.js";
|
||||
import { CLI_COMMANDS } from "../cliCommandNames.js";
|
||||
import { logCliCommand } from "../cliLogging.js";
|
||||
import type { Logger } from "pino";
|
||||
import type { CliInstance } from "./types.js";
|
||||
import { withCommandCancellation } from "./commandCancellation.js";
|
||||
import { inspectPythonPycInputSchema } from "../domain/python/pythonPyc.js";
|
||||
|
||||
import { CLI_COMMANDS } from "../cliCommandNames.js";
|
||||
import { createPythonPycService } from "../composition/pythonPyc.js";
|
||||
import { PYTHON_PYC_TOOL_CONTRACTS } from "../contracts/python/pythonPycToolContracts.js";
|
||||
import { contractNamed } from "./contractCliDescription.js";
|
||||
import {
|
||||
analysisEvidenceDeliveryOptions,
|
||||
deliverCliAnalysisEvidence,
|
||||
parseCliAnalysisEvidenceDelivery,
|
||||
} from "./analysisEvidenceDelivery.js";
|
||||
import { pythonPycDeliverySchema } from "../domain/analysisView/pythonPycView.js";
|
||||
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
||||
import { cliDecimalField } from "./options.js";
|
||||
deliverWithService,
|
||||
registerContractCommand,
|
||||
} from "./contractCommand.js";
|
||||
import type { CliInstance } from "./types.js";
|
||||
|
||||
/** Inspect operator-selected pyc files through the same shared workflow as MCP. */
|
||||
export const registerPythonPycCommands = (
|
||||
cli: CliInstance,
|
||||
logger: Logger,
|
||||
): void => {
|
||||
cli.command(CLI_COMMANDS.inspectPythonPyc, {
|
||||
description:
|
||||
"Inspect a supported CPython 3.7–3.15 pyc header and marshal code objects without executing bytecode",
|
||||
args: z.object({
|
||||
path: localPathStringSchema.describe("Local CPython .pyc file"),
|
||||
}),
|
||||
options: z.object({
|
||||
// Decimal tokens only: numeric coercion would read "0x10" as 16 and
|
||||
// "1e2" as 100. The shared fields keep their bounds and defaults.
|
||||
maxFileBytes: cliDecimalField(
|
||||
inspectPythonPycInputSchema.shape.max_file_bytes,
|
||||
),
|
||||
maxObjects: cliDecimalField(
|
||||
inspectPythonPycInputSchema.shape.max_objects,
|
||||
),
|
||||
maxDepth: cliDecimalField(inspectPythonPycInputSchema.shape.max_depth),
|
||||
maxStringBytes: cliDecimalField(
|
||||
inspectPythonPycInputSchema.shape.max_string_bytes,
|
||||
),
|
||||
...analysisEvidenceDeliveryOptions,
|
||||
view: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
"JSON python-pyc view; requires --detail summary and --evidence_output",
|
||||
),
|
||||
}),
|
||||
run: ({ args, options }) =>
|
||||
withCommandCancellation((signal) =>
|
||||
logCliCommand(logger, CLI_COMMANDS.inspectPythonPyc, async () => {
|
||||
const delivery = parseCliAnalysisEvidenceDelivery(
|
||||
"inspect_python_pyc",
|
||||
options,
|
||||
pythonPycDeliverySchema,
|
||||
);
|
||||
if (!delivery.ok) return projectAnalysisError(delivery.error);
|
||||
const service = createPythonPycService();
|
||||
return deliverCliAnalysisEvidence({
|
||||
callerInputPath: args.path,
|
||||
run: () =>
|
||||
service.inspect(
|
||||
{
|
||||
path: absoluteCallerPath(args.path),
|
||||
max_file_bytes: options.maxFileBytes,
|
||||
max_objects: options.maxObjects,
|
||||
max_depth: options.maxDepth,
|
||||
max_string_bytes: options.maxStringBytes,
|
||||
},
|
||||
{ signal },
|
||||
),
|
||||
service,
|
||||
providerId: "rea-python-pyc",
|
||||
operation: "inspect_python_pyc",
|
||||
signal,
|
||||
detail: delivery.value.detail,
|
||||
evidenceOutput: delivery.value.evidenceOutput,
|
||||
...(delivery.value.view === undefined
|
||||
? {}
|
||||
: { view: delivery.value.view }),
|
||||
});
|
||||
}),
|
||||
),
|
||||
});
|
||||
registerContractCommand(
|
||||
cli,
|
||||
logger,
|
||||
contractNamed(PYTHON_PYC_TOOL_CONTRACTS, "inspect_python_pyc"),
|
||||
{
|
||||
command: CLI_COMMANDS.inspectPythonPyc,
|
||||
description:
|
||||
"Inspect a supported CPython 3.7–3.15 pyc header and marshal code objects without executing bytecode",
|
||||
positionals: ["path"],
|
||||
callerPaths: ["path"],
|
||||
evidenceDelivery: true,
|
||||
viewDelivery: { invalidJson: "view must be valid JSON" },
|
||||
fields: {
|
||||
path: { description: "Local CPython .pyc file" },
|
||||
view: {
|
||||
description:
|
||||
"JSON python-pyc view; requires --detail summary and --evidence_output",
|
||||
},
|
||||
},
|
||||
},
|
||||
deliverWithService(createPythonPycService, "rea-python-pyc"),
|
||||
);
|
||||
};
|
||||
+48
-119
@@ -1,128 +1,57 @@
|
||||
import { parseCliJsonOption } from "./cliJsonOption.js";
|
||||
import { z } from "incur";
|
||||
import { producerLimitsOption } from "./producerLimitsOption.js";
|
||||
import { rustProducerLimitsSchema } from "../domain/rust/rustResourceLimits.js";
|
||||
import { inspectRustBinaryInputSchema } from "../domain/rust/rustBinary.js";
|
||||
import { cliDecimalField } from "./options.js";
|
||||
import { createRustBinaryService } from "../composition/rust.js";
|
||||
import { CLI_COMMANDS } from "../cliCommandNames.js";
|
||||
import { logCliCommand } from "../cliLogging.js";
|
||||
import {
|
||||
analysisEvidenceDeliveryOptions,
|
||||
deliverCliAnalysisEvidence,
|
||||
} from "./analysisEvidenceDelivery.js";
|
||||
import { inspectRustBinaryRequestSchema } from "../domain/rust/rustBinaryQuery.js";
|
||||
import { analysisInputErrorFromIssues } from "../domain/inputIssueProjection.js";
|
||||
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
||||
import type { Logger } from "pino";
|
||||
|
||||
import { CLI_COMMANDS } from "../cliCommandNames.js";
|
||||
import { createRustBinaryService } from "../composition/rust.js";
|
||||
import { RUST_TOOL_CONTRACTS } from "../contracts/rust/rustToolContracts.js";
|
||||
import { contractNamed } from "./contractCliDescription.js";
|
||||
import {
|
||||
deliverWithService,
|
||||
registerContractCommand,
|
||||
} from "./contractCommand.js";
|
||||
import { PRODUCER_LIMITS_HELP } from "./producerLimitsOption.js";
|
||||
import type { CliInstance } from "./types.js";
|
||||
import { withCommandCancellation } from "./commandCancellation.js";
|
||||
import { localPathStringSchema } from "../domain/localPath.js";
|
||||
import { absoluteCallerPath } from "../filesystem/callerPath.js";
|
||||
import { machoSliceArchitectureSchema } from "../domain/apple/machoTargetSelection.js";
|
||||
|
||||
/** Resolve caller paths and inspect embedded Rust dependencies through the shared workflow. */
|
||||
export const registerRustCommands = (
|
||||
cli: CliInstance,
|
||||
logger: Logger,
|
||||
): void => {
|
||||
cli.command(CLI_COMMANDS.inspectRustBinary, {
|
||||
description:
|
||||
"Inspect an embedded cargo-auditable Rust dependency list without target execution",
|
||||
args: z.object({
|
||||
path: localPathStringSchema.describe(
|
||||
"Local ELF, PE, thin Mach-O or universal Mach-O image",
|
||||
),
|
||||
}),
|
||||
options: z.object({
|
||||
...analysisEvidenceDeliveryOptions,
|
||||
producerLimits: producerLimitsOption(rustProducerLimitsSchema),
|
||||
maxRustcHashes: cliDecimalField(
|
||||
inspectRustBinaryInputSchema.shape.max_rustc_hashes,
|
||||
),
|
||||
architecture: machoSliceArchitectureSchema
|
||||
.optional()
|
||||
.describe(
|
||||
"Slice to read from a universal Mach-O, by CPU family; when several slices share a family, arm64 (arm64 or arm64v8), arm64e (arm64e or arm64e.x1) or x86_64 names the subtype, and a value matching several slices is refused; required for universal files",
|
||||
),
|
||||
detail: z
|
||||
.enum(["complete", "summary"])
|
||||
.optional()
|
||||
.describe(
|
||||
"Default complete; a view selects compact delivery with --evidence_output",
|
||||
),
|
||||
view: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
"JSON package or edge selection; retains complete Evidence with --evidence_output",
|
||||
),
|
||||
}),
|
||||
run: ({ args, options }) =>
|
||||
withCommandCancellation((signal) =>
|
||||
logCliCommand(logger, CLI_COMMANDS.inspectRustBinary, async () => {
|
||||
const decoded = parseCliJsonOption(
|
||||
"inspect_rust_binary",
|
||||
"view",
|
||||
options.view,
|
||||
"view must be a JSON package or edge selection",
|
||||
);
|
||||
if (!decoded.ok) return projectAnalysisError(decoded.error);
|
||||
const view = decoded.value;
|
||||
const input = inspectRustBinaryRequestSchema.safeParse({
|
||||
path: absoluteCallerPath(args.path),
|
||||
...(options.producerLimits === undefined
|
||||
? {}
|
||||
: { producer_limits: options.producerLimits }),
|
||||
...(options.maxRustcHashes === undefined
|
||||
? {}
|
||||
: { max_rustc_hashes: options.maxRustcHashes }),
|
||||
...(options.architecture === undefined
|
||||
? {}
|
||||
: { architecture: options.architecture }),
|
||||
...(options.detail === undefined ? {} : { detail: options.detail }),
|
||||
...(view === undefined ? {} : { view }),
|
||||
});
|
||||
if (!input.success)
|
||||
return projectAnalysisError(
|
||||
analysisInputErrorFromIssues(
|
||||
"inspect_rust_binary",
|
||||
input.error.issues,
|
||||
{ view },
|
||||
),
|
||||
);
|
||||
const service = createRustBinaryService();
|
||||
return deliverCliAnalysisEvidence({
|
||||
callerInputPath: args.path,
|
||||
run: () =>
|
||||
service.inspect(
|
||||
{
|
||||
path: input.data.path,
|
||||
...(input.data.producer_limits === undefined
|
||||
? {}
|
||||
: { producer_limits: input.data.producer_limits }),
|
||||
...(input.data.max_rustc_hashes === undefined
|
||||
? {}
|
||||
: { max_rustc_hashes: input.data.max_rustc_hashes }),
|
||||
...(input.data.architecture === undefined
|
||||
? {}
|
||||
: { architecture: input.data.architecture }),
|
||||
},
|
||||
{ signal },
|
||||
),
|
||||
service,
|
||||
providerId: "rust-auditable",
|
||||
operation: "inspect_rust_binary",
|
||||
signal,
|
||||
detail:
|
||||
input.data.detail ??
|
||||
(input.data.view === undefined ? "complete" : "summary"),
|
||||
evidenceOutput: options.evidence_output,
|
||||
...(input.data.view === undefined
|
||||
? {}
|
||||
: { view: { kind: "rust" as const, ...input.data.view } }),
|
||||
});
|
||||
}),
|
||||
),
|
||||
});
|
||||
registerContractCommand(
|
||||
cli,
|
||||
logger,
|
||||
contractNamed(RUST_TOOL_CONTRACTS, "inspect_rust_binary"),
|
||||
{
|
||||
command: CLI_COMMANDS.inspectRustBinary,
|
||||
description:
|
||||
"Inspect an embedded cargo-auditable Rust dependency list without target execution",
|
||||
positionals: ["path"],
|
||||
callerPaths: ["path"],
|
||||
jsonText: ["producer_limits"],
|
||||
evidenceDelivery: true,
|
||||
viewDelivery: {
|
||||
invalidJson: "view must be a JSON package or edge selection",
|
||||
kind: "rust",
|
||||
viewSelectsSummary: true,
|
||||
},
|
||||
fields: {
|
||||
path: {
|
||||
description: "Local ELF, PE, thin Mach-O or universal Mach-O image",
|
||||
},
|
||||
producer_limits: { description: PRODUCER_LIMITS_HELP },
|
||||
architecture: {
|
||||
description:
|
||||
"Slice to read from a universal Mach-O, by CPU family; when several slices share a family, arm64 (arm64 or arm64v8), arm64e (arm64e or arm64e.x1) or x86_64 names the subtype, and a value matching several slices is refused; required for universal files",
|
||||
},
|
||||
detail: {
|
||||
description:
|
||||
"Default complete; a view selects compact delivery with --evidence_output",
|
||||
},
|
||||
view: {
|
||||
description:
|
||||
"JSON package or edge selection; retains complete Evidence with --evidence_output",
|
||||
},
|
||||
},
|
||||
},
|
||||
deliverWithService(createRustBinaryService, "rust-auditable"),
|
||||
);
|
||||
};
|
||||
@@ -111,10 +111,6 @@ export const erlangBeamSummaryDeliveryShape = {
|
||||
detail: z.literal("summary"),
|
||||
view: erlangBeamViewRequestSchema.exactOptional(),
|
||||
};
|
||||
export const erlangBeamDeliverySchema = z.discriminatedUnion("detail", [
|
||||
z.strictObject(erlangBeamCompleteDeliveryShape),
|
||||
z.strictObject(erlangBeamSummaryDeliveryShape),
|
||||
]);
|
||||
const readReport = createAnalysisViewResultReader(erlangBeamSchema);
|
||||
type Table = "atoms" | "exports" | "imports" | "lambdas" | "chunks";
|
||||
type Row = NonNullable<ErlangBeam[Table]>[number];
|
||||
|
||||
@@ -40,10 +40,6 @@ export const pythonPycSummaryDeliveryShape = {
|
||||
detail: z.literal("summary"),
|
||||
view: pythonPycViewRequestSchema.exactOptional(),
|
||||
};
|
||||
export const pythonPycDeliverySchema = z.discriminatedUnion("detail", [
|
||||
z.strictObject(pythonPycCompleteDeliveryShape),
|
||||
z.strictObject(pythonPycSummaryDeliveryShape),
|
||||
]);
|
||||
|
||||
export const pythonPycViewItemsSchema = z.array(
|
||||
code.partial().extend({ source_index: z.int().nonnegative() }),
|
||||
|
||||
@@ -134,7 +134,38 @@ const registeredField = (
|
||||
};
|
||||
|
||||
const contractShape = (description: ContractCliDescription) =>
|
||||
description.contract.inputSchema.shape as Record<string, z.ZodType>;
|
||||
description.fields;
|
||||
|
||||
/** A caller-relative target, or the first probe the positional admits. */
|
||||
const positionalValue = (
|
||||
description: ContractCliDescription,
|
||||
parameter: ContractCliParameter,
|
||||
): unknown =>
|
||||
parameter.callerPath
|
||||
? "rel/target"
|
||||
: probes(contractShape(description)[parameter.field]!).find(
|
||||
(value) =>
|
||||
value !== undefined &&
|
||||
admits(registeredField(description, parameter), value),
|
||||
);
|
||||
|
||||
/** Parsed option values plus admitted positionals. */
|
||||
const argvValues = (
|
||||
description: ContractCliDescription,
|
||||
options: Readonly<Record<string, unknown>>,
|
||||
): Record<string, unknown> => ({
|
||||
...(registered.get(description.command)!.options as z.ZodObject).parse(
|
||||
options,
|
||||
),
|
||||
...Object.fromEntries(
|
||||
description.parameters
|
||||
.filter(({ positional }) => positional)
|
||||
.map((parameter) => [
|
||||
parameter.key,
|
||||
positionalValue(description, parameter),
|
||||
]),
|
||||
),
|
||||
});
|
||||
|
||||
const each = descriptions.map(
|
||||
(description) => [description.command, description] as const,
|
||||
@@ -150,6 +181,13 @@ describe("CLI commands projected from tool contracts", () => {
|
||||
"inspect-devicetree",
|
||||
"inspect-go-binary",
|
||||
"inspect-v8-code-cache",
|
||||
"inspect-rust-binary",
|
||||
"inspect-python-pyc",
|
||||
"inspect-erlang-beam",
|
||||
"inspect-model-artifact",
|
||||
"inspect-apk-signature",
|
||||
"inspect-desktop-application",
|
||||
"inspect-evm-interface",
|
||||
]),
|
||||
);
|
||||
});
|
||||
@@ -202,6 +240,12 @@ describe("CLI commands projected from tool contracts", () => {
|
||||
expect(admits(contractField, "rel/x"), parameter.field).toBe(false);
|
||||
expect(admits(contractField, absoluteCallerPath("rel/x"))).toBe(true);
|
||||
}
|
||||
if (parameter.viewSelection) {
|
||||
// The view is JSON text decoded when the command runs; see below.
|
||||
for (const value of [undefined, "", "{", "{}"])
|
||||
expect(admits(cliField, value), parameter.key).toBe(true);
|
||||
continue;
|
||||
}
|
||||
for (const value of probes(contractField)) {
|
||||
const expected = expectedAdmission(parameter, contractField, value);
|
||||
if (admits(cliField, value) !== expected)
|
||||
@@ -233,15 +277,7 @@ describe("CLI commands projected from tool contracts", () => {
|
||||
it.each(each)(
|
||||
"%s maps parsed argv to an admitted contract input",
|
||||
(_command, description) => {
|
||||
const command = registered.get(description.command)!;
|
||||
const values = {
|
||||
...(command.options as z.ZodObject).parse({}),
|
||||
...Object.fromEntries(
|
||||
description.parameters
|
||||
.filter(({ positional }) => positional)
|
||||
.map(({ key }) => [key, "rel/target"]),
|
||||
),
|
||||
};
|
||||
const values = argvValues(description, {});
|
||||
const input = description.toContractInput(values);
|
||||
expect(description.contract.inputSchema.safeParse(input).error).toBe(
|
||||
undefined,
|
||||
@@ -287,3 +323,98 @@ describe("CLI commands projected from tool contracts", () => {
|
||||
).toThrow("probe input keyword not has no CLI projection");
|
||||
});
|
||||
});
|
||||
|
||||
const delivering = each.filter(
|
||||
([, description]) => description.delivery !== undefined,
|
||||
);
|
||||
|
||||
describe("Evidence delivery of contract commands", () => {
|
||||
it.each(delivering)(
|
||||
"%s refuses summary delivery without --evidence_output before analysis",
|
||||
(_command, description) => {
|
||||
const refused = description.delivery!(
|
||||
argvValues(description, { detail: "summary" }),
|
||||
);
|
||||
expect(refused.ok).toBe(false);
|
||||
if (!refused.ok)
|
||||
expect(refused.error.issues.map(({ path }) => path)).toEqual([
|
||||
["evidence_output"],
|
||||
]);
|
||||
expect(
|
||||
description.delivery!(
|
||||
argvValues(description, {
|
||||
detail: "summary",
|
||||
evidence_output: "out.json",
|
||||
}),
|
||||
).ok,
|
||||
).toBe(true);
|
||||
},
|
||||
);
|
||||
|
||||
it.each(
|
||||
delivering.filter(([, description]) =>
|
||||
description.parameters.some(({ viewSelection }) => viewSelection),
|
||||
),
|
||||
)(
|
||||
"%s decodes --view when it runs and admits what its contract admits",
|
||||
(_command, description) => {
|
||||
const view = description.parameters.find(
|
||||
({ viewSelection }) => viewSelection,
|
||||
)!;
|
||||
const malformed = description.delivery!(
|
||||
argvValues(description, {
|
||||
detail: "summary",
|
||||
evidence_output: "out.json",
|
||||
[view.key]: "{",
|
||||
}),
|
||||
);
|
||||
expect(malformed.ok).toBe(false);
|
||||
if (!malformed.ok)
|
||||
expect(malformed.error.issues).toEqual([
|
||||
expect.objectContaining({ path: ["view"], reason: "invalid_format" }),
|
||||
]);
|
||||
const mismatches: string[] = [];
|
||||
for (const detail of ["complete", "summary"] as const)
|
||||
for (const text of ["{}", "[]", "null", '{"unknown":1}']) {
|
||||
const values = argvValues(description, {
|
||||
detail,
|
||||
evidence_output: "out.json",
|
||||
[view.key]: text,
|
||||
});
|
||||
const expected = description.contract.inputSchema.safeParse({
|
||||
...description.toContractInput(values),
|
||||
detail,
|
||||
view: JSON.parse(text) as unknown,
|
||||
}).success;
|
||||
if (description.delivery!(values).ok !== expected)
|
||||
mismatches.push(`${detail} ${text} expected ${String(expected)}`);
|
||||
}
|
||||
expect(mismatches).toEqual([]);
|
||||
},
|
||||
);
|
||||
|
||||
it("refuses a union input whose branches differ outside the delivery", () => {
|
||||
const inputSchema = z.discriminatedUnion("detail", [
|
||||
z.object({
|
||||
detail: z.literal("complete").optional(),
|
||||
limit: z.number().optional(),
|
||||
}),
|
||||
z.object({
|
||||
detail: z.literal("summary"),
|
||||
limit: z.number().max(1).optional(),
|
||||
}),
|
||||
]);
|
||||
expect(() =>
|
||||
describeContractCommand(
|
||||
{ name: "probe", inputSchema },
|
||||
{ command: "probe", description: "Probe", evidenceDelivery: true },
|
||||
),
|
||||
).toThrow("probe.limit differs between input branches");
|
||||
expect(() =>
|
||||
describeContractCommand(
|
||||
{ name: "probe", inputSchema },
|
||||
{ command: "probe", description: "Probe" },
|
||||
),
|
||||
).toThrow("probe.detail differs between input branches");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user