Skip to content

Commit fed642e

Browse files
tbitcsoz-agent
andcommitted
feat: Phase B1 — execution profile filtering in runner._resolve_for_activity
_resolve_for_activity now checks the active execution profile's allowed_providers/allowed_provider_types before using a profile. If the primary provider is blocked, fallback chain entries are tried. If no fallback is allowed, emits a warning and degrades gracefully. New method: _filter_by_execution_profile() — surgical, additive, wrapped in try/except for graceful degradation. No behavior change when no execution profile is configured (backward compatible). 483 tests pass (0 regressions from this change). Co-Authored-By: Oz <oz-agent@warp.dev>
1 parent 5f49b72 commit fed642e

1 file changed

Lines changed: 57 additions & 2 deletions

File tree

src/specsmith/agent/runner.py

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,12 @@ def _resolve_for_activity(self, activity: str):
359359
360360
Respects an explicit per-session profile / endpoint override so
361361
the ``--agent`` and ``--endpoint`` CLI flags still win.
362+
363+
When an execution profile is active, the resolved agent profile's
364+
provider is checked against the execution profile's allowed
365+
provider types / IDs. If not allowed, the profile's fallback
366+
chain is tried. This ensures a "local-only" execution profile
367+
never routes to a cloud provider.
362368
"""
363369
if self.profile_id is None and self._routing is None:
364370
return (None, None)
@@ -368,15 +374,64 @@ def _resolve_for_activity(self, activity: str):
368374
store = ProfileStore.load()
369375
if self.profile_id:
370376
profile = store.get(self.profile_id)
371-
return (profile, profile.endpoint_id or None)
377+
profile = self._filter_by_execution_profile(profile, store)
378+
return (profile, profile.endpoint_id or None) if profile else (None, None)
372379
target_id = store.routes.get(activity) or store.default_profile_id
373380
if not target_id:
374381
return (None, None)
375382
profile = store.get(target_id)
376-
return (profile, profile.endpoint_id or None)
383+
profile = self._filter_by_execution_profile(profile, store)
384+
return (profile, profile.endpoint_id or None) if profile else (None, None)
377385
except Exception: # noqa: BLE001
378386
return (None, None)
379387

388+
def _filter_by_execution_profile(self, profile, store) -> Any:
389+
"""Check if a profile's provider is allowed by the active execution profile.
390+
391+
If not allowed, try fallback chain entries. Returns the original
392+
profile if no execution profile is loaded (graceful degradation).
393+
"""
394+
try:
395+
from specsmith.agent.execution_profiles import ExecutionProfileStore
396+
397+
ep_store = ExecutionProfileStore.load()
398+
exec_profile = ep_store.default()
399+
400+
# Check if the primary profile's provider is allowed.
401+
if exec_profile.allows_provider(profile.provider, profile.provider):
402+
return profile
403+
404+
# Try fallback chain entries.
405+
for fallback_str in (profile.fallback_chain or []):
406+
parts = fallback_str.split("/", 1)
407+
fb_provider = parts[0] if parts else ""
408+
if exec_profile.allows_provider(fb_provider, fb_provider):
409+
# Create a modified profile using the fallback.
410+
from specsmith.agent.profiles import Profile
411+
412+
fb_model = parts[1] if len(parts) > 1 else ""
413+
return Profile(
414+
id=f"{profile.id}-fallback",
415+
role=profile.role,
416+
provider=fb_provider,
417+
model=fb_model,
418+
endpoint_id=profile.endpoint_id,
419+
capabilities=profile.capabilities,
420+
fallback_chain=[],
421+
)
422+
423+
# No allowed provider found — return None so caller degrades.
424+
self._emit_event(
425+
type="system",
426+
message=(
427+
f"\u26a0 execution profile '{exec_profile.id}' blocks "
428+
f"provider '{profile.provider}' and all fallbacks"
429+
),
430+
)
431+
return None
432+
except Exception: # noqa: BLE001 — graceful degradation
433+
return profile # no execution profile loaded → allow everything
434+
380435
def _load_routing(self) -> Any | None:
381436
try:
382437
from specsmith.agent.profiles import ProfileStore

0 commit comments

Comments
 (0)