Skip to content

[amplifier-foundation/app-cli] Bundle-declared tool source overrides (modules.tools[].source) recorded in mount plan but not consumed at activation #346

Description

@bkrabach

Component: amplifier-foundation, amplifier-app-cli (Issues disabled on amplifier-foundation; filed here per ecosystem convention)

Summary

When a bundle-declared tool's source is overridden via modules.tools[].source in settings, the override is accepted at bundle preparation time, recorded in the mount plan as intent, and then silently discarded at session activation. For bundle-declared tools (like tool-skills), the source hint is never consulted — only for novel tools added post-prepare does lazy activation honor it.

Issue Details

Scope

This affects bundle-declared tools only — tools already present in the bundle. For novel tools added via settings (absent from the bundle), the lazy-activation path in foundation's PreparedResolver does consume the source hint. The restriction is specific: cannot override the source of a bundle-declared tool.

Root Cause (End-to-End Trace, Current Code)

Install time — where sources ARE consumed:

  • app-cli runtime/config.py:104-123 builds override set: get_source_overrides()overrides.<id>.source, get_module_sources()sources.modules, provider extraction from config.providers[].source
  • These feed combined_sources = {**module_sources, **source_overrides, **provider_sources} (:123)
  • But modules.tools[].source is absent from this set. Tool overrides are applied AFTER prepare via get_tool_overrides() (:212) → _apply_tool_overrides (:608) → merge_module_items (lib/merge_utils.py:131), synced into mount plan (:288-296).
  • Installation is already complete. The source key reaches the mount plan but was never fed to any installer.

Session time — why the recorded source still doesn't run:

  • core _session_init.py:204 passes source_hint=tool_config.get("source") to loader
  • The mounted module-source-resolver is foundation's PreparedResolver
  • Its sync resolve() method (_prepared.py:222) discards the hint: _hint = ... # noqa: F841
  • The async path (_prepared.py:249-252) uses the hint ONLY on the lazy-activation path (when module_id not in self._paths)
  • A bundle-declared tool is always in self._paths, so the fast path returns the prepared (stock) path
  • Even after resolution, loader.py:324 tries the installed entry point first

Mechanical proof: Mount plans record intent, not code identity.

Working Workarounds

Both feed combined_sources at install time (safe):

  1. sources.modules.<module-id> — documented override surface, settings.py:491 + config.py:104
  2. overrides.<id>.source — documented override surface, settings.py:1128 + config.py:104

Documentation Status

modules.tools[].source is not a documented override surface. Documented use of settings.yaml modules.tools is tool config only (foundation docs/BUNDLE_GUIDE.md:1307, allowed_write_paths example). The source-override design spec routes sources through resolver surfaces (app-cli docs/specs/2026-03-12-bundle-source-override-design.md:17).

However, the schema shape itself invites the expectation: settings modules.tools[] mirrors bundle tools[] where source IS consumed (foundation _dataclass.py:391-392).

Two-Path Reality (Previously Contradictory Observations Reconciled)

  • Bundle-declared tool path: recorded, never consulted ← this issue
  • Novel module lazy-activation: source hint IS honored ← both truths coexist

Scope Impact

Affects anyone expressing a tool-source pin/redirect via modules.tools[].source: private-mirror users (enterprise/gitea), fork pinning, local tool development copying bundle schema into settings. Hooks share the same post-prepare merge path (_apply_hook_overrides, runtime/config.py:540+) — same class; provisioning flows (profile.py:813 → engine.py:1018 → incus.file_push) also affected.

The provenance-integrity angle affects every consumer of mount plans as audit artifacts (evals, session forensics).

Reproduction

  1. Set modules.tools[tool-skills].source: "https://custom-mirror/repo@custom-sha" in settings
  2. Prepare a bundle with tool-skills included
  3. Check mount plan: source key is recorded for tool-skills
  4. Run a session: the custom source is never consulted, stock tool-skills loads
  5. Evidence: mount-plan provenance integrity gate (during 20260728T114548Z valid vs 20260728T105215Z void runs)

Fix Shape

Two complementary approaches, in order of implementability:

  1. Extract at the :123 seam (app-cli install path): extend combined_sources at runtime/config.py:123 with tool/hook source extraction (template exists at :112-120 for providers).

  2. Fail loud on mismatch (foundation activation path): when a mount-plan entry's source differs from the prepared source for that module id, raise at _prepared.py:222 (the point where the hint is currently discarded). Identity check, no new mechanism required.

No new mechanism required in either case.

Related Issues

Same class as #342 and #344 (override-ladder members). Different from #26/#282 (both involve reproducibility but differ on URI pinning vs source shadowing).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions