Skip to content

feat(bundle): read an environment_universe folder as the inverse of environment-universe get, whatever its names - #232

Open
earakely-scale wants to merge 5 commits into
mainfrom
edgararakelyan/fd-3987-universe-metadata-keys
Open

earakely-scale wants to merge 5 commits into
mainfrom
edgararakelyan/fd-3987-universe-metadata-keys

Conversation

@earakely-scale

@earakely-scale earakely-scale commented Oct 16, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

A bundle's environment_universe folder is laid out as environment-universe get --output-dir writes one, but the reader only took one folder level.

  • What get writes: each environment to <environment_name>/<file> and each metadata file to metadata/<key>/<file>, names verbatim. A key or name holding /, such as scenarios/<name>.yaml, comes out as nested folders.
  • What failed: the reader refused those folders ("holds its one file directly … with no folders inside"). A stored universe with such keys couldn't be downloaded and written back from a bundle without renaming them.
  • What else was refused: names holding __, characters an @local id can't hold, or a segment opening or closing with a space, even though get writes them all.

The folder is now read as the exact inverse of get, whatever it holds:

  • Nesting. Any folder directly holding one file is an entry named by its path. That's an environment from the universe's folder, or a metadata key from metadata/. A folder holding only folders is part of their names, and a folder can hold its own file and nested entries at once, so a and a/b both work.
  • Ids. A name an @local id 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.
  • Still refused, each with its fix:
    • a file directly in the universe folder or directly in metadata/;
    • a folder directly holding two or more files;
    • a folder with nothing beneath it, which is now named at its deepest point;
    • metadata spelled in another case.
  • The format's own limits, written in the docstring: an environment whose name starts with metadata/ reads back as a metadata key, so it needs an artifact folder of its own.

Where the format lives. The folder format now has a module of its own, artifact/artifacts/environment_universe_folder.py, used in both directions:

  • environment_path and metadata_path say where get writes each file.
  • read_folder is their inverse. It takes a folder's listing and raises UniverseFolderError naming paths in the folder.

The bundle keeps only the walk, now one walk for both files and folders, and puts the bundle's path in front of each problem. universe_layout and its helpers have left bundle/authoring.py.

Bundle authoring and get only, so not hot path.

Tests

  • Round trip through the real CLI. It runs environment-universe put, get --output-dir into a bundle, then a bundle write, and compares the stored universe with the original: names, keys, filenames and bytes.
    • The universe: environments gmail, drive/shared, g__mail, crm: v2 and slack , and keys manifest, scenarios, scenarios/a.yaml, scenarios/b.yaml, deep/x/y/z.json, m__k and q&a?.
    • The ledger: a rerun reuses everything, and changing one deep file rewrites only the universe, naming the file.
  • Ids. Names an id can hold keep their ids, verbatim. g__mail and q&a? get the hashed form. A name spelled like another's hash keeps an id of its own.
  • Nesting. A folder holding its own file and nested ones gives an entry for each, and a folder holding only folders is part of a name.
  • Refusals. Metadata in 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.
  • The format module on its own: read_folder reads back what environment_path and metadata_path lay out, and names each problem by its path in the folder.
  • Unit tier: 8,364 passed.

🤖 Generated with Claude Code

RetriggerView in GreptileConfidence Score: 5/5

The PR appears safe to merge; no new blocking issue was found.

Summary

The PR lets bundles read nested environment names and metadata keys, with hashed IDs for names that local IDs cannot hold.

  • Bundles can read downloaded universes with nested names intact.

Reviews (5) · Last reviewed commit: "refactor(artifact): give the environment..." · Reviewed by Greptile

…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>
@earakely-scale
earakely-scale requested a review from a team as a code owner October 16, 2026 17:59
Comment thread src/agent_env/bundle/authoring.py Outdated
Comment thread src/agent_env/bundle/authoring.py Outdated
…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>
Comment thread src/agent_env/bundle/authoring.py Outdated
@earakely-scale

earakely-scale commented Oct 16, 2026 •

Copy link
Copy Markdown
Collaborator Author

Chaos verification

On 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.

  • The universes: each case stores a random universe under bare ids, with 1–6 environments and 0–10 metadata keys. Names are 1–4 levels deep and include prefix pairs, ASCII punctuation, non-Latin letters, __ and spaces at the ends of path segments.
  • The steps: download it with the real environment-universe get --output-dir into a bundle, write the bundle, and compare names, keys, filenames and bytes with the original.
Old New
300 cases (298 with nested names) 298 refused, 2 accepted (the 2 with flat names an id can hold) 300/300 round-trip exactly
200 more, names kept apart as a case-insensitive disk would — 200/200
  • Ids: 546 children's ids are the name verbatim and 3,805 are …__sha256__<hash>, each the form expected for its name, with no two alike.
  • Ledger: a rerun reused everything (100/100), and changing one file rewrote only the universe (100/100).
  • End to end: every tenth case also went through agent-env run, loading the universe into a local sandbox: 30/30 exit 0.

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 artifacts/u: v1, unchanged, with the same digests and ids. Every run exited 0 on both versions.

CoopBench. This is the materialized copy, with its universe's 15 scenarios/<name>.yaml keys put back as get writes them, metadata/scenarios/<name>.yaml/<file>. The stored artifacts weren't touched.

  • Old, dry run: 15 refusals, one per key.
  • New, dry run: passes.
  • New, full task on the local provider: 18/18 steps. The universe loaded into the env (all 11 services re-ingested), world-warmup and collect-evidence passed, and the judges scored the solver's work.
  • The stored universe: 22 metadata keys, 15 holding /, each under its own verbatim id (…__metadata__scenarios/<name>.yaml), with none flattened.
  • The run key appears in no log.

macOS

The 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 get itself merges them on such a disk. The results match Ubuntu's:

Old New
300 fuzz cases (298 nested) 298 refused, 2 accepted 300/300 round-trip exactly
200 more — 200/200
60 flat universes, written by old, then run by new — 60/60 v1, unchanged
CoopBench, its 15 keys nested dry run refuses all 15 dry run passes; full task runs 18/18 steps
  • Ids and ledger: every id is the expected form. A rerun reused everything (100/100), and a change rewrote only the universe (100/100).
  • End to end: 30/30 agent-env runs exited 0.
  • Decomposed names (NFD). APFS keeps folder names as written, so a universe folder can hold decomposed names, as some Mac tools write them.
    • Both versions read flat ones in NFC: café, résumé.yaml.
    • Only the new one reads nested ones (scenarios/résumé.yaml), and it round-trips them through get.

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:

  • Fuzz: 300/300 and 200/200 round-trip on new, and old refuses 298/300.
  • Ids: the same verbatim and hashed counts as the earlier rounds.
  • No regression: 60/60 v1, unchanged.
  • CoopBench: old refuses the 15 nested keys; new runs 18/18.

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:

  • Fuzz: new round-trips 300/300 and 200/200, and old refuses 298/300.
  • No regression: 60/60 v1, unchanged.
  • CoopBench: old refuses the 15 nested keys; new runs 18/18 steps.
  • NFD (macOS): both versions read flat names in NFC, and only new reads and round-trips nested ones.

earakely-scale and others added 3 commits October 16, 2026 11:58
… 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>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant