Skip to content

Commit cb5060b

Browse files
oh(post-f11): профили 5 пост-MVP ролей + стек #229-#232 (#233)
* oh(post-f11): монтирование oh-contour router в прод-API и main.py - create_app (Starlette): FastAPI sub-app через Mount (FastAPI APIRoute требует middleware context), env-флаг OH_CONTOUR_HTTP_ENABLED=0 - main.py (NiceGUI): app.include_router(oh_contour_router) - 2 новых теста: монтирование в create_app, выключение по env (14/14) - .env.example: OH_CONTOUR_*/OPENHANDS_API_KEY (без секретов) - docs/отчёт: монтирование отражено как выполненное - ruff --fix: I001 в app.py (включая pre-existing) и тестах Регресс 141/141, ruff чист Co-authored-by: openhands <openhands@all-hands.dev> * oh(post-f11): связка ветки с Cloud для реального E2E Найдено при подготовке E2E: - ветка не создавалась/не пушилась до стадий — Cloud клонирует по selected_branch и не находил её на remote - finalize diff'ил локальное дерево, а изменения пушат Cloud-агенты - Coder/Tester не получали инструкцию commit+push Изменения: - GitHubHelper: has_remote, prepare_branch (create+push), sync_branch (fetch+reset --hard remote/branch; workspace — выделенный клон) - runner.run: prepare_branch до стадий; _finalize: sync_branch до diff - profiles: Coder/Tester обязаны push'ить в ветку - 4 новых теста (реальный bare remote + «cloud-клон») Регресс 145/145, ruff чист Co-authored-by: openhands <openhands@all-hands.dev> * coordination: журнал post-F11 — мерж, монтирование, реальный E2E Co-authored-by: openhands <openhands@all-hands.dev> * oh(post-f11): async run + атомарный state - ContourStore: атомарная запись (tmp + os.replace) + threading.Lock на read-modify-write — частично записанный state невозможен - ContourService: run_task_async (фоновый поток), run-lock сериализует lifecycle'ы (одна workspace), is_running, running-флаг в status; дубликат запуска → RuntimeError - API: POST /run?background=true → {state: started}, 409 на дубликат, 404 на неизвестную задачу - 7 новых тестов (atomic tmp/конкурентные save, async started/drain/ duplicate/unknown, HTTP background 200/404) Регресс 152/152, ruff чист Co-authored-by: openhands <openhands@all-hands.dev> * oh(post-f11): профили 5 пост-MVP ролей + отметки отчёта - PROFILES + RBAC + build_prompt инструкции: devops (deploy/**, docker-compose в deny), android (android_companion/aios_core/android_*), ml (ml_*/model_*/models/analytics), research (reports|docs/research), documentation (docs/**) - Все роли с доменными allowed_paths (least privilege), секреты в deny - 10 новых тестов (5 render + 5 permissions, инвариант профиль↔инструкция) - Отчёт F11: E2E/монтирование/async/atomic/профили отмечены выполненными Регресс 162/162, ruff чист Co-authored-by: openhands <openhands@all-hands.dev> * coordination: журнал post-F11 — async/atomic, профили, итоги Co-authored-by: openhands <openhands@all-hands.dev> * build(deps): pytest-asyncio в requirements.txt и lock CI (aios-validation, coverage) ставит только requirements.txt и падает с ModuleNotFoundError: pytest_asyncio — тесты его импортируют, а пакет был только в pyproject [dev]. Контракт minimal ⊆ direct ⊆ lock сохранён (check_dependency_contract --strict: 0 ошибок). Co-authored-by: openhands <openhands@all-hands.dev> * coordination: журнал — добивка CI #233 ([ops], pytest-asyncio) Co-authored-by: openhands <openhands@all-hands.dev> * docs(oh): убран .env.example из диффа — переменные уже описаны в docs/OPENHANDS_INTEGRATION.md No-secrets gate блокирует любые правки .env*; комментарии перенесены в документацию контура (таблица переменных в OPENHANDS_INTEGRATION.md). Co-authored-by: openhands <openhands@all-hands.dev> * docs: регенерация PROJECT_INVENTORY.md после изменения requirements Co-authored-by: openhands <openhands@all-hands.dev> * coordination: журнал — финал добивки CI #233 Co-authored-by: openhands <openhands@all-hands.dev> --------- Co-authored-by: openhands <openhands@all-hands.dev>
1 parent 25155a6 commit cb5060b

