Skip to content

feat(bundle): a bundle.toml names the plugin packages a bundle requires - #377

Merged
earakely-scale merged 6 commits into
mainfrom
feat/bundle-requires
Oct 29, 2026
Merged

earakely-scale merged 6 commits into
mainfrom
feat/bundle-requires

Conversation

@earakely-scale

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

Copy link
Copy Markdown
Collaborator

Summary

A bundle shared with someone now says which plugin packages it needs, and a machine without them is told how to install them before anything runs.

  • bundle.toml: an optional file at the bundle root, requires = ["agentenv-browser>=1.2", ...], pip (PEP 508) requirements, markers allowed. URLs are refused, and so is a requirement holding a character that can't be printed, which could make the install prompt show a different package from the one it installs.
  • Refused before it runs: resolving a bundle (run, run --dry-run, bundle register) checks each requirement against what's installed, including what any extras it names need, and refuses with every unmet one and the one command that installs them all:
    Error: bundle.toml: requires 'agentenv-acme>=1.2', which isn't installed; install it with: agent-env plugin add 'agentenv-acme>=1.2'
    
  • agent-env itself: a requirement on agentenv-framework (or its protocol) says to upgrade agent-env with the installer that installed it; plugin add doesn't change agent-env, so it's left out of the command and never prompted for.
  • Offered in a terminal: agent-env run and bundle register then ask Install agentenv-acme>=1.2 with agent-env plugin add? [y/N]. Yes installs through plugin add and runs the same command again in a new process, so the new plugins load. No, or no terminal, refuses as above. A dry run never asks, and the rerun never asks twice.
  • Created by default: a run or registration of a folder run by its path, that is writable and has no bundle.toml, writes one requiring >= the installed version of each plugin distribution whose step, env or artifact type the bundle uses. The bound drops a local version label (1.2.0+acme.1 → >=1.2.0). It is created whole or not at all (staged and hard-linked into place), never over a file that appeared meanwhile; on a filesystem without hard links, or a full one, nothing is written and the run gives the hint. A dry run says would write bundle.toml: ... instead.
  • Never edited: an existing bundle.toml stays as written. A run says what it doesn't require and the line to declare, so requires = [] is how a bundle opts out. Read-only folders and the OpenEnv service path get the same hint and write nothing. An installed bundle (run by name) says nothing of what to declare, since whoever runs it can't edit it; its requirements are still checked.
  • Configuration-only types: a type registered only by this machine's config.toml gets one hint line, never a requirement. When an installed package ships its class, the line names it ("…which this machine's configuration registers, from agentenvhub-sdk 0.21.2; whoever runs it needs that package and its configuration").
  • bundle init --from-task writes the same bundle.toml from the needs it already detects; that detection moved to bundle/requires.py so both share it.

Review guide

  • bundle/requires.py: the refusal (unmet_problems), detection (Providers, uses, detected, shared with the importer), and declare / _create, driven by one Declaring setting (declaring_for).
  • plugins/_requirements.py: unmet, the check of requirements against what's installed, extras included, beside incompatibility(), sharing its version comparison.
  • bundle/parse.py: reads bundle.toml (it was a reserved name).
  • bundle/resolve.py: raises UnmetRequirements (a BundleError) alongside every other problem.
  • bundle/run.py, bundle/register.py: say or write it. Registration.requires carries the lines, so bundle register prints them once, not in both passes.
  • cli/_install_prompt.py: the prompt and the re-exec.

Test plan

  • make lint
  • Unit tier: new tst/unit/bundle/requires_test.py (parse, detection, unmet messages, create/dry-run/hint/opt-out/read-only, racing creators, no-link filesystems, run/register/CLI, the prompt's yes/no/no-TTY/rerun/failed-install paths); updated importer, parse and run tests
  • E2E + chaos on an agent-environments-admin VM: 84 cases on e832512, all as designed, and identical on 70f92eb (results in the comments below)

🤖 Generated with Claude Code

RetriggerView in GreptileConfidence Score: 5/5

The PR appears safe to merge; no new actionable issues were found.

Summary

Adds optional bundle.toml requirements, checks installed packages before running, and offers installation in a terminal.

  • Bundles stop before running when their declared packages are missing.
  • New and imported bundles record the plugin packages their types use.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Read bundle] --> B[Check required packages]
  B -->|Met| C[Plan and check bundle]
  B -->|Unmet| D[Refuse with install instructions]
  D -->|Terminal user agrees| E[Install and restart command]
  E --> A
  C --> F[Write declaration or show hint]
  F --> G[Register or run]
Loading

Reviews (6) · Last reviewed commit: "refactor(bundle): one detection model, o..." · Reviewed by Greptile

A bundle's optional bundle.toml lists the pip requirements it needs
(requires = ["agentenv-browser>=1.2"]). Resolving a bundle refuses one
this environment doesn't meet, before anything runs, naming the one
agent-env plugin add command that installs them; in a terminal, run and
bundle register offer to run it and then run the command again.

A run or registration of a writable folder run by its path that has no
bundle.toml creates one from the plugin distributions whose types the
bundle uses, at least the version installed, never over a file that
appeared meanwhile. A dry run says what it would write. An existing
bundle.toml is never edited: the run says what to add, so requires = []
opts out. A type only configuration registers gets a hint, never a
requirement. bundle init --from-task writes the same file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@earakely-scale
earakely-scale requested a review from a team as a code owner October 28, 2026 22:19
…t left empty

On a full disk the in-place fallback could leave an empty bundle.toml,
which reads as requires = [] and silently opts the bundle out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment thread src/agent_env/bundle/requires.py Outdated
Comment thread src/agent_env/bundle/requires.py Outdated
Comment thread src/agent_env/bundle/requires.py Outdated
…, and nothing is written without links

A plugin installed at a local version (1.2.0+acme.1) is required at
>=1.2.0, a bound pip takes. A requirement naming extras is met only when
what those extras need is installed too. A filesystem without hard links
gets the hint rather than a bundle.toml written in place, which another
run could read half-written.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@earakely-scale

Copy link
Copy Markdown
Collaborator Author

E2E + chaos verification (1a0086e)

One Ubuntu 24.04 VM in agent-environments-admin (m5.2xlarge, Docker 28, uv 0.10), torn down after. The wheel came from this branch. Two fake plugins were installed offline from a local wheel folder: agentenv-e2e-acme 1.2.0 (a step type) and agentenv-e2e-broken (fails on import). Each environment a case changes got a fresh venv, and the install prompt was answered through a real pty. 74 cases, all as designed.

With the plugin

Case Result
run of a folder without bundle.toml wrote bundle.toml: requires = ["agentenv-e2e-acme>=1.2.0"]; the run passes; a rerun says nothing
run --dry-run would write …, no file
requires = [], read-only folder hint with the line to add; file untouched / none created; run passes
marker false (sys_platform == 'win32') ignored, run passes
config file registering the plugin's own class still the plugin's: bundle.toml written
config file registering a class no distribution ships one "configuration registers" hint, no file
bundle register --dry-run, then bundle register dry run writes nothing; register writes it in the check pass, prints it once; again: nothing to write
task run --id reg-check, then bundle init --from-task reg-check passes; the imported bundle has the same bundle.toml and runs
echo-mcp example (Docker build + deploy) with the plugin step appended passes (echoes: 1, acme: 1), bundle.toml written, no containers left

Without it

Case Result
run, run --dry-run, bundle register (no TTY) exit 1: bundle.toml: requires 'agentenv-e2e-acme>=1.2.0', which isn't installed; install it with: agent-env plugin add 'agentenv-e2e-acme>=1.2.0'; nothing written to the store
two unmet + one marker-false one plugin add naming both
installed but too old (>=2) and agentenv-e2e-acme 1.2.0 is installed

The prompt (pty)

Case Result
yes, in a venv / a uv tool install / bundle register installs, Running it again: …, the rerun passes (exit 0)
no / Ctrl-C / SIGTERM exit 1 / 1 (Aborted!) / -15, nothing installed
--dry-run, or the rerun marker set never asks
unreachable index uv's error, exit 1, environment unchanged (pip freeze identical)
no version that satisfies >=2 uv's "no solution", exit 1, still 1.2.0
the broken plugin offered installed, fails to load, plugin add restores the environment, exit 1, no loop

Bad bundle.toml: invalid TOML, not UTF-8, not a list, unknown key, URL requirement, not a requirement, a folder, Bundle.TOML, unreadable. Each was refused with its own one-line message. A symlinked one is followed, as aliases.toml is.

Chaos

Case Result
60 runs SIGKILLed at random moments 35 left it whole, 25 left none, 0 partial, 0 staging files
12 runs racing on one folder exactly one wrote it, all 12 passed
vfat (no hard links) / linkat failing with injected EIO / full tmpfs nothing written, no staging files, the hint given, the run passes
an installed plugin that fails to load not a known step type (registered by … but failed to load), no prompt

Found and fixed along the way:

  • a full disk could leave an empty bundle.toml, which reads as requires = [] (21694b4);
  • Greptile's three findings (1a0086e).

🤖 Generated with Claude Code

…from, and say to upgrade agent-env itself

A type only configuration registers is hinted with the installed package
its class's module comes from, when one ships it, so whoever runs the
bundle knows what to install as well as which configuration it needs.

A bundle.toml requiring agent-env itself (agentenv-framework or its
protocol) says to upgrade agent-env with the installer that installed
it: plugin add refuses to change agent-env, so it's left out of the
command and the prompt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@earakely-scale

earakely-scale commented Oct 28, 2026 •

Copy link
Copy Markdown
Collaborator Author

Follow-up verification (3b5ade9, e832512, then 70f92eb)

The full VM round ran on a fresh agent-environments-admin VM for each commit (torn down after). On e832512: 84 cases, all as designed. On 70f92eb, the behaviour-neutral refactor, every case's exit code, prompt result and message matched e832512's. Every earlier case gave the same result, and chaos was clean again (60 SIGKILLs: 0 partial, 0 staging files; 12 racing runs: one writer, all passed).

New cases:

Case Result
config file registering agentenv_e2e_shipper:ShippedCheck, a class an installed package ships with no entry point …which this machine's configuration registers, from agentenv-e2e-shipper 1.0.0; whoever runs it needs that package and its configuration; no bundle.toml
config file registering a class from a module on PYTHONPATH the plain line, no package named
requires = ["agentenv-framework>=99"], at a terminal …and agentenv-framework 0.9.1375 is installed; upgrade agent-env with the installer that installed it, exit 1, no prompt
that beside a missing plugin, answering yes only the plugin is offered; it installs and the command reruns, which is refused for agent-env alone, without asking again (exit 1)
a bundle a plugin ships (agent_env.bundles), using the plugin's own step type with no bundle.toml, run by name and with --dry-run passes, says nothing about bundle.toml; the installed folder is untouched
the same folder copied and run by path wrote bundle.toml: requires = ["agentenv-e2e-bundled>=1.0.0"]
a requirement holding a terminal escape (\u001b[2K\r…), at a terminal requires: 'evil; os_name != "\x1b[2K\ragentenv-browser"' holds a character that can't be printed…, exit 1, no prompt

🤖 Generated with Claude Code

earakely-scale and others added 2 commits October 28, 2026 18:41
…one running an installed bundle what to declare

A bundle.toml string can carry control or format characters, which the
install prompt and the plugin add command would print as they are, so a
shared bundle could make the prompt show one package while installing
another. Such a requirement is refused where the file is read.

An installed bundle's runner can't edit it, so runs by name, the A2A
validator's writes and an installed bundle served by OpenEnv say nothing
of what to declare; its requirements are still checked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… installed-package check beside the plugin one

The importer's needs are the same Use records a run detects, with their
Provider, and its bundle.toml comes from detected(), so how a requirement
is written lives in one place. A Declaring setting (write, would write,
hint, quiet), worked out once by declaring_for, replaces the own, dry_run
and installed flags and register's write_requires. unmet() and the extras
check move to plugins/_requirements.py, beside incompatibility(), which
now shares their version comparison. The tests stand in for installed
packages through one fake_plugins helper.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@earakely-scale
earakely-scale merged commit 7c2ee00 into main Oct 29, 2026
28 checks passed
@earakely-scale
earakely-scale deleted the feat/bundle-requires branch October 29, 2026 04:24
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