Repository navigation
feat(bundle): write environment and environment_universe artifacts from their artifact folders - #91
Conversation
…om their artifact folders An environment's artifact.toml names its environment_name and either the file artifact to wrap (file, a bundle file artifact or a store one) or nothing, to wrap the folder's one file, written as <id>__file. A universe's folder is laid out as environment-universe get --output-dir writes one: each <environment_name>/ folder holds that environment's file, written as <universe>__<name> over <universe>__<name>__file, and each metadata/<key>/ folder a metadata file, written as <universe>__metadata__<key>; environment_artifacts adds environments by name or store id. The stored documents' own key names are refused with a hint, and a universe with no environments, or two of one name, is refused before any write. The ledger tracks both: each base artifact type's listing in plan.py says what its write reads, and every kind but tasks and evals now hashes the versions of the bundle writes it names, so a universe is rewritten when an environment it pins is. No recorded digest changes (a test pins them), so SCHEME stays 1. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…t file modes The data-bundle test bound a module-level DATA dict over the tst/data path that the gateway multi test reads when it runs, so that test failed with a TypeError. The digest test's fixture now sets each file to 0644, since a built image hashes other permission bits and the umask decided them. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… write - A universe folder left without its file (empty, or holding only what the OS leaves) is refused rather than dropped, so the environment it names isn't silently missing. - A folder name that opens or closes with a space is refused, and the environment's own id is validated as well as its file's, so neither fails partway through a write. - The ids a universe's environments and metadata files are written under, and the file an environment wraps from its folder, are claimed in the one-writer check, so one clashing with an image the bundle builds is refused before anything is written. - Messages: renaming an environment's folder renames the environment, so the refusal says how to keep the name; an environment folder with no file suggests `file`; a stored name for the file isn't also reported as an empty folder; a file nested under metadata/<key>/ is told where it goes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…iverse write beside them - EnvironmentArtifact.derived_file_id and EnvironmentUniverseArtifact.derived_environment_id / derived_metadata_id are the only places these ids are spelled. The bundle's universe layout, the plan's one-writer check, from_toml and the CLI all use them, so none can drift from the others. - `artifact environment put` and `environment-universe put --metadata` now name the files they write `<id>__file` and `<id>__metadata__<key>` (derive_id), as a bundle does, instead of `<id>-file` and `<id>-metadata-<key>`. Stored ids keep resolving; new writes get new names. - `environment-universe get --output-dir` and the bundle's layout share the metadata folder's name. - The keys that name an environment's file are read from toml_stored_names, not restated. - FileArtifact and EnvironmentArtifact are imported at module top in these modules; nothing forces the imports into the functions. - A universe's metadata/ folder spelled in another case (Metadata/) is refused, as a case near miss of any reserved name in a bundle is, instead of becoming an environment named Metadata. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…he env a task loads it into A load_artifact step loading the bundle's own environment or universe into an env is checked before anything is written: each environment it seeds (a universe's folders and what its environment_artifacts names, or an environment's own name) must be the environment_name of one of the env's servers (an MCP server's or website's own, or each of a multi's envs'). One named after no server is a typo or a stray, which a multi would skip without a word and an MCP server or website would fail on mid-run. A store environment or universe isn't checked: a server loading a shared universe takes its own environment from it and skips the rest. A check that reads no store skips a store env's servers. The slow data-bundle test's universe no longer adds an environment for another server, which this now refuses. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
| for ref in refs: | ||
| for what, name in self._seeded(ref, store_latest): | ||
| if name not in servers: | ||
| listed = ", ".join(repr(server) for server in sorted(servers)) | ||
| self.problems.append( |
There was a problem hiding this comment.
Shared universe loads are refused
When a task loads a bundle-written universe into one server, the new plan check rejects every environment with a different name. The server already loads its matching environment and skips the others. A universe containing items and mail can no longer load into the items server. Keep the matching-environment check without rejecting the extras.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agent_env/bundle/plan.py
Line: 485-489
Comment:
**Shared universe loads are refused**
When a task loads a bundle-written universe into one server, the new plan check rejects every environment with a different name. The server already loads its matching environment and skips the others. A universe containing `items` and `mail` can no longer load into the `items` server. Keep the matching-environment check without rejecting the extras.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.There was a problem hiding this comment.
Intended. A universe the bundle writes is data its author wrote for the env they load it into, so an environment there that matches no server is treated as a typo or a stray folder and refused before any write, for a single server as for a multi (a multi would otherwise skip it without a word). The skip-the-rest behavior is kept where it's legitimately used: a store universe shared across servers still loads into one server, taking its own environment (test_a_store_universe_loaded_into_one_server_gives_it_its_own_environment). A bundle that wants one server to take part of a universe can name that server's environment artifact instead.
There was a problem hiding this comment.
That makes sense, and the distinction is valid. _check_loaded_environments intentionally ensures every environment introduced by a bundle-written universe maps to a server in the task’s bundle environment, preventing typo or stray folders from being silently accepted. Store universes retain their shared-universe behavior, and tasks needing only one environment can reference that environment artifact directly.
I’m withdrawing this finding; no change is needed.
…lti of bundle and store servers A slow-tier test writes, deploys and reads back the combinations the data-bundle test left out: - a universe whose environments come from its folder, a bundle environment over a store file pinned by version, and a store environment, loaded into a multi of two bundle servers and one the CLI put in the store; each server holds exactly the data meant for it; - a store universe shared across servers, loaded into one bundle server, which takes its own environment and skips the rest; - a dry run afterwards reuses everything. The check reads each server's state behind the gateway rather than over MCP: every items server registers list_items under the same name, which the gateway serves from only one of them. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A bundle's
artifacts/<name>/folder whoseartifact.tomldeclarestype = "environment"ortype = "environment_universe"is now written byagent-env run, asartifact environment putandartifact environment-universe putwrite them. Until now the run refused such a folder as one it can't write.What an artifact folder holds
environment_nameis required. The file it wraps is either the one file in its folder (descriptiondefaults to the file's name) or the file artifactfilenames: a bundle artifact, a store id, or{ artifact = "…", version = n }.<environment_name>/folder holds one environment's file, and eachmetadata/<key>/folder one metadata file.environment_artifactsadds environments from the bundle or the store. Its environments are the folders', in name order, then the ones it names.service_name,file_artifact_id,file_artifact_ref,service_artifact_refs,environment_artifact_refs,service_artifact_ids,metadata,metadata_refs) are refused, naming the key to write instead.Checked before anything is written
filenaming anything but a file artifact, orenvironment_artifactsanything but environment artifacts. These are typed refs, checked in the resolver for bundle artifacts and in the plan for store ones.filenames one;filetogether withdescription.<environment_name>/ormetadata/<key>/, a folder holding anything but one file (empty included), or folders nested inside one;__, opening or closing with a space, or giving an idvalidate_local_idrefuses;environment_artifacts. A store environment's name is read at the version the plan read;bundle check, which reads no store, counts only the bundle's.Writing and reuse
FileArtifact.put_attempt, now shared withfileartifacts), so a write that fails partway never blocks the next.folder_walk), and now the versions of what any written document references, tasks and evals aside. So a universe is rewritten when an environment it names is, and an environment when its file is.ledger_digest_testpins nine digests recorded by v0.9.1278 across files, file universes, built images, envs, agents and tasks.Round trip
A universe downloaded with
environment-universe get --output-dirand dropped into a bundle as-is writes an equivalent universe. A unit test compares the canonical documents, setting aside ids, versions, timestamps, object URLs and file descriptions; environment order is name order. A real 9-environment, 42 MB universe was written in about a second and reused on the rerun; the only field that differed from the original was its legacy unpinnedfile_artifact_id.Tests
artifact_toml_test.py(both environment forms, store pins, universe composition, the round trip, refusals, a store-free check), plustoml_refs_test.py,materialize_test.pyandledger_digest_test.py.--dry-runand byrun, with the stores unchanged.Not hot path: only the bundle path changes. The two artifact classes gain class variables and classmethods, and their models, schemas and dumps are unchanged.
🤖 Generated with Claude Code
The PR appears safe to merge; no actionable new issue or outstanding previous finding remains.
Fix with agent prompt
Summary
agent-env runcan now write environment and environment-universe artifacts from bundle folders. The bundle planner checks their layouts and references, and the ledger tracks their source files and referenced versions.Reviews (3) · Last reviewed commit: "test(bundle): a universe composed every ..." · Reviewed by Greptile