22 files changed

Lines changed: 539 additions & 51 deletions

OPENHANDS_IMPLEMENTATION_REPORT.md

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
## Agent Architecture
4848

4949
Роли (`models.AgentRole`): MVP — orchestrator/architect/coder/tester/reviewer;
50-
подключены профилями security/qa; задекларированы devops/android/ml/research/documentation.
50+
подключены профилями security/qa и (post-F11) devops/android/ml/research/documentation.
5151
Роль = `AgentProfile` (RBAC-имя, read/write, allowed_paths) + self-contained промпт
5252
(`build_prompt`). Разговоры изолированы, контур склеивает их состоянием задачи.
5353

@@ -104,19 +104,20 @@ store (рестарт), api (auth/404/flow).
104104
проверен fake-клиентом по протоколу `ConversationClient`.
105105
2. HTTP `run` синхронный — длинные lifecycle блокируют запрос.
106106
3. State-файл без атомарной записи/lock — при первой реальной конкуренции.
107-
4. Роутер не смонтирован в `main.py` — решение владельца entrypoint'а.
107+
4. ~~Роутер не смонтирован~~ — смонтирован в `create_app` (sub-app `Mount`) и `main.py`
108+
(post-F11); выключается env `OH_CONTOUR_HTTP_ENABLED=0`.
108109

109110
## Remaining Work
110111

