Export an authenticated capture and environment manifest as a pinned agentenv bundle with provenance, a replay verifier and smoke task. Followups preserve immutable input ownership, bounded discovery, caller paths and numeric values; bind bundle writes and package copying to retained filesystem owners; verify POSIX modes; retain cleanup authority; and verify framework/npm executable selection. Default MCP remains stdio. Validation: pinned Linux toolchain, typecheck/build/lint/format/docs/knip, 452 focused tests, ten product-catalog cases, real GitHub response replay in the generated non-root Docker image, and the real pinned agentenv framework smoke workflow. Platform and resource-measurement limits are recorded in the feature verification document. Co-authored-by: gensokyo-dev <gensokyo-dev@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
117 lines
7.0 KiB
Markdown
117 lines
7.0 KiB
Markdown
# Export an environment manifest as an agent environment bundle
|
||
|
||
Issue: https://github.com/morluto/rea/issues/4933
|
||
|
||
## Outcome
|
||
|
||
`rea environment-export <manifest.json> --evidence <capture.json> <dir>
|
||
[--package-tarball <path>] [--name <override>]` writes the manifest and
|
||
capture pair that `environment-serve` serves as a bundle folder the agentenv
|
||
framework runs with `agent-env run <dir>`: a README with provenance, an env
|
||
folder whose Dockerfile installs this exact `rea-agents` version and serves the
|
||
copied pair, a Python replay verifier and a smoke task. The hosted
|
||
`real-agentenv` lane exports such a bundle from the source-owned Grafana-shaped
|
||
capture through the built CLI, packs the checkout into the image and runs it
|
||
with the pinned framework release on the runner's Docker. This increment
|
||
completes the agent-environment work of the issue; serving came in the
|
||
previous increment.
|
||
|
||
## Requirements
|
||
|
||
- Read each of the two files once through the regular-file reader, parse it
|
||
from those bytes and authenticate and cross-check the pair with
|
||
`loadEnvironmentSource` as `environment-serve` does, so the bytes copied into
|
||
the bundle are the bytes that were authenticated; every refusal reports
|
||
operation `environment-export`, and a manifest projected from a different
|
||
capture is refused before anything is written.
|
||
- Refuse a bundle directory that exists and is not empty, or that names a file,
|
||
with an `invalid_request` whose issue path is `directory`; an absent or empty
|
||
directory is accepted. Write every file with the exclusive-create flag so
|
||
nothing is ever overwritten.
|
||
- Write `README.md`, `envs/<name>/Dockerfile`, `envs/<name>/env.toml`,
|
||
`envs/<name>/manifest.json` and `envs/<name>/evidence.json` as byte-exact
|
||
copies of the inputs (mode `0600` on POSIX; Windows retains inherited ACLs
|
||
and does not provide an owner-only guarantee), `artifacts/check_<name>/verify.py` and
|
||
`tasks/smoke.json`, where `<name>` defaults to the manifest's kebab-case name
|
||
and `--name` must be a kebab-case label of at most 64 characters that is not a
|
||
device name Windows reserves (`con`, `prn`, `aux`, `nul`, `com1`–`com9`,
|
||
`lpt1`–`lpt9`), since the env folder could not be created there.
|
||
- The Dockerfile reads `FROM node:24.18.0-bookworm-slim`, sets
|
||
`MCP_HOST=0.0.0.0` and `MCP_PORT=18765`, installs Debian's `python3` for the
|
||
framework's readiness probe, installs
|
||
`rea-agents@<PRODUCT_IDENTITY.packageVersion>` from the registry or, with
|
||
`--package-tarball`, copies the tarball as `rea-agents.tgz`, installs it
|
||
from `/tmp/` and removes it in the same layer, copies the pair to `/env/`,
|
||
switches to the image's `node` user
|
||
and runs `rea environment-serve /env/manifest.json --evidence
|
||
/env/evidence.json`.
|
||
- `verify.py` mirrors the framework's echo example: `async def verify(mcp_url)`
|
||
over `mcp.client.streamable_http`, lists the tools and checks the count
|
||
equals the manifest's, calls the first `GET` tool without path parameters
|
||
with no arguments and checks the replayed `status` equals the recorded status
|
||
of that tool's first cited record, prints the description and returns
|
||
`[{"id": "replay", "result": <bool>, "description": <both checks>}]`; with no
|
||
such tool it checks the count only and says so.
|
||
- `tasks/smoke.json` is `deploy_env` of `<name>` on the local sandbox followed
|
||
by `env_outcome_verifier` with `file_artifact_id` `check_<name>` and
|
||
`verifier_id` `replay`. The README records the capture artifact SHA-256, both
|
||
Evidence ids, the manifest view digest, the tool count and the package
|
||
source, states what replay does and does not establish, names the run
|
||
command as `agent-env run .` from the bundle root (the folder it sits in,
|
||
whatever the caller named it), and carries the rights note.
|
||
- Print one JSON document with `status: "exported"`, the directory, name, tool
|
||
count, both Evidence ids, the pinned `package_source` and the written files
|
||
with sizes. Register the flat `environment-export` command in the CLI
|
||
inventory; no MCP tool is added.
|
||
- Add the `agentenv` CI scope, the reusable `real-agentenv.yml` workflow
|
||
(`workflow_dispatch` and `workflow_call` with `build_cache_artifact`,
|
||
`ubuntu-24.04`, SHA-pinned actions, `setup-python` 3.12, a virtual
|
||
environment with `agentenv-framework==0.9.1334`, the runner's Docker, 30
|
||
minutes), the `verify:agentenv:environment` script and
|
||
`scripts/verify-agentenv-environment.mjs`, which requires absolute
|
||
`REA_AGENT_ENV_COMMAND` and `REA_DOCKER_COMMAND` like every other lane
|
||
verifier, runs `agent-env run <dir> --sandbox local` bounded to 15 minutes
|
||
and resolves nothing from `PATH`; wire `ci.yml` at the four spots and extend
|
||
the two CI guard tests.
|
||
|
||
## Acceptance and limits
|
||
|
||
Services tests cover the planned file set, the Dockerfile for both package
|
||
sources, the verifier text, the smoke task, the README, the name override and
|
||
its refusals, writing into an absent and an empty directory with byte-exact
|
||
copies and correct sizes, the tarball copy, and the refusals of a non-empty
|
||
directory, a file path, a missing tarball and an inline manifest. The
|
||
conformance test pins the registry install to the `package.json` version. The
|
||
cli-boundary test writes the folder from the fixture pair through the built
|
||
CLI, refuses a non-empty directory leaving it unchanged, and refuses a
|
||
mismatched pair without creating the directory. The CI guard tests enumerate
|
||
the new consumer, route and classification cases. The local end-to-end run
|
||
recorded in `verification.md` is the proof that one exported bundle runs under
|
||
`agent-env run` with the smoke task passed and no container left behind.
|
||
|
||
The export copies bytes and never inspects the tarball; it does not build or
|
||
run the image itself. Its writes are not transactional: a host failure after
|
||
the first file leaves the partial folder for inspection, as `docs/cli.md`
|
||
states. The verifier checks one replayed status and the tool count, not every
|
||
tool. The lane passes `--sandbox local` as designed although
|
||
`tasks/smoke.json` already pins `sandbox_type: local` on its deploy step, so
|
||
the flag restates what the task fixes. The lane verifier imports the
|
||
Grafana-shaped HAR builder from `tests/fixtures/historicalHar.ts` through
|
||
Node's type stripping rather than carrying a second copy under `scripts/`; the
|
||
`agentenv` scope lists that fixture so a change to it runs the lane. The lane
|
||
proves one framework release on one hosted runner and claims nothing about
|
||
any hosted platform. Three additions to the
|
||
designed Dockerfile and file set were needed by the framework, each found by
|
||
the local run: `MCP_PORT=18765` as the listener default for the port the
|
||
framework maps, Debian's `python3` because the framework's local sandbox
|
||
probes readiness with `python3 -c "import socket; …"` inside the container and
|
||
the slim Node image has none, and `env.toml` naming the environment, because
|
||
the framework otherwise reads the name from Python sources the Node image does
|
||
not have.
|
||
|
||
## Deferred issue criteria
|
||
|
||
This increment closes the agent-environment scope of #4933 together with the
|
||
previous three. Running the lane on the maintainer's CI and any follow-up on
|
||
hosted platforms are outside this PR.
|