/** * ADR-0027: pdf_to_md_bundle capability adapter. * * Converts a PDF in the run's workspace into a Markdown bundle (md + extracted * images) by calling the MinerU document parsing service, writing outputs into * the workspace, and recording consumption on a UsageFact (ADR-0026). * * Invariants (ADR-0027): * 1. Credential isolation — the capability credential is resolved in Hub and * never reaches the Agent process. The MineruClient receives it as a * call argument, not from the environment. * 2. Workspace containment — input and output paths are confined to the * run's workspace dir (ADR-0018 AgentSurface). Escapes are rejected. * 3. Mandatory fact — a successful invocation always writes ≥1 UsageFact * with kind=external_capability, even when costUsd is null (ADR-0022: * missing cost ≠ zero). */ import { mkdir, writeFile } from "node:fs/promises"; import { join, resolve, relative, isAbsolute } from "node:path"; import type { PrismaClient } from "@prisma/client"; import { LocalSecretEnvelope } from "../security/secretEnvelope.js"; import { resolveCapabilityCredential } from "./capabilityConnections.js"; import { DocmindClientError, type CapabilityProviderClient } from "./docmindClient.js"; import { CAPABILITIES, type CapabilityAdapter, type CapabilityInvocationInput, type CapabilityInvocationResult, type CapabilityOutputArtifact, } from "./types.js"; const CAPABILITY_ID = "pdf_to_md_bundle" as const; const PROVIDER_ID = "aliyun_docmind"; /** Thrown when a requested path escapes the workspace root (ADR-0018). */ export class CapabilityPathEscape extends Error { constructor(readonly requested: string, readonly workspaceDir: string) { super(`capability path escapes workspace: ${requested} (root ${workspaceDir})`); this.name = "CapabilityPathEscape"; } } /** Resolve a workspace-relative path, rejecting escapes (ADR-0018 AgentSurface). */ function confineToWorkspace(requestedPath: string, workspaceDir: string): string { if (isAbsolute(requestedPath)) { const rel = relative(workspaceDir, requestedPath); if (rel.startsWith("..") || rel === "") { throw new CapabilityPathEscape(requestedPath, workspaceDir); } return requestedPath; } const resolved = resolve(workspaceDir, requestedPath); const rel = relative(workspaceDir, resolved); if (rel.startsWith("..")) { throw new CapabilityPathEscape(requestedPath, workspaceDir); } return resolved; } export interface PdfToMdBundleDeps { readonly secrets: LocalSecretEnvelope; readonly client: CapabilityProviderClient; readonly prisma: PrismaClient; } /** Build the pdf_to_md_bundle adapter. The client is injectable for testing. */ export function createPdfToMdBundleAdapter(deps: PdfToMdBundleDeps): CapabilityAdapter { return { capabilityId: CAPABILITY_ID, async invoke(input: CapabilityInvocationInput): Promise { const descriptor = CAPABILITIES[CAPABILITY_ID]; // 1. Resolve org-scoped credential (fail-closed, ADR-0024/0027). const credential = await resolveCapabilityCredential(deps.prisma, deps.secrets, { organizationId: input.organizationId, capabilityId: CAPABILITY_ID, }); // 2. Confine input + output paths to the workspace (ADR-0018). const absoluteInput = confineToWorkspace(input.inputPath, input.workspaceDir); const absoluteOutputDir = confineToWorkspace(input.outputDir, input.workspaceDir); await mkdir(absoluteOutputDir, { recursive: true }); // 3. Call the backing service. let result; try { result = await deps.client.parse(credential, { inputFilePath: absoluteInput }); } catch (e) { if (e instanceof DocmindClientError) throw e; throw new DocmindClientError( e instanceof Error ? e.message : String(e), "docmind_unreachable", ); } if (result.markdown === "") { throw new DocmindClientError("docmind returned empty markdown", "docmind_no_output"); } // 4. Write outputs into the workspace. const artifacts: CapabilityOutputArtifact[] = []; const mdPath = join(absoluteOutputDir, "document.md"); await writeFile(mdPath, result.markdown, "utf8"); artifacts.push({ path: relative(input.workspaceDir, mdPath), kind: "markdown" }); for (const image of result.images) { const imagePath = join(absoluteOutputDir, image.filename); const rel = relative(absoluteOutputDir, imagePath); if (rel.startsWith("..")) { // Defensive: image filename must not escape the output dir. throw new CapabilityPathEscape(image.filename, absoluteOutputDir); } await writeFile(imagePath, image.data); artifacts.push({ path: relative(input.workspaceDir, imagePath), kind: "image" }); } // 5. Write the UsageFact (ADR-0026/0027). Always written on success; // costUsd null means unknown, NOT zero (ADR-0022). const occurredAt = new Date(); await deps.prisma.usageFact.create({ data: { runId: input.runId, occurredAt, kind: "external_capability", provider: PROVIDER_ID, model: null, inputTokens: null, outputTokens: null, quantity: result.pageCount, unit: descriptor.meteringUnit, costUsd: result.costUsd, costSource: result.costUsd !== null ? "provider_reported" : "unknown", capabilityId: CAPABILITY_ID, correlationId: result.requestId, metadata: {}, }, }); return { artifacts, consumption: { provider: PROVIDER_ID, model: null, inputTokens: null, outputTokens: null, quantity: result.pageCount, unit: descriptor.meteringUnit, costUsd: result.costUsd, correlationId: result.requestId, }, }; }, }; }