111-
- Реальный E2E: submit задачипрогон против Cloud → draft PR (нужен `OPENHANDS_API_KEY`).
112-
- Монтирование router в host-приложение (`app.include_router(oh_contour_router)`).
113-
- Асинхронный run (фоновая задача + polling status).
114-
- Профили и промпты остальных 5 ролей (devops/android/ml/research/documentation).
115-
- Атомарная запись state (tmp+rename) при конкуренции.
112+
- ~~Реальный E2E~~ (выполнен post-F11: задача `e9079198ebf8`4 Cloud-разговора → draft PR #231).
113+
- ~~Монтирование router в host-приложение~~ (выполнено post-F11).
114+
- ~~Асинхронный run (фоновая задача + polling status)~~ и ~~атомарная запись state~~
115+
(выполнено post-F11: run_task_async + run-lock; tmp+os.replace).
116+
- ~~Профили и промпты остальных 5 ролей~~ (выполнено post-F11: доменные allowed_paths + RBAC + инструкции).
116117

117118
## Recommended Next Steps
118119

119120
1. Мерж стека #218#227 (по порядку или squash).
120-
2. Монтирование router + production env (`OH_CONTOUR_TOKEN`, `OH_CONTOUR_REPO`, ключи).
121+
2. Production env на хосте (`OH_CONTOUR_TOKEN`, `OH_CONTOUR_REPO`, ключи).
121122
3. Первый реальный E2E на безопасной микрозадаче (docstring/типы) — проверка всей цепочки.
122123
4. По результатам E2E: async run, атомарный state, профили остальных ролей.

aios_core/api/app.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,13 +218,34 @@ def create_starlette_app(self) -> Starlette:
218218
if os.environ.get("AIOS_MONETIZATION_ENABLED", "1").strip().lower() not in {"0", "false", "no"}:
219219
try:
220220
import logging as _lg
221+
221222
from aios_core.api.monetization_routes import get_monetization_routes
222223
routes.extend(get_monetization_routes())
223224
_lg.getLogger("AIOS.API").info("💰 monetization routes registered: /api/v2/mon/*")
224225
except Exception as _mon_exc:
225226
import logging as _lg
226227
_lg.getLogger("AIOS.API").warning(f"⚠️ monetization routes not loaded: {_mon_exc}")
227228

229+
# OpenHands-контур: HTTP API поверх ContourService
230+
# (выключается env OH_CONTOUR_HTTP_ENABLED=0).
231+
# Router — FastAPI, поэтому оборачивается в sub-app: FastAPI APIRoute
232+
# требует fastapi middleware context, которого нет в plain Starlette.
233+
if os.environ.get("OH_CONTOUR_HTTP_ENABLED", "1").strip().lower() not in {"0", "false", "no"}:
234+
try:
235+
import logging as _lg
236+
237+
from fastapi import FastAPI
238+
from starlette.routing import Mount
239+
240+
from aios_core.openhands import oh_contour_router
241+
contour_app = FastAPI(title="AIOS OpenHands Contour")
242+
contour_app.include_router(oh_contour_router)
243+
routes.append(Mount("/", contour_app, name="oh-contour"))
244+
_lg.getLogger("AIOS.API").info("🤖 oh-contour routes registered: /api/v1/oh-contour/*")
245+
except Exception as _oh_exc:
246+
import logging as _lg
247+
_lg.getLogger("AIOS.API").warning(f"⚠️ oh-contour routes not loaded: {_oh_exc}")
248+
228249
async def _value_error_response(request: Request, exc: ValueError):
229250
# Например: неизвестный профиль платформы (?profile=...).
230251
return JSONResponse({"error": str(exc)}, status_code=400)

aios_core/openhands/api.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,9 +120,22 @@ def submit_task(req: SubmitRequest, x_octopus_token: str = Header(default="")):
120120

121121

122122
@router.post("/tasks/{task_id}/run")
123-
def run_task(task_id: str, x_octopus_token: str = Header(default="")):
124-
"""Синхронно выполнить MVP-lifecycle задачи. Возвращает RunResult."""
123+
def run_task(task_id: str, background: bool = False, x_octopus_token: str = Header(default="")):
124+
"""Выполнить MVP-lifecycle задачи.
125+
126+
``background=true`` — запуск в фоновом потоке (202-подобный ответ
127+
``{"ok", "task_id", "state": "started"}``); прогресс — через GET status.
128+
Иначе — синхронно, возвращает RunResult.
129+
"""
125130
_check_token(x_octopus_token)
131+
if background:
132+
try:
133+
state = get_service().run_task_async(task_id)
134+
except KeyError:
135+
raise HTTPException(status_code=404, detail="Task not found") from None
136+
except RuntimeError as exc:
137+
raise HTTPException(status_code=409, detail=str(exc)) from None
138+
return {"ok": True, "task_id": task_id, "state": state}
126139
try:
127140
result = get_service().run_task(task_id)
128141
except KeyError:

aios_core/openhands/github.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,38 @@ def push_branch(self, branch: str, remote: str = "origin") -> None:
100100
"""Push ветки с tracking."""
101101
self.git.run("push", "-u", remote, branch)
102102

103+
def has_remote(self, remote: str = "origin") -> bool:
104+
"""Есть ли настроенный remote (в тестах локальных репо его нет)."""
105+
return self.git.run("remote", "get-url", remote, check=False).returncode == 0
106+
107+
def prepare_branch(self, branch: str, base: str = "main", remote: str = "origin") -> str:
108+
"""Создать ветку от base и запушить (если remote настроен).
109+
110+
Cloud-разговоры клонируют репозиторий по ``selected_branch`` — ветка
111+
обязана существовать на remote до старта стадий.
112+
"""
113+
self.create_branch(branch, base)
114+
if self.has_remote(remote):
115+
self.push_branch(branch, remote)
116+
return branch
117+
118+
def sync_branch(self, branch: str, remote: str = "origin") -> None:
119+
"""Подтянуть состояние ветки с remote.
120+
121+
Cloud-агенты пушат изменения в ветку; локальное дерево перед diff
122+
обязано отражать remote (workspace — выделенный клон контура,
123+
reset --hard в нём безопасен).
124+
"""
125+
if not self.has_remote(remote):
126+
return
127+
self.git.run("fetch", remote, branch)
128+
exists = self.git.run("rev-parse", "--verify", branch, check=False)
129+
if exists.returncode == 0:
130+
self.git.run("checkout", branch)
131+
else:
132+
self.git.run("checkout", "-b", branch, f"{remote}/{branch}")
133+
self.git.run("reset", "--hard", f"{remote}/{branch}")
134+
103135
# ── GitHub API ────────────────────────────────────────────────
104136

105137
def create_pull_request(

aios_core/openhands/permissions.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,69 @@ def _perms(**kw) -> AgentPermissions:
7878
role=AgentRole.QA,
7979
permissions=_perms(read="all", write="reports", allowed_paths=("reports/qa/**",)),
8080
),
81+
# Пост-MVP роли (подключены post-F11): узкие allowed_paths под домен.
82+
AgentRole.DEVOPS: AgentProfile(
83+
role=AgentRole.DEVOPS,
84+
permissions=_perms(
85+
read="project",
86+
write="deploy",
87+
allowed_paths=("deploy/**", "deployment/**", "scripts/deploy*.py", "reports/devops/**"),
88+
# docker-compose*.yml — protected по AGENTS.md, devops их не трогает.
89+
deny_paths=(".env*", "data/.llm_keys.json", "**/*secret*", "**/*token*", "docker-compose*.yml"),
90+
secret_allowlist=(),
91+
),
92+
),
93+
AgentRole.ANDROID: AgentProfile(
94+
role=AgentRole.ANDROID,
95+
permissions=_perms(
96+
read="project",
97+
write="workspace",
98+
allowed_paths=(
99+
"android_companion/**",
100+
"aios_core/android_*.py",
101+
"tests/test_android*.py",
102+
"reports/android/**",
103+
),
104+
deny_paths=(".env*", "data/.llm_keys.json", "**/*secret*", "**/*token*"),
105+
secret_allowlist=(),
106+
),
107+
),
108+
AgentRole.ML: AgentProfile(
109+
role=AgentRole.ML,
110+
permissions=_perms(
111+
read="project",
112+
write="workspace",
113+
allowed_paths=(
114+
"aios_core/ml_*.py",
115+
"aios_core/model_*.py",
116+
"models/**",
117+
"analytics/**",
118+
"tests/test_ml*.py",
119+
"reports/ml/**",
120+
),
121+
deny_paths=(".env*", "data/.llm_keys.json", "**/*secret*", "**/*token*"),
122+
secret_allowlist=(),
123+
),
124+
),
125+
AgentRole.RESEARCH: AgentProfile(
126+
role=AgentRole.RESEARCH,
127+
permissions=_perms(
128+
read="all",
129+
write="reports",
130+
allowed_paths=("reports/research/**", "docs/research/**"),
131+
secret_allowlist=(),
132+
),
133+
),
134+
AgentRole.DOCUMENTATION: AgentProfile(
135+
role=AgentRole.DOCUMENTATION,
136+
permissions=_perms(
137+
read="project",
138+
write="docs",
139+
allowed_paths=("docs/**", "README*.md", "*.md"),
140+
deny_paths=(".env*", "data/.llm_keys.json", "**/*secret*", "**/*token*"),
141+
secret_allowlist=(),
142+
),
143+
),
81144
}
82145

