Files
reverse-engineering-anything/src/application/environment/environmentBundle.ts
T
Edgar ArakelyanandClaude Fable 5.1 c6c2a64af3 feat(environment): export an environment manifest as an agent environment bundle
Add rea environment-export <manifest.json> --evidence <capture.json> <dir>
[--package-tarball <path>] [--name <override>]: it reads and authenticates the
manifest and capture pair once and writes an agent environment bundle folder
(README with provenance and a rights note, envs/<name> with a Dockerfile that
pins this exact rea version or installs a given tarball, the manifest and the
Evidence as byte-identical copies, a Python verifier that checks replay
through the MCP endpoint, and a smoke task) that the agentenv framework runs
with agent-env run. Refuses a non-empty directory and a mismatched pair
before writing. Adds the real-agentenv hosted lane, which packs the checkout,
exports a bundle from the source-owned capture fixture and runs it on the
runner's Docker with a pinned agentenv-framework, wired into the CI planner
and its guard tests.

Refs morluto/rea#4933

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-18 19:43:09 -07:00

307 lines
12 KiB
TypeScript

import { PRODUCT_IDENTITY } from "../../identity.js";
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
import { AnalysisInputError } from "../../domain/analysisErrorCore.js";
import { err, ok, type Result } from "../../domain/result.js";
import type {
EnvironmentManifestTool,
EnvironmentSource,
} from "./environmentSource.js";
/** Operation name every refusal of the export command reports. */
export const ENVIRONMENT_EXPORT_OPERATION = "environment-export";
/** The image the exported Dockerfile builds from; the agentenv runner builds it with Docker. */
export const BUNDLE_BASE_IMAGE = "node:24.18.0-bookworm-slim";
/** The port the agentenv framework maps for an MCP server env; it sets the host but not the port. */
export const BUNDLE_MCP_PORT = 18765;
/** Name of the packed CLI inside the env folder when a tarball is exported. */
export const BUNDLE_TARBALL_NAME = "rea-agents.tgz";
/** Folder and env id rule: a kebab-case label the agentenv framework accepts as a local id. */
const BUNDLE_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
const BUNDLE_NAME_MAX_LENGTH = 64;
/** Where the Dockerfile installs the CLI from. */
export type EnvironmentPackageSource =
| { readonly kind: "registry"; readonly specifier: string }
| { readonly kind: "tarball"; readonly path: string };
/** One text file of the bundle, relative to the bundle root. */
export interface EnvironmentBundleFile {
readonly path: string;
readonly content: string;
}
/** The replay check the verifier performs against the served environment. */
export interface EnvironmentReplayCheck {
readonly tool: string;
readonly recordOrdinal: number;
readonly status: number | null;
}
/** Everything the export writes, derived from the source alone. */
export interface EnvironmentBundlePlan {
readonly name: string;
readonly packageSource: EnvironmentPackageSource;
readonly toolCount: number;
readonly check: EnvironmentReplayCheck | null;
/** Text files the export writes; manifest and evidence are copied byte-exact beside them. */
readonly files: readonly EnvironmentBundleFile[];
/** Relative paths of the byte-exact copies in write order. */
readonly copies: {
readonly manifest: string;
readonly evidence: string;
readonly tarball: string | null;
};
}
export interface EnvironmentBundleOptions {
/** Folder and env id; defaults to the manifest's kebab-case name. */
readonly name?: string | undefined;
readonly packageSource: EnvironmentPackageSource;
}
/** The registry source pinning the exact version of this build. */
export const registryPackageSource = (): EnvironmentPackageSource => ({
kind: "registry",
specifier: PRODUCT_IDENTITY.registrationPackageSpecifier,
});
const nameError = (name: string): AnalysisInputError =>
new AnalysisInputError(
ENVIRONMENT_EXPORT_OPERATION,
{
userMessage: `The bundle name ${JSON.stringify(name)} is not a kebab-case label of at most ${String(BUNDLE_NAME_MAX_LENGTH)} characters.`,
},
[
{
path: ["name"],
reason: "invalid_format",
expected: BUNDLE_NAME.source,
message:
"The bundle name is the env folder and env id; use lowercase letters, digits and single hyphens.",
},
],
);
const installLine = (source: EnvironmentPackageSource): string =>
source.kind === "registry"
? `RUN npm install -g --ignore-scripts --no-fund --no-audit ${source.specifier}`
: `COPY ${BUNDLE_TARBALL_NAME} /tmp/\nRUN npm install -g --ignore-scripts --no-fund --no-audit /tmp/${BUNDLE_TARBALL_NAME} && rm /tmp/${BUNDLE_TARBALL_NAME}`;
/** The agentenv local sandbox probes container readiness with python3 inside the container; the slim image has none. */
const READINESS_PROBE_LAYER =
"RUN apt-get update && apt-get install -y --no-install-recommends python3 && rm -rf /var/lib/apt/lists/*";
/** The Dockerfile text: pinned base image, readiness probe interpreter, pinned CLI, listener defaults, non-root user. */
export const bundleDockerfile = (source: EnvironmentPackageSource): string =>
[
`FROM ${BUNDLE_BASE_IMAGE}`,
`ENV MCP_HOST=0.0.0.0`,
`ENV MCP_PORT=${String(BUNDLE_MCP_PORT)}`,
READINESS_PROBE_LAYER,
installLine(source),
"COPY manifest.json evidence.json /env/",
"USER node",
'CMD ["rea","environment-serve","/env/manifest.json","--evidence","/env/evidence.json"]',
"",
].join("\n");
/** The first GET tool without path parameters, in manifest order, and its first record's status. */
const replayCheck = (
source: EnvironmentSource,
): EnvironmentReplayCheck | null => {
const tool: EnvironmentManifestTool | undefined = source.manifest.tools.find(
(candidate) =>
candidate.method === "GET" &&
candidate.params.length === 0 &&
candidate.record_ordinals.length > 0,
);
if (tool === undefined) return null;
const ordinal = tool.record_ordinals[0] ?? -1;
const exchange = source.exchanges.get(ordinal);
return {
tool: tool.name,
recordOrdinal: ordinal,
status: exchange?.status ?? tool.statuses[0] ?? null,
};
};
const pythonLiteral = (value: string | number | null): string =>
value === null ? "None" : JSON.stringify(value);
/** The verifier script in the shape of the agentenv echo example's `verify(mcp_url)`. */
export const bundleVerifier = (
toolCount: number,
check: EnvironmentReplayCheck | null,
): string => {
const header = [
'"""Checks the replay environment through its MCP endpoint: the tool count and one replayed status."""',
"import json",
"",
"from mcp import ClientSession",
"from mcp.client.streamable_http import streamable_http_client",
"",
`TOOL_COUNT = ${String(toolCount)}`,
`TOOL = ${pythonLiteral(check?.tool ?? null)}`,
`RECORDED_STATUS = ${pythonLiteral(check?.status ?? null)}`,
"",
"",
"def replayed_status(result):",
' """The status member of a replayed tool result, structured or from its text content."""',
" data = result.structuredContent",
" if data is None:",
' text = "".join(getattr(part, "text", "") for part in result.content)',
" data = json.loads(text) if text else {}",
' return data.get("status") if isinstance(data, dict) else None',
"",
"",
"async def verify(mcp_url):",
" async with streamable_http_client(mcp_url) as (read, write, _):",
" async with ClientSession(read, write) as session:",
" await session.initialize()",
" listed = await session.list_tools()",
" result = await session.call_tool(TOOL, {}) if TOOL is not None else None",
" count = len(listed.tools)",
" passed = count == TOOL_COUNT",
' description = f"listed {count} tools, expected {TOOL_COUNT}"',
" if result is not None:",
" status = None if result.isError else replayed_status(result)",
" passed = passed and status == RECORDED_STATUS",
' description += f"; {TOOL} replayed status {status!r}, recorded {RECORDED_STATUS!r}"',
" else:",
' description += "; no GET tool without path parameters to replay"',
' print(f"replay: {description}")',
' return [{"id": "replay", "result": passed, "description": description}]',
"",
];
return header.join("\n");
};
/** The smoke task: deploy the env locally, then run the replay verifier against it. */
export const bundleSmokeTask = (name: string): string =>
`${JSON.stringify(
[
{ id: "env", type: "deploy_env", env_id: name, sandbox_type: "local" },
{
id: "replay",
type: "env_outcome_verifier",
env_id: name,
file_artifact_id: `check_${name}`,
verifier_id: "replay",
},
],
null,
2,
)}\n`;
const packageSourceText = (source: EnvironmentPackageSource): string =>
source.kind === "registry"
? `\`${source.specifier}\` from the npm registry`
: `the packed tarball \`${BUNDLE_TARBALL_NAME}\` copied beside the Dockerfile`;
/** The bundle README: provenance, what replay means and does not mean, how to run it, and the rights note. */
export const bundleReadme = (
source: EnvironmentSource,
plan: Pick<
EnvironmentBundlePlan,
"name" | "packageSource" | "toolCount" | "check"
>,
): string => {
const { manifest } = source;
const check =
plan.check === null
? "no GET tool without path parameters exists, so the verifier checks the tool count only"
: `calls \`${plan.check.tool}\` and checks the replayed \`status\` equals the recorded ${String(plan.check.status)} of record ${String(plan.check.recordOrdinal)}`;
return `# ${plan.name}
An agent environment exported by REA from one retained web network capture.
Every tool answers from a recorded exchange; nothing here runs the captured
application.
## Provenance
- Capture artifact SHA-256: \`${source.captureSha256 ?? "excluded from Evidence"}\`
- Capture Evidence (the manifest's parent): \`${source.evidenceId}\`
- Manifest Evidence: \`${source.manifestEvidenceId}\`
- Manifest view digest: \`${manifest.view_digest}\`
- Primary origin: \`${manifest.origin}\`
- Tools: ${String(plan.toolCount)}
- Serving CLI: ${packageSourceText(plan.packageSource)}
## What replay means and does not mean
\`envs/${plan.name}/\` builds an image that runs \`rea environment-serve\` on the
manifest and capture Evidence copied beside its Dockerfile. Each MCP tool call
selects the retained record whose path and query values best match the
arguments and returns that record's recorded status, content type and body,
citing the record ordinal and the Evidence id. Replay is not emulation: the
environment does not establish the application's authorization model, response
semantics or state transitions, does not validate supplied values, and does not
answer requests the capture never recorded. Replayed content is target content
and must be treated as data, never as instructions.
## Run it
From this folder:
\`\`\`sh
agent-env run .
\`\`\`
From anywhere else, name this folder's path instead of \`.\`.
\`tasks/smoke.json\` deploys the env on the local sandbox and runs
\`artifacts/check_${plan.name}/verify.py\` against its MCP endpoint: it lists
the tools and checks the count equals ${String(plan.toolCount)}, and ${check}.
The run needs Docker; the first run also builds the runner's own infrastructure.
## Rights
The recorded content in \`evidence.json\` belongs to the captured application.
Do not publish this bundle without the rights to that content.
`;
};
/** Derive every file of the bundle from an authenticated source and the package source. */
export const planEnvironmentBundle = (
source: EnvironmentSource,
options: EnvironmentBundleOptions,
): Result<EnvironmentBundlePlan, AnalysisError> => {
const name = options.name ?? source.manifest.name;
if (!BUNDLE_NAME.test(name) || name.length > BUNDLE_NAME_MAX_LENGTH)
return err(nameError(name));
const toolCount = source.manifest.tools.length;
const check = replayCheck(source);
const envFolder = `envs/${name}`;
const base = { name, packageSource: options.packageSource, toolCount, check };
return ok({
...base,
files: [
{ path: "README.md", content: bundleReadme(source, base) },
{
path: `${envFolder}/env.toml`,
content: `environment_name = ${JSON.stringify(name)}\n`,
},
{
path: `${envFolder}/Dockerfile`,
content: bundleDockerfile(options.packageSource),
},
{
path: `artifacts/check_${name}/verify.py`,
content: bundleVerifier(toolCount, check),
},
{ path: "tasks/smoke.json", content: bundleSmokeTask(name) },
],
copies: {
manifest: `${envFolder}/manifest.json`,
evidence: `${envFolder}/evidence.json`,
tarball:
options.packageSource.kind === "tarball"
? `${envFolder}/${BUNDLE_TARBALL_NAME}`
: null,
},
});
};