Skip to content

fix(registry): resolve relative includes against the declaring bundle's base_path - #303

Open
Sam Schillace (ramparte) wants to merge 1 commit into
mainfrom
fix/include-relative-to-declaring-bundle
Open

fix(registry): resolve relative includes against the declaring bundle's base_path#303
Sam Schillace (ramparte) wants to merge 1 commit into
mainfrom
fix/include-relative-to-declaring-bundle

Conversation

@ramparte

Copy link
Copy Markdown
Contributor

Problem

BundleRegistry resolves 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.

  • registry.py:193-195SimpleSourceResolver(..., base_path=Path.cwd()), captured once, never re-derived per bundle
  • sources/file.py:43-44 — any .//../ source resolves against that single base_path
  • _compose_includes never consulted the declaring bundle's base_path, which the registry already tracks via bundle.base_path (set in _load_from_path) and already uses for namespace resolution

Reproduced with a bundle at <repo>/bundles/child.md including ./base.md:

cd <repo>/bundles && load child.md  -> session populated, 13 tools / 15 hooks / 11 agents
cd /tmp/anywhere  && load child.md  -> "Include Failed (skipping): File not found: /tmp/anywhere/base.md"
                                        session {}, 0 tools / 0 hooks / 0 agents

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 by b667815 (PR #279), which fixed the identical class of bug for session/providers/tools/hooks source: 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=None falls back to current behavior.

Design note — why this is in registry.py and not sources/file.py. Threading a per-call base_path override down into SimpleSourceResolver.resolve() / FileSourceHandler.resolve() was considered and rejected for two reasons:

  1. Concurrency. _compose_includes Phase 2 loads includes in parallel via asyncio.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.
  2. Cache-key ambiguity. _load_single caches bundles and detects cycles keyed on the literal URI string. If two bundles in different directories both declare ./base.md and 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_single avoids 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():

Include not found (skipping): ./base.md (included by child @ /tmp/.../bundles)

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 empty session after 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 proving git+/file:///namespace/no-base_path sources are untouched.

The bug was verified to reproduce without the fix: stashing registry.py yields session {}, tools [], and the "Include Failed (skipping)" warning.

Lint/type: pyright identical to baseline. One new PIE810 ruff warning at the new helper — the exact same pattern that b667815's _resolve_relative_source already triggers on origin/main; mirrored the precedent's style rather than diverging.

Related

Sibling fix for module source: fields: #279.

…'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) 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>
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