83146
# RBAC-пермишены на роль (resource:action, wildcards по aios_core/rbac.py).
@@ -89,6 +152,11 @@ def _perms(**kw) -> AgentPermissions:
89152
AgentRole.REVIEWER: ("repo:read", "reports:write"),
90153
AgentRole.SECURITY: ("repo:read", "reports:write"),
91154
AgentRole.QA: ("repo:read", "reports:write"),
155+
AgentRole.DEVOPS: ("repo:read", "deploy:write"),
156+
AgentRole.ANDROID: ("repo:read", "android:write"),
157+
AgentRole.ML: ("repo:read", "ml:write"),
158+
AgentRole.RESEARCH: ("repo:read", "reports:write"),
159+
AgentRole.DOCUMENTATION: ("repo:read", "docs:write"),
92160
}
93161

94162

aios_core/openhands/profiles.py

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,14 @@
2424
),
2525
AgentRole.CODER: (
2626
"Ты — Coder. Выполни изменение строго по задаче и design-документу. "
27-
"Минимальная область правки; новая функциональность — с тестами."
27+
"Минимальная область правки; новая функциональность — с тестами. "
28+
"По завершении ОБЯЗАТЕЛЬНО закоммить изменения и запушь их в текущую "
29+
"ветку (git push) — без push изменения будут потеряны."
2830
),
2931
AgentRole.TESTER: (
3032
"Ты — Tester. Напиши/обнови тесты под изменение и прогони их. Product-код "
31-
"не изменяй. Отчёт: passed/failed/skipped/warnings и оставшиеся риски."
33+
"не изменяй. Отчёт: passed/failed/skipped/warnings и оставшиеся риски. "
34+
"Изменённые тесты закоммить и запушь в текущую ветку (git push)."
3235
),
3336
AgentRole.REVIEWER: (
3437
"Ты — независимый Reviewer (не Coder). Проверь diff: соответствие задаче, "
@@ -45,6 +48,32 @@
4548
"Ты — QA. Функционально проверь изменение: happy path, edge cases, "
4649
"regression. Отчёт в reports/qa/."
4750
),
51+
AgentRole.DEVOPS: (
52+
"Ты — DevOps. Работай только с deploy/deployment-инфраструктурой: "
53+
"systemd-манифесты, скрипты деплоя, health checks, логи запуска/останова, "
54+
"rollback. docker-compose файлы и секреты не трогай (protected). "
55+
"Изменения закоммить и запушь в текущую ветку (git push)."
56+
),
57+
AgentRole.ANDROID: (
58+
"Ты — Android-агент. Работай с android_companion/ и aios_core/android_*.py: "
59+
"RPA, Appium/ADB-автоматизация, навигация. Product-код вне android-домена "
60+
"не изменяй. Изменения закоммить и запушь в текущую ветку (git push)."
61+
),
62+
AgentRole.ML: (
63+
"Ты — ML-агент. Работай с aios_core/ml_*.py, aios_core/model_*.py, models/, "
64+
"analytics/: обучение, скоринг, реестр моделей. Метрики и выводы — в "
65+
"reports/ml/. Изменения закоммить и запушь в текущую ветку (git push)."
66+
),
67+
AgentRole.RESEARCH: (
68+
"Ты — Research-агент. Исследуй вопрос по коду и документации, код не "
69+
"изменяй. Результат — отчёт в reports/research/ или docs/research/ "
70+
"с выводами и источниками."
71+
),
72+
AgentRole.DOCUMENTATION: (
73+
"Ты — Documentation-агент. Обновляй документацию строго под реальный код: "
74+
"docs/ и README. Не описывай функциональность, которой нет. Изменения "
75+
"закоммить и запушь в текущую ветку (git push)."
76+
),
4877
}
4978

5079

aios_core/openhands/runner.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,9 @@ def run(self, task_id: str, title: str, description: str, extras: TaskExtras | N
107107
"""Выполнить полный MVP-lifecycle задачи (с retry по state machine)."""
108108
extras = extras or TaskExtras(task_id=task_id)
109109
branch = extras.branch or f"agent/oh-{task_id}"
110+
if self._github is not None:
111+
# Cloud клонирует репозиторий по selected_branch — ветка нужна на remote.
112+
self._github.prepare_branch(branch, self._base)
110113
status: str = TaskStatus.PENDING
111114
last_error: str | None = None
112115

@@ -260,6 +263,8 @@ def _finalize(self, task_id: str, title: str, description: str, extras: TaskExtr
260263
if self._github is None:
261264
self._audit.log("finalize_skipped", task_id, AgentRole.ORCHESTRATOR, reason="no github helper")
262265
return
266+
# Изменения пушат Cloud-агенты — локальное дерево синхронизируем с remote.
267+
self._github.sync_branch(branch)
263268
changed = self._github.changed_files(self._base)
264269
allowed, denied = check_paths(AgentRole.CODER, changed)
265270
self._audit.log(

0 commit comments

Comments
 (0)