Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions .planning/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ This roadmap now enters milestone v1.1: Cloud API and Azure Function Hosting. Th
- [x] **Phase 3: Specialist Delegation and Routing Visibility** - Make specialist participation and provider routing fully visible
- [x] **Phase 4: Autonomous Repo Execution and Validation Guardrails** - Turn the system into a safe default-doer for repo work
- [x] **Phase 5: Polished Operator Workbench** - Replace prototype interaction with a durable operator-grade UI
- [ ] **Phase 6: API Boundary and Control Plane Contract** - Extract a shared orchestration service layer and expose it through a stable HTTP API
- [ ] **Phase 7: Worker Boundary and Cloud-Safe Execution Profiles** - Separate cloud ingress from long-running repo execution and local-only provider assumptions
- [x] **Phase 6: API Boundary and Control Plane Contract** - Extract a shared orchestration service layer and expose it through a stable HTTP API
- [x] **Phase 7: Worker Boundary and Cloud-Safe Execution Profiles** - Separate cloud ingress from long-running repo execution and local-only provider assumptions

## Phase Details

Expand Down Expand Up @@ -48,9 +48,9 @@ Plans:
**Plans**: 3 plans

Plans:
- [ ] 07-01: Introduce the worker boundary and background run dispatch contract
- [ ] 07-02: Add cloud-safe provider and execution profiles with explicit capability enforcement
- [ ] 07-03: Validate end-to-end API-driven runs across local and cloud-safe execution modes
- [x] 07-01: Introduce the worker boundary and background run dispatch contract
- [x] 07-02: Add cloud-safe provider and execution profiles with explicit capability enforcement
- [x] 07-03: Validate end-to-end API-driven runs across local and cloud-safe execution modes

## Progress

Expand All @@ -64,5 +64,5 @@ Phases execute in numeric order: 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> 8
| 3. Specialist Delegation and Routing Visibility | 3/3 | Complete | 2007-03-21 |
| 4. Autonomous Repo Execution and Validation Guardrails | 3/3 | Complete | 2007-03-21 |
| 5. Polished Operator Workbench | 3/3 | Complete | 2007-03-22 |
| 6. API Boundary and Control Plane Contract | 0/3 | Planned | - |
| 7. Worker Boundary and Cloud-Safe Execution Profiles | 0/3 | Planned | - |
| 6. API Boundary and Control Plane Contract | 3/3 | Complete | 2026-06-10 |
| 7. Worker Boundary and Cloud-Safe Execution Profiles | 3/3 | Complete | 2026-06-14 |
30 changes: 20 additions & 10 deletions .planning/STATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ gsd_state_version: 1.0
milestone: v1.1
milestone_name: milestone
status: completed
stopped_at: Phase 6 complete
last_updated: "2026-06-10T18:30:00+03:00"
last_activity: 2026-06-10 - Completed quick task 260610-ppt: PR #1 follow-up truthful Quickstart and Configuration guidance
stopped_at: Phase 7 complete
last_updated: "2026-06-14T00:00:00+03:00"
last_activity: 2026-06-14 - Completed Phase 7: Worker Boundary and Cloud-Safe Execution Profiles
progress:
total_phases: 2
completed_phases: 1
total_plans: 3
completed_plans: 4
completed_phases: 2
total_plans: 6
completed_plans: 7
percent: 100
---

Expand All @@ -25,10 +25,10 @@ See: .planning/PROJECT.md (updated 2026-03-22)

## Current Position

Phase: 07 (azure-functions-and-cloud-control-plane) - READY TO START
Plan: 07-01 (next)
Status: Phase 6 complete - Shared control-plane API delivered with /api/v1 REST router, Command Center parity validation, and external API documentation. Ready for Azure Functions deployment.
Last activity: 2026-06-10 - Completed quick task 260610-ppt: PR #1 follow-up truthful Quickstart and Configuration guidance
Phase: 07 (worker-boundary-and-cloud-safe-execution-profiles) - COMPLETE
Plan: 07-03 (last completed)
Status: Phase 7 complete - Worker boundary and cloud-safe execution profiles delivered. WorkerBoundary async dispatch, ExecutionProfile enforcement, IncompatibleProviderError, and --profile CLI flag are all in place. All three plans (07-01, 07-02, 07-03) completed.
Last activity: 2026-06-14 - Completed Phase 7: Worker Boundary and Cloud-Safe Execution Profiles

## Performance Metrics

