import { AnalysisWorkScope } from "./AnalysisWorkScope.js"; import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js"; import { jsonStringByteLength } from "../domain/jsonStringByteLength.js"; import { createAnalysisExecutionEvidence } from "../domain/analysisObservation.js"; import { REA_WORKFLOW_PROVIDER } from "../domain/investigationProviders.js"; import { workflowAnalysisProfile, workflowAnalysisProfileParameters, } from "./WorkflowAnalysisProfile.js"; import type { AnalysisOperation, AnalysisOperationPort, } from "./AnalysisProvider.js"; import { err, ok } from "../domain/result.js"; import type { EnhancedResult } from "./EnhancedToolTypes.js"; import type { AnalysisProfileCommitment } from "../domain/analysisProfile.js"; import type { AnalysisError } from "../domain/analysisErrorBase.js"; import type { WorkflowSnapshotRecordInput } from "./binary/BinarySessionRecords.js"; import { analysisProfileSchema, committedProviderSchema, } from "../domain/analysisProfile.js"; import type { BinaryTarget } from "../domain/binaryTargetTypes.js"; import { createEvidence, MISSING_EVIDENCE_SUBJECT_LIMITATION, evidenceEnvelopeForAdmission, type Evidence, type EvidenceLocation, } from "../domain/evidence.js"; import type { JsonValue } from "../domain/jsonValue.js"; import type { UnknownRegistryPort } from "./investigation/InvestigationRecordPort.js"; import { UnknownRegistryError } from "../domain/unknownRegistryError.js"; import { EvidenceLedger } from "./investigation/EvidenceLedger.js"; import { recordEvidenceSources, retainAnalysisResultSources, withEvidenceSources, type AnalysisResultWithSources, } from "./investigation/recordEvidenceSources.js"; import { preserveCompletedSourceEvidence } from "./investigation/sourcePublicationRecovery.js"; import { admitInvestigationAuthentication, type RecoveryAdmission, } from "./investigation/InvestigationRetention.js"; import { estimateJsonStorage } from "./JsonAuthenticationHeadroom.js"; import { workflowUnknownInput } from "./investigation/WorkflowUnknown.js"; import { renderSourceLimitations, sourceLimitationGroupKey, sourceLimitationKey, type SourceLimitation, } from "./SourceLimitations.js"; /** * Bound for caveats a workflow observes once per item. Provider caveats can * name per-item addresses, so they do not deduplicate and would otherwise grow * with the number of items. */ export interface ComposedLimitationBudget { /** Operations the workflow calls once per item. */ readonly perItemOperations: ReadonlySet; /** * First distinct per-item caveats retained, grouped by provider and text. * Kept lines are rendered in the usual sorted order. */ readonly maxDistinct: number; /** Retained caveat text as JSON-encoded UTF-8 bytes; defaults to 8,000. */ readonly maxBytes?: number; /** Where the omitted per-item caveats remain, as a clause. */ readonly retainedIn: string; } const DECOMPILE_ALL_LIMITATIONS: ComposedLimitationBudget = { perItemOperations: new Set(["procedure_info", "procedure_pseudo_code"]), maxDistinct: 8, retainedIn: "decompile-index.json keeps the latest record for each function; decompile-journal.jsonl keeps each attempt and its caveats", }; /** The caveat budget for a composed workflow, when it has one. */ export const composedLimitationBudget = ( operation: string, ): ComposedLimitationBudget | undefined => operation === "decompile_all" ? DECOMPILE_ALL_LIMITATIONS : undefined; /** What one composed call answered, or admitted before a failure stopped it. */ export interface ComposedWorkflowObservation { readonly result: JsonValue; readonly limitations: readonly string[]; readonly cachePolicy?: "live"; readonly evidenceLinks?: readonly string[]; } /** A composed outcome; only a stopped failure carries its admitted observation. */ export type ComposedWorkflowResult = AnalysisResultWithSources & { readonly partial?: ComposedWorkflowObservation; }; /** Retain the limits of exactly the observations used by one composed call. */ export const executeComposedWorkflow = async ( analysis: AnalysisOperationPort, execute: (observed: AnalysisOperationPort) => EnhancedResult, budget?: ComposedLimitationBudget, ): Promise => { const limitations = new Map(); const evidenceLinks = new Set(); const sourceLedger = new EvidenceLedger(); const perItemGroups = new Set(); let perItemBytes = 0; let omittedObservations = 0; let cachePolicy: "live" | undefined; const workScope = new AnalysisWorkScope(); const observed: AnalysisOperationPort = { resourceScope: analysis.resourceScope, execute: async (operation, parameters, options) => { let execution = await analysis.execute(operation, parameters, { ...options, workScope, }); if (execution.ok && execution.value.sourceEvidence !== undefined) { const recorded = recordEvidenceSources( (source) => sourceLedger.record(source), execution.value.sourceEvidence, ); if (!recorded.ok) return recorded; } if ( execution.ok && options?.retainEvidence === true && execution.value.evidenceId === undefined ) { const source = createAnalysisExecutionEvidence( operation, parameters, execution.value, "observed", ); const recorded = recordEvidenceSources( (evidence) => sourceLedger.record(evidence), [source], ); if (!recorded.ok) return recorded; execution = ok({ ...execution.value, evidenceId: source.evidence_id }); } if (execution.ok && execution.value.evidenceId !== undefined) evidenceLinks.add(execution.value.evidenceId); if (execution.ok && execution.value.cachePolicy === "live") cachePolicy = "live"; if (execution.ok) for (const limitation of execution.value.limitations) { const fact = { provider: execution.value.provider, operation, limitation, } satisfies SourceLimitation; if (budget?.perItemOperations.has(operation) === true) { // Reject oversized text before allocating a grouping key. Complete // caveats are already retained by the export's per-function owner. const maxBytes = budget.maxBytes ?? 8_000; const bytes = jsonStringByteLength(limitation, maxBytes); if (bytes > maxBytes) { omittedObservations += 1; continue; } const group = sourceLimitationGroupKey(fact); if (!perItemGroups.has(group)) { if ( perItemGroups.size >= budget.maxDistinct || perItemBytes + bytes > maxBytes ) { omittedObservations += 1; continue; } perItemGroups.add(group); perItemBytes += bytes; } } limitations.set(sourceLimitationKey(fact), fact); } return execution; }, }; const result = await Promise.resolve() .then(() => execute(observed)) .finally(() => workScope.clear()); const sources = sourceLedger.recordsForAnalysis(); if (!result.ok && !("partialResult" in result)) return withEvidenceSources(result, sources); for (const source of sources) evidenceLinks.add(source.evidence_id); const observation = (value: JsonValue): ComposedWorkflowObservation => ({ result: value, limitations: [ ...renderSourceLimitations([...limitations.values()]), ...(omittedObservations === 0 ? [] : [ `${omittedObservations} per-item provider limitation observation${omittedObservations === 1 ? " was" : "s were"} omitted from this answer; ${budget?.retainedIn}.`, ]), ], ...(cachePolicy === undefined ? {} : { cachePolicy }), ...(evidenceLinks.size === 0 ? {} : { evidenceLinks: [...evidenceLinks].sort(compareUnicodeCodePoints) }), }); return withEvidenceSources( result.ok ? ok(observation(result.value)) : { ok: false as const, error: result.error, partial: observation(result.partialResult), }, sources, ); }; /** Scope of the exact-address reference lookups composed by trace_feature. */ export const TRACE_FEATURE_REFERENCE_LIMITATION = "Reference queries cover only each matched start address, not interior bytes of strings and procedures. Empty references and truncated: false do not establish complete reference coverage."; /** Build the shared Evidence record for a successful composed binary workflow. */ export interface WorkflowEvidenceInput { readonly target: BinaryTarget | undefined; readonly operation: AnalysisOperation; readonly parameters: Readonly>; readonly result: JsonValue; readonly upstreamProfile: AnalysisProfileCommitment | undefined; readonly upstreamLimitations: readonly string[]; readonly upstreamEvidenceLinks?: readonly string[]; } const WORKFLOW_DERIVATION_LIMITATION = "Derived by an REA composed workflow."; const workflowLimitations = ( operation: AnalysisOperation, ): readonly string[] => operation === "trace_feature" ? [WORKFLOW_DERIVATION_LIMITATION, TRACE_FEATURE_REFERENCE_LIMITATION] : [WORKFLOW_DERIVATION_LIMITATION]; export const createWorkflowEvidence = ( input: WorkflowEvidenceInput, ): Evidence => createEvidence(input.target, REA_WORKFLOW_PROVIDER, { operation: input.operation, parameters: input.parameters, result: input.result, ...(input.upstreamProfile === undefined ? {} : { analysisProfile: workflowAnalysisProfile( input.upstreamProfile, input.operation, ), }), confidence: "derived", ...(input.upstreamEvidenceLinks === undefined ? {} : { evidenceLinks: input.upstreamEvidenceLinks }), limitations: [ ...workflowLimitations(input.operation), ...input.upstreamLimitations, ], ...(input.operation === "batch_decompile" ? { locations: batchProcedureLocations(input.result) } : {}), }); const admitWorkflowRecovery = ( observation: ComposedWorkflowObservation, evidenceInput: Pick< WorkflowEvidenceInput, "target" | "operation" | "parameters" | "upstreamProfile" >, admitRecovery: RecoveryAdmission, ) => { const admissionInput = { operation: evidenceInput.operation, parameters: evidenceInput.parameters, target: evidenceInput.target === undefined ? null : { path: evidenceInput.target.path, sha256: evidenceInput.target.sha256, format: evidenceInput.target.format, architecture: evidenceInput.target.architecture ?? null, }, profile: evidenceInput.upstreamProfile ?? null, result: observation.result, limitations: observation.limitations, links: observation.evidenceLinks ?? [], }; // The completed result can also generate a location per batch item and // the workflow profile embeds the upstream profile. Admit these copies // before constructing or hashing the recovery Evidence. const shell = evidenceEnvelopeForAdmission( evidenceInput.target, REA_WORKFLOW_PROVIDER, { operation: evidenceInput.operation, parameters: {}, result: null, ...(evidenceInput.upstreamProfile === undefined ? {} : { analysisProfile: { provider: REA_WORKFLOW_PROVIDER, parameters: workflowAnalysisProfileParameters( null, evidenceInput.operation, ), digest: "0".repeat(64), }, }), confidence: "derived", limitations: workflowLimitations(evidenceInput.operation), locations: [], evidenceLinks: [], }, ); const additional = estimateJsonStorage([ observation.result, evidenceInput.upstreamProfile ?? null, shell, ...(evidenceInput.target === undefined ? [MISSING_EVIDENCE_SUBJECT_LIMITATION] : []), ]); return admitRecovery(admissionInput, evidenceInput.operation, additional); }; /** Experimental shared recovery of a workflow that completed before source admission. */ export const retainComposedWorkflowSources = ( recordEvidence: Parameters[1], result: Awaited>, evidenceInput: Omit< WorkflowEvidenceInput, "result" | "upstreamLimitations" | "upstreamEvidenceLinks" >, evidenceForAnalysis?: (id: string) => Evidence | undefined, admitRecovery: RecoveryAdmission = (input, operation, additional) => admitInvestigationAuthentication(input, operation, {}, additional), ): ReturnType< typeof retainAnalysisResultSources<{ readonly result: JsonValue; readonly limitations: readonly string[]; readonly cachePolicy?: "live"; readonly evidenceLinks?: readonly string[]; }> > => { const retained = retainAnalysisResultSources( evidenceInput.operation, recordEvidence, result, ); if (result.ok && !retained.ok) { const admitted = admitWorkflowRecovery( result.value, evidenceInput, admitRecovery, ); if (!admitted.ok) { retained.error.retainSourcePublication({ kind: "omitted", reason: admitted.error._tag === "AnalysisResourceConstraintError" ? "insufficient-authentication-headroom" : "recovery-input-invalid", source_closure: "not-established", }); return retained; } const evidence = createWorkflowEvidence({ ...evidenceInput, result: result.value.result, upstreamLimitations: result.value.limitations, ...(result.value.evidenceLinks === undefined ? {} : { upstreamEvidenceLinks: result.value.evidenceLinks }), }); preserveCompletedSourceEvidence( retained.error, evidence, evidenceForAnalysis, ); } return retained; }; /** Retain a stopped workflow's admitted observation as partial workflow Evidence on its failure. */ export const retainPartialWorkflowEvidence = ( outcome: ComposedWorkflowResult, context: Pick< Parameters[0], "target" | "operation" | "parameters" | "upstreamProfile" >, admitRecovery: RecoveryAdmission = (input, operation, additional) => admitInvestigationAuthentication(input, operation, {}, additional), ): void => { if ( outcome.ok || outcome.partial === undefined || outcome.error.partialObservation !== undefined ) return; const admitted = admitWorkflowRecovery( outcome.partial, context, admitRecovery, ); if (!admitted.ok) { outcome.error.retainPartialObservation({ kind: "omitted-observation", reason: admitted.error._tag === "AnalysisResourceConstraintError" ? "insufficient-authentication-headroom" : "recovery-input-invalid", }); return; } const evidence = createWorkflowEvidence({ ...context, result: outcome.partial.result, upstreamLimitations: outcome.partial.limitations, ...(outcome.partial.evidenceLinks === undefined ? {} : { upstreamEvidenceLinks: outcome.partial.evidenceLinks }), }); // A partial observation the failing read already reported stays its own. outcome.error.retainPartialObservation(evidence); }; const batchProcedureLocations = (result: JsonValue): EvidenceLocation[] => { if ( result === null || typeof result !== "object" || Array.isArray(result) || !Array.isArray(result.items) ) return []; const addresses = new Set(); for (const item of result.items) { if (item === null || typeof item !== "object" || Array.isArray(item)) continue; const procedure = item.procedure; if ( procedure !== null && typeof procedure === "object" && !Array.isArray(procedure) && procedure.status === "resolved" && typeof procedure.address === "string" ) addresses.add(procedure.address); } return [...addresses].map((address) => ({ kind: "address", address })); }; /** Convert workflow Evidence into its exact replay binding, when it has a profile. */ export const workflowSnapshotRecord = ( evidence: Evidence, operation: AnalysisOperation, bindingProfile: AnalysisProfileCommitment | undefined, cachePolicy?: "live", ): WorkflowSnapshotRecordInput | undefined => { if ( evidence.analysis_profile === null || bindingProfile === undefined || cachePolicy === "live" ) return undefined; return { operation, bindingProfile, parameters: evidence.parameters, execution: { result: evidence.normalized_result, rawResult: evidence.raw_result, provider: committedProviderSchema.parse(evidence.provider), analysisProfile: analysisProfileSchema.parse(evidence.analysis_profile), limitations: evidence.limitations, locations: evidence.locations, subject: evidence.subject === null ? null : { path: evidence.subject.local_path, sha256: evidence.subject.digest.sha256, format: evidence.subject.format, ...(evidence.subject.architecture === null ? {} : { architecture: evidence.subject.architecture }), }, }, }; }; interface WorkflowUnknownInput { readonly name: string; readonly result: JsonValue; readonly evidenceId: string; readonly recordUnknown: UnknownRegistryPort["recordUnknown"] | undefined; /** The caller confirmed the completed Evidence is in its investigation. */ readonly evidenceRetained?: boolean; } /** Retain residual workflow questions using the shared investigation policy. */ export const recordWorkflowUnknowns = ({ name, result, evidenceId, recordUnknown, evidenceRetained, }: WorkflowUnknownInput): | ReturnType | { readonly ok: true; readonly value: null } => { if ( !["trace_feature", "trace_call_path", "inspect_native_api"].includes( name, ) || recordUnknown === undefined || typeof result !== "object" || result === null || Array.isArray(result) || !Array.isArray(result.residual_unknowns) ) return { ok: true, value: null }; const questions = result.residual_unknowns; const total = questions.reduce( (count, value) => count + (typeof value === "string" ? 1 : 0), 0, ); let created = 0; let existing = 0; const stopped = (error: AnalysisError, index: number) => err( error.retainWorkflowUnknownPublication({ evidence_id: evidenceId, evidence_retention: evidenceRetained === true ? "retained" : "unverified", total_questions: total, created_questions: created, existing_questions: existing, next_question_index: index, }), ); for (let index = 0; index < questions.length; index += 1) { const question = questions[index]; if (typeof question !== "string") continue; const input = workflowUnknownInput(evidenceId, { question, severity: "medium", domain: name === "inspect_native_api" ? "native-api" : "control-flow", required_authority: "shipped-artifact", required_confidence: "observed", required_environment: null, recommended_probes: [ { operation: name, rationale: name === "inspect_native_api" ? "Confirm the unsupported boundary with a capable provider or ABI probe." : "Continue with a focused query or another available provider.", }, ], relationships: [], }); if (!input.ok) return stopped(input.error, index); const recorded = recordUnknown(input.value); if (recorded.ok) created += 1; else if ( recorded.error instanceof UnknownRegistryError && recorded.error.reason === "already-exists" ) existing += 1; else return stopped(recorded.error, index); } return { ok: true, value: null }; };