diff --git a/.env.example b/.env.example index 301a8c3..e410814 100644 --- a/.env.example +++ b/.env.example @@ -2,6 +2,8 @@ EGO_ENV=development EGO_API_HOST=0.0.0.0 EGO_API_PORT=8000 +EGO_WEB_PORT=4173 +EGO_BASE_IMAGE_PULL_POLICY=missing EGO_DB_PATH=./artifacts/runtime/egoagentos.sqlite3 # PostgreSQL takes precedence when set. Compose uses this owner only for one-shot # migration/security jobs; long-lived services receive the separate runtime logins below. @@ -58,10 +60,11 @@ AGENTTEAMS_MODEL= # injected at runtime only and must never be committed or copied into an evidence bundle. EGO_AGENT_MODEL_BASE_URL= EGO_AGENT_MODEL_API_KEY= -EGO_AGENT_MODEL=agnes-2.5-flash -AGENTTEAMS_CONTROLLER_URL=http://127.0.0.1:18080 +EGO_AGENT_MODEL=agnes-2.5-pro +AGENTTEAMS_CONTROLLER_URL=http://agentteams-controller:8090 +AGENTTEAMS_CONTROLLER_PORT=18090 AGENTTEAMS_AUTH_TOKEN= -AGENTTEAMS_MATRIX_URL=http://127.0.0.1:18080 +AGENTTEAMS_MATRIX_URL=http://agentteams-controller:6167 AGENTTEAMS_MATRIX_ACCESS_TOKEN= # Explicit native PostgreSQL/PolarDB-PG bridge URLs. Keep the long-lived bridge in # verify mode with only its runtime URL; use the owner URL plus apply mode in a separate @@ -72,6 +75,7 @@ EGO_AGENTTEAMS_MIGRATION_DATABASE_URL= EGO_AGENTTEAMS_MIGRATION_MODE=verify EGO_AGENTTEAMS_BRIDGE_DB=./artifacts/runtime/agentteams-bridge.sqlite3 EGO_AGENTTEAMS_HTTP_TIMEOUT=15 +EGO_AGENTTEAMS_BRIDGE_PORT=8010 # Reserved live benchmark opt-in. Until the per-scenario fault/replay harness exists, # both unset and set states return lowercase skip; the set state reports UNIMPLEMENTED. AGENTTEAMS_BENCHMARK_LIVE= diff --git a/.gitignore b/.gitignore index bf35eda..ac26678 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,7 @@ artifacts/runtime/ data/agent-memory/ submission/dist/ .tmp/ +.runtime/ # Local/stale setup output is not a submission input. ps.txt diff --git a/README.md b/README.md index 9ec6cc7..5751d57 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ digest,并在任何实验执行之前给出 `PASS/WARN/FAIL`。 ```bash export EGO_AGENT_MODEL_BASE_URL=https://apihub.agnes-ai.com/v1 -export EGO_AGENT_MODEL=agnes-2.5-flash +export EGO_AGENT_MODEL=agnes-2.5-pro read -s EGO_AGENT_MODEL_API_KEY export EGO_AGENT_MODEL_API_KEY export EGO_OPERATOR_KEY="$(openssl rand -hex 32)" @@ -70,6 +70,37 @@ VITE_API_ROOT=http://127.0.0.1:8000/api/v1 npm --prefix apps/web run dev -- --po 真实专家,必须另行部署 HTTPS FastAPI 服务并在构建时把 `VITE_API_ROOT` 指向它,不能把供应商 API key 塞进 GitHub Pages。 +### 官方 AgentTeams 本地实机路径(GPU 延后) + +下面的一键部署固定使用官方 AgentTeams `v1.2.3`,创建 1 个 Manager、4 个 Worker、Team、 +L2 Human/Matrix 身份、GPU 门控 Project/Workflow,并启动 PostgreSQL、EgoAgentOS API、Web 与 +AgentTeams Bridge。密钥通过隐藏提示输入,之后只保存在 gitignored 的 `.runtime/live-stack.env` +(0600);命令和公开验收清单都不打印原值。 + +```bash +python3 scripts/deploy_local_live_stack.py all +``` + +本机入口: + +- Web: +- EgoAgentOS API / OpenAPI: / +- AgentTeams Bridge: +- Controller(只绑定本机): +- Matrix / Element: / + +Team 为 `ego-researchops`,Project 为 `egoagentos-gpu-gated-v1`。在 GPU 主机接入前,Workflow +必须保持 `paused`,原因是 `GPU Worker intentionally not attached yet`。官方 Team 状态把 Leader +单独计数,因此显示 `leaderReady=true + readyWorkers=3/3`;Controller 中实际存在并运行的是 +4 个 Worker 资源。可公开的地址、状态、房间 ID、digest 与验收结果写入 +`.runtime/live-stack-public.json`,Token、Matrix 初始密码和两个互不复用的操作密钥只在私有 env 中。 + +2026-09-02 的真实验收区分两条链:官方 Matrix Team room 收到 4/4 个不同 Agent 的实时回复, +基础设施为 `LIVE_LOCAL PASS`;同日自定义输入专家链真实调用 `agnes-2.5-pro`,PI 与 Scout 完成并 +写入 compact receipt,但 Architect 连续两次返回非合同 JSON,因而 `FAILED_CLOSED`,没有生成 +矩阵或执行任务。完整、无密钥的证据边界见 +[`docs/acceptance/live-local-2026-09-02.md`](docs/acceptance/live-local-2026-09-02.md)。 + 本地评委复现的目标路径只需要 Docker: ```bash @@ -90,10 +121,8 @@ docker compose up --build 默认场景明确标记为 **SYNTHETIC DEMO DATA**。它真实运行控制面、本地 PostgreSQL 16 状态、审批、哈希、评测、证据门禁与审计,但不会声称已经使用 8×RTX 4090 训练,也不会伪造 TDSQL Nexa、TencentDB Agent Memory、AgentTeams、Nacos 或 Higress 在线状态。生产数据权威通过 `EGO_NEXA_DATABASE_URL` 指向 Nexa;直接启动 API 且不设置外部数据库时,使用 SQLite 开发 fallback。 -截至 2026-08-29,本机已通过 `docker compose config` 和真实 PostgreSQL 16.14 -数据层的 32/32 集成测试;API/Web 镜像构建在拉取 Docker Hub metadata 时网络超时, -因此本仓库不把这次 `docker compose up --build` 记作已验证镜像构建。评委网络可用时可 -直接走上述一键路径;原生启动路径不依赖镜像拉取: +截至 2026-09-02,本机已经完成 Compose 镜像构建、PostgreSQL 数据迁移与权限初始化、API/Web/ +Bridge 启动和端到端健康检查。原生启动路径仍可用于不希望运行完整 AgentTeams 的开发场景: 若不使用 Docker: diff --git a/apps/agentteams_bridge/Dockerfile b/apps/agentteams_bridge/Dockerfile index 22bd4dc..f191b28 100644 --- a/apps/agentteams_bridge/Dockerfile +++ b/apps/agentteams_bridge/Dockerfile @@ -1,15 +1,17 @@ -FROM python:3.12-slim +FROM python:3.11-slim WORKDIR /app -COPY pyproject.toml requirements-api.lock ./ -COPY apps ./apps -COPY benchmarks ./benchmarks +COPY requirements-api.lock ./ RUN pip install --no-cache-dir -r requirements-api.lock \ && addgroup --system egoagentos \ && adduser --system --ingroup egoagentos --home /nonexistent --no-create-home egoagentos \ && mkdir -p /data \ && chown -R egoagentos:egoagentos /data +COPY pyproject.toml ./ +COPY apps ./apps +COPY benchmarks ./benchmarks + ENV EGO_AGENTTEAMS_BRIDGE_DB=/data/bridge.sqlite3 VOLUME ["/data"] USER egoagentos diff --git a/apps/api/Dockerfile b/apps/api/Dockerfile index 9389c9c..d9ca34d 100644 --- a/apps/api/Dockerfile +++ b/apps/api/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.9-slim AS runtime +FROM python:3.11-slim AS runtime ENV PYTHONDONTWRITEBYTECODE=1 \ PYTHONUNBUFFERED=1 \ @@ -7,19 +7,20 @@ ENV PYTHONDONTWRITEBYTECODE=1 \ WORKDIR /app COPY requirements-api.lock ./ +RUN pip install --no-cache-dir --require-hashes -r requirements-api.lock \ + && addgroup --system egoagentos \ + && adduser --system --ingroup egoagentos --home /nonexistent --no-create-home egoagentos \ + && mkdir -p /data \ + && chown -R egoagentos:egoagentos /data + COPY apps/api ./apps/api COPY benchmarks ./benchmarks COPY contracts ./contracts +COPY integrations ./integrations COPY protocols ./protocols COPY skill_runtime ./skill_runtime COPY skills ./skills -RUN pip install --no-cache-dir --require-hashes -r requirements-api.lock \ - && addgroup --system egoagentos \ - && adduser --system --ingroup egoagentos --home /nonexistent --no-create-home egoagentos \ - && mkdir -p /data \ - && chown -R egoagentos:egoagentos /data - USER egoagentos EXPOSE 8000 diff --git a/apps/api/README.md b/apps/api/README.md index e746a4e..1ae8189 100644 --- a/apps/api/README.md +++ b/apps/api/README.md @@ -84,16 +84,18 @@ Configure the model plane only on the API process: ```bash export EGO_AGENT_MODEL_BASE_URL=https://apihub.agnes-ai.com/v1 -export EGO_AGENT_MODEL=agnes-2.5-flash +export EGO_AGENT_MODEL=agnes-2.5-pro read -s EGO_AGENT_MODEL_API_KEY export EGO_AGENT_MODEL_API_KEY ``` The returned truth boundary is intentionally narrower than an AgentTeams or experiment claim: -the four provider responses are `LIVE`; deterministic compilation and private focus memory are -`LIVE_LOCAL`; official AgentTeams/Matrix, repository or literature retrieval, and physical GPU -execution remain `NOT_RUN` until their own receipts exist. A reviewer `PASS` or `WARN` produces -`PLAN_READY_FOR_HUMAN_REVIEW`, never an automatic experiment dispatch. +provider responses are `LIVE`; deterministic compilation and private focus memory are +`LIVE_LOCAL`; repository/literature retrieval and physical GPU execution remain `NOT_RUN` until +their own receipts exist. AgentTeams/Matrix truth is evaluated separately: the 2026-09-02 local +stack has infrastructure-level `LIVE_LOCAL` receipts, but no completed Project workflow or GPU +Decision. A reviewer `PASS` or `WARN` produces `PLAN_READY_FOR_HUMAN_REVIEW`, never an automatic +experiment dispatch. Send the operator credential on every mutation, for example: diff --git a/apps/api/expert_runs.py b/apps/api/expert_runs.py index 4fce0e2..dfce817 100644 --- a/apps/api/expert_runs.py +++ b/apps/api/expert_runs.py @@ -37,6 +37,12 @@ ZERO_HASH = "0" * 64 ROLE_ORDER = ("research-pi", "scout", "experiment-architect", "reviewer") +ROLE_MAX_TOKENS = { + "research-pi": 2400, + "scout": 2400, + "experiment-architect": 4096, + "reviewer": 4096, +} def _now() -> str: @@ -239,7 +245,7 @@ def from_environment( ) -> "ExpertRunService": base_url = os.getenv("EGO_AGENT_MODEL_BASE_URL", "").strip() api_key = os.getenv("EGO_AGENT_MODEL_API_KEY", "") - model = os.getenv("EGO_AGENT_MODEL", "agnes-2.5-flash").strip() + model = os.getenv("EGO_AGENT_MODEL", "agnes-2.5-pro").strip() gateway = None if base_url and api_key: gateway = OpenAICompatibleModelGateway(base_url, api_key, model) @@ -424,7 +430,7 @@ def _call_role( role=role, system_prompt=prompt, input_payload=payload, - max_tokens=1800, + max_tokens=ROLE_MAX_TOKENS[role], ) _validate_role_output( role, diff --git a/apps/api/service.py b/apps/api/service.py index 3143c76..68c5078 100644 --- a/apps/api/service.py +++ b/apps/api/service.py @@ -1,7 +1,10 @@ """Application service implementing the deterministic ResearchOps workflow.""" import hashlib +import json import os +import urllib.error +import urllib.request import uuid from datetime import datetime, timezone from pathlib import Path @@ -66,6 +69,7 @@ GPU_ROLLBACK_POINT = ( "Restore the baseline-ltx configuration pointer and preserve all failed-run evidence." ) +DIRECT_HTTP = urllib.request.build_opener(urllib.request.ProxyHandler({})) AGENT_ROLES = [ ("research-pi", "Research PI", "Decomposes goals and owns state transitions"), @@ -1511,27 +1515,67 @@ def integrations() -> Dict[str, Any]: ] states = [] for integration_id, name, role, environment_key in definitions: - configured = bool(os.getenv(environment_key)) + endpoint = os.getenv(environment_key, "").strip() + configured = bool(endpoint) + status = ( + IntegrationTruth.CONFIGURED_UNVERIFIED + if configured + else IntegrationTruth.NOT_CONFIGURED + ) + detail = ( + "Endpoint is configured, but this local adapter does not claim a live handshake." + if configured + else "Optional external adapter is not configured; deterministic local mode remains available." + ) + if integration_id == "hiclaw" and configured: + try: + with DIRECT_HTTP.open(endpoint, timeout=3) as response: + payload = json.loads(response.read()) + if not isinstance(payload, dict): + raise TypeError("AgentTeams Bridge payload must be an object") + team = payload.get("team") + if ( + response.status == 200 + and payload.get("live") is True + and isinstance(team, dict) + ): + status = IntegrationTruth.READY + detail = ( + "Verified through AgentTeams Bridge: Team %s is %s; " + "leaderReady=%s and subordinate Workers=%s/%s." + % ( + team.get("name", "unknown"), + team.get("phase", "unknown"), + team.get("leaderReady", False), + team.get("readyWorkers", 0), + team.get("totalWorkers", 0), + ) + ) + else: + status = IntegrationTruth.UNAVAILABLE + detail = "AgentTeams Bridge responded without a verified live handshake." + except ( + OSError, + TypeError, + json.JSONDecodeError, + urllib.error.URLError, + ): + status = IntegrationTruth.UNAVAILABLE + detail = ( + "AgentTeams Bridge is configured but its live handshake is unavailable." + ) state = IntegrationState( id=integration_id, name=name, role=role, - status=( - IntegrationTruth.CONFIGURED_UNVERIFIED - if configured - else IntegrationTruth.NOT_CONFIGURED - ), + status=status, endpoint_configured=configured, checked_at=now, - detail=( - "Endpoint is configured, but this local adapter does not claim a live handshake." - if configured - else "Optional external adapter is not configured; deterministic local mode remains available." - ), + detail=detail, ) states.append(state.model_dump(mode="json")) return { - "mode": "adapter_metadata_only", + "mode": "verified_handshake_or_metadata", "truth_policy": ( "No external integration is reported ready without a verified handshake. " "This build intentionally performs no fake cloud calls." diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 2f15995..e4c788b 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -1341,7 +1341,7 @@ function AcceptanceReadiness({
{language === "zh" ? "本地每 Agent 数据库 + FOCUS.MD · 合同已验证" : "LOCAL PER-AGENT DB + FOCUS.MD · CONTRACT VERIFIED"} - {language === "zh" ? "TDSQL NEXA / AGENT MEMORY / 官方 AGENTTEAMS / GPU · 未运行" : "TDSQL NEXA / AGENT MEMORY / OFFICIAL AGENTTEAMS / GPU · NOT RUN"} + {language === "zh" ? "TDSQL NEXA / AGENT MEMORY / GPU · 未运行 · AGENTTEAMS 状态见集成事实" : "TDSQL NEXA / AGENT MEMORY / GPU · NOT RUN · SEE INTEGRATION TRUTH FOR AGENTTEAMS"} None: + # Authorization headers and request bodies must never reach logs. + return + + def _forward(self) -> None: + try: + content_length = int(self.headers.get("Content-Length", "0")) + except ValueError: + self.send_error(400, "invalid Content-Length") + return + if content_length > MAX_REQUEST_BYTES: + self.send_error(413, "request body too large") + return + request_body = self.rfile.read(content_length) if content_length else None + request_headers = { + name: value + for name, value in self.headers.items() + if name.lower() not in HOP_BY_HOP_HEADERS + and name.lower() not in {"host", "content-length"} + } + request_headers["Host"] = f"{UPSTREAM_HOST}:{UPSTREAM_PORT}" + if request_body is not None: + request_headers["Content-Length"] = str(len(request_body)) + + upstream = http.client.HTTPConnection(UPSTREAM_HOST, UPSTREAM_PORT, timeout=65) + try: + upstream.request(self.command, self.path, body=request_body, headers=request_headers) + response = upstream.getresponse() + response_body = response.read() + self.send_response(response.status, response.reason) + for name, value in response.getheaders(): + if name.lower() not in HOP_BY_HOP_HEADERS and name.lower() != "content-length": + self.send_header(name, value) + self.send_header("Content-Length", str(len(response_body))) + self.send_header("Connection", "close") + self.end_headers() + if self.command != "HEAD": + self.wfile.write(response_body) + except (OSError, http.client.HTTPException): + self.send_error(502, "Controller upstream unavailable") + finally: + upstream.close() + self.close_connection = True + + do_GET = _forward + do_HEAD = _forward + do_POST = _forward + do_PUT = _forward + do_PATCH = _forward + do_DELETE = _forward + do_OPTIONS = _forward + + +if __name__ == "__main__": + ThreadingHTTPServer(("0.0.0.0", 8080), ControllerProxy).serve_forever() diff --git a/deploy/web/local_server.py b/deploy/web/local_server.py new file mode 100644 index 0000000..7a7ee49 --- /dev/null +++ b/deploy/web/local_server.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python3 +"""Serve the built EgoAgentOS SPA and proxy its same-origin API locally.""" + +from __future__ import annotations + +import http.client +import os +from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer +from pathlib import Path +from typing import Optional +from urllib.parse import urlsplit + + +WEB_ROOT = Path(os.environ.get("EGO_WEB_ROOT", "/usr/share/egoagentos")) +BACKEND_HOST = os.environ.get("EGO_WEB_BACKEND_HOST", "backend") +BACKEND_PORT = int(os.environ.get("EGO_WEB_BACKEND_PORT", "8000")) +MAX_REQUEST_BYTES = 8 * 1024 * 1024 +HOP_BY_HOP_HEADERS = { + "connection", + "keep-alive", + "proxy-authenticate", + "proxy-authorization", + "te", + "trailer", + "transfer-encoding", + "upgrade", +} + + +class EgoWebHandler(SimpleHTTPRequestHandler): + protocol_version = "HTTP/1.1" + + def __init__(self, *args: object, **kwargs: object) -> None: + super().__init__(*args, directory=str(WEB_ROOT), **kwargs) + + def log_message(self, _format: str, *_args: object) -> None: + # API requests can contain operator credentials; keep the server silent. + return + + def _content_length(self) -> Optional[int]: + try: + return int(self.headers.get("Content-Length", "0")) + except ValueError: + self.send_error(400, "invalid Content-Length") + return None + + def _proxy_api(self) -> None: + content_length = self._content_length() + if content_length is None: + return + if content_length > MAX_REQUEST_BYTES: + self.send_error(413, "request body too large") + return + request_body = self.rfile.read(content_length) if content_length else None + request_headers = { + name: value + for name, value in self.headers.items() + if name.lower() not in HOP_BY_HOP_HEADERS + and name.lower() not in {"host", "content-length"} + } + request_headers["Host"] = f"{BACKEND_HOST}:{BACKEND_PORT}" + if request_body is not None: + request_headers["Content-Length"] = str(len(request_body)) + + upstream = http.client.HTTPConnection(BACKEND_HOST, BACKEND_PORT, timeout=65) + try: + upstream.request(self.command, self.path, body=request_body, headers=request_headers) + response = upstream.getresponse() + response_body = response.read() + self.send_response(response.status, response.reason) + for name, value in response.getheaders(): + if name.lower() not in HOP_BY_HOP_HEADERS and name.lower() != "content-length": + self.send_header(name, value) + self.send_header("Content-Length", str(len(response_body))) + self.send_header("Connection", "close") + self.end_headers() + if self.command != "HEAD": + self.wfile.write(response_body) + except (OSError, http.client.HTTPException): + self.send_error(502, "API upstream unavailable") + finally: + upstream.close() + self.close_connection = True + + def _serve_spa(self, *, head_only: bool = False) -> None: + if self.path == "/healthz": + payload = b"ok\n" + self.send_response(200) + self.send_header("Content-Type", "text/plain; charset=utf-8") + self.send_header("Content-Length", str(len(payload))) + self.end_headers() + if not head_only: + self.wfile.write(payload) + return + + relative = urlsplit(self.path).path.lstrip("/") + target = (WEB_ROOT / relative).resolve() + try: + target.relative_to(WEB_ROOT.resolve()) + except ValueError: + self.send_error(404) + return + if relative and target.is_file(): + if head_only: + super().do_HEAD() + else: + super().do_GET() + return + self.path = "/index.html" + if head_only: + super().do_HEAD() + else: + super().do_GET() + + def do_GET(self) -> None: + if self.path.startswith("/api/"): + self._proxy_api() + else: + self._serve_spa() + + def do_HEAD(self) -> None: + if self.path.startswith("/api/"): + self._proxy_api() + else: + self._serve_spa(head_only=True) + + def do_POST(self) -> None: + self._proxy_api() + + do_PUT = do_POST + do_PATCH = do_POST + do_DELETE = do_POST + do_OPTIONS = do_POST + + +if __name__ == "__main__": + if not (WEB_ROOT / "index.html").is_file(): + raise SystemExit("built web assets are missing") + ThreadingHTTPServer(("0.0.0.0", 80), EgoWebHandler).serve_forever() diff --git a/docker-compose.yml b/docker-compose.yml index e77c552..4c3f8fb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,24 @@ name: egoagentos-researchops services: + agentteams-controller-proxy: + profiles: ["agentteams"] + image: python:3.11-slim + command: ["python", "/opt/egoagentos/controller_proxy.py"] + pull_policy: ${EGO_BASE_IMAGE_PULL_POLICY:-missing} + environment: + NO_PROXY: 127.0.0.1,localhost,agentteams-controller + no_proxy: 127.0.0.1,localhost,agentteams-controller + volumes: + - ./deploy/agentteams/controller_proxy.py:/opt/egoagentos/controller_proxy.py:ro + networks: + - agentteams-official + ports: + - "127.0.0.1:${AGENTTEAMS_CONTROLLER_PORT:-18090}:8080" + restart: unless-stopped + security_opt: + - no-new-privileges:true + postgres: image: postgres:16-alpine environment: @@ -21,9 +39,9 @@ services: - no-new-privileges:true api-migrate: - build: - context: . - dockerfile: apps/api/Dockerfile + platform: linux/amd64 + image: egoagentos-local-api:latest + pull_policy: never command: - python - -c @@ -63,7 +81,27 @@ services: security_opt: - no-new-privileges:true + api-storage-init: + image: python:3.11-slim + platform: linux/amd64 + pull_policy: ${EGO_BASE_IMAGE_PULL_POLICY:-missing} + user: "0:0" + command: + - sh + - -c + - >- + mkdir -p /var/lib/egoagentos/agent-memory /data/artifacts && + chown -R 100:101 /var/lib/egoagentos/agent-memory /data + volumes: + - egoagentos-agent-memory:/var/lib/egoagentos/agent-memory + - egoagentos-artifacts:/data + restart: "no" + security_opt: + - no-new-privileges:true + backend: + platform: linux/amd64 + image: egoagentos-local-api:latest build: context: . dockerfile: apps/api/Dockerfile @@ -72,7 +110,7 @@ services: EGO_NEXA_DATABASE_URL: ${EGO_NEXA_DATABASE_URL:-} EGO_DATABASE_MIGRATION_MODE: verify EGO_TENANT_ID: ${EGO_TENANT_ID:-local} - EGO_CORS_ORIGINS: http://localhost:4173,http://127.0.0.1:4173 + EGO_CORS_ORIGINS: ${EGO_CORS_ORIGINS:-http://localhost:4173,http://127.0.0.1:4173} EGO_MCP_APPROVAL_HMAC_SECRET: ${EGO_MCP_APPROVAL_HMAC_SECRET:-} EGO_OPERATOR_KEY: ${EGO_OPERATOR_KEY:?Set EGO_OPERATOR_KEY in .env} EGO_OPERATOR_ID: ${EGO_OPERATOR_ID:-operator} @@ -83,40 +121,59 @@ services: TENCENT_AGENT_MEMORY_API_KEY: ${TENCENT_AGENT_MEMORY_API_KEY:-} TENCENT_AGENT_MEMORY_SERVICE_ID: ${TENCENT_AGENT_MEMORY_SERVICE_ID:-} TENCENT_AGENT_MEMORY_SPACE_ID: ${TENCENT_AGENT_MEMORY_SPACE_ID:-} - EGO_HICLAW_URL: ${EGO_HICLAW_URL:-} + EGO_HICLAW_URL: ${EGO_HICLAW_URL:-http://agentteams-bridge:8010/api/v1/agentteams/health?team=ego-researchops} EGO_NACOS_URL: ${EGO_NACOS_URL:-} EGO_HIGRESS_URL: ${EGO_HIGRESS_URL:-} + EGO_AGENT_MODEL_BASE_URL: ${EGO_AGENT_MODEL_BASE_URL:-} + EGO_AGENT_MODEL_API_KEY: ${EGO_AGENT_MODEL_API_KEY:-} + EGO_AGENT_MODEL: ${EGO_AGENT_MODEL:-agnes-2.5-pro} + EGO_ARTIFACT_ROOT: /data/artifacts + NO_PROXY: 127.0.0.1,localhost,postgres,agentteams-controller + no_proxy: 127.0.0.1,localhost,postgres,agentteams-controller volumes: - egoagentos-agent-memory:/var/lib/egoagentos/agent-memory + - egoagentos-artifacts:/data depends_on: postgres: condition: service_healthy api-security: condition: service_completed_successfully + api-storage-init: + condition: service_completed_successfully ports: - - "127.0.0.1:8000:8000" + - "127.0.0.1:${EGO_API_PORT:-8000}:8000" restart: unless-stopped security_opt: - no-new-privileges:true web: - build: - context: apps/web - dockerfile: Dockerfile + image: python:3.11-slim + platform: linux/amd64 + command: ["python", "/opt/egoagentos/local_server.py"] + pull_policy: ${EGO_BASE_IMAGE_PULL_POLICY:-missing} + environment: + EGO_WEB_ROOT: /usr/share/egoagentos + EGO_WEB_BACKEND_HOST: backend + EGO_WEB_BACKEND_PORT: "8000" + NO_PROXY: 127.0.0.1,localhost,backend + no_proxy: 127.0.0.1,localhost,backend + volumes: + - ./apps/web/dist:/usr/share/egoagentos:ro + - ./deploy/web/local_server.py:/opt/egoagentos/local_server.py:ro depends_on: backend: condition: service_healthy ports: - - "127.0.0.1:4173:80" + - "127.0.0.1:${EGO_WEB_PORT:-4173}:80" restart: unless-stopped security_opt: - no-new-privileges:true agentteams-bridge-migrate: profiles: ["agentteams"] - build: - context: . - dockerfile: apps/agentteams_bridge/Dockerfile + platform: linux/amd64 + image: egoagentos-local-agentteams-bridge:latest + pull_policy: never command: - python - -c @@ -158,6 +215,8 @@ services: agentteams-bridge: profiles: ["agentteams"] + platform: linux/amd64 + image: egoagentos-local-agentteams-bridge:latest build: context: . dockerfile: apps/agentteams_bridge/Dockerfile @@ -183,10 +242,15 @@ services: AGENTTEAMS_AUTH_TOKEN: ${AGENTTEAMS_AUTH_TOKEN:-} AGENTTEAMS_MATRIX_URL: ${AGENTTEAMS_MATRIX_URL:-http://host.docker.internal:18080} AGENTTEAMS_MATRIX_ACCESS_TOKEN: ${AGENTTEAMS_MATRIX_ACCESS_TOKEN:-} + NO_PROXY: 127.0.0.1,localhost,backend,postgres,agentteams-controller + no_proxy: 127.0.0.1,localhost,backend,postgres,agentteams-controller extra_hosts: - "host.docker.internal:host-gateway" + networks: + - default + - agentteams-official ports: - - "127.0.0.1:8010:8010" + - "127.0.0.1:${EGO_AGENTTEAMS_BRIDGE_PORT:-8010}:8010" restart: unless-stopped security_opt: - no-new-privileges:true @@ -194,3 +258,9 @@ services: volumes: egoagentos-postgres: egoagentos-agent-memory: + egoagentos-artifacts: + +networks: + agentteams-official: + external: true + name: agentteams-net diff --git a/docs/acceptance/2026-09-02-final-live-model.md b/docs/acceptance/2026-09-02-final-live-model.md index 268950f..7044cee 100644 --- a/docs/acceptance/2026-09-02-final-live-model.md +++ b/docs/acceptance/2026-09-02-final-live-model.md @@ -1,5 +1,9 @@ # 最终真实模型验收:ResearchOS + compact memory +> 这是 17:44 CST 冻结的模型面验收快照。随后完成的官方 AgentTeams/Matrix 本地基础设施 +> 验收见 [`live-local-2026-09-02.md`](live-local-2026-09-02.md);本表中的 `NOT_RUN` 只描述 +> 此旧快照本身,不能代表当天稍后的当前状态。 + 时间:2026-09-02 17:44 CST Trace:`trace_fd42c6c404304e139b1ec86ee3114f39` @@ -24,7 +28,7 @@ Trace:`trace_fd42c6c404304e139b1ec86ee3114f39` ## 完整输入 -控制面输入是 [`examples/egolite/research-goal.yaml`](../../examples/egolite/research-goal.yaml) +控制面输入是 [`examples/egolite/goal.yaml`](../../examples/egolite/goal.yaml) 和 [`examples/egolite/experiment-plan.yaml`](../../examples/egolite/experiment-plan.yaml):目标任务 `ego-lite-001`,三种轻量 backbone,R2 人工审批,24 GPU-hour 上限,验收阈值为 FPS 不低于 10 且 MPJPE 相对退化不超过 5%。两份输入摘要分别为: diff --git a/docs/acceptance/live-local-2026-09-02.md b/docs/acceptance/live-local-2026-09-02.md new file mode 100644 index 0000000..e80581c --- /dev/null +++ b/docs/acceptance/live-local-2026-09-02.md @@ -0,0 +1,98 @@ +# LIVE_LOCAL acceptance — 2026-09-02 + +This note freezes the credential-free acceptance boundary for the local deployment. It is +not a physical-GPU receipt and does not turn the GitHub Pages static replay into a live +backend. + +## Infrastructure result + +- Official source: `agentscope-ai/AgentTeams`, tag `v1.2.3`, commit + `223ddc2b8073e4c8b93bcbb15e1d717f196c04d9`. +- Controller and Manager phase: running. Manager runtime: `qwenpaw`; configured model: + `agnes-2.5-pro`. +- Team `ego-researchops`: `Active`, `leaderReady=true`, subordinate count `3/3`. +- Worker resources: `ego-research-lead`, `ego-architect`, `ego-reviewer`, and + `ego-memory-curator`; all four reported `Running` with Matrix identities. +- Matrix Human: `@ego-judge:matrix-local.agentteams.io:18080`, permission level 2, joined + to the Team room. Raw access token and initial password are excluded. +- Project `egoagentos-gpu-gated-v1`: `paused`; reason + `GPU Worker intentionally not attached yet`. +- PostgreSQL identity check: `egoagentos:egoagentos_owner`. +- EgoAgentOS API health: `ok`; database engine: PostgreSQL; immutable audit trigger: + `trigger_immutable_predecessor_guarded_hash_chain`. +- AgentTeams Bridge live handshake: `true`. + +The official local installer did not publish Controller port 8090. A repository-owned, +no-access-log proxy joins `agentteams-net` and exposes it only at `127.0.0.1:18090`; the +official checkout and official images remain unchanged. On Colima, a runtime-only installer +copy changes only the Docker socket bind source to `/var/run/docker.sock`. + +## Matrix multi-Agent receipt + +The L2 Human sent a read-only acceptance request with a real `m.mentions` entry. The first +request event was `$-laB7Nwa3W3PqUTiRXkoVxf7VwgCCM1JseD6AQ4Ki7w`. The room subsequently +contained 36 post-request Agent events from four distinct senders: + +| Agent | Final observed event | Reported boundary | +|---|---|---| +| Research Lead | `$N_ZsZXv9MxD5_fCw6Q_UNDu8KNwbt0R2tOREFm8uQuk` | real Matrix mention sent; workflow remained paused | +| Experiment Architect | `$Ugp71JFkIjSBK3B4UsKOTLkboT-dhKRPgDclI-oS70M` | `READY`, `GPU=NOT_ATTACHED` | +| Independent Reviewer | `$D3otsN_gJddf5xujZ6bnrizn2cVaxTa1HRsGxCcqRWM` | `READY`, `GPU=NOT_ATTACHED` | +| Memory Curator | `$8PFburKNwscUynfPghk7ordiN3f0L7ifFNiFP3BQ0mo` | `READY`, `GPU=NOT_ATTACHED` | + +Result: `LIVE_LOCAL PASS`, 4/4 distinct Agent identities. Agent prose about its own model +identity is not accepted as proof; the configured model is taken from the Controller/Manager +resource status and live provider receipts. + +## Custom-input model-plane receipt + +Input mode: `idea`. + +```text +目标:验证一个成本受控的确定性科研规划链。Baseline 是同一训练代码在三个固定随机种子上的结果; +想法是只改变一种数据增强。要求冻结数据划分、基线、指标和种子,设计最小消融矩阵与失败判据; +本次只生成计划和独立审查,不执行 GPU。 +``` + +Run `expert_2f342498149e478f98aaa47b604678f6` verified the live model catalog and selected +`agnes-2.5-pro`. The append-only event chain remained valid with final digest +`ee503007a760e9cef9e76e5a0d812caada0d077fec206490846cc11c1b30821b`. + +| Role | Result | HTTP/model receipt | Compact receipt | +|---|---|---|---| +| Research PI | completed | HTTP 200, 9,110 ms, request/response SHA-256 present | compacted, digest present | +| Context Scout | completed | HTTP 200, 10,632 ms, request/response SHA-256 present | compacted, digest present | +| Experiment Architect | failed | two responses failed the exact JSON-object contract | none | +| Independent Reviewer | not started | upstream contract failed | none | + +Final decision: `FAILED_CLOSED`; `execution_started=false`. No experiment matrix, GPU job, +or fabricated Reviewer verdict was produced. Increasing the Architect/Reviewer output budget +did not resolve this provider-compatibility failure, so the acceptance stopped instead of +continuing to consume model calls. + +## Recovery evidence + +An earlier run reached the first compact boundary and failed because the named Docker volume +was root-owned. The deployment now uses a one-shot root storage initializer to create/chown the +two volume roots, then runs the long-lived API as UID 100/GID 101. The rerun proved PI and Scout +compact writes succeed; the earlier failed event record was not rewritten. + +## Browser acceptance + +The rebuilt local Web UI was exercised in real Chrome at `1440x900` and `390x844`, in +English and Chinese. The three custom-input modes loaded their examples and cleared them; +without an in-memory operator key, the live-run action remained disabled. Page-level +horizontal overflow and browser console warnings/errors were both zero. + +After the sticky-navigation regression was fixed, `#compose`, `#protocol`, and `#acceptance` +all landed 15.88–16.16 px below the combined topbar/operator boundary in both viewports. +The RXP and acceptance chains remained locally horizontally scrollable on mobile without +making the document wider than the viewport. + +## Secret and claim boundary + +- Tokens, passwords, API keys and operator keys are only in `.runtime/live-stack.env` (0600). +- `.runtime/live-stack-public.json` contains URLs, identities, status, room ID and credential + hashes, but no raw credential value. +- Controller, Matrix, PostgreSQL, API and Bridge are bound to loopback on this host. +- GPU remains `NOT_ATTACHED`; TDSQL Nexa cloud/PITR and a public HTTPS backend remain unverified. diff --git a/docs/architecture.md b/docs/architecture.md index 2026fe1..9ce802f 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -18,7 +18,7 @@ flowchart TB SK["6 versioned Skill contracts"] -. "workflow contract" .-> R MCP["4 independently runnable MCP servers"] -. "execution-profile bridge" .-> API MCP -. "may carry RXP documents" .-> RXP - AT["Official AgentTeams Controller + Matrix\noptional live deployment"] -. "Project API + TeamHarness" .-> AB["Durable AgentTeams bridge\ncorrelation · replan · recovery"] + AT["Official AgentTeams Controller + Matrix\nLIVE_LOCAL deployment"] -. "Project API + TeamHarness" .-> AB["Durable AgentTeams bridge\ncorrelation · replan · recovery"] AB -. "validated artifact receipts" .-> API AB -. "RXP digests and cell refs" .-> RXP GPU["Fashion-MNIST FP32 vs AMP\nreal bounded single-GPU adapter"] -. "unrun external execution" .-> AB @@ -93,12 +93,14 @@ memory telemetry, independent review, and final Decision under explicit 900-seco 0.25-GPU-hour, and 100-MiB limits. [`../semifinal_acceptance/`](../semifinal_acceptance/) recomputes the content-addressed bundle and rejects Matrix gaps, receipt reuse, forged origin, trace/Decision drift, resource overrun, and incomplete recovery evidence. No official -AgentTeams/GPU execution is bundled, so a locally valid contract still reports -`CONTRACT_PASS_ORIGIN_UNVERIFIED`. +AgentTeams-to-GPU execution is bundled, so a locally valid contract still reports +`CONTRACT_PASS_ORIGIN_UNVERIFIED` for the experiment origin. -This profile still reports `not configured` until real health, Team/Worker readiness, -Project API, and Matrix responses are captured. Unit fixtures validate the contract and -state machine; they are not live-execution evidence. +The 2026-09-02 local profile captured real Controller/Manager health, an Active Team, +four Running Worker resources, a paused Project, Bridge connectivity, and 36 Matrix events +from four distinct Agent identities. This is `LIVE_LOCAL` collaboration-infrastructure +evidence, not a completed Project workflow, Skill invocation, fault/recovery scenario, or +physical-GPU experiment. See [`acceptance/live-local-2026-09-02.md`](acceptance/live-local-2026-09-02.md). ## Deployment profiles @@ -109,9 +111,9 @@ state machine; they are not live-execution evidence. roles/RLS, candidate-only memory writes, append-only ledgers, LISTEN/NOTIFY, CAS/advisory locks, restart recovery, and migration replay. This is the production architecture path, but it does not imply a cloud database was exercised. -- `agentteams` (opt-in executable profile): external official AgentTeams deployment plus - `apps/agentteams_bridge`; the bridge itself performs fail-closed Controller/Team/Matrix - probes before dispatch. +- `agentteams` (opt-in executable profile): official AgentTeams deployment plus + `apps/agentteams_bridge`; the 2026-09-02 loopback deployment passed Controller/Team/Worker/ + Matrix probes and remains paused before GPU dispatch. - `platform` (target contract): PolarDB-PG-compatible DB, object storage, OTel collector, AgentTeams, Higress, and Nacos. A fail-closed preflight checks schema, roles, policies, topology and notification capabilities, but the current health API still reports external @@ -119,7 +121,8 @@ state machine; they are not live-execution evidence. - `lab`: platform profile plus the real Fashion-MNIST scheduler/GPU adapter and a trusted dataset root. The adapter is implemented; the external run remains unverified. -The local profile is a functioning control-plane path, not a static UI. Agent reasoning, -the model workload, and hardware telemetry in the included EgoLite scenario are deterministic -synthetic fixtures and visibly labeled. The separate Fashion-MNIST adapter operates on real -data/GPU only when explicitly launched; no such launch is claimed by this repository snapshot. +The local profile is a functioning control-plane path, not a static UI. The public GitHub Pages +build remains a static replay. The separate local stack can make real model calls and run the +official collaboration services, while the included EgoLite metrics remain deterministic +synthetic fixtures. The Fashion-MNIST adapter operates on real data/GPU only when explicitly +launched; no such launch is claimed by this repository snapshot. diff --git a/docs/claims-evidence.md b/docs/claims-evidence.md index 7dda382..7cfc83f 100644 --- a/docs/claims-evidence.md +++ b/docs/claims-evidence.md @@ -38,8 +38,8 @@ real database URL and is not included in the default 242. | CPU hashing recovery branch works | synthetic control-flow fixture only | before/after fixture + trace sequence; requires physical-run evidence for a performance claim | | bounded Fashion-MNIST FP32/AMP workload adapter is executable | contract-verified; external origin unverified | 13 experiment tests cover one-CUDA-GPU fail-closed execution, resource limits, raw artifacts, telemetry, manifests, and offline verification; no live run is bundled | | 8×RTX 4090 experiment ran | not claimed | real scheduler logs + manifests + metric artifacts | -| AgentTeams bridge contract/state/fault behavior | verified with explicit contract fixtures; not live | 41 AgentTeams tests: official pins, inbound authentication, persistent lease fencing, PostgreSQL-capable checkpoints, full hash-chain verification, endpoint call shapes, result digests, reassign, R2, compensation, skill-evidence levels, and trace truth gates | -| AgentTeams Matrix collaboration is live | not verified on this host | real Controller version/health, active Team and ready Workers, Project/workflow IDs, Matrix event IDs, official spawn/tool trace, content-addressed artifacts, scoped R2 receipt, final trace hash | +| AgentTeams bridge contract/state/fault behavior | contract-verified; live local connectivity verified | 41 AgentTeams tests plus the 2026-09-02 Controller/Team/Worker/Matrix/Bridge receipt; scenario-level replan/reassign/R2/compensation remains fixture-only | +| AgentTeams Matrix collaboration is live | `LIVE_LOCAL` infrastructure smoke: Active Team, four Running Worker resources, paused Project, and 36 post-request events from four Agent identities | [`acceptance/live-local-2026-09-02.md`](acceptance/live-local-2026-09-02.md); a release claim still requires official spawn/tool/artifact events, scoped R2 receipt, final trace hash, and GPU origin | | semifinal acceptance bundle detects incomplete or forged evidence | verified locally; origin remains unverified | 16 tests cover eight MVP acceptance scenarios, Matrix/Decision closure, receipt uniqueness, raw metric policy, trace consistency, recovery checkpoints, resource limits, and negative origin promotion | | committed RXP Bench artifact is reproducible infrastructure evidence | verified as a synthetic local artifact | 14 scenarios × 5 repetitions × 3 profiles = 210 trials; corpus digest `eed5d4e06adc4713a765b3961643cda538b393bf651a848a24077a77b15098a4`; semantic result digest `05cab481a525210026d07377bb841ca0cd73f27790e9856b3c29211320b6b996` | | canonical 14-scenario AgentTeams target benchmark passes | not claimed; 70/70 target trials are honest `SKIP`, and live opt-in remains `UNIMPLEMENTED/SKIP` until the per-scenario fault/replay harness exists | benchmark tests enforce fail-closed opt-in plus the future trace contract; generic completion is never launched as pseudo release evidence and can never become PASS | @@ -54,7 +54,7 @@ determinism, hash, and rollback claims refer to a different project and absent e | Claim | Current state | Required evidence | |---|---|---| -| Judge input can drive four visible expert roles through the server-side model gateway | implementation verified with a deterministic fake-gateway E2E; the same gateway has a separate four-call live-provider acceptance artifact, but this new endpoint has not been rerun against that provider in this checkout | authenticated `POST /api/v1/expert-runs`; four HTTP 200 role receipts from this endpoint; persisted run JSON; valid event chain | +| Judge input can drive visible expert roles through the server-side model gateway | live endpoint exercised: PI and Scout returned HTTP 200; Architect failed the exact JSON-object contract after two attempts; Reviewer was not started; run failed closed | authenticated `POST /api/v1/expert-runs`; persisted run `expert_2f342498149e478f98aaa47b604678f6`; request/response digests; valid event chain; no GPU dispatch | | Role hand-offs are inspectable | verified locally | each role exposes its payload field names, upstream-role list, payload SHA-256, schema-validated output, request/response digests, latency, and focus-memory receipt | | Architect output controls the compiled tree and matrix | verified locally | fake-gateway E2E compiles architect branches, metrics, folds, and seeds into a non-empty deterministic matrix | | Independent review occurs before physical execution | verified locally | reviewer receives the exact compiled-plan digest; final state is planning-only and `execution_started=false` | diff --git a/docs/competition-mapping.md b/docs/competition-mapping.md index 6fb63d3..aa4609d 100644 --- a/docs/competition-mapping.md +++ b/docs/competition-mapping.md @@ -10,7 +10,7 @@ | 复赛维度 | 权重 | EgoAgentOS 设计回应 | 已落库证据 | 当前真实边界 | |---|---:|---|---|---| | 场景价值与可迁移性 | 20% | 把具身 AI 的高成本实验从聊天式黑盒改为目标、矩阵、授权、执行、复核和决策闭环 | `apps/api/`、`protocols/rxp/`、6 个 Skills、14 场景 benchmark、真实 Fashion-MNIST 单 GPU adapter | adapter 与验收器已实现,但官方 GPU/AgentTeams origin 未验证;尚无真实科研用户基线和跨领域验收 | -| 多 Agent 协作 | 25% | AgentTeams 负责真实团队协作;PI、Scout、Architect、Runtime、Evaluator、Reviewer、Memory Curator 分权;中间结果可 replan,超时可改派,重启可恢复 | `apps/agentteams_bridge/`、`integrations/agentteams/`、`tests/agentteams/`、live runbook | bridge 与合同已完成;官方 live 服务与逐场景 fault/replay harness 均未验收,所以不能声称核心链已接通 | +| 多 Agent 协作 | 25% | AgentTeams 负责真实团队协作;PI、Scout、Architect、Runtime、Evaluator、Reviewer、Memory Curator 分权;中间结果可 replan,超时可改派,重启可恢复 | `apps/agentteams_bridge/`、`integrations/agentteams/`、`tests/agentteams/`、live runbook、四 Agent Matrix receipt | 官方 Controller/Manager/Team/Workers/Matrix 已 `LIVE_LOCAL`;完整委派/Skill/R2/终态与逐场景 fault harness 尚未验收 | | Skill 工程化 | 20% | Skill 不是提示词附件,而是可发现、版本固定、可调用、可追踪、可回滚的运行时资产 | `skills/`、`skill_runtime/`、`/api/v1/skills/*`、`tests/skills/` | 本地 runtime 已验证;真实 Worker 的 discovery/tool invocation 仍待 live trace | | 工程实现与安全审计 | 30% | 确定性控制面拥有状态、授权、幂等、证据和最终验收;RXP 固化实验承诺链;benchmark 与 acceptance bundle 独立验证 adapter 证据 | RXP schemas/verifier、PostgreSQL 生产路径与四类角色、R2 token、append-only hash-chain、LISTEN/NOTIFY、PolarDB preflight、evidence gate、persistent bundle | 本地/contract 证据不等于生产安全认证,也不证明真实 GPU/AgentTeams/PolarDB/PITR | | 开源贡献 | 5% | 发布代码、协议文档、JSON Schema、Skill 包、adapter、负对照与可复现测试 | Apache-2.0 repository、README、runbooks、CI commands | 尚无正式 release/adoption 证据;RXP 是项目协议,不是行业标准 | @@ -59,9 +59,9 @@ gate 中 `SKIP` 与 `FAIL`/`ERROR` 一样阻止放行。 | 能力 | Code | 本地/contract 证据 | 官方 live 证据 | |---|:---:|:---:|:---:| -| AgentTeams resource + official contract pin | ✓ | ✓ | — | +| AgentTeams resource + official contract pin | ✓ | ✓ | ✓ `LIVE_LOCAL` | | Project create/pause/resume/replan/complete 与 task cancel/replacement bridge | ✓ | ✓ | — | -| Matrix structured dispatch 与 response correlation | ✓ | ✓ | — | +| Matrix structured dispatch 与 response correlation | ✓ | ✓ | ✓ 四 Agent smoke;非完整 workflow | | conflict/replan、timeout/reassign、restart/resume、compensation | ✓ | ✓ | — | | R2 Grant 进入恢复链 | ✓ | ✓ | — | | PostgreSQL bridge checkpoint/event/receipt、append-only 与最小权限 | ✓ | ✓,本地 16.14 | — PolarDB/PITR | @@ -83,10 +83,10 @@ live evidence。 当前可安全使用的总述是: -> EgoAgentOS 已实现并本地验证一个面向官方 AgentTeams Project/TeamHarness/Matrix 的 -> fail-closed bridge、PostgreSQL 生产数据合同、RXP 实验承诺层、真实 Fashion-MNIST 单 -> GPU adapter 与一键证据验收层;官方 AgentTeams/GPU origin、PolarDB/PITR 和 14 场景 -> target release benchmark 尚待部署环境验收,当前外部结果明确为 `UNVERIFIED`/`SKIP`。 +> EgoAgentOS 已在本地运行官方 AgentTeams Controller/Manager、Active Team、四个 Worker +> 资源和 Matrix 四 Agent smoke,并验证 fail-closed bridge、PostgreSQL 数据合同、RXP、 +> Fashion-MNIST 单 GPU adapter 与一键验收层;完整 AgentTeams Project/Skill/R2/Decision、 +> GPU origin、Nexa/PITR 和 14 场景 target release benchmark 尚待验收。 详细 release 判据见 [`semifinal-scorecard.md`](semifinal-scorecard.md),live 操作见 [`agentteams-live-runbook.md`](agentteams-live-runbook.md),claim 状态见 diff --git a/docs/demo-runbook.md b/docs/demo-runbook.md index 0572385..46a8de1 100644 --- a/docs/demo-runbook.md +++ b/docs/demo-runbook.md @@ -1,7 +1,8 @@ # Judge replay runbook -This is the semifinal replay path for the repository snapshot dated 2026-08-29. It -does not require a GPU, AgentTeams, Nacos, Higress, PolarDB, or other cloud credentials. +This is the semifinal replay path for the repository snapshot dated 2026-09-02. The +static replay does not require a GPU or cloud credentials. A separate official +AgentTeams `LIVE_LOCAL` path is documented below and deliberately stops before GPU. ## 0. Truth boundary before the demo @@ -13,7 +14,7 @@ does not require a GPU, AgentTeams, Nacos, Higress, PolarDB, or other cloud cred | RXP API | schema catalog, synthetic fixture, structural ledger verification | RXP persistence in the task store or issuer trust | | Skill API | six packages discovered, three deterministic handlers, digest-bound traces | durable rollout state or Nacos publication | | PostgreSQL | real PostgreSQL 16 store/role/ledger contract, 32/32 integration tests | PolarDB cloud deployment or PITR | -| AgentTeams | executable bridge/finalization contracts and honest `UNIMPLEMENTED/SKIP` target | per-scenario live fault/replay harness or official Matrix collaboration | +| AgentTeams | static replay proves only the bridge contract; the separate local-live acceptance proves official Controller/Manager, four Worker resources, Matrix and Bridge connectivity | a physical GPU run or public hosted Controller | ## 1. Start the local stack @@ -42,10 +43,10 @@ export NO_PROXY=127.0.0.1,localhost export no_proxy=127.0.0.1,localhost ``` -Evidence boundary: `docker compose config` and the real PostgreSQL 16 data-layer suite -were verified on 2026-08-29. API/Web image build was not verified on that host because -Docker Hub metadata requests timed out. On a network with registry access, the command -above remains the intended replay path. The native fallback avoids image pulls: +Evidence boundary: on 2026-09-02 the full local Compose stack passed PostgreSQL, API, +Web and Bridge health checks. The official AgentTeams path additionally passed four +distinct Matrix Agent replies. The native fallback remains useful when only the +deterministic replay is needed: ```bash uv sync --python 3.9 --extra dev @@ -73,7 +74,7 @@ printing it: ```bash export EGO_AGENT_MODEL_BASE_URL=https://apihub.agnes-ai.com/v1 -export EGO_AGENT_MODEL=agnes-2.5-flash +export EGO_AGENT_MODEL=agnes-2.5-pro read -s EGO_AGENT_MODEL_API_KEY export EGO_AGENT_MODEL_API_KEY ``` @@ -84,6 +85,24 @@ model receipts, four per-Agent compact receipts, a valid event chain, a compiled and `execution_started=false`. It must still show official AgentTeams, Matrix transport, retrieval, and physical GPU as `NOT_RUN` unless separate receipts have been ingested. +## 1A. Deploy and verify official AgentTeams locally + +```bash +python3 scripts/deploy_local_live_stack.py all +``` + +This pins official AgentTeams `v1.2.3`, configures `agnes-2.5-pro`, creates Team +`ego-researchops`, four Worker resources and L2 Human `ego-judge`, joins the Human to the +Team room, and creates paused Project `egoagentos-gpu-gated-v1`. It then starts the +PostgreSQL-backed EgoAgentOS API and AgentTeams Bridge. The generated public receipt is +`.runtime/live-stack-public.json`; raw credentials stay only in the mode-0600 +`.runtime/live-stack.env`. + +Do not resume the workflow until a GPU host has been supplied and independently reviewed. +The 2026-09-02 local run reports `leaderReady=true`, three ready subordinate Workers and +four running Worker resources in total. The Matrix smoke received messages from all four +Agent identities. See [the frozen acceptance note](acceptance/live-local-2026-09-02.md). + ## 2. Run the six-minute cockpit replay 1. Open the task cockpit for `ego-lite-001`; confirm the `SYNTHETIC DEMO` marker. diff --git a/docs/final-submission-20260902.md b/docs/final-submission-20260902.md index a31029f..05b8f90 100644 --- a/docs/final-submission-20260902.md +++ b/docs/final-submission-20260902.md @@ -61,12 +61,15 @@ CPU/GPU 预算、并发、row shards、checkpoint/resume、缓存、barrier、va | OpenAI-compatible 外部模型 harness | `LIVE · PASS`:最终 4/4 HTTP 200;此前 3 条坏 trace 均 fail-closed,最终包同时冻结失败恢复证据 | | TDSQL Nexa instance | `NOT_CONFIGURED / NOT_RUN` | | TencentDB Agent Memory instance | `NOT_CONFIGURED / NOT_RUN` | -| 官方 AgentTeams Controller + Matrix + GPU | `NOT_RUN`,不能由本地角色标签代替 | +| 官方 AgentTeams Controller / Manager / 4 Workers / Matrix | `LIVE_LOCAL · PASS`:真实本地服务与四 Agent Matrix smoke;Project 暂停在 GPU gate | +| 完整 AgentTeams workflow + Skill trace + GPU + Decision | `NOT_RUN`,不能由基础设施 smoke 或本地角色标签代替 | 本轮 receipt:`trace_fd42c6c404304e139b1ec86ee3114f39`。它验证的不是 Ego3D 模型精度, 而是外部模型角色输出进入本地确定性 Research Compiler、资源门、13 阶段控制面和 per-agent compact 后仍可完整离线复核。完整输入、输出、时序、摘要与限制见 [`acceptance/2026-09-02-final-live-model.md`](acceptance/2026-09-02-final-live-model.md)。 +当前 AgentTeams/Matrix 基础设施验收见 +[`acceptance/live-local-2026-09-02.md`](acceptance/live-local-2026-09-02.md)。 ## 评委入口 diff --git a/docs/input-output-contract.md b/docs/input-output-contract.md index 78a989b..ad9c523 100644 --- a/docs/input-output-contract.md +++ b/docs/input-output-contract.md @@ -36,5 +36,7 @@ EgoAgentOS 的主输入不是一句开放式 prompt,而是一组冻结、可 消费/状态机回放、独立复核、`acceptance.json` 和 `SHA256SUMS.json`。 真实性标签必须一起阅读:外部模型调用为 `LIVE`,本地控制面为 `LIVE_LOCAL`,EgoLite -指标仍为 `SYNTHETIC_FIXTURE`,官方 AgentTeams Controller、Matrix 与物理 GPU 均为 -`NOT_RUN`。后两者只有连接官方部署并取得相应 receipt 后才能升级。 +指标仍为 `SYNTHETIC_FIXTURE`。2026-09-02 的独立本地验收已将官方 AgentTeams Controller、 +Manager、四个 Worker 与 Matrix 升级为 `LIVE_LOCAL`;Project 保持 GPU gate 暂停,物理 GPU、 +完整 workflow、Skill tool trace 与终态 Decision 仍为 `NOT_RUN`。详见 +[`acceptance/live-local-2026-09-02.md`](acceptance/live-local-2026-09-02.md)。 diff --git a/docs/judge-feedback-implementation.md b/docs/judge-feedback-implementation.md index d166208..26b4310 100644 --- a/docs/judge-feedback-implementation.md +++ b/docs/judge-feedback-implementation.md @@ -193,7 +193,7 @@ Release gate 的不可妥协指标是: 以下项目需要新的外部资源或操作授权,本仓库当前不声称已完成: -- 官方 AgentTeams Controller、Team/Worker、Matrix 同一运行链; +- 官方 AgentTeams Project 从委派、Skill/tool 到 R2 恢复和终态 Decision 的完整运行链; - 一次真实单 GPU Fashion-MNIST 受控实验及可认证调度来源; - PolarDB-PG writer/reader、四个专用登录和 provider identity; - PITR、备份恢复、多可用区 failover 与实测 RPO/RTO; @@ -202,3 +202,8 @@ Release gate 的不可妥协指标是: 建议的最低成本 live 验收预算为一张 GPU、一次 physical launch、最多 900 秒 / 0.25 GPU-hour;云数据库演练必须另设费用上限、临时恢复实例和 teardown owner。任何实际运行前仍需 操作者确认资源、凭据与费用授权。 + +已完成的基础设施前置验收单独记录为 `LIVE_LOCAL`:官方 Controller/Manager、Active Team、 +四个 Running Worker 资源、暂停 Project、Bridge handshake,以及来自四个 Agent 身份的 +Matrix smoke。它不等价于上面的完整实验链;证据见 +[`acceptance/live-local-2026-09-02.md`](acceptance/live-local-2026-09-02.md)。 diff --git a/docs/semifinal-change-log.md b/docs/semifinal-change-log.md index 772c093..af9d445 100644 --- a/docs/semifinal-change-log.md +++ b/docs/semifinal-change-log.md @@ -10,10 +10,10 @@ Skill runtime、可持久恢复的 AgentTeams bridge、PostgreSQL 生产数据路径,以及成本受控的 真实 Fashion-MNIST GPU adapter + content-addressed 一键验收包。 -最大剩余缺口没有被文档掩盖:当前没有官方 AgentTeams live stack、真实 Worker/Matrix -事件、逐场景 fault/replay harness、GPU receipt 和 PolarDB/PITR 演练,所以外部 origin 只达到 -`CONTRACT_PASS_ORIGIN_UNVERIFIED`,canonical target benchmark 为 `SKIP`,不能申报 -“已接通”“动态多 Agent 已跑通”或“真实 GPU 实验已完成”。 +最大剩余缺口没有被文档掩盖:官方 AgentTeams Controller/Manager、Active Team、四个 +Worker 资源和 Matrix 四 Agent smoke 已在本地运行;但完整委派/Skill/R2/Decision、逐场景 +fault/replay harness、GPU receipt 和 Nexa/PITR 演练尚缺,所以实验 origin 仍只达到 +`CONTRACT_PASS_ORIGIN_UNVERIFIED`,canonical target benchmark 为 `SKIP`。 ## 初版 → 复赛差异 @@ -21,7 +21,7 @@ Skill runtime、可持久恢复的 AgentTeams bridge、PostgreSQL 生产数据 |---|---|---|---|---| | 实验确定性 | approval token、RunManifest、七类 EvidenceGate;一个任务内的控制语义 | RXP/1 将完整实验矩阵拆成 per-cell Intent → Grant → Receipt → Evidence → Decision,并记录 missing decisions | `protocols/rxp/`、committed schemas、canonical/Merkle vectors、protocol tests | 随机模型结果的科学正确性、任意 GPU 的字节确定性 | | Benchmark | 静态 judge replay 与普通单元测试 | 14 个版本化场景、负对照、deterministic core、AgentTeams target、独立 oracle、置信区间、persistent evidence bundle、release replay | `benchmarks/`、committed raw JSON/Markdown/hash、benchmark tests | AgentTeams target 尚未真实通过;逐场景 harness 未实现,committed target 是 70/70 `SKIP` | -| AgentTeams | CRD/resource template 与 message envelope;没有 runtime bridge | 官方 commit 契约锁;Project/TeamHarness/Matrix bridge;动态 replan;timeout reassign;restart/resume;compensation;R2 恢复;artifact digest 验收 | `apps/agentteams_bridge/`、`integrations/agentteams/`、`tests/agentteams/`、live runbook | 官方服务、真实 Matrix room、3+ Worker 真实协作与场景 fault injection | +| AgentTeams | CRD/resource template 与 message envelope;没有 runtime bridge | 官方 commit 契约锁;Project/TeamHarness/Matrix bridge;动态 replan;timeout reassign;restart/resume;compensation;R2 恢复;artifact digest 验收 | `apps/agentteams_bridge/`、`integrations/agentteams/`、`tests/agentteams/`、live runbook、`LIVE_LOCAL` receipt | 完整 task lifecycle、Skill tool、R2/Decision 与场景 fault injection | | Trace 真值 | 可读 audit/event 模型 | `egoagentos.agentteams-trace/v1` schema;project/task/correlation/context 绑定;3+ Worker、Skill、HITL、review、Decision、RXP 五链与 official response 校验 | benchmark-owned schema/verifier;adapter 自报值不作为真值 | 外部系统实际产生的合格 trace | | Skill 工程化 | 6 个 `SKILL.md` 合同 | 文件系统 discovery、package digest/version pin、typed invocation、idempotent correlation、failure trace、canary/retire/rollback、FastAPI endpoints | `skill_runtime/`、`apps/api/skill_runtime_api.py`、`tests/skills/` | 真实 AgentTeams Worker 已安装并成功调用这些 Skill | | 数据持久化 | SQLite 开发状态 | PostgreSQL 生产路径;控制面和 bridge 分库 URL;MVCC/row lock/CAS;四类最小权限角色;candidate→validator→validated memory;append-only trigger;commit-only LISTEN/NOTIFY;校验和迁移与 fail-closed PolarDB preflight | `apps/api/migrations/postgres/`、`apps/agentteams_bridge/migrations/postgres/`、`deploy/postgres/`、`tests/postgres/`、recovery runbook | PolarDB-PG 云实例、只读节点、备份/PITR、跨区容灾和实测 RPO/RTO | @@ -59,11 +59,12 @@ Skill runtime、可持久恢复的 AgentTeams bridge、PostgreSQL 生产数据 ### 仍需 live 验收 -1. 在官方 AgentTeams pin 或兼容 release 上部署 Team/Worker/Manager; -2. 使用非 synthetic Ego task 和真实 Matrix credential 跑通至少 3 个不同 Worker; -3. 为 14 个 canonical scenario 分别执行 fault driver,不能复用一条 generic trace; -4. 持久化每个 trial 的 trace/manifest/artifacts 并执行 `make benchmark-release`; -5. 只有 release gate 无 `FAIL`、`ERROR`、`SKIP` 后,才更新 live claim。 +1. 已完成:在官方 AgentTeams `v1.2.3` pin 上部署 Manager、Team 和四个 Worker; +2. 已完成基础 smoke:真实 Matrix room 收到四个 Agent 身份的响应; +3. 待完成:非 synthetic Project 的委派、ACK、Skill/tool、R2、review 与终态 Decision; +4. 待完成:为 14 个 canonical scenario 分别执行 fault driver,不能复用一条 generic trace; +5. 待完成:持久化每个 trial 的 trace/manifest/artifacts 并执行 `make benchmark-release`; +6. 只有 release gate 无 `FAIL`、`ERROR`、`SKIP` 后,才更新完整实验 live claim。 ## 与评分权重的变化关系 @@ -95,7 +96,7 @@ final newline 与 tab 检查。 | 旧表述风险 | 当前允许表述 | |---|---| -| “EgoAgentOS 使用 AgentTeams 完成了实验” | “EgoAgentOS 提供面向官方 AgentTeams 的可执行 bridge;合同测试已通过,live 尚未验收” | +| “EgoAgentOS 使用 AgentTeams 完成了实验” | “官方 AgentTeams 基础设施与四 Agent Matrix smoke 已 `LIVE_LOCAL`;完整实验 workflow/GPU 未运行” | | “多 Agent 动态协作已证明” | “replan/reassign/recovery 逻辑已通过本地 fault contract;仍需官方事件链” | | “Skill 已在 Worker 中调用” | “本地 Skill runtime 已调用;AgentTeams `TOOL_INVOKED` 证据尚缺” | | “RXP 让 AI 实验确定” | “RXP 固化实验承诺、授权、验收和完整性边界,不保证随机训练结果或科学结论” | diff --git a/docs/semifinal-scorecard.md b/docs/semifinal-scorecard.md index 832e53d..c8a8e61 100644 --- a/docs/semifinal-scorecard.md +++ b/docs/semifinal-scorecard.md @@ -12,13 +12,14 @@ > 配置,因此 live 状态是 `NOT_CONFIGURED/NOT_RUN`。下文保留的 PolarDB 记录是上一轮评委意见 > 的历史验收,不代表本轮已跑 Nexa。 -**工程候选已形成,但复赛 AgentTeams 硬门槛仍为 `BLOCKED`。** 仓库已经提供可执行 +**工程候选已形成,但复赛 AgentTeams 完整业务硬门槛仍为 `BLOCKED`。** 仓库已经提供可执行 Controller/Matrix bridge、PostgreSQL checkpoint/receipt/event backend、动态 replan、超时 改派、恢复与补偿、R2 HITL 恢复链、结构化 correlation envelope、Skill 证据分级、 Fashion-MNIST 单 GPU adapter、离线验收包和 fail-closed benchmark adapter;这些行为已有 -本地 contract/fixture 测试。当前主机没有可用的官方 AgentTeams 服务、真实 Team/Worker、 -Matrix 凭据、GPU receipt 与逐场景 fault/replay harness,因此不能产生官方运行证据。已提交 -benchmark 中 `agentteams-rxp-target` 为 70/70 `SKIP`;live opt-in 仍明确返回 +本地 contract/fixture 测试。当前主机已运行官方 Controller/Manager、Active Team、四个 +Worker 资源、暂停 Project、Bridge 与真实 Matrix room,并取得四 Agent smoke receipt;但没有 +完整 task lifecycle、Skill invocation、R2 恢复、GPU receipt 与逐场景 fault/replay harness。 +已提交 benchmark 中 `agentteams-rxp-target` 为 70/70 `SKIP`;live opt-in 仍明确返回 `UNIMPLEMENTED/SKIP`,不是 `PASS`。 证据必须按以下三层表述: @@ -26,8 +27,8 @@ benchmark 中 `agentteams-rxp-target` 为 70/70 `SKIP`;live opt-in 仍明确 | 层级 | 可以主张 | 不可以主张 | |---|---|---| | 已实现 | bridge、schema、adapter、runbook 与 release verifier 已进入代码库 | 这些代码已经驱动官方服务 | -| 本地验证 | fixture/contract、离线契约锁、状态机、RXP、Skill runtime 与安全 oracle 通过测试 | fixture 是 AgentTeams Worker 的真实输出 | -| 官方 live | 仅当 Controller、TeamHarness、Matrix 与真实 Worker 共同产生可复核 trace 后成立 | 用静态回放、mock response、角色标签或自报 `pass` 替代 live trace | +| 本地验证 | fixture/contract、状态机、RXP、Skill runtime、安全 oracle,以及官方服务/Matrix 基础 smoke | 基础 smoke 是完整 Project 或 GPU 实验 | +| 官方 live | Controller/Manager、Team/Workers、暂停 Project、Bridge 与 Matrix smoke 已本地成立;完整实验需同一 correlation 的 task/artifact/Decision/GPU trace | 用静态回放、mock response、角色标签或自报 `pass` 替代 live trace | ## 硬门槛与否决项 @@ -36,8 +37,8 @@ benchmark 中 `agentteams-rxp-target` 为 70/70 `SKIP`;live opt-in 仍明确 | 硬门槛 / 否决风险 | 必须提交的机器证据 | 当前状态 | Release 判定 | |---|---|---|---| -| 核心业务链真实使用 AgentTeams | 同一 Project 的官方 create/workflow/spawn/Matrix 标识,覆盖创建、委派、接单、执行、验收与终态 | bridge 合同完成;无官方 live run | **未满足** | -| 至少 3 个不同职能 Agent | trace 中至少 3 个真实 AgentTeams Worker,`id`、Matrix user、role 唯一且事件 actor 可解析 | 资源定义 7 个 Worker;仅本地合同验证 | **未满足 live 证明** | +| 核心业务链真实使用 AgentTeams | 同一 Project 的官方 create/workflow/spawn/Matrix 标识,覆盖创建、委派、接单、执行、验收与终态 | 官方服务和暂停 Project 已 live;完整 lifecycle 未运行 | **未满足** | +| 至少 3 个不同职能 Agent | trace 中至少 3 个真实 AgentTeams Worker,`id`、Matrix user、role 唯一且事件 actor 可解析 | 四个 Running Worker 与四 Agent Matrix smoke 已 `LIVE_LOCAL`;尚无 task lifecycle | **部分满足** | | 动态协作而非固定脚本 | 中间结果触发 conflict/replan;timeout 触发 cancel/replacement/reassign;恢复后继续原 correlation | 逻辑与故障测试完成;无官方事件链 | **未满足 live 证明** | | 核心 Skill 可发现、调用、追踪 | Worker 声明、spawn 授权和官方成功 `tool_result`;版本及 package digest 与任务 trace 关联 | 本地 registry/API 可运行;AgentTeams `TOOL_INVOKED` 未 live 验证 | **部分满足** | | 高风险动作有人类授权 | R2 先 pause,单次 scope-bound Grant 被 EgoAgentOS 消费,再 resume/replan;重放被拒 | bridge 与本地 approval/RXP 测试完成;无 live receipt | **部分满足** | @@ -55,7 +56,7 @@ benchmark 中 `agentteams-rxp-target` 为 70/70 `SKIP`;live opt-in 仍明确 | 复赛维度 | 权重 | 当前可复核优势 | 仍需补齐的评审证据 | 当前判定 | |---|---:|---|---|---| | 场景价值与可迁移性 | 20% | 面向具身 AI 实验的目标→矩阵→执行→评测→复核→决策闭环;RXP 与 adapter 为领域无关合同;真实 Fashion-MNIST FP32/AMP 工作负载已代码就绪 | 一次官方 AgentTeams+GPU 同源运行、研究员手工基线,以及第二领域的迁移映射 | `PARTIAL` | -| 多 Agent 协作 | 25% | 7 个职责分离 Worker;bridge 映射 Project/TeamHarness/Matrix;实现 conflict/replan、timeout/reassign/resume/compensation | 一条官方 live trace 证明至少 3 Worker 的动态协作与终态验收 | `BLOCKED` | +| 多 Agent 协作 | 25% | 官方 Controller/Manager、Active Team、四个 Worker 与四 Agent Matrix smoke 已 `LIVE_LOCAL`;bridge 实现 conflict/replan、timeout/reassign/resume/compensation | 同一 Project 的委派、Skill、R2、动态分支和终态验收 trace | `PARTIAL / BUSINESS GATE BLOCKED` | | Skill 工程化 | 20% | 6 个版本化 Skill 包;本地 discovery、digest pin、invocation trace、canary/retire/rollback 已实现并测试 | 在真实 Worker 上证明包存在、spawn 授权、成功调用、失败与版本回滚 | `PARTIAL` | | 工程实现与安全审计 | 30% | RXP/1、14 场景 benchmark、独立 trace oracle、content-addressed bundle、TDSQL Nexa SQL adapter、TencentDB Agent Memory v3、per-agent compact、PostgreSQL-compatible 事务/最小权限/append-only/LISTEN-NOTIFY、R2/重放/篡改门禁 | 官方 AgentTeams live fault injection、Nexa/Agent Memory 实例验收、外部 effect exactly-once 与真实恢复时间 | `PARTIAL` | | 开源贡献 | 5% | Apache-2.0 代码、JSON Schema、Skill、adapter、测试、runbook 与可复现实验协议均公开可读 | tag/release、干净机安装记录,以及外部 issue、复用或反馈证据 | `PARTIAL` | diff --git a/experiments/egolite_agentteam/run.py b/experiments/egolite_agentteam/run.py index c96638c..8844f14 100644 --- a/experiments/egolite_agentteam/run.py +++ b/experiments/egolite_agentteam/run.py @@ -546,7 +546,7 @@ def main() -> int: parser.add_argument( "--base-url", default=os.getenv("EGO_AGENT_MODEL_BASE_URL", ""), help="non-secret URL" ) - parser.add_argument("--model", default=os.getenv("EGO_AGENT_MODEL", "agnes-2.5-flash")) + parser.add_argument("--model", default=os.getenv("EGO_AGENT_MODEL", "agnes-2.5-pro")) args = parser.parse_args() api_key = os.getenv("EGO_AGENT_MODEL_API_KEY", "") if not args.base_url or not api_key: diff --git a/integrations/agentteams/README.md b/integrations/agentteams/README.md index fefa4c1..97a23a6 100644 --- a/integrations/agentteams/README.md +++ b/integrations/agentteams/README.md @@ -28,6 +28,12 @@ replay. This proves live model HTTP calls and model-output contract handling onl It deliberately records official AgentTeams Controller, Matrix, and physical GPU as `NOT_RUN`; those labels cannot be promoted by a successful model response. +That historical harness label remains correct for its own frozen artifact. Separately, the +2026-09-02 local deployment verified the official Controller/Manager, Active Team, four Running +Worker resources, a paused Project, Bridge handshake, and Matrix messages from four Agent +identities as `LIVE_LOCAL`. It did not execute the Project through a physical GPU or terminal +Decision. See [`../../docs/acceptance/live-local-2026-09-02.md`](../../docs/acceptance/live-local-2026-09-02.md). + ## Official contract pin Implementation was checked against only the official diff --git a/integrations/agentteams/local-ready-team.yaml.tmpl b/integrations/agentteams/local-ready-team.yaml.tmpl new file mode 100644 index 0000000..558da44 --- /dev/null +++ b/integrations/agentteams/local-ready-team.yaml.tmpl @@ -0,0 +1,106 @@ +# Local no-GPU bring-up for the official AgentTeams v1.2.3 Controller. +# Secrets are injected into the official installer, never this manifest. +# The GPU Runtime and Evaluator Workers are deliberately added only after a +# GPU host is supplied and accepted by the independent resource reviewer. +apiVersion: agentteams.io/v1beta1 +kind: Worker +metadata: + name: ego-research-lead +spec: + model: "${AGENTTEAMS_MODEL}" + runtime: qwenpaw + identity: | + Name: Ego Research Lead + Role: Team Leader for evidence-gated ResearchOps + soul: | + Coordinate through TeamHarness. Preserve planner, reviewer, and operator + separation. A Matrix message is never an execution approval. + agents: | + Turn a research request into a bounded project DAG. Delegate only through + TeamHarness, require ACK and artifacts, and stop at the human R2 gate. + state: Running +--- +apiVersion: agentteams.io/v1beta1 +kind: Worker +metadata: + name: ego-architect +spec: + model: "${AGENTTEAMS_MODEL}" + runtime: qwenpaw + identity: | + Name: Experiment Architect + Role: Produces falsifiable and resource-bounded experiment matrices + soul: | + Freeze baseline, splits, metrics, seeds, budget, falsification controls, + and rollback before asking for execution approval. + agents: | + Inspect the supplied input mode, design the experiment tree, and return a + deterministic workflow. Never approve or execute your own plan. + state: Running +--- +apiVersion: agentteams.io/v1beta1 +kind: Worker +metadata: + name: ego-reviewer +spec: + model: "${AGENTTEAMS_MODEL}" + runtime: qwenpaw + identity: | + Name: Independent Reviewer + Role: Challenges scientific claims, provenance, and compute plans + soul: | + Try to falsify success. Missing evidence stays missing even after a human + agrees. Reject duplicated preprocessing, idle compute, and unsafe scope. + agents: | + Review plans and evidence independently. Never mutate or execute the work + under review; emit explicit PASS, REVISE, or BLOCKED findings. + state: Running +--- +apiVersion: agentteams.io/v1beta1 +kind: Worker +metadata: + name: ego-memory-curator +spec: + model: "${AGENTTEAMS_MODEL}" + runtime: qwenpaw + identity: | + Name: Memory Curator + Role: Maintains compact, evidence-linked research context + soul: | + A model guess is not memory. Preserve conflicts and failure patterns, and + compact only after a small stage has a durable evidence reference. + agents: | + Produce concise stage summaries for the agent-owned Markdown and database + record. Refuse unvalidated facts and keep source and context versions. + state: Running +--- +apiVersion: agentteams.io/v1beta1 +kind: Team +metadata: + name: ego-researchops +spec: + description: Four-agent, no-GPU ResearchOps readiness team for EgoAgentOS + teamName: ego-researchops + workerMembers: + - name: ego-research-lead + role: team_leader + - name: ego-architect + role: worker + - name: ego-reviewer + role: worker + - name: ego-memory-curator + role: worker + peerMentions: true + heartbeatEvery: 5m +--- +apiVersion: agentteams.io/v1beta1 +kind: Human +metadata: + name: ego-judge +spec: + displayName: EgoAgentOS Judge + username: ego-judge + permissionLevel: 2 + accessibleTeams: [ego-researchops] + accessibleWorkers: [] + note: Least-privilege Matrix user for the local semifinal acceptance path diff --git a/integrations/agentteams/readiness-workflow.json b/integrations/agentteams/readiness-workflow.json new file mode 100644 index 0000000..db50c91 --- /dev/null +++ b/integrations/agentteams/readiness-workflow.json @@ -0,0 +1,50 @@ +[ + { + "taskId": "intake", + "title": "Classify the input mode and freeze the research objective", + "assignedRole": "ego-research-lead", + "dependsOn": [] + }, + { + "taskId": "experiment-matrix", + "title": "Build the falsifiable experiment tree, controls, and budget", + "assignedRole": "ego-architect", + "dependsOn": ["intake"] + }, + { + "taskId": "plan-review", + "title": "Independently review leakage, duplication, resource use, and stop conditions", + "assignedRole": "ego-reviewer", + "dependsOn": ["experiment-matrix"] + }, + { + "taskId": "human-r2", + "title": "Wait for a scoped one-use EgoAgentOS R2 grant", + "assignedRole": "ego-research-lead", + "dependsOn": ["plan-review"] + }, + { + "taskId": "gpu-execution", + "title": "Execute only after the GPU Worker and allowlisted MCP tools are attached", + "assignedRole": "ego-research-lead", + "dependsOn": ["human-r2"] + }, + { + "taskId": "deterministic-evaluation", + "title": "Evaluate raw artifacts with the frozen metric implementation", + "assignedRole": "ego-reviewer", + "dependsOn": ["gpu-execution"] + }, + { + "taskId": "memory-compact", + "title": "Commit validated findings and compact each agent context", + "assignedRole": "ego-memory-curator", + "dependsOn": ["deterministic-evaluation"] + }, + { + "taskId": "decision", + "title": "Issue KEEP, REJECT, or REVISE from independent evidence review", + "assignedRole": "ego-reviewer", + "dependsOn": ["memory-compact"] + } +] diff --git a/scripts/deploy_local_live_stack.py b/scripts/deploy_local_live_stack.py new file mode 100644 index 0000000..9e8ffa2 --- /dev/null +++ b/scripts/deploy_local_live_stack.py @@ -0,0 +1,1166 @@ +#!/usr/bin/env python3 +"""Deploy and verify the local official AgentTeams + EgoAgentOS live stack. + +Secret values are accepted through a masked prompt or an existing process +environment, persisted only below the gitignored ``.runtime`` directory with +mode 0600, and never printed by this wrapper. +""" + +from __future__ import annotations + +import argparse +import getpass +import hashlib +import json +import os +import re +import secrets +import shutil +import socket +import subprocess +import sys +import time +import urllib.error +import urllib.parse +import urllib.request +from datetime import datetime, timezone +from pathlib import Path +from typing import Any, Dict, Iterable, Mapping, Optional, Sequence, Tuple + + +ROOT = Path(__file__).resolve().parents[1] +RUNTIME = ROOT / ".runtime" +PRIVATE_ENV = RUNTIME / "live-stack.env" +OFFICIAL_ENV = RUNTIME / "agentteams-manager.env" +OFFICIAL_ROOT = RUNTIME / "AgentTeams" +OFFICIAL_REPOSITORY = "https://github.com/agentscope-ai/AgentTeams.git" +OFFICIAL_TAG = "v1.2.3" +OFFICIAL_COMMIT = "223ddc2b8073e4c8b93bcbb15e1d717f196c04d9" +BASE_RUNTIME_IMAGE = "python:3.11-slim" +INSTALL_LOG = RUNTIME / "agentteams-bootstrap.log" +APPLY_LOG = RUNTIME / "agentteams-apply.log" +PUBLIC_MANIFEST = RUNTIME / "live-stack-public.json" +RENDERED_RESOURCES = RUNTIME / "agentteams-resources.yaml" +RESOURCE_TEMPLATE = ROOT / "integrations/agentteams/local-ready-team.yaml.tmpl" +WORKFLOW_TEMPLATE = ROOT / "integrations/agentteams/readiness-workflow.json" + +CONTROLLER_URL = "http://127.0.0.1:18090" +CONTROLLER_DOCKER_URL = "http://agentteams-controller:8090" +MATRIX_URL = "http://127.0.0.1:18080" +MATRIX_DOCKER_URL = "http://agentteams-controller:6167" +EGO_API_URL = "http://127.0.0.1:8000" +BRIDGE_URL = "http://127.0.0.1:8020" +MODEL_BASE_URL = "https://apihub.agnes-ai.com/v1" +MODEL_NAME = "agnes-2.5-pro" +TEAM_NAME = "ego-researchops" +HUMAN_NAME = "ego-judge" +PROJECT_ID = "egoagentos-gpu-gated-v1" +GPU_PAUSE_REASON = "GPU Worker intentionally not attached yet" +WORKERS = ( + "ego-research-lead", + "ego-architect", + "ego-reviewer", + "ego-memory-curator", +) +DIRECT_HTTP = urllib.request.build_opener(urllib.request.ProxyHandler({})) + + +class DeploymentError(RuntimeError): + """An actionable, secret-free deployment failure.""" + + +def _utc_now() -> str: + return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z") + + +def _private_directory(path: Path) -> None: + path.mkdir(parents=True, exist_ok=True) + path.chmod(0o700) + + +def _validate_env_value(name: str, value: str) -> None: + if not value: + raise DeploymentError("%s must not be empty" % name) + if any(character in value for character in ("\n", "\r", "\0")): + raise DeploymentError("%s contains an unsafe control character" % name) + + +def _read_env(path: Path) -> Dict[str, str]: + values: Dict[str, str] = {} + if not path.exists(): + return values + for raw_line in path.read_text(encoding="utf-8").splitlines(): + line = raw_line.strip() + if not line or line.startswith("#") or "=" not in line: + continue + name, value = line.split("=", 1) + values[name.strip()] = value + return values + + +def _write_private_env(path: Path, values: Mapping[str, str]) -> None: + _private_directory(path.parent) + for name, value in values.items(): + _validate_env_value(name, value) + payload = "# Generated locally; never commit or paste this file.\n" + payload += "\n".join("%s=%s" % item for item in sorted(values.items())) + "\n" + temporary = path.with_name(".%s.%s.tmp" % (path.name, secrets.token_hex(8))) + try: + descriptor = os.open(temporary, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) + try: + encoded = payload.encode("utf-8") + written = 0 + while written < len(encoded): + chunk_size = os.write(descriptor, encoded[written:]) + if chunk_size <= 0: + raise OSError("private env write made no progress") + written += chunk_size + os.fsync(descriptor) + finally: + os.close(descriptor) + os.replace(temporary, path) + path.chmod(0o600) + finally: + temporary.unlink(missing_ok=True) + + +def _secret() -> str: + return secrets.token_hex(32) + + +def _ensure_private_env() -> Dict[str, str]: + values = _read_env(PRIVATE_ENV) + model_key = values.get("AGENTTEAMS_LLM_API_KEY") or os.getenv("AGENTTEAMS_LLM_API_KEY", "") + if not model_key: + model_key = getpass.getpass("Agnes/OpenAI-compatible API key (hidden): ").strip() + _validate_env_value("AGENTTEAMS_LLM_API_KEY", model_key) + + generated = { + "AGENTTEAMS_ADMIN_PASSWORD": _secret(), + "EGO_POSTGRES_PASSWORD": _secret(), + "EGO_RUNTIME_PASSWORD": _secret(), + "EGO_AGENTTEAMS_RUNTIME_PASSWORD": _secret(), + "EGO_OPERATOR_KEY": _secret(), + "EGO_AGENTTEAMS_BRIDGE_OPERATOR_KEY": _secret(), + "EGO_MCP_APPROVAL_HMAC_SECRET": _secret(), + "EGO_TRUSTED_MEMORY_SERVICE_TOKEN": _secret(), + } + for name, value in generated.items(): + values.setdefault(name, value) + + values.update( + { + "AGENTTEAMS_LLM_API_KEY": model_key, + "AGENTTEAMS_CONTROLLER_URL": CONTROLLER_DOCKER_URL, + "AGENTTEAMS_CONTROLLER_PORT": "18090", + "AGENTTEAMS_MATRIX_URL": MATRIX_DOCKER_URL, + "AGENTTEAMS_MODEL": MODEL_NAME, + "EGO_AGENT_MODEL_BASE_URL": MODEL_BASE_URL, + "EGO_AGENT_MODEL_API_KEY": model_key, + "EGO_AGENT_MODEL": MODEL_NAME, + "EGO_POSTGRES_DB": "egoagentos", + "EGO_POSTGRES_USER": "egoagentos_owner", + "EGO_POSTGRES_PORT": "5432", + "EGO_RUNTIME_USER": "egoagentos_api_runtime", + "EGO_AGENTTEAMS_RUNTIME_USER": "egoagentos_bridge_runtime_login", + "EGO_API_PORT": "8000", + "EGO_WEB_PORT": "4173", + "EGO_AGENTTEAMS_BRIDGE_PORT": "8020", + "EGO_BASE_IMAGE_PULL_POLICY": "never", + "EGO_TENANT_ID": "local-live", + "EGO_OPERATOR_ID": "local.operator", + "EGO_ALLOW_UNAUTHENTICATED_DEMO": "false", + "EGO_FOCUS_MEMORY_MODE": "required", + "EGO_CORS_ORIGINS": ( + "http://localhost:4173,http://127.0.0.1:4173,https://mythrise.github.io" + ), + "NO_PROXY": "127.0.0.1,localhost,agentteams-controller", + "no_proxy": "127.0.0.1,localhost,agentteams-controller", + } + ) + + independent = [values[name] for name in generated] + if len(set(independent)) != len(independent): + raise DeploymentError("generated operator and database secrets are not independent") + if values["EGO_OPERATOR_KEY"] == values["EGO_AGENTTEAMS_BRIDGE_OPERATOR_KEY"]: + raise DeploymentError("API and Bridge operator keys must be different") + _write_private_env(PRIVATE_ENV, values) + return values + + +def _run( + command: Sequence[str], + *, + env: Optional[Mapping[str, str]] = None, + input_bytes: Optional[bytes] = None, + check: bool = True, +) -> subprocess.CompletedProcess[bytes]: + result = subprocess.run( + list(command), + cwd=ROOT, + env=dict(env) if env is not None else None, + input=input_bytes, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=False, + ) + if check and result.returncode != 0: + error = result.stderr.decode("utf-8", errors="replace").strip() + raise DeploymentError("command failed (%s): %s" % (command[0], error[-1200:])) + return result + + +def _run_to_private_log( + command: Sequence[str], + *, + env: Mapping[str, str], + log_path: Path, +) -> None: + _private_directory(log_path.parent) + descriptor = os.open(log_path, os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o600) + try: + with os.fdopen(descriptor, "ab", closefd=True) as stream: + result = subprocess.run( + list(command), + cwd=ROOT, + env=dict(env), + stdout=stream, + stderr=subprocess.STDOUT, + check=False, + ) + finally: + log_path.chmod(0o600) + if result.returncode != 0: + raise DeploymentError( + "%s failed; inspect the protected log at %s" % (Path(command[0]).name, log_path) + ) + + +def _docker_names(*, running_only: bool = False) -> set[str]: + command = ["docker", "ps"] + if not running_only: + command.append("-a") + command.extend(["--format", "{{.Names}}"]) + output = _run(command).stdout.decode("utf-8") + return {line.strip() for line in output.splitlines() if line.strip()} + + +def _port_available(port: int) -> bool: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as probe: + return probe.connect_ex(("127.0.0.1", port)) != 0 + + +def _ensure_base_runtime_image() -> None: + inspected = _run( + ["docker", "image", "inspect", BASE_RUNTIME_IMAGE], + check=False, + ) + if inspected.returncode == 0: + return + print("[prepare] pulling the shared Python runtime image", flush=True) + _run(["docker", "pull", BASE_RUNTIME_IMAGE]) + + +def _ensure_official_checkout() -> None: + if not OFFICIAL_ROOT.exists(): + print("[prepare] cloning official AgentTeams %s" % OFFICIAL_TAG, flush=True) + _private_directory(RUNTIME) + _run( + [ + "git", + "clone", + "--depth", + "1", + "--branch", + OFFICIAL_TAG, + OFFICIAL_REPOSITORY, + str(OFFICIAL_ROOT), + ] + ) + actual = ( + _run(["git", "-C", str(OFFICIAL_ROOT), "rev-parse", "HEAD"]).stdout.decode("utf-8").strip() + ) + if actual != OFFICIAL_COMMIT: + raise DeploymentError( + "official checkout drift: expected %s, got %s" % (OFFICIAL_COMMIT, actual) + ) + dirty = ( + _run(["git", "-C", str(OFFICIAL_ROOT), "status", "--porcelain"]) + .stdout.decode("utf-8") + .strip() + ) + if dirty: + raise DeploymentError("official checkout has local modifications") + + +def _render_resources() -> None: + template = RESOURCE_TEMPLATE.read_text(encoding="utf-8") + rendered = template.replace("${AGENTTEAMS_MODEL}", MODEL_NAME) + unresolved = re.findall(r"\$\{[A-Z][A-Z0-9_]*\}", rendered) + if unresolved: + raise DeploymentError("unresolved resource placeholders: %s" % unresolved) + RENDERED_RESOURCES.write_text(rendered, encoding="utf-8") + RENDERED_RESOURCES.chmod(0o644) + + +def prepare() -> None: + if shutil.which("docker") is None: + raise DeploymentError("Docker is required") + _run(["docker", "info"]) + _private_directory(RUNTIME) + _ensure_base_runtime_image() + _ensure_official_checkout() + _render_resources() + for port in (5432, 8000, 8020, 18080, 18001, 18088, 18090, 18888): + if not _port_available(port): + name = "an existing official AgentTeams service" if port >= 18000 else "a process" + print("[prepare] port %d is already used by %s" % (port, name), flush=True) + print("[prepare] official contract pin and local resources are ready", flush=True) + + +def _official_install_env(private: Mapping[str, str]) -> Dict[str, str]: + workspace = RUNTIME / "agentteams-manager" + _private_directory(workspace) + environment = dict(os.environ) + environment.update( + { + # Docker Desktop stores its selected context below the real HOME. + # Changing HOME here makes a healthy daemon look unavailable. + "HOME": os.environ.get("HOME", str(Path.home())), + "AGENTTEAMS_NON_INTERACTIVE": "1", + "AGENTTEAMS_LANGUAGE": "en", + "AGENTTEAMS_VERSION": OFFICIAL_TAG, + "AGENTTEAMS_LLM_PROVIDER": "openai-compat", + "AGENTTEAMS_OPENAI_BASE_URL": MODEL_BASE_URL, + "AGENTTEAMS_LLM_API_KEY": private["AGENTTEAMS_LLM_API_KEY"], + "AGENTTEAMS_DEFAULT_MODEL": MODEL_NAME, + "AGENTTEAMS_EMBEDDING_MODEL": "", + "AGENTTEAMS_ADMIN_USER": "ego-admin", + "AGENTTEAMS_ADMIN_PASSWORD": private["AGENTTEAMS_ADMIN_PASSWORD"], + "AGENTTEAMS_LOCAL_ONLY": "1", + "AGENTTEAMS_PORT_GATEWAY": "18080", + "AGENTTEAMS_PORT_CONSOLE": "18001", + "AGENTTEAMS_PORT_ELEMENT_WEB": "18088", + "AGENTTEAMS_PORT_MANAGER_CONSOLE": "18888", + "AGENTTEAMS_MATRIX_DOMAIN": "matrix-local.agentteams.io:18080", + "AGENTTEAMS_MATRIX_CLIENT_DOMAIN": "matrix-client-local.agentteams.io", + "AGENTTEAMS_AI_GATEWAY_DOMAIN": "aigw-local.agentteams.io", + "AGENTTEAMS_FS_DOMAIN": "fs-local.agentteams.io", + "AGENTTEAMS_CONSOLE_DOMAIN": "console-local.agentteams.io", + "AGENTTEAMS_MANAGER_RUNTIME": "qwenpaw", + "AGENTTEAMS_DEFAULT_WORKER_RUNTIME": "qwenpaw", + "AGENTTEAMS_MATRIX_E2EE": "0", + "AGENTTEAMS_MOUNT_SOCKET": "1", + "AGENTTEAMS_DOCKER_PROXY": "1", + "AGENTTEAMS_DATA_DIR": "egoagentos-agentteams-data", + "AGENTTEAMS_WORKSPACE_DIR": str(workspace), + "AGENTTEAMS_HOST_SHARE_DIR": str(ROOT), + "AGENTTEAMS_WORKER_IDLE_TIMEOUT": "720", + "AGENTTEAMS_DASHBOARD": "0", + "AGENTTEAMS_UPGRADE_KEEP_ALL": "1", + "AGENTTEAMS_ENV_FILE": str(OFFICIAL_ENV), + "AGENTTEAMS_TIMEZONE": "Asia/Shanghai", + } + ) + return environment + + +def _installer_for_current_docker_context() -> Tuple[Path, bool]: + """Return the pristine installer, or a runtime-only Colima compatibility copy.""" + + official = OFFICIAL_ROOT / "install/agentteams-install.sh" + endpoint = ( + _run( + [ + "docker", + "context", + "inspect", + "--format", + "{{.Endpoints.docker.Host}}", + _run(["docker", "context", "show"]).stdout.decode("utf-8").strip(), + ] + ) + .stdout.decode("utf-8") + .strip() + ) + if ".colima/" not in endpoint: + return official, False + + source = official.read_text(encoding="utf-8") + marker = "detect_socket() {\n local socket_path\n" + if source.count(marker) != 1: + raise DeploymentError("official installer socket probe changed; refusing blind patch") + replacement = ( + marker + + """ + # EgoAgentOS local compatibility: Docker commands use the host Colima + # context, while bind-mount paths are resolved inside the Colima VM. + if [ -n "${AGENTTEAMS_CONTAINER_SOCKET_SOURCE:-}" ]; then + echo "${AGENTTEAMS_CONTAINER_SOCKET_SOURCE}" + return 0 + fi +""" + ) + patched = source.replace(marker, replacement, 1) + target = RUNTIME / "agentteams-install-colima.sh" + target.write_text(patched, encoding="utf-8") + target.chmod(0o700) + return target, True + + +def install_agentteams() -> None: + private = _ensure_private_env() + names = _docker_names() + official = {name for name in names if name.startswith("agentteams-")} + running = _docker_names(running_only=True) + if official: + if {"agentteams-controller", "agentteams-manager"}.issubset(running): + print("[install] official Controller and Manager already running", flush=True) + return + failed_bootstrap = official == {"agentteams-controller"} + if failed_bootstrap: + state = ( + _run( + ["docker", "inspect", "--format", "{{.State.Status}}", "agentteams-controller"] + ) + .stdout.decode("utf-8") + .strip() + ) + failed_bootstrap = state == "created" + if not failed_bootstrap: + raise DeploymentError( + "partial/stopped AgentTeams containers found; refusing automatic replacement: %s" + % ", ".join(sorted(official)) + ) + print( + "[install] replacing the failed Created-only Controller; data volume is preserved", + flush=True, + ) + conflicts = [port for port in (18080, 18001, 18088, 18888) if not _port_available(port)] + if conflicts: + raise DeploymentError("AgentTeams ports are occupied: %s" % conflicts) + print("[install] starting the official v1.2.3 local Docker installer", flush=True) + installer, colima_compat = _installer_for_current_docker_context() + official_log = Path(os.environ.get("HOME", str(Path.home()))) / "agentteams-install.log" + descriptor = os.open(official_log, os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o600) + os.close(descriptor) + official_log.chmod(0o600) + try: + install_environment = _official_install_env(private) + if colima_compat: + install_environment["AGENTTEAMS_CONTAINER_SOCKET_SOURCE"] = "/var/run/docker.sock" + _run_to_private_log( + ["bash", str(installer), "manager"], + env=install_environment, + log_path=INSTALL_LOG, + ) + finally: + if official_log.exists(): + official_log.chmod(0o600) + running = _docker_names(running_only=True) + if not {"agentteams-controller", "agentteams-manager"}.issubset(running): + raise DeploymentError("official installer exited without a running Controller and Manager") + print("[install] official Controller and Manager are running", flush=True) + + +def _capture_controller_token(timeout: int = 120) -> str: + deadline = time.monotonic() + timeout + while time.monotonic() < deadline: + result = _run( + [ + "docker", + "exec", + "agentteams-controller", + "sh", + "-c", + ( + "cat /var/run/agentteams/cli-token 2>/dev/null || " + "cat /var/run/hiclaw/cli-token 2>/dev/null" + ), + ], + check=False, + ) + token = result.stdout.decode("utf-8", errors="replace").strip() + if result.returncode == 0 and token: + return token + time.sleep(2) + raise DeploymentError("Controller service-account token was not minted within 120 seconds") + + +def _ensure_controller_proxy() -> None: + print("[configure] exposing the official Controller on localhost:18090", flush=True) + _run_to_private_log( + [ + "docker", + "compose", + "--env-file", + str(PRIVATE_ENV), + "--profile", + "agentteams", + "up", + "-d", + "agentteams-controller-proxy", + ], + env=os.environ, + log_path=RUNTIME / "controller-proxy.log", + ) + deadline = time.monotonic() + 90 + while time.monotonic() < deadline: + try: + with DIRECT_HTTP.open(CONTROLLER_URL + "/healthz", timeout=5) as response: + if response.status == 200 and response.read().strip() == b"ok": + return + except (OSError, urllib.error.URLError): + pass + time.sleep(2) + raise DeploymentError("Controller proxy did not expose /healthz within 90 seconds") + + +def _json_request( + base_url: str, + path: str, + *, + token: str = "", + method: str = "GET", + body: Optional[Any] = None, + expected: Iterable[int] = (200,), +) -> Tuple[int, Any]: + headers = {"Accept": "application/json"} + if token: + headers["Authorization"] = "Bearer %s" % token + data = None + if body is not None: + headers["Content-Type"] = "application/json" + data = json.dumps(body, separators=(",", ":")).encode("utf-8") + request = urllib.request.Request( + base_url.rstrip("/") + path, + data=data, + headers=headers, + method=method, + ) + accepted = set(expected) + try: + with DIRECT_HTTP.open(request, timeout=20) as response: + status = response.status + payload_bytes = response.read() + except urllib.error.HTTPError as error: + status = error.code + payload_bytes = error.read() + if status not in accepted: + message = payload_bytes.decode("utf-8", errors="replace")[-1000:] + raise DeploymentError("HTTP %s %s returned %d: %s" % (method, path, status, message)) + if not payload_bytes: + return status, {} + try: + return status, json.loads(payload_bytes) + except json.JSONDecodeError as error: + raise DeploymentError("HTTP %s returned malformed JSON" % path) from error + + +def _wait_json( + path: str, + token: str, + predicate: Any, + *, + description: str, + timeout: int = 720, +) -> Any: + deadline = time.monotonic() + timeout + last: Any = None + announced = 0.0 + while time.monotonic() < deadline: + try: + _, last = _json_request(CONTROLLER_URL, path, token=token) + if predicate(last): + return last + except DeploymentError as error: + last = {"error": str(error)} + if time.monotonic() - announced >= 15: + print("[wait] %s" % description, flush=True) + announced = time.monotonic() + time.sleep(3) + raise DeploymentError("timed out waiting for %s; last=%s" % (description, last)) + + +def _apply_resources(controller_token: str) -> None: + apply_script = OFFICIAL_ROOT / "install/agentteams-apply.sh" + resource_file = RENDERED_RESOURCES + human_status, _ = _json_request( + CONTROLLER_URL, + "/api/v1/humans/%s" % HUMAN_NAME, + token=controller_token, + expected=(200, 404), + ) + if human_status == 200: + documents = re.split(r"(?m)^---\s*$", RENDERED_RESOURCES.read_text(encoding="utf-8")) + documents = [document.strip() for document in documents if "kind: Human" not in document] + resource_file = RUNTIME / "agentteams-resources-existing-human.yaml" + resource_file.write_text("\n---\n".join(documents) + "\n", encoding="utf-8") + resource_file.chmod(0o644) + environment = dict(os.environ) + environment["AGENTTEAMS_ENV_FILE"] = str(OFFICIAL_ENV) + _run_to_private_log( + ["bash", str(apply_script), "-f", str(resource_file)], + env=environment, + log_path=APPLY_LOG, + ) + + +def _matrix_login(username: str, password: str) -> str: + login = { + "type": "m.login.password", + "identifier": {"type": "m.id.user", "user": username}, + "password": password, + } + result = _run( + [ + "docker", + "exec", + "-i", + "agentteams-controller", + "curl", + "-sf", + "-X", + "POST", + "-H", + "Content-Type: application/json", + "--data-binary", + "@-", + "http://127.0.0.1:6167/_matrix/client/v3/login", + ], + input_bytes=json.dumps(login).encode("utf-8"), + ) + try: + token = json.loads(result.stdout).get("access_token", "") + except json.JSONDecodeError as error: + raise DeploymentError("Matrix login returned malformed JSON") from error + if not token: + raise DeploymentError("Matrix login did not return an access token") + return str(token) + + +def _matrix_join(room_id: str, token: str) -> None: + quoted_room = urllib.parse.quote(room_id, safe="") + _json_request( + MATRIX_URL, + "/_matrix/client/v3/join/%s" % quoted_room, + token=token, + method="POST", + body={}, + ) + + +def _workflow_tasks(controller_token: str) -> list[Dict[str, Any]]: + matrix_ids: Dict[str, str] = {} + for worker in WORKERS: + _, payload = _json_request( + CONTROLLER_URL, "/api/v1/workers/%s" % worker, token=controller_token + ) + matrix_user_id = str(payload.get("matrixUserID", "")) + if not matrix_user_id: + raise DeploymentError("Worker %s has no Matrix identity" % worker) + matrix_ids[worker] = matrix_user_id + + source = json.loads(WORKFLOW_TEMPLATE.read_text(encoding="utf-8")) + tasks: list[Dict[str, Any]] = [] + for item in source: + role = item["assignedRole"] + if role not in matrix_ids: + raise DeploymentError("workflow references unknown role %s" % role) + tasks.append( + { + "taskId": item["taskId"], + "title": item["title"], + "assignedTo": matrix_ids[role], + "dependsOn": item["dependsOn"], + "status": "planned", + } + ) + return tasks + + +def _configure_project(controller_token: str, team: Mapping[str, Any]) -> Any: + room_id = str(team.get("teamRoomID", "")) + if not room_id: + raise DeploymentError("Team has no Matrix room") + path = "/api/v1/projects/%s/workflow?team=%s&includeTasks=true" % ( + PROJECT_ID, + urllib.parse.quote(TEAM_NAME), + ) + status, workflow = _json_request( + CONTROLLER_URL, path, token=controller_token, expected=(200, 404) + ) + if status == 404: + _, workflow = _json_request( + CONTROLLER_URL, + "/api/v1/projects", + token=controller_token, + method="POST", + body={ + "project_id": PROJECT_ID, + "title": "EgoAgentOS GPU-gated research acceptance", + "team_id": TEAM_NAME, + "source": "egoagentos-bootstrap", + "requester": "@%s:matrix-local.agentteams.io:18080" % HUMAN_NAME, + "source_room_id": room_id, + }, + expected=(201,), + ) + if workflow.get("status") == "paused": + _, workflow = _json_request( + CONTROLLER_URL, + "/api/v1/projects/%s/resume?team=%s" % (PROJECT_ID, TEAM_NAME), + token=controller_token, + method="POST", + ) + _, workflow = _json_request( + CONTROLLER_URL, + "/api/v1/projects/%s/replan?team=%s" % (PROJECT_ID, TEAM_NAME), + token=controller_token, + method="POST", + body={"tasks": _workflow_tasks(controller_token)}, + ) + _, workflow = _json_request( + CONTROLLER_URL, + "/api/v1/projects/%s/pause?team=%s" % (PROJECT_ID, TEAM_NAME), + token=controller_token, + method="POST", + body={"reason": GPU_PAUSE_REASON}, + ) + return workflow + + +def _sha256_secret(value: str) -> str: + return hashlib.sha256(value.encode("utf-8")).hexdigest() + + +def _optional_acceptance_summary() -> Dict[str, Any]: + summary: Dict[str, Any] = {} + matrix_path = RUNTIME / "matrix-live-smoke-result.json" + if matrix_path.is_file(): + try: + matrix = json.loads(matrix_path.read_text(encoding="utf-8")) + summary["matrix_multi_agent"] = { + "status": "PASS" if matrix.get("passed") is True else "PARTIAL", + "distinct_agent_senders": matrix.get("distinct_worker_senders", []), + "event_count": len(matrix.get("replies", [])), + "request_event_id": matrix.get("request_event_id"), + "gpu": matrix.get("gpu"), + } + except (OSError, json.JSONDecodeError, TypeError): + summary["matrix_multi_agent"] = {"status": "UNREADABLE"} + expert_path = RUNTIME / "ego-live-expert-run.json" + if expert_path.is_file(): + try: + expert = json.loads(expert_path.read_text(encoding="utf-8")) + summary["custom_input_experts"] = { + "status": str(expert.get("status", "UNKNOWN")).upper(), + "run_id": expert.get("run_id"), + "model": expert.get("provider", {}).get("model"), + "event_chain_valid": expert.get("event_chain_valid"), + "event_chain_sha256": expert.get("event_chain_sha256"), + "roles": [ + {"role": item.get("role"), "status": item.get("status")} + for item in expert.get("roles", []) + ], + "decision": expert.get("decision"), + } + except (OSError, json.JSONDecodeError, TypeError): + summary["custom_input_experts"] = {"status": "UNREADABLE"} + return summary + + +def _write_public_manifest( + private: Mapping[str, str], + *, + controller: Mapping[str, Any], + manager: Mapping[str, Any], + team: Mapping[str, Any], + human: Mapping[str, Any], + workers: Sequence[Mapping[str, Any]], + workflow: Mapping[str, Any], + compose: Optional[Mapping[str, Any]] = None, +) -> None: + controller_token = private["AGENTTEAMS_AUTH_TOKEN"] + matrix_token = private["AGENTTEAMS_MATRIX_ACCESS_TOKEN"] + payload = { + "schema": "egoagentos.local-live-stack/v1", + "generated_at": _utc_now(), + "truth": "LIVE_LOCAL", + "official_agentteams": { + "repository": OFFICIAL_REPOSITORY, + "tag": OFFICIAL_TAG, + "commit": OFFICIAL_COMMIT, + "installer_compatibility": { + "status": "APPLIED_RUNTIME_ONLY", + "reason": "Colima bind mounts resolve inside the Linux VM", + "socket_source": "/var/run/docker.sock", + "official_checkout_modified": False, + }, + "controller_url": CONTROLLER_URL, + "controller": controller, + "manager": manager, + "team": { + "name": team.get("name"), + "phase": team.get("phase"), + "leader_ready": team.get("leaderReady"), + "ready_workers": team.get("readyWorkers"), + "total_workers": team.get("totalWorkers"), + "ready_worker_resources": sum( + worker.get("phase") == "Running" for worker in workers + ), + "room_id": team.get("teamRoomID"), + }, + "workers": [ + { + "name": worker.get("name"), + "phase": worker.get("phase"), + "runtime": worker.get("runtime"), + "matrix_user_id": worker.get("matrixUserID"), + } + for worker in workers + ], + }, + "matrix": { + "url": MATRIX_URL, + "element_url": "http://127.0.0.1:18088/#/login", + "user": human.get("matrixUserID"), + "permission_level": human.get("permissionLevel"), + "team_room_id": team.get("teamRoomID"), + }, + "project": { + "id": PROJECT_ID, + "team": TEAM_NAME, + "status": workflow.get("status"), + "pause_reason": workflow.get("pause_reason"), + "workflow_url": ( + "%s/api/v1/projects/%s/workflow?team=%s&includeTasks=true" + % (CONTROLLER_URL, PROJECT_ID, TEAM_NAME) + ), + "workflow": workflow, + "tracked_template": str(WORKFLOW_TEMPLATE.relative_to(ROOT)), + }, + "egoagentos": { + "api_url": EGO_API_URL, + "api_docs": EGO_API_URL + "/docs", + "bridge_url": BRIDGE_URL, + "bridge_docs": BRIDGE_URL + "/docs", + "postgres_url": "postgresql://127.0.0.1:5432/egoagentos", + "compose": compose or {"status": "NOT_VERIFIED"}, + }, + "credentials": { + "stored_at": str(PRIVATE_ENV), + "mode": "0600", + "values_in_manifest": False, + "controller_token_sha256": _sha256_secret(controller_token), + "matrix_token_sha256": _sha256_secret(matrix_token), + "independent_operator_keys": ( + private["EGO_OPERATOR_KEY"] != private["EGO_AGENTTEAMS_BRIDGE_OPERATOR_KEY"] + ), + }, + "gpu": { + "status": "NOT_ATTACHED", + "reason": "GPU machine intentionally deferred by operator", + }, + "live_acceptance": _optional_acceptance_summary(), + } + PUBLIC_MANIFEST.write_text( + json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=True) + "\n", + encoding="utf-8", + ) + PUBLIC_MANIFEST.chmod(0o644) + + +def configure_agentteams() -> None: + private = _ensure_private_env() + controller_token = _capture_controller_token() + _ensure_controller_proxy() + print("[configure] applying four Workers, Team, and L2 Matrix user", flush=True) + _apply_resources(controller_token) + + manager = _wait_json( + "/api/v1/managers/default", + controller_token, + lambda item: item.get("phase") == "Running", + description="official Manager/default phase=Running", + ) + team = _wait_json( + "/api/v1/teams/%s" % TEAM_NAME, + controller_token, + lambda item: ( + item.get("phase") == "Active" + and item.get("leaderReady") is True + and item.get("readyWorkers") == len(WORKERS) - 1 + and item.get("totalWorkers") == len(WORKERS) - 1 + ), + description="Team active with 4/4 ready Workers", + ) + workers = [ + _wait_json( + "/api/v1/workers/%s" % worker, + controller_token, + lambda item: item.get("phase") == "Running" and bool(item.get("matrixUserID")), + description="Worker/%s phase=Running" % worker, + ) + for worker in WORKERS + ] + human = _wait_json( + "/api/v1/humans/%s" % HUMAN_NAME, + controller_token, + lambda item: ( + item.get("phase") == "Active" + and bool(item.get("matrixUserID")) + and bool(item.get("initialPassword")) + ), + description="L2 Human/ego-judge phase=Active", + ) + room_id = str(team["teamRoomID"]) + if room_id not in human.get("rooms", []): + raise DeploymentError("ego-judge was not joined to the Team room") + + matrix_token = _matrix_login(HUMAN_NAME, str(human["initialPassword"])) + _matrix_join(room_id, matrix_token) + _, matrix_identity = _json_request( + MATRIX_URL, + "/_matrix/client/v3/account/whoami", + token=matrix_token, + ) + if matrix_identity.get("user_id") != human.get("matrixUserID"): + raise DeploymentError("Matrix token identity does not match Human/ego-judge") + + private.update( + { + "AGENTTEAMS_AUTH_TOKEN": controller_token, + "AGENTTEAMS_MATRIX_ACCESS_TOKEN": matrix_token, + "AGENTTEAMS_MATRIX_USER_ID": str(human["matrixUserID"]), + "AGENTTEAMS_MATRIX_USER_PASSWORD": str(human["initialPassword"]), + "AGENTTEAMS_TEAM": TEAM_NAME, + "AGENTTEAMS_TEAM_ROOM_ID": room_id, + "AGENTTEAMS_PROJECT_ID": PROJECT_ID, + } + ) + _write_private_env(PRIVATE_ENV, private) + + _, controller = _json_request(CONTROLLER_URL, "/api/v1/version", token=controller_token) + workflow = _configure_project(controller_token, team) + _write_public_manifest( + private, + controller=controller, + manager=manager, + team=team, + human=human, + workers=workers, + workflow=workflow, + ) + print( + "[configure] 4/4 Worker resources ready; Matrix room and paused workflow created", + flush=True, + ) + + +def deploy_ego() -> None: + private = _ensure_private_env() + required = { + "AGENTTEAMS_AUTH_TOKEN", + "AGENTTEAMS_MATRIX_ACCESS_TOKEN", + "EGO_OPERATOR_KEY", + "EGO_AGENTTEAMS_BRIDGE_OPERATOR_KEY", + } + missing = sorted(required - set(private)) + if missing: + raise DeploymentError("configure AgentTeams before Compose; missing %s" % missing) + if shutil.which("npm") is None: + raise DeploymentError("npm is required to build the local web application") + web_root = ROOT / "apps/web" + if not (web_root / "node_modules").is_dir(): + print("[ego] installing locked web dependencies", flush=True) + _run_to_private_log( + ["npm", "--prefix", str(web_root), "ci"], + env=os.environ, + log_path=RUNTIME / "web-build.log", + ) + print("[ego] building current web assets", flush=True) + _run_to_private_log( + ["npm", "--prefix", str(web_root), "run", "build"], + env=os.environ, + log_path=RUNTIME / "web-build.log", + ) + if not (web_root / "dist/index.html").is_file(): + raise DeploymentError("web build completed without dist/index.html") + print("[ego] building API, PostgreSQL, web, and AgentTeams Bridge", flush=True) + _run_to_private_log( + [ + "docker", + "compose", + "--env-file", + str(PRIVATE_ENV), + "--profile", + "agentteams", + "build", + "backend", + "agentteams-bridge", + ], + env=os.environ, + log_path=RUNTIME / "ego-compose.log", + ) + _run_to_private_log( + [ + "docker", + "compose", + "--env-file", + str(PRIVATE_ENV), + "--profile", + "agentteams", + "up", + "-d", + ], + env=os.environ, + log_path=RUNTIME / "ego-compose.log", + ) + print("[ego] Compose services started", flush=True) + + +def _wait_http_json(url: str, timeout: int = 180) -> Any: + deadline = time.monotonic() + timeout + last = "not requested" + while time.monotonic() < deadline: + try: + with DIRECT_HTTP.open(url, timeout=15) as response: + payload = json.loads(response.read()) + if response.status == 200: + return payload + except (OSError, urllib.error.URLError, json.JSONDecodeError) as error: + last = type(error).__name__ + time.sleep(3) + raise DeploymentError("timed out waiting for %s (%s)" % (url, last)) + + +def verify() -> None: + private = _read_env(PRIVATE_ENV) + controller_token = private.get("AGENTTEAMS_AUTH_TOKEN", "") + matrix_token = private.get("AGENTTEAMS_MATRIX_ACCESS_TOKEN", "") + if not controller_token or not matrix_token: + raise DeploymentError("live tokens are missing from the private runtime env") + _, controller = _json_request(CONTROLLER_URL, "/api/v1/version", token=controller_token) + _, manager = _json_request(CONTROLLER_URL, "/api/v1/managers/default", token=controller_token) + _, team = _json_request(CONTROLLER_URL, "/api/v1/teams/%s" % TEAM_NAME, token=controller_token) + workers = [] + for worker in WORKERS: + _, payload = _json_request( + CONTROLLER_URL, "/api/v1/workers/%s" % worker, token=controller_token + ) + workers.append(payload) + _, human = _json_request( + CONTROLLER_URL, "/api/v1/humans/%s" % HUMAN_NAME, token=controller_token + ) + _, workflow = _json_request( + CONTROLLER_URL, + "/api/v1/projects/%s/workflow?team=%s&includeTasks=true" % (PROJECT_ID, TEAM_NAME), + token=controller_token, + ) + _, matrix_identity = _json_request( + MATRIX_URL, "/_matrix/client/v3/account/whoami", token=matrix_token + ) + _, joined_rooms = _json_request( + MATRIX_URL, "/_matrix/client/v3/joined_rooms", token=matrix_token + ) + api_health = _wait_http_json(EGO_API_URL + "/api/v1/health") + web_api_health = _wait_http_json("http://127.0.0.1:4173/api/v1/health") + bridge_health = _wait_http_json(BRIDGE_URL + "/api/v1/agentteams/health?team=" + TEAM_NAME) + postgres = ( + _run( + [ + "docker", + "compose", + "--env-file", + str(PRIVATE_ENV), + "exec", + "-T", + "postgres", + "psql", + "-U", + "egoagentos_owner", + "-d", + "egoagentos", + "-tAc", + "select current_database() || ':' || current_user;", + ] + ) + .stdout.decode("utf-8") + .strip() + ) + if postgres != "egoagentos:egoagentos_owner": + raise DeploymentError("PostgreSQL identity check failed") + + checks = { + "api_health": api_health, + "bridge_live": bridge_health.get("live") is True, + "matrix_user": matrix_identity.get("user_id"), + "matrix_team_room_joined": team.get("teamRoomID") in joined_rooms.get("joined_rooms", []), + "web_same_origin_api": web_api_health.get("status") == "ok", + "postgres_identity": postgres, + "verified_at": _utc_now(), + } + expected = ( + manager.get("phase") == "Running" + and team.get("phase") == "Active" + and team.get("leaderReady") is True + and team.get("readyWorkers") == len(WORKERS) - 1 + and team.get("totalWorkers") == len(WORKERS) - 1 + and all(worker.get("phase") == "Running" for worker in workers) + and human.get("permissionLevel") == 2 + and TEAM_NAME in human.get("accessibleTeams", []) + and team.get("teamRoomID") in joined_rooms.get("joined_rooms", []) + and web_api_health.get("status") == "ok" + and workflow.get("status") == "paused" + and workflow.get("pause_reason") == GPU_PAUSE_REASON + and bridge_health.get("live") is True + ) + if not expected: + raise DeploymentError("one or more live acceptance invariants failed") + _write_public_manifest( + private, + controller=controller, + manager=manager, + team=team, + human=human, + workers=workers, + workflow=workflow, + compose={"status": "PASS", "checks": checks}, + ) + print("[verify] LIVE_LOCAL acceptance passed; secrets remain in .runtime", flush=True) + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "command", + choices=( + "prepare", + "install-agentteams", + "configure-agentteams", + "deploy-ego", + "verify", + "all", + ), + ) + args = parser.parse_args() + try: + if args.command in {"prepare", "all"}: + prepare() + if args.command in {"install-agentteams", "all"}: + install_agentteams() + if args.command in {"configure-agentteams", "all"}: + configure_agentteams() + if args.command in {"deploy-ego", "all"}: + deploy_ego() + if args.command in {"verify", "all"}: + verify() + except (DeploymentError, OSError, ValueError) as error: + print("ERROR: %s" % error, file=sys.stderr) + return 2 + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/agentteams/test_local_deployment_manifest.py b/tests/agentteams/test_local_deployment_manifest.py new file mode 100644 index 0000000..86febdc --- /dev/null +++ b/tests/agentteams/test_local_deployment_manifest.py @@ -0,0 +1,214 @@ +from __future__ import annotations + +import json +import subprocess +from pathlib import Path + +import pytest + +from scripts import deploy_local_live_stack as deployer + + +ROOT = Path(__file__).resolve().parents[2] + + +def test_local_ready_manifest_has_exactly_four_no_gpu_workers() -> None: + manifest = (ROOT / "integrations/agentteams/local-ready-team.yaml.tmpl").read_text( + encoding="utf-8" + ) + assert manifest.count("kind: Worker") == 4 + assert "name: ego-research-lead" in manifest + assert "name: ego-architect" in manifest + assert "name: ego-reviewer" in manifest + assert "name: ego-memory-curator" in manifest + assert "name: ego-runtime" not in manifest + assert "name: ego-evaluator" not in manifest + assert "permissionLevel: 2" in manifest + assert "accessibleTeams: [ego-researchops]" in manifest + + +def test_readiness_workflow_is_acyclic_and_gpu_gated() -> None: + tasks = json.loads( + (ROOT / "integrations/agentteams/readiness-workflow.json").read_text(encoding="utf-8") + ) + ids = [task["taskId"] for task in tasks] + assert len(ids) == len(set(ids)) + seen = set() + for task in tasks: + assert set(task["dependsOn"]).issubset(seen) + seen.add(task["taskId"]) + assert ids.index("human-r2") < ids.index("gpu-execution") + assert ids.index("gpu-execution") < ids.index("deterministic-evaluation") + assert tasks[-1]["assignedRole"] == "ego-reviewer" + + +def test_runtime_directory_is_gitignored() -> None: + ignored = (ROOT / ".gitignore").read_text(encoding="utf-8").splitlines() + assert ".runtime/" in ignored + + +def test_local_deployment_uses_agnes_pro_model() -> None: + script = (ROOT / "scripts/deploy_local_live_stack.py").read_text(encoding="utf-8") + compose = (ROOT / "docker-compose.yml").read_text(encoding="utf-8") + env_example = (ROOT / ".env.example").read_text(encoding="utf-8") + assert 'MODEL_NAME = "agnes-2.5-pro"' in script + assert "EGO_AGENT_MODEL:-agnes-2.5-pro" in compose + assert "EGO_AGENT_MODEL=agnes-2.5-pro" in env_example + assert "AGENTTEAMS_CONTROLLER_PORT=18090" in env_example + assert "http://agentteams-controller:8090" in env_example + + +def test_deployer_preserves_real_home_for_docker_context() -> None: + script = (ROOT / "scripts/deploy_local_live_stack.py").read_text(encoding="utf-8") + assert '"HOME": os.environ.get("HOME", str(Path.home()))' in script + assert '"HOME": str(runtime_home)' not in script + assert "official_log.chmod(0o600)" in script + assert '"/var/run/docker.sock"' in script + assert "agentteams-install-colima.sh" in script + assert 'official_checkout_modified": False' in script + assert '"AGENTTEAMS_UPGRADE_KEEP_ALL": "1"' in script + + +def test_official_checkout_must_match_pin_and_be_clean( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + checkout = tmp_path / "AgentTeams" + subprocess.run(["git", "init", "-q", str(checkout)], check=True) + subprocess.run( + ["git", "-C", str(checkout), "config", "user.email", "test@example.invalid"], check=True + ) + subprocess.run(["git", "-C", str(checkout), "config", "user.name", "Test"], check=True) + (checkout / "README.md").write_text("official\n", encoding="utf-8") + subprocess.run(["git", "-C", str(checkout), "add", "README.md"], check=True) + subprocess.run(["git", "-C", str(checkout), "commit", "-qm", "fixture"], check=True) + commit = subprocess.check_output( + ["git", "-C", str(checkout), "rev-parse", "HEAD"], text=True + ).strip() + monkeypatch.setattr(deployer, "OFFICIAL_ROOT", checkout) + monkeypatch.setattr(deployer, "OFFICIAL_COMMIT", "0" * 40) + with pytest.raises(deployer.DeploymentError, match="official checkout drift"): + deployer._ensure_official_checkout() + + monkeypatch.setattr(deployer, "OFFICIAL_COMMIT", commit) + + deployer._ensure_official_checkout() + (checkout / "README.md").write_text("modified\n", encoding="utf-8") + with pytest.raises(deployer.DeploymentError, match="local modifications"): + deployer._ensure_official_checkout() + + +def test_base_runtime_image_is_pulled_only_when_missing( + monkeypatch: pytest.MonkeyPatch, +) -> None: + commands: list[tuple[str, ...]] = [] + + def missing_then_pull(command, **_kwargs): + commands.append(tuple(command)) + return subprocess.CompletedProcess( + command, + 1 if command[:3] == ["docker", "image", "inspect"] else 0, + stdout=b"", + stderr=b"", + ) + + monkeypatch.setattr(deployer, "_run", missing_then_pull) + deployer._ensure_base_runtime_image() + assert commands == [ + ("docker", "image", "inspect", "python:3.11-slim"), + ("docker", "pull", "python:3.11-slim"), + ] + + commands.clear() + monkeypatch.setattr( + deployer, + "_run", + lambda command, **_kwargs: subprocess.CompletedProcess( + command, 0, stdout=b"", stderr=b"" + ), + ) + deployer._ensure_base_runtime_image() + assert commands == [] + + +def test_private_env_write_is_atomic_and_owner_only(tmp_path: Path) -> None: + target = tmp_path / "runtime" / "live-stack.env" + deployer._write_private_env(target, {"TOKEN": "opaque-test-value"}) + + assert deployer._read_env(target) == {"TOKEN": "opaque-test-value"} + assert target.stat().st_mode & 0o777 == 0o600 + assert list(target.parent.glob(".*.tmp")) == [] + + +def test_private_env_rejects_control_characters(tmp_path: Path) -> None: + with pytest.raises(deployer.DeploymentError, match="unsafe control character"): + deployer._write_private_env( + tmp_path / "live-stack.env", + {"TOKEN": "unsafe\nvalue"}, + ) + + +def test_private_env_atomic_failure_preserves_previous_file( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + target = tmp_path / "runtime" / "live-stack.env" + deployer._write_private_env(target, {"TOKEN": "original-value"}) + + def fail_replace(_source, _target): + raise OSError("simulated replace failure") + + monkeypatch.setattr(deployer.os, "replace", fail_replace) + with pytest.raises(OSError, match="simulated replace failure"): + deployer._write_private_env(target, {"TOKEN": "replacement-value"}) + + assert deployer._read_env(target) == {"TOKEN": "original-value"} + assert list(target.parent.glob(".*.tmp")) == [] + + +def test_compose_exposes_controller_without_routing_it_through_higress() -> None: + compose = (ROOT / "docker-compose.yml").read_text(encoding="utf-8") + proxy = (ROOT / "deploy/agentteams/controller_proxy.py").read_text(encoding="utf-8") + assert "agentteams-controller-proxy:" in compose + assert "agentteams-net" in compose + assert "AGENTTEAMS_CONTROLLER_PORT:-18090" in compose + assert 'UPSTREAM_HOST = "agentteams-controller"' in proxy + assert "UPSTREAM_PORT = 8090" in proxy + assert '"127.0.0.1:${AGENTTEAMS_CONTROLLER_PORT:-18090}:8080"' in compose + + +def test_local_http_acceptance_bypasses_system_proxy() -> None: + script = (ROOT / "scripts/deploy_local_live_stack.py").read_text(encoding="utf-8") + assert "urllib.request.ProxyHandler({})" in script + assert "DIRECT_HTTP.open" in script + + +def test_redeploy_reuses_existing_human_resource() -> None: + script = (ROOT / "scripts/deploy_local_live_stack.py").read_text(encoding="utf-8") + assert "agentteams-resources-existing-human.yaml" in script + assert "if human_status == 200:" in script + assert 'if "kind: Human" not in document' in script + assert "_matrix_join(room_id, matrix_token)" in script + assert '"matrix_team_room_joined"' in script + assert '"web_same_origin_api"' in script + assert '"live_acceptance": _optional_acceptance_summary()' in script + + +def test_local_compose_reuses_available_python_runtime_and_current_web_build() -> None: + compose = (ROOT / "docker-compose.yml").read_text(encoding="utf-8") + api_dockerfile = (ROOT / "apps/api/Dockerfile").read_text(encoding="utf-8") + bridge_dockerfile = (ROOT / "apps/agentteams_bridge/Dockerfile").read_text(encoding="utf-8") + web_server = (ROOT / "deploy/web/local_server.py").read_text(encoding="utf-8") + assert "FROM python:3.11-slim" in api_dockerfile + assert "COPY integrations ./integrations" in api_dockerfile + assert "FROM python:3.11-slim" in bridge_dockerfile + assert "./apps/web/dist:/usr/share/egoagentos:ro" in compose + assert "EGO_ARTIFACT_ROOT: /data/artifacts" in compose + assert "egoagentos-artifacts:/data" in compose + assert "api-storage-init:" in compose + assert "chown -R 100:101 /var/lib/egoagentos/agent-memory /data" in compose + assert 'BACKEND_HOST = os.environ.get("EGO_WEB_BACKEND_HOST", "backend")' in web_server + assert 'self.path = "/index.html"' in web_server + assert compose.count("image: egoagentos-local-api:latest") == 2 + assert compose.count("image: egoagentos-local-agentteams-bridge:latest") == 2 + assert '"build",\n "backend",\n "agentteams-bridge"' in ( + ROOT / "scripts/deploy_local_live_stack.py" + ).read_text(encoding="utf-8") diff --git a/tests/api/test_api.py b/tests/api/test_api.py index a759da2..2b21f64 100644 --- a/tests/api/test_api.py +++ b/tests/api/test_api.py @@ -1,3 +1,5 @@ +import io +import json import sqlite3 from pathlib import Path @@ -52,7 +54,7 @@ def test_health_cors_and_truthful_integrations(client: TestClient) -> None: assert health.json()["mode"] == "deterministic-local" integrations = client.get("/api/v1/integrations").json() - assert integrations["mode"] == "adapter_metadata_only" + assert integrations["mode"] == "verified_handshake_or_metadata" assert "ready" not in {item["status"] for item in integrations["items"]} preflight = client.options( @@ -66,6 +68,90 @@ def test_health_cors_and_truthful_integrations(client: TestClient) -> None: assert preflight.headers["access-control-allow-origin"] == "http://localhost:4173" +def test_agentteams_integration_requires_a_live_bridge_handshake( + client: TestClient, monkeypatch: pytest.MonkeyPatch +) -> None: + class Response(io.BytesIO): + status = 200 + + def __enter__(self): + return self + + def __exit__(self, *_args): + self.close() + + payload = { + "live": True, + "team": { + "name": "ego-researchops", + "phase": "Active", + "leaderReady": True, + "readyWorkers": 3, + "totalWorkers": 3, + }, + } + monkeypatch.setenv("EGO_HICLAW_URL", "http://127.0.0.1:18090/bridge-health") + monkeypatch.setattr( + "apps.api.service.DIRECT_HTTP.open", + lambda *_args, **_kwargs: Response(json.dumps(payload).encode()), + ) + + integrations = client.get("/api/v1/integrations").json() + agentteams = next(item for item in integrations["items"] if item["id"] == "hiclaw") + assert agentteams["status"] == "ready" + assert "leaderReady=True" in agentteams["detail"] + + +@pytest.mark.parametrize( + "payload", + [ + [], + {"live": True, "team": []}, + {"live": False, "team": {"name": "ego-researchops"}}, + ], +) +def test_agentteams_integration_fails_closed_on_invalid_bridge_payload( + client: TestClient, monkeypatch: pytest.MonkeyPatch, payload: object +) -> None: + class Response(io.BytesIO): + status = 200 + + def __enter__(self): + return self + + def __exit__(self, *_args): + self.close() + + monkeypatch.setenv("EGO_HICLAW_URL", "http://127.0.0.1:18090/bridge-health") + monkeypatch.setattr( + "apps.api.service.DIRECT_HTTP.open", + lambda *_args, **_kwargs: Response(json.dumps(payload).encode()), + ) + + integrations = client.get("/api/v1/integrations").json() + agentteams = next(item for item in integrations["items"] if item["id"] == "hiclaw") + assert agentteams["status"] == "unavailable" + assert "ready" not in {item["status"] for item in integrations["items"]} + + +def test_agentteams_integration_is_unavailable_when_bridge_cannot_be_reached( + client: TestClient, monkeypatch: pytest.MonkeyPatch +) -> None: + def unreachable(*_args, **_kwargs): + raise OSError("offline") + + monkeypatch.setenv("EGO_HICLAW_URL", "http://127.0.0.1:18090/bridge-health") + monkeypatch.setattr( + "apps.api.service.DIRECT_HTTP.open", + unreachable, + ) + + integrations = client.get("/api/v1/integrations").json() + agentteams = next(item for item in integrations["items"] if item["id"] == "hiclaw") + assert agentteams["status"] == "unavailable" + assert agentteams["endpoint_configured"] is True + + def test_e2e_happy_path_pauses_for_human_then_completes(client: TestClient) -> None: reset = client.post("/api/v1/demo/reset", json={}) assert reset.status_code == 200 diff --git a/tests/api/test_expert_runs.py b/tests/api/test_expert_runs.py index 8ad365a..282c695 100644 --- a/tests/api/test_expert_runs.py +++ b/tests/api/test_expert_runs.py @@ -17,6 +17,9 @@ class FakeLiveGateway: model = "test-live-model" base_url = "https://model.invalid/v1" + def __init__(self) -> None: + self.max_tokens_by_role: Dict[str, int] = {} + def list_models(self) -> list[str]: return [self.model] @@ -28,6 +31,7 @@ def complete_json( input_payload: Mapping[str, Any], max_tokens: int, ) -> ModelCall: + self.max_tokens_by_role[role] = max_tokens digest_match = re.search(r"input_digest MUST equal ([0-9a-f]{64})", system_prompt) review_match = re.search(r"reviewed_digest MUST equal ([0-9a-f]{64})", system_prompt) input_digest = digest_match.group(1) if digest_match else "" @@ -140,6 +144,12 @@ def test_live_expert_run_calls_all_roles_and_freezes_auditable_result(tmp_path: ) assert all(item["receipt"]["http_status"] == 200 for item in run["roles"]) assert all(item["memory_receipt"]["compacted"] is True for item in run["roles"]) + assert client.app.state.expert_runs.gateway.max_tokens_by_role == { + "research-pi": 2400, + "scout": 2400, + "experiment-architect": 4096, + "reviewer": 4096, + } assert run["compile"]["matrix_cell_count"] > 0 assert run["decision"] == { "status": "PLAN_READY_FOR_HUMAN_REVIEW",