Expand Down Expand Up @@ -72,6 +72,9 @@ Last activity: 2026-06-10 - Completed quick task 260610-ppt: PR #1 follow-up tru
| Phase 06 P01 | 1 min | 4 tasks | 4 files |
| Phase 06 P02 | 1 min | 4 tasks | 4 files |
| Phase 06 P03 | 1 min | 4 tasks | 4 files |
| Phase 07 P01 | 1 min | 2 tasks | 2 files |
| Phase 07 P02 | 1 min | 3 tasks | 3 files |
| Phase 07 P03 | 1 min | 2 tasks | 2 files |

## Accumulated Context

Expand Down Expand Up @@ -121,6 +124,13 @@ Recent decisions affecting current work:
- Milestone v1.1: Continue phase numbering from 6 instead of resetting roadmap numbering
- Milestone v1.1: Use Azure Functions as the cloud control-plane host and keep long-running repo execution behind a worker boundary
- Milestone v1.1: Keep the Operator Workbench and the external HTTP API on one shared orchestration contract
- Phase 07 planning: WorkerBoundary uses asyncio.create_task for background dispatch with no new dependencies
- Phase 07 planning: Cloud-safe profile is strictly opt-in via --profile flag; local execution path is unchanged
- Phase 07-01: WorkerProfile is a string enum for clean serialization and CLI parsing
- Phase 07-01: WorkerBoundary tracks status in a plain dict; terminal states are "done" and "error:<msg>"
- Phase 07-02: IncompatibleProviderError carries provider and profile name in a human-readable message
- Phase 07-02: assert_provider_allowed guard is placed at the top of _execute_chain_step before any subprocess is spawned
- Phase 07-03: SUBPROCESS_PROVIDERS frozenset is the single source of truth for which providers require subprocess access

### Pending Todos

Expand Down
64 changes: 64 additions & 0 deletions .planning/phases/07-worker-boundary/07-PLAN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Phase 07 Plan: Worker Boundary and Cloud-Safe Execution Profiles

**Phase**: 07 — Worker Boundary and Cloud-Safe Execution Profiles
**Goal**: Make long-running execution explicit and safe when the control plane is hosted away from the local workstation. Separate cloud ingress from local-only providers.
**Requirements**: WRKR-01, WRKR-02, WRKR-03

## Context

Phase 6 delivered a stable REST API and shared orchestration contract. Phase 7 introduces a worker boundary so HTTP ingress never waits on long-running repo execution, and adds execution profiles so cloud-hosted runs can explicitly reject local-only subprocess providers.

The local execution path is preserved intact. Cloud-safe is strictly opt-in via `--profile cloud-safe`.

## Plans

### 07-01: Worker boundary and background run dispatch contract

**Files created:**
- `maf_starter/worker_boundary.py` — `WorkerProfile` enum (LOCAL, CLOUD_SAFE), `WorkerBoundary` class with async dispatch and run status
- `tests/test_worker_boundary.py` — unit tests for submit_async, get_status, done/pending states

**Design decisions:**
- `WorkerBoundary.submit_async(run_id, workflow)` dispatches via `asyncio.create_task` — no new dependencies
- Status is tracked in a plain dict keyed by run_id; values are `"pending"`, `"running"`, `"done"`, or `"error:<message>"`
- `submit_async` returns immediately with the run_id so the HTTP layer is never blocked
- `WorkerProfile` is a string enum to allow clean serialization and CLI parsing

### 07-02: Cloud-safe provider and execution profiles

**Files created / modified:**
- `maf_starter/execution_profile.py` — `ExecutionProfile` dataclass, `CLOUD_SAFE_PROFILE` and `LOCAL_PROFILE` constants, `IncompatibleProviderError`
- `maf_starter/provider_fallback.py` — guard added at the top of `_execute_chain_step` to reject subprocess providers when profile is CLOUD_SAFE
- `maf_starter/devui_overrides.py` — `--profile` CLI flag added (choices: local, cloud-safe; default: local) wired through to settings context
- `tests/test_execution_profile.py` — unit tests for profile enforcement and IncompatibleProviderError

**Design decisions:**
- `IncompatibleProviderError(RuntimeError)` carries provider name and profile name in a clear message
- Subprocess providers are `gemini-cli`, `claude-cli`, `codex-cli` — same set already used throughout `provider_fallback.py`
- LOCAL profile imposes no restrictions; CLOUD_SAFE rejects all subprocess providers on first check before any subprocess is spawned
- `ExecutionProfile` is a frozen dataclass with `profile: WorkerProfile` and `capabilities: tuple[str, ...]`
- `CLOUD_SAFE_PROFILE` capabilities list: `["api-only", "no-subprocess"]`
- `LOCAL_PROFILE` capabilities list: `["api", "subprocess", "repo-execution"]`

### 07-03: End-to-end validation

