Repository navigation
feat(bundle): write any artifact type from its folder through its own hooks, and read a universe's as the inverse of get - #232
Conversation
…nvironment-universe get, whatever its names A folder directly holding one file is an environment named by its path from the universe's folder, or, under metadata/, a metadata key named by its path from there, so names and keys holding / round-trip as the nested folders get writes. A name an @Local id can't hold as it is (__, characters outside the id set, a segment opening or closing with a space, too long) is written under a hash of it instead of being refused; every name that works today keeps its ids. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…d can take, and list every path to a folder A hashed id is now <base>__sha256__<16 hex>: names holding __ are always hashed, so no name's own id holds it, and a name spelled like another's hash keeps an id of its own. Folders are listed by every path to them, guarding only against a link back to a folder that holds it, so metadata in another case is refused even as a link to an environment's folder. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Chaos verificationOn an Ubuntu 24.04 VM (m5.2xlarge) and a stock macOS 15.8 VM (below), the old release (agentenv-framework 0.9.1334 from PyPI) against this PR (481e31c, with the review fixes). Both commits gave the same results. Fuzzed round trip.
No regression. 60 flat universes of the kind the old reader accepts were written by the old release, then run by this PR in the same state root. All 60 came back CoopBench. This is the materialized copy, with its universe's 15
macOSThe same battery ran on a stock macOS 15.8 VM (mac-m4.metal; APFS, case-insensitive). Both fuzz passes kept apart names that differ only in case, since
After the maintainability refactor (6c68921)The whole battery ran again on fresh Ubuntu and macOS VMs. That covered the fuzz on both versions, the 60-universe no-regression run, CoopBench (dry runs and the full task) and the NFD probe. Every result matched the earlier rounds:
After moving the format into its own module (7a517bb)The whole battery ran again on fresh Ubuntu and macOS VMs, with the same results as every earlier round on both:
After artifact types describe their own folders (81c3944)On fresh Ubuntu and macOS VMs, the battery's results match every earlier round on both:
A plugin's artifact types, installed as a real
After the hook shrinks to
|
… id is refused naming the fix Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…its files, and its children's ids from its own naming _Walk now records every folder it visits, so universe_layout reads files and folders from one walk with one set of rules, and _subfolders goes. _child_id derives through EnvironmentUniverseArtifact.derived_environment_id and derived_metadata_id, which the CLI uses too, adding only the hashed fallback. Every id is unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ule of its own, read and written from it environment_universe_folder holds the format both ways: environment_path and metadata_path, where environment-universe get writes each file, and read_folder, their inverse, which takes a folder's listing and raises UniverseFolderError naming paths in the folder. The bundle keeps only the walk and the bundle-relative prefix on each problem; messages and ids are unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…r, so a plugin's type is written as a core one is The bundle kept a table of core types and what each one's write reads from its folder, and branched on the environment and universe readers to list child ids and the environments a load seeds. A plugin type with a from_toml of its own was refused. Artifact.bundle_folder(ctx) now returns what from_toml reads (files, which the ledger hashes) and the ids it writes besides its own (children, each with its path in the folder and the environment it seeds). Each core type describes its own folder, and the plan, ledger and materialize ask the type. A type is written from its folder when one class defines both hooks, so a plugin type defining both is planned, checked, written and reused as a core one is. The universe folder module no longer imports the universe class: it owns METADATA and the ids of what's written from each folder. Ids, digests and messages are unchanged, except the duplicate-name message, which names the type and the folder by its path. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
🤖 Automated Claude routine — Env Pod PR Context Review. This is a bot, not Edgar Arakelyan; not a human review. Context from a sibling PR rather than from this diff: the folder round-trip this PR pins carries a universe's files, but none of the stored document's own fields — and #170 adds the first such field whose loss would change what a restored env does.
With both landed, a snapshot-derived universe downloaded with If carrying it is out of scope, the docstring's "The format's own limits" list looks like the right place to say that a universe's non-file fields don't survive the folder round-trip, so the next field with runtime meaning doesn't get added in the same shape. Ordering-wise it's whichever of the two lands second that would want the note. Generated by Claude Code |
…, and check a file's one file in accept_toml bundle_folder is now optional. Its default is every file in the folder and nothing besides, which is what a file universe, a skill and a CLI read, so they drop their overrides, and a plugin type whose folder is all its files needs only a from_toml. A type is written from its folder when its from_toml isn't the base's. Hashing every file can only rewrite more often, never reuse a stale write. A file's folder reads as a one-file universe: the rule that it holds one file moves into FileArtifact.accept_toml, which the plan runs before anything is written, beside the checks a skill and a CLI make of their folders. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…en_besides), and check folders in accept_toml bundle_folder becomes written_besides(ctx): the artifacts from_toml writes besides ctx.id, none by default. What a write reads is every file in its folder, which the ledger hashes; a folder may hold none when the type's toml names what it's made of (toml_refs), as an environment's file and a universe's environment_artifacts do. The environment's and the universe's folder rules move into their accept_toml, which the plan runs before anything is written: one file or none for an environment, and the universe's layout and "needs an environment". Only the universe overrides written_besides, naming its environments, their files and its metadata files, as read_folder names them; an environment's <id>__file can't collide, since artifact names can't hold "__". Messages are unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This comment has been minimized.
This comment has been minimized.
… is written The plan dropped a BundleError from written_besides, so a plugin type whose hook refuses its folder, and whose accept_toml doesn't, passed planning and failed partway through the run. The file check now reports it, less any problem accept_toml already reported, as a universe's does for its layout. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…names the environments its folder holds itself FolderChild carried the environment a written artifact seeds, which put an environment concept in the base artifact module. It's now an id and its path in the folder. The plan's environment checks (what a load seeds, and names of their own) ask a type that seeds environments from its folder for seeded_environments(ctx), (path, environment_name) pairs, which the environment universe answers; a type without it holds none. Messages are unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…_environments, and keep type wording out of helpers An environment artifact answers seeded_environments with its own name, the path "" for its folder, so the plan's environment checks read no artifact's accept_toml result for a name: what a load seeds, names of their own, and a referenced bundle environment's name all come through the one capability. Envs keep theirs from env.toml. AuthoringContext.not_one_file takes its remedy from the caller, FileArtifact naming file_artifact_universe itself, and no_files words a folder holding nothing in place of exposing the walk's ignore list. A comment in the plan no longer names a type, and four long lines are wrapped. Messages are unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…s that need a file say so The plan's walk refused an artifact folder holding no file unless its type declared toml_refs, which stood in for "its toml names what it's made of". The walk now accepts an empty folder for every type, and a type whose write needs a file checks for one in its accept_toml, with the walk's message: a file, a file universe and a CLI. A skill folder is only one when it holds SKILL.md; an environment and a universe already check their folders. A plugin type built only from its toml values is written from a folder holding nothing else. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ents mixin, not a method name the plan looks up The plan found seeded_environments by getattr, so nothing declared the contract: a plugin couldn't discover it, and one that misspelt it lost the seeding checks without a word. SeedsEnvironments, beside EnvironmentArtifact and exported from agent_env.artifact, is an abstract mixin the environment and the universe declare, and the plan asks only a type that declares it. A plugin declaring it without the method is skipped when it loads, naming what it lacks, as for any unimplemented abstract method. The base artifact module still says nothing of environments. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Summary
A bundle's
environment_universefolder is laid out asenvironment-universe get --output-dirwrites one, but the reader only took one folder level.getwrites: each environment to<environment_name>/<file>and each metadata file tometadata/<key>/<file>, names verbatim. A key or name holding/, such asscenarios/<name>.yaml, comes out as nested folders.__, characters an@localid can't hold, or a segment opening or closing with a space, even thoughgetwrites them all.The folder is now read as the exact inverse of
get, whatever it holds:metadata/. A folder holding only folders is part of their names, and a folder can hold its own file and nested entries at once, soaanda/bboth work.@localid can hold keeps the ids it has today:<universe>__<name>over<universe>__<name>__file, and<universe>__metadata__<key>. Any other name is written under…__sha256__<16 hex of its SHA-256>in its place. Names holding__are always hashed, so no name's own id can take that form, and a name spelled like another's hash keeps an id of its own. That covers__(which could clash with another child's ids), characters outside the id set, a segment opening or closing with a space, and ids over 4096 bytes. The universe still stores the name or key itself.metadata/;metadataspelled in another case.metadata/reads back as a metadata key, so it needs an artifact folder of its own.The bundle no longer checks artifact types. The bundle used to keep a table of the core types and what each one's write reads from its folder. It branched on the environment and universe readers to list child ids and the environments a load seeds. A plugin type with a
from_tomlof its own was refused ("writing … isn't supported yet"). Now each type's authoring hooks carry what the bundle needs:from_tomlwrites the artifact. Any type whosefrom_tomlisn't the base's is written from its folder.accept_toml. Environment and universe folders hold onlyartifact.tomlwhen their toml names what they're made of (file,environment_artifacts). A plugin type built only from its toml values works the same way.accept_tomlmakes the checks the plan runs before anything is written:type = "file"folder holds one file (a file's folder reads like a file universe holding one file), and a file universe or CLI folder holds at least one;fileis set;skillandclikeep their existing folder checks there.written_besides(ctx)names the artifactsfrom_tomlwrites besides its own id, each aFolderChild(its id and its path in the folder). The plan uses it to refuse an id clash before any write.environment_universeoverrides it: its environments, their files and its metadata files, asread_foldernames them.<id>__filecan't collide, since artifact names can't hold__.SeedsEnvironmentsis an abstract mixin, besideEnvironmentArtifactand exported fromagent_env.artifact, for a type whose write seeds environments. Itsseeded_environments(ctx)returns(path, environment_name)for each environment it seeds, with the path""meaning the folder itself.environment(its own name) andenvironment_universe(each environment its folder holds) declare it, and the plan asks only types that declare it.accept_tomlresult for a name.artifact/artifacts/environment_universe_folder.py.environment_pathandmetadata_pathsay wheregetwrites each file, andread_folderis their inverse.METADATAand the ids of what's written from each folder, so it doesn't import the universe class.bundle/authoring.pykeeps only generic helpers:listing(),not_one_file(remedy),no_files(...)andrefuse(..., within=).getdoes) lands with bundle import, its first consumer.Bundle authoring and
getonly, so not hot path.Tests
environment-universe put,get --output-dirinto a bundle, then a bundle write, and compares the stored universe with the original: names, keys, filenames and bytes.gmail,drive/shared,g__mail,crm: v2andslack, and keysmanifest,scenarios,scenarios/a.yaml,scenarios/b.yaml,deep/x/y/z.json,m__kandq&a?.g__mailandq&a?get the hashed form. A name spelled like another's hash keeps an id of its own.Metadatain another case is refused even as a link to an environment's folder. A nested folder with two files is refused, naming it, and the empty-folder test now names the deepest empty folder. The six cases the old reader refused for nesting,__or edge spaces are now accepted layouts.read_folderreads back whatenvironment_pathandmetadata_pathlay out, and names each problem by its path in the folder.plugin_artifact_types_test.py). The test uses a plugin type that subclasses no core type and writes a file artifact per file, each seeding an environment.from_tomlof its own is written by it, rewritten when its folder changes, and checked by theaccept_tomlit inherits.🤖 Generated with Claude Code


Confidence Score: 5/5
The PR appears safe to merge; no blocking issue remains.Summary
Bundles now use each artifact type’s hooks to check and write its folder. Universe folders support nested names and reserved hash IDs.
Diagram
%%{init: {'theme': 'neutral'}}%% flowchart LR F[Artifact folder] --> P[Bundle plan] P --> A[accept_toml checks] P --> W[written_besides IDs] P --> S[SeedsEnvironments names] A --> C[Checked plan] W --> C S --> C C --> L[Ledger checks files and references] L --> R[Reuse saved version] L --> T[from_toml writes artifact]Reviews (13) · Last reviewed commit: "refactor(artifact): declare seeding envi..." · Reviewed by Greptile