Skip to content

Commit ecb478f

Browse files
OgeonX-Airoot
andauthored
Add bounded MAF fan-out and isolated worker execution (#3)
* test(workers): cover bounded fanout and sandbox policy * feat(workers): add isolated MAF task execution * feat(workers): expose bounded MAF process adapter * fix(ci): pin compatible agent framework orchestration --------- Co-authored-by: root <root@Kimi.localdomain>
1 parent f537373 commit ecb478f

8 files changed

Lines changed: 543 additions & 0 deletions

maf_starter/approval_policy.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,35 @@ def is_execution_approved(prompt: str, approval_word: str) -> bool:
142142
return normalized == approval_token or normalized.startswith("approve") or normalized.startswith("approved") or normalized.startswith("proceed")
143143

144144

145+
def classify_external_action(action: str, target: str) -> ExecutionRiskDecision:
146+
normalized = str(action or "").strip().lower()
147+
external = {"push", "deploy", "message", "production_mutation"}
148+
destructive = {"delete"}
149+
if normalized in external:
150+
return _decision(
151+
"externally_visible",
152+
action_kind=normalized,
153+
reason=f"{normalized} requires deterministic approval before execution.",
154+
external_targets=[target],
155+
)
156+
if normalized in destructive:
157+
return _decision(
158+
"destructive",
159+
action_kind=normalized,
160+
reason=f"{normalized} requires deterministic approval before execution.",
161+
external_targets=[target],
162+
)
163+
return _decision("blocked", action_kind=normalized or "unknown", reason="Unknown external action is blocked.")
164+
165+
166+
def require_action_approval(action: str, target: str, *, approved: bool) -> None:
167+
decision = classify_external_action(action, target)
168+
if decision.blocked:
169+
raise PermissionError(decision.scope.reason)
170+
if decision.approval_required and not approved:
171+
raise PermissionError(decision.scope.reason)
172+
173+
145174
def _decision(
146175
classification: RiskLevel,
147176
*,

maf_starter/loop_sandbox.py

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
from __future__ import annotations
2+
3+
import re
4+
import subprocess
5+
from dataclasses import dataclass
6+
from datetime import UTC, datetime
7+
from pathlib import Path, PurePosixPath
8+
9+
10+
_SAFE_ID = re.compile(r"^[A-Za-z0-9_-]+$")
11+
_BLOCKED_PARTS = {".git", ".env", "secrets", "credentials", "state"}
12+
13+
14+
@dataclass(frozen=True)
15+
class MutationContract:
16+
goal_id: str
17+
work_item_id: str
18+
repo_root: Path
19+
worktree_path: Path
20+
base_ref: str
21+
deadline: datetime
22+
path_allowlist: tuple[str, ...]
23+
idempotency_key: str
24+
implementation_owner: str
25+
26+
def validate(self, *, now: datetime | None = None) -> None:
27+
current = now or datetime.now(UTC)
28+
if not _SAFE_ID.fullmatch(self.goal_id) or not _SAFE_ID.fullmatch(self.work_item_id):
29+
raise ValueError("Goal and work-item IDs must be path-safe")
30+
if not self.idempotency_key.strip() or not self.implementation_owner.strip():
31+
raise ValueError("Idempotency key and implementation owner are required")
32+
if not self.path_allowlist:
33+
raise ValueError("At least one allowed path pattern is required")
34+
if self.deadline.tzinfo is None or self.deadline <= current:
35+
raise TimeoutError("Mutation work item deadline has expired")
36+
repo = self.repo_root.resolve()
37+
worktree = self.worktree_path.resolve()
38+
if repo == worktree or repo in worktree.parents:
39+
raise ValueError("Mutation worktree must be distinct from and outside the source repository")
40+
41+
42+
@dataclass(frozen=True)
43+
class SandboxManifest:
44+
goal_id: str
45+
work_item_id: str
46+
worktree_path: str
47+
branch: str
48+
base_sha: str
49+
idempotency_key: str
50+
implementation_owner: str
51+
52+
53+
@dataclass(frozen=True)
54+
class ArtifactManifest:
55+
goal_id: str
56+
work_item_id: str
57+
idempotency_key: str
58+
changed_files: tuple[str, ...]
59+
worktree_path: str
60+
61+
62+
class GitWorktreeSandbox:
63+
def __init__(self, contract: MutationContract) -> None:
64+
self.contract = contract
65+
self._manifest: SandboxManifest | None = None
66+
67+
def create(self) -> SandboxManifest:
68+
self.contract.validate()
69+
if self.contract.worktree_path.exists():
70+
raise FileExistsError(f"Worktree already exists: {self.contract.worktree_path}")
71+
self.contract.worktree_path.parent.mkdir(parents=True, exist_ok=True)
72+
base_sha = self._git_source("rev-parse", self.contract.base_ref).stdout.strip()
73+
branch = f"codex/{self.contract.goal_id}-{self.contract.work_item_id}"
74+
self._git_source("worktree", "add", "-b", branch, str(self.contract.worktree_path), base_sha)
75+
self._manifest = SandboxManifest(
76+
self.contract.goal_id,
77+
self.contract.work_item_id,
78+
str(self.contract.worktree_path.resolve()),
79+
branch,
80+
base_sha,
81+
self.contract.idempotency_key,
82+
self.contract.implementation_owner,
83+
)
84+
return self._manifest
85+
86+
def authorize_mutation(self, actor: str) -> None:
87+
self.contract.validate()
88+
if actor != self.contract.implementation_owner:
89+
raise PermissionError(f"Mutation owner is {self.contract.implementation_owner}, not {actor}")
90+
91+
def resolve_write_path(self, relative_path: str) -> Path:
92+
self.contract.validate()
93+
if self._manifest is None:
94+
raise RuntimeError("Sandbox has not been created")
95+
normalized = relative_path.replace("\\", "/").lstrip("./")
96+
pure = PurePosixPath(normalized)
97+
if not normalized or pure.is_absolute() or ".." in pure.parts or any(part.lower() in _BLOCKED_PARTS for part in pure.parts):
98+
raise ValueError(f"Write path is blocked: {relative_path}")
99+
if not any(pure.match(pattern) for pattern in self.contract.path_allowlist):
100+
raise ValueError(f"Write path is outside the allowlist: {relative_path}")
101+
target = (self.contract.worktree_path / Path(*pure.parts)).resolve()
102+
if self.contract.worktree_path.resolve() not in target.parents:
103+
raise ValueError(f"Write path escapes the worktree: {relative_path}")
104+
return target
105+
106+
def capture_artifacts(self) -> ArtifactManifest:
107+
if self._manifest is None:
108+
raise RuntimeError("Sandbox has not been created")
109+
result = subprocess.run(
110+
["git", "-C", str(self.contract.worktree_path), "status", "--porcelain", "--untracked-files=all"],
111+
check=True,
112+
capture_output=True,
113+
text=True,
114+
)
115+
changed = tuple(sorted(line[3:].strip() for line in result.stdout.splitlines() if len(line) >= 4))
116+
return ArtifactManifest(
117+
self.contract.goal_id,
118+
self.contract.work_item_id,
119+
self.contract.idempotency_key,
120+
changed,
121+
str(self.contract.worktree_path.resolve()),
122+
)
123+
124+
def _git_source(self, *args: str) -> subprocess.CompletedProcess[str]:
125+
return subprocess.run(["git", "-C", str(self.contract.repo_root), *args], check=True, capture_output=True, text=True)

maf_starter/loop_worker_cli.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
from __future__ import annotations
2+
3+
import argparse
4+
import asyncio
5+
import json
6+
import sys
7+
from dataclasses import asdict, dataclass
8+
from typing import Any
9+
10+
from maf_starter.loop_workers import (
11+
REQUIRED_SPECIALIST_ROLES,
12+
SpecialistResult,
13+
SpecialistSpec,
14+
run_bounded_specialists,
15+
)
16+
17+
18+
@dataclass(frozen=True)
19+
class WorkerEnvelope:
20+
succeeded: bool
21+
peakConcurrency: int
22+
roles: tuple[str, ...]
23+
evidenceUris: tuple[str, ...]
24+
summary: str
25+
26+
27+
async def execute_request(payload: dict[str, Any]) -> WorkerEnvelope:
28+
required = ("goalId", "workItemId", "attempt", "isRepair", "correlationId")
29+
missing = [name for name in required if name not in payload]
30+
if missing:
31+
raise ValueError(f"Missing worker request fields: {', '.join(missing)}")
32+
33+
active = 0
34+
peak = 0
35+
lock = asyncio.Lock()
36+
37+
async def execute(spec: SpecialistSpec) -> SpecialistResult:
38+
nonlocal active, peak
39+
async with lock:
40+
active += 1
41+
peak = max(peak, active)
42+
try:
43+
await asyncio.sleep(0.01)
44+
uri = f"cas://evidence/worker/{payload['goalId']}/{payload['attempt']}/{spec.role}"
45+
return SpecialistResult(spec.role, "succeeded", f"{spec.role} analysis complete", (uri,))
46+
finally:
47+
async with lock:
48+
active -= 1
49+
50+
aggregate = await run_bounded_specialists(
51+
[SpecialistSpec(role) for role in REQUIRED_SPECIALIST_ROLES],
52+
execute,
53+
max_fan_out=3,
54+
timeout_seconds=30,
55+
)
56+
evidence = tuple(uri for result in aggregate.results for uri in result.artifacts)
57+
return WorkerEnvelope(
58+
aggregate.complete,
59+
peak,
60+
tuple(result.role for result in aggregate.results),
61+
evidence,
62+
"repair fan-out completed" if payload["isRepair"] else "feature fan-out completed",
63+
)
64+
65+
66+
def main() -> int:
67+
parser = argparse.ArgumentParser()
68+
parser.add_argument("--request", help="JSON worker request; stdin is used when omitted")
69+
args = parser.parse_args()
70+
try:
71+
payload = json.loads(args.request if args.request is not None else sys.stdin.read())
72+
result = asyncio.run(execute_request(payload))
73+
except (ValueError, json.JSONDecodeError) as error:
74+
print(json.dumps({"error": str(error)}), file=sys.stderr)
75+
return 2
76+
print(json.dumps(asdict(result), separators=(",", ":")))
77+
return 0
78+
79+
80+
if __name__ == "__main__":
81+
raise SystemExit(main())

maf_starter/loop_workers.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
from __future__ import annotations
2+
3+
import asyncio
4+
from collections.abc import Awaitable, Callable, Sequence
5+
from dataclasses import dataclass
6+
from typing import Any, Literal
7+
8+
9+
REQUIRED_SPECIALIST_ROLES = ("research", "architecture", "security", "test")
10+
TerminalStatus = Literal["succeeded", "failed", "cancelled", "timed_out"]
11+
12+
13+
@dataclass(frozen=True)
14+
class SpecialistSpec:
15+
role: str
16+
read_only: bool = True
17+
capabilities: tuple[str, ...] = ("read_repo", "search_repo")
18+
19+
def __post_init__(self) -> None:
20+
if self.role not in REQUIRED_SPECIALIST_ROLES:
21+
raise ValueError(f"Unsupported specialist role: {self.role}")
22+
if not self.read_only or any(capability.startswith(("write", "delete", "execute")) for capability in self.capabilities):
23+
raise ValueError("Specialists must remain read-only")
24+
25+
26+
@dataclass(frozen=True)
27+
class SpecialistResult:
28+
role: str
29+
status: str
30+
summary: str
31+
artifacts: tuple[str, ...]
32+
33+
34+
@dataclass(frozen=True)
35+
class SpecialistAggregate:
36+
results: tuple[SpecialistResult, ...]
37+
38+
@property
39+
def complete(self) -> bool:
40+
return all(result.status == "succeeded" for result in self.results)
41+
42+
43+
def aggregate_specialist_results(results: Sequence[SpecialistResult]) -> SpecialistAggregate:
44+
by_role: dict[str, SpecialistResult] = {}
45+
for result in results:
46+
if result.role in by_role:
47+
raise ValueError(f"Duplicate specialist result: {result.role}")
48+
if result.status not in {"succeeded", "failed", "cancelled", "timed_out"}:
49+
raise ValueError(f"Specialist result is not terminal: {result.role}")
50+
by_role[result.role] = result
51+
missing = set(REQUIRED_SPECIALIST_ROLES) - set(by_role)
52+
extra = set(by_role) - set(REQUIRED_SPECIALIST_ROLES)
53+
if missing or extra:
54+
raise ValueError(f"Specialist fan-in mismatch; missing={sorted(missing)}, extra={sorted(extra)}")
55+
return SpecialistAggregate(tuple(by_role[role] for role in REQUIRED_SPECIALIST_ROLES))
56+
57+
58+
async def run_bounded_specialists(
59+
specs: Sequence[SpecialistSpec],
60+
execute: Callable[[SpecialistSpec], Awaitable[SpecialistResult]],
61+
*,
62+
max_fan_out: int = 3,
63+
timeout_seconds: float = 1800,
64+
) -> SpecialistAggregate:
65+
if max_fan_out < 1:
66+
raise ValueError("max_fan_out must be positive")
67+
if timeout_seconds <= 0:
68+
raise ValueError("timeout_seconds must be positive")
69+
roles = tuple(spec.role for spec in specs)
70+
if len(set(roles)) != len(roles) or set(roles) != set(REQUIRED_SPECIALIST_ROLES):
71+
raise ValueError("Exactly one specialist for every required role is required")
72+
73+
semaphore = asyncio.Semaphore(max_fan_out)
74+
75+
async def invoke(spec: SpecialistSpec) -> SpecialistResult:
76+
async with semaphore:
77+
result = await execute(spec)
78+
if result.role != spec.role:
79+
raise ValueError(f"Specialist {spec.role} returned result for {result.role}")
80+
return result
81+
82+
try:
83+
async with asyncio.timeout(timeout_seconds):
84+
results = await asyncio.gather(*(invoke(spec) for spec in specs))
85+
except TimeoutError:
86+
raise TimeoutError("Specialist fan-out exceeded its task-attempt deadline") from None
87+
return aggregate_specialist_results(results)
88+
89+
90+
def build_maf_fanout_workflow(
91+
dispatcher: Any,
92+
specialists: Sequence[Any],
93+
aggregator: Any,
94+
*,
95+
builder: Any | None = None,
96+
max_iterations: int = 3,
97+
) -> Any:
98+
if len(specialists) != len(REQUIRED_SPECIALIST_ROLES):
99+
raise ValueError("MAF workflow requires all four specialist executors")
100+
if builder is None:
101+
from agent_framework import WorkflowBuilder
102+
103+
builder = WorkflowBuilder(
104+
start_executor=dispatcher,
105+
max_iterations=max_iterations,
106+
name="cas_task_worker",
107+
description="Bounded read-only specialist fan-out with typed terminal fan-in",
108+
)
109+
return builder.add_fan_out_edges(dispatcher, specialists).add_fan_in_edges(specialists, aggregator).build()

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
agent-framework==1.0.0rc5
22
agent-framework-devui==1.0.0b260319
3+
agent-framework-orchestrations==1.0.0b260319
34
autogen-agentchat>=0.7.5,<0.8.0
45
autogen-core>=0.7.5,<0.8.0
56
autogen-ext[anthropic,ollama,openai]>=0.7.5,<0.8.0

0 commit comments

Comments
 (0)