**Files created / modified:**
- `tests/test_phase7_e2e.py` — three integration tests:
1. Cloud-safe profile rejects gemini-cli subprocess provider with `IncompatibleProviderError`
2. Local profile accepts all providers (gemini, anthropic, gemini-cli, claude-cli, codex-cli)
3. Async dispatch via `WorkerBoundary.submit_async` returns run_id immediately without blocking
- `STATE.md` updated: Phase 7 marked complete

## Verification

All tests pass with:
```
cd C:\PersonalRepo\portfolio\autogen && python -m pytest tests/ -v
```

## Constraints

- No new pip dependencies — asyncio and stdlib only
- Local path unchanged — subprocess providers still work under LOCAL profile
- `IncompatibleProviderError` is informative: `"Provider {name} requires subprocess access which is not available in cloud-safe profile"`
- snake_case modules, PascalCase dataclasses, UPPER_SNAKE_CASE module constants — consistent with existing maf_starter patterns
21 changes: 21 additions & 0 deletions autogen_starter/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
collect_provider_statuses,
create_model_client,
)
from maf_starter.execution_profile import CLOUD_SAFE_PROFILE, LOCAL_PROFILE
from maf_starter.worker_boundary import WorkerProfile

DEFAULT_CHAT_SYSTEM_MESSAGE = (
"You are a collaborative assistant. Work with the human in short iterations. "
Expand All @@ -29,8 +31,22 @@
)


def _profile_choices() -> tuple[str, ...]:
return tuple(p.value for p in WorkerProfile)


def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="AutoGen AgentChat starter.")
parser.add_argument(
"--profile",
choices=_profile_choices(),
default=WorkerProfile.LOCAL.value,
help=(
"Execution profile: 'local' (default) allows all providers including "
"subprocess-backed CLI tools; 'cloud-safe' restricts to API-only providers "
"and rejects subprocess execution."
),
)
subparsers = parser.add_subparsers(dest="command", required=True)

subparsers.add_parser("providers", help="Show provider readiness.")
Expand Down Expand Up @@ -140,8 +156,13 @@ def main() -> int:
parser = build_parser()
args = parser.parse_args()

# Resolve execution profile from --profile flag (default: local).
profile = CLOUD_SAFE_PROFILE if args.profile == WorkerProfile.CLOUD_SAFE.value else LOCAL_PROFILE

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Enforce the selected cloud-safe profile

When --profile cloud-safe is used with AUTOGEN_PROVIDER=gemini-cli, claude-cli, or codex-cli, the selected profile is only assigned and printed; run_blocking_chat and run_resumable_step still call create_model_client(settings) without checking or passing it, and dashboard starts the app without preserving it. Consequently, a command advertised as disabling subprocess providers still constructs and runs them.

Useful? React with 👍 / 👎.


try:
settings = load_settings()
if profile.profile != WorkerProfile.LOCAL:
print(f"[profile] Active execution profile: {profile.profile.value} — subprocess providers are disabled.")
if args.command == "providers":
print_provider_statuses(settings)
return 0
Expand Down
72 changes: 72 additions & 0 deletions maf_starter/execution_profile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
from __future__ import annotations

from dataclasses import dataclass

from maf_starter.worker_boundary import WorkerProfile


# Providers that require subprocess access and cannot run in a cloud-hosted context.
SUBPROCESS_PROVIDERS: frozenset[str] = frozenset({"gemini-cli", "claude-cli", "codex-cli"})


class IncompatibleProviderError(RuntimeError):
"""Raised when a provider requires capabilities that the active execution
profile does not allow.

Example::

raise IncompatibleProviderError("gemini-cli", WorkerProfile.CLOUD_SAFE)
"""

def __init__(self, provider: str, profile: WorkerProfile) -> None:
self.provider = provider
self.profile = profile
super().__init__(
f"Provider {provider!r} requires subprocess access which is not "
f"available in {profile.value!r} profile."
)


@dataclass(frozen=True)
class ExecutionProfile:
"""Describes the execution capabilities available in the current hosting context.

Attributes:
profile: The broad classification of the execution environment.
capabilities: Explicit capability tokens that other modules can inspect
to decide whether a feature or provider is allowed.
"""

profile: WorkerProfile
capabilities: tuple[str, ...]

def allows_subprocess(self) -> bool:
"""Return True when subprocess-backed providers are permitted."""
return "subprocess" in self.capabilities

def assert_provider_allowed(self, provider: str) -> None:
"""Raise IncompatibleProviderError if *provider* is a subprocess provider
and the profile does not allow subprocess access.

Args:
provider: Provider key such as ``"gemini-cli"``, ``"claude-cli"``,
or ``"gemini"``. Non-subprocess providers are always
allowed and this method returns immediately.
"""
if provider in SUBPROCESS_PROVIDERS and not self.allows_subprocess():
raise IncompatibleProviderError(provider, self.profile)


# Pre-built profile constants -------------------------------------------------

LOCAL_PROFILE = ExecutionProfile(
profile=WorkerProfile.LOCAL,
capabilities=("api", "subprocess", "repo-execution"),
)
"""Local workstation profile — all providers and repo execution are permitted."""

CLOUD_SAFE_PROFILE = ExecutionProfile(
profile=WorkerProfile.CLOUD_SAFE,
capabilities=("api-only", "no-subprocess"),
)
"""Cloud-safe profile — API-only providers only; subprocess providers are rejected."""
5 changes: 5 additions & 0 deletions maf_starter/provider_fallback.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ async def get_final_response(self):
AnthropicClient = None

from maf_starter.config import Settings, activate_run_scope, reset_run_scope
from maf_starter.execution_profile import CLOUD_SAFE_PROFILE, LOCAL_PROFILE, ExecutionProfile
from maf_starter.routing_policy import RoutingPlan, build_routing_plan
from maf_starter.routing_types import CapabilityChange, ChainStep, RouteAttempt

Expand Down Expand Up @@ -317,7 +318,11 @@ async def _execute_chain_step(
prior_error: Exception,
attempt_log: list[RouteAttempt] | None = None,
fallback_index: int = 0,
profile: ExecutionProfile = LOCAL_PROFILE,
):
# Guard: reject subprocess-backed providers when the profile disallows them.
profile.assert_provider_allowed(step.provider)
Comment on lines +321 to +324

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Pass the execution profile through fallback calls

The new guard always receives LOCAL_PROFILE in production because both internal _execute_chain_step call sites omit the profile argument and no runtime path passes CLOUD_SAFE_PROFILE. Therefore, when a cloud-safe MAF run reaches a CLI fallback, the guard permits it and the subprocess is still spawned; the active profile needs to be carried through the middleware and forwarded here.

Useful? React with 👍 / 👎.


if step.provider == "gemini":
client = OpenAIChatClient(
model_id=step.model or settings.model,
Expand Down
75 changes: 75 additions & 0 deletions maf_starter/worker_boundary.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
from __future__ import annotations

import asyncio
from enum import Enum
from typing import Any, Awaitable, Callable


class WorkerProfile(str, Enum):
LOCAL = "local"
CLOUD_SAFE = "cloud-safe"


_RunStatus = str # "pending" | "running" | "done" | "error:<message>"


class WorkerBoundary:
"""Dispatch long-running workflow executions asynchronously so HTTP ingress
returns a run_id immediately instead of waiting for the full execution path.

Status values returned by get_status:
"pending" — task queued, not yet started
"running" — task is actively executing
"done" — task completed successfully
"error:<msg>" — task raised an exception; <msg> is str(exc)
"""

def __init__(self, profile: WorkerProfile = WorkerProfile.LOCAL) -> None:
self.profile = profile
self._status: dict[str, _RunStatus] = {}
self._tasks: dict[str, asyncio.Task[Any]] = {}

def submit_async(
self,
run_id: str,
workflow: Callable[[], Awaitable[Any]],
) -> str:
"""Dispatch *workflow* as a background asyncio task and return *run_id*
immediately. The caller never waits for the workflow to finish.

Args:
run_id: Stable identifier for this run (caller's responsibility to
generate a unique value, e.g. via uuid.uuid4().hex).
workflow: Zero-argument async callable that encapsulates the full
long-running execution path for this run.

Returns:
run_id — the same value passed in, ready for the HTTP response.
"""
self._status[run_id] = "pending"
task = asyncio.create_task(self._run(run_id, workflow))
self._tasks[run_id] = task
Comment on lines +50 to +51

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Release completed tasks from the worker registry

Every submission stores its task in _tasks, but completed tasks are never removed and there is no cleanup method. In a long-lived HTTP worker, each run permanently grows this registry and retains a completed Task; add completion cleanup while retaining the separately tracked status.

Useful? React with 👍 / 👎.

return run_id

async def _run(self, run_id: str, workflow: Callable[[], Awaitable[Any]]) -> None:
self._status[run_id] = "running"
try:
await workflow()
self._status[run_id] = "done"
except Exception as exc: # noqa: BLE001
self._status[run_id] = f"error:{exc}"

def get_status(self, run_id: str) -> _RunStatus | None:
"""Return the current status string for *run_id*, or None if unknown."""
return self._status.get(run_id)

def is_done(self, run_id: str) -> bool:
"""Return True when the run has reached a terminal state (done or error)."""
status = self._status.get(run_id)
if status is None:
return False
return status == "done" or status.startswith("error:")

def all_run_ids(self) -> tuple[str, ...]:
"""Return all known run IDs in submission order."""
return tuple(self._status)
Loading
Loading