fix(registry): resolve relative includes against the declaring bundle's base_path - #303
Open
Sam Schillace (ramparte) wants to merge 1 commit into
Open
fix(registry): resolve relative includes against the declaring bundle's base_path#303Sam Schillace (ramparte) wants to merge 1 commit into
Sam Schillace (ramparte) wants to merge 1 commit into
Conversation
…'s base_path
BundleRegistry resolved relative bundle includes (includes: - bundle: ./base.md)
against Path.cwd() captured once at registry-construction time (the CLI
process's invocation directory), instead of against the including bundle
file's own directory. A bundle loaded with the CLI's invocation directory
different from the bundle's own directory would silently fail to include
its relative dependency: the include is skipped with a warning (not a
raise), producing a bundle that 'loads successfully' but is empty --
which detonates far later and elsewhere as an unrelated
ValueError: Configuration must specify session.orchestrator.
Root cause: registry.py's _source_resolver is constructed once with
base_path=Path.cwd() (registry.py:193-195), and sources/file.py's
FileSourceHandler resolves './x' / '../x' against that single base_path
for every include, regardless of which bundle declared it.
Fix: anchor literal relative include sources ('./x', '../x') to the
DECLARING bundle's own base_path in _compose_includes(), via a new
_anchor_relative_include_source() helper, before the source reaches
_load_single(). This mirrors the sibling fix in bundle/_dataclass.py
(commit b667815), which anchored relative session/providers/tools/hooks
source: fields to the declaring bundle's base_path instead of the app's --
this is the same idea applied to includes:. Anchoring at this point (Phase
1 of _compose_includes, before Phase 2's parallel _load_single calls) also
keeps the cache/loading-chain keys (both keyed on the URI string) unambiguous
even when two different bundles declare the same relative include text
from different directories -- no changes to sources/file.py or
sources/resolver.py were needed, and no shared mutable state is introduced,
so concurrent include resolution (asyncio.gather in Phase 2) stays safe.
Also improves the actionability of include-failure messages (unregistered
namespace, not-found, and failed-to-load paths, both strict-raise and
non-strict-warn) to name the including bundle and the base_path resolution
was attempted against, via a new _describe_including_bundle() helper --
previously only the missing target was named, which does not distinguish
'declaring bundle's own base_path was wrong' from 'target genuinely does
not exist'. No change to strict/non-strict semantics itself.
Tests: tests/test_include_relative_to_declaring_bundle.py covers: cwd
matching the bundle dir (guard), cwd elsewhere (the bug), 3-level nested
includes each anchored to their own directory, '../' includes, non-relative
includes (git+, file://, namespace refs) left untouched, and a genuinely
missing include still warning with the including bundle + base_path named.
Generated with [Amplifier](https://github.com/microsoft/amplifier)
Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
Sam Schillace (ramparte)
requested review from
Brian Krabach (bkrabach) and
Salil Das (sadlilas)
August 12, 2026 22:19
Sam Schillace (ramparte)
added a commit
to ramparte/my-amplifier
that referenced
this pull request
Aug 12, 2026
Both overlay bundles use an absolute file:// path to include my-amplifier-base instead of a relative reference. This is a workaround for a registry resolution bug: BundleRegistry resolves relative includes against Path.cwd() captured at registry-construction time (the CLI invocation directory) rather than against the including bundle's own base_path. This causes relative includes to silently drop when the CLI is invoked from directories other than bundles/. The absolute path workaround is machine-specific and will not resolve on clones with different home directories (e.g., WSL with /home/samschillace instead of /home/ramparte), breaking bundle composition on those systems. Revert when microsoft/amplifier-foundation#303 merges (anchors relative includes to base_path). Restore '- bundle: ./my-amplifier-base.md' in both overlays and verify with 'amplifier bundle show' from a directory outside bundles/ — should report 13 tools / 15 hooks / 11 agents. Generated with [Amplifier](https://github.com/microsoft/amplifier) Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
BundleRegistryresolves relative bundle includes (includes: - bundle: ./base.md) againstPath.cwd()captured once at registry-construction time — the CLI process's invocation directory — instead of against the including bundle file's own directory.registry.py:193-195—SimpleSourceResolver(..., base_path=Path.cwd()), captured once, never re-derived per bundlesources/file.py:43-44— any.//../source resolves against that singlebase_path_compose_includesnever consulted the declaring bundle'sbase_path, which the registry already tracks viabundle.base_path(set in_load_from_path) and already uses for namespace resolutionReproduced with a bundle at
<repo>/bundles/child.mdincluding./base.md:The second case does not raise. It yields a bundle that "loads successfully" but is empty, detonating much later and elsewhere as
ValueError: Configuration must specify session.orchestrator— an error naming nothing related to the cause. This cost a real debugging session to track down.What this does
Anchors relative include sources to the declaring bundle's
base_path, following the precedent set byb667815(PR #279), which fixed the identical class of bug forsession/providers/tools/hookssource:fields.New
_anchor_relative_include_source(source, base_path)mirrors_resolve_relative_source's semantics exactly: only literal.//../strings are rewritten to an absolute path; URIs,git+,file://, and already-resolved namespace refs pass through untouched;base_path=Nonefalls back to current behavior.Design note — why this is in
registry.pyand notsources/file.py. Threading a per-callbase_pathoverride down intoSimpleSourceResolver.resolve()/FileSourceHandler.resolve()was considered and rejected for two reasons:_compose_includesPhase 2 loads includes in parallel viaasyncio.gather, and nested composition can run concurrently for sibling chains with different declaring bundles. A mutate-then-restore override on the shared handler would be a genuine race._load_singlecaches bundles and detects cycles keyed on the literal URI string. If two bundles in different directories both declare./base.mdand resolution became context-dependent, the same cache key would ambiguously refer to two different files — a latent bug the global-cwd design never had (it was globally consistent, if globally wrong).Resolving to an absolute path before the value reaches
_load_singleavoids both: the cache/cycle key becomes the fully-resolved absolute path, unambiguous regardless of declaring bundle or concurrency. Smaller change, confined to one file.Diagnosability
All include warn/raise sites now name the including bundle and the base_path used, via a new
_describe_including_bundle():Open question for reviewers — deliberately NOT implemented
Strict mode already raises
BundleDependencyError; non-strict (the CLI default) warns. This PR does not change that. But a failed include silently producing an empty bundle is precisely what made this hard to diagnose. A bounded improvement — raise if the root bundle ends up with an emptysessionafter all includes are processed, regardless of strict mode — would close the gap without making every include failure fatal. That is a semantics decision, so it is flagged here rather than bundled in.Testing
1549 -> 1559 passing, zero regressions. 10 new tests in
tests/test_include_relative_to_declaring_bundle.py: cwd matching the bundle dir (guards existing behavior), cwd unrelated (the bug), nested C->B->A across four directories,../includes, missing-include message content, and five cases provinggit+/file:///namespace/no-base_path sources are untouched.The bug was verified to reproduce without the fix: stashing
registry.pyyieldssession {},tools [], and the "Include Failed (skipping)" warning.Lint/type: pyright identical to baseline. One new
PIE810ruff warning at the new helper — the exact same pattern thatb667815's_resolve_relative_sourcealready triggers onorigin/main; mirrored the precedent's style rather than diverging.Related
Sibling fix for module
source:fields: #279.