Files
ss-tools/specs/050-mcp-interface/tasks.md
busya 07145ac961 docs(specs): wave 3 operations accounting — notifications, atomic investigation, poisoned-run, MCP parity
- 046: T022 CLOSED (notification wiring, NOTIFY-001); T023 offline CLOSED (poisoned-run
  quarantine/recovery, SCHED-SOAK offline); T024 remains live-blocked (72h soak).
- 047: T019/T020/T021 CLOSED (atomic triage, one-active, exact-context; 22 unit + 7 PG race
  vectors); T026 CLOSED (INV-NOTIFY-001); T028 CLOSED (INV-ATOMIC-001).
- 050: T051 CLOSED (shared start-error classifier + 17-vector REST/MCP parity matrix).
- Matrix: G-NOTIFICATIONS, G-INVESTIGATION-ATOMIC, G-MCP-PARITY -> CLOSED; G-DLQ-SOAK ->
  PARTIAL (offline policy closed; 72h soak and tab canaries remain live).
- WORKSTATE: wave-3 checkpoint incl. stale-base integration adaptations and the resolved
  PostgreSQL host-routing (VPN/bridge) incident.
- Full backend suite on the integrated release commit: 11472 passed, 0 failed.
2026-09-22 12:57:19 +03:00

108 lines
52 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 050-mcp-interface — Tasks
> Правило: `[ ]` не начата; `[~]` в работе; `[x]` только с доказательством (команда + вывод).
## Phase 0 — OAuth skeleton + `/mcp` probe
- [x] T001 Зависимость MCP SDK/FastMCP в `backend/requirements.txt`; каркас `backend/src/mcp_server/` с монтированием `/mcp` в FastAPI app.
- [x] T002 Authorization Server на Authlib: `/oauth/authorize` (поверх существующей web-сессии: local password + ADFS OIDC), JWKS, AS metadata (RFC 8414).
- [x] T003 `/oauth/token`: authorization_code + PKCE S256 (обязателен для публичных клиентов), refresh_token с ротацией, client_credentials для service-principal.
- [x] T004 DCR (RFC 7591): регистрация публичных клиентов с first-party scope bound; список/отзыв в Admin.
- [x] T005 Resource Server: RFC 9728 metadata для `/mcp`, `401 + WWW-Authenticate`, валидация access-JWT (`aud=mcp`, jti/blacklist) на каждый запрос; `resolve_principal()` адаптер.
- [x] T005a Transport and domain input limits: server-owned request byte/JSON-depth/collection/string limits, ScenarioGraph and SQL/DSL size limits, and per-session rate limits; rejected payloads create no ToolInvocationRecord, gate, draft, run, or mutation.
- [x] T006 Refresh rotation + reuse-detection: повтор ротированного refresh токена отзывaет семейство (расширение `TokenBlacklist`); fault-injection тесты.
- [x] T007 2–3 пробных инструмента (read-only: list_environments, get_health_summary, search_dashboards) с явной регистрацией и курируемыми схемами.
- [x] T008 CI: скриптованный клиент проходит discovery → DCR → PKCE → token → `tools/list` без ручных шагов (SC-007); подключение MCP Inspector; документация в README/INSTALL. Доказательство (remediation round 1, 2026-09-03): `pytest tests/test_mcp_client_flow_http.py -q` → `2 passed` — оба потока целиком поверх реального HTTP: (a) machine-поток: RFC 9728 discovery → AS metadata (grant `client_credentials`, `client_secret_post`) → confidential DCR с одноразовым `client_secret` → `client_credentials` → signed aud=mcp токен с `principal_type=service` → `/mcp` initialize → tools/list (gated/human-only тулы невидимы) → tools/call; (b) пользовательский поток: public DCR → PKCE S256 `GET /oauth/authorize` с Bearer веб-сессии (документированный SPA-mediated consent-путь) → 302 code → обмен → identity-only токен → `/mcp` tools/list с live-RBAC (RUN_PROD-тул скрыт для RUN-юзера); wrong secret → `invalid_client`. Реализовано: грант `client_credentials` (`services/mcp_oauth.py`), `oauth_clients.secret_hash` (миграция 0017, idempotent-guard), service-principal short-circuit в `McpTokenVerifier`, AS metadata расширена. Документация: INSTALL.md §«MCP клиент». MCP Inspector подключается по тому же discovery-контракту (Streamable HTTP `/mcp`). Ограничение браузера-без-Bearer (cookie-consent HTML): **РЕШЕНО 2026-09-04 — не строим в 050**. Документированный и протестированный путь — SPA-mediated authorize: аутентифицированный фронтенд вызывает `GET /oauth/authorize` с Bearer веб-сессии (SC-007 «zero manual token management» выполнен: DCR → PKCE → code → токены автоматизированы полностью). Отдельная интерактивная consent-страница (login+consent на cookie-сессии) — новая frontend-поверхность со своим UX/i18n/security-ревью, непропорциональная first-party perimeter scope (MCPX-FR-020: только локальные клиенты). Fallback для browser-only клиента описан в INSTALL.md §«MCP клиент». Если browser-only third-party клиенты станут требованием — это отдельное продуктовое решение через architecture amendment (MCPX-FR-020 stand).
- [x] T008a DCR abuse controls: rate limits, redirect-URI validation, reviewed first-party scope bounds, admin audit/revocation, and tests proving registration cannot grant permissions or escalate scopes.
- [x] T008b Local-perimeter deployment tests and docs: reject or disable non-local MCP/LLM/VLM endpoint configuration by default; assert PII is permitted only for configured enterprise-local providers while credentials/cookies/tokens/secrets remain rejected everywhere. *(Closure review 2026-09-03: тестов на отклонение non-local эндпоинтов не найдено; секрет-гигиена provenance подтверждена — PARTIAL.)* Доказательство (remediation round 5, 2026-09-04): deny-by-default guard `Core.EndpointLocality` (`src/core/utils/endpoint_locality.py`) в choke points `LLMProviderService.create_provider/update_provider` — типизированный отказ `endpoint_not_local:<reason>` ДО персистенции (create не добавляет row, update не мутирует), route-слой маппит в HTTP 400 (не 500). Локальность: loopback/local-литералы, RFC1918/ULA private IP, enterprise DNS-суффиксы (`.local/.internal/.lan/.corp/.intranet`, расширяемо), DNS-имена с полностью приватным резолвом; нерезолвимое = fail closed; substring-spoofing (`https://api.openai.com/localhost`) отклоняется по hostname; пустой base_url (публичное SDK-облако по умолчанию) отклоняется. Escape hatches env-документированы (`LLM_ALLOW_NONLOCAL_ENDPOINTS`, `LLM_NONLOCAL_ENDPOINT_ALLOWED_HOSTS`, `LLM_LOCAL_HOST_SUFFIXES`), по умолчанию закрыты. MCP-транспорт: server-owned JSON-depth limit (typed 400 pre-dispatch) + per-session rate limit с `Retry-After` (E6) + существующие body-limit/DNS-rebinding defaults. Docs: INSTALL.md §«Локальный периметр». Тесты: `tests/test_endpoint_locality.py` (22) + `tests/test_mcp_transport_limits.py` (5) + provider/route slices → `pytest tests/test_endpoint_locality.py tests/services/test_llm_provider.py tests/api/test_llm.py tests/api/test_encryption_health.py -q` → `107 passed`. PII-часть (секрет-гигиена provenance) подтверждена closure review round 1 и E13-отклонениями authoring chain.
## Phase 1 — Parity catalog (37 tools)
- [x] T010 Каталог-реестр инструментов по доменам: env/health/tasks, git/deploy/migration/backup/maintenance, superset ops, baseline, scenario. Каждый инструмент декларирует `required_permission(resource, action)` из канонического словаря; единая серверная политика поглощает `_tool_filter`.
- [x] T011 Инструменты env/health/tasks (list_environments, get_health_summary, get_task_status, maintenance CRUD, llm status).
- [x] T012 Инструменты git/deploy/migration/backup. Доказательство: `pytest tests/test_mcp_ops_parity.py tests/test_mcp_server.py tests/test_mcp_approvals.py tests/test_mcp_maintenance.py tests/test_mcp_oauth.py -q` → `70 passed`; зарегистрированы `create_branch`, `commit_changes`, `deploy_dashboard`, `execute_migration`, `run_backup`, `run_llm_documentation`, `run_llm_validation` (curated inputs, approval-gated; reviewed dispatchers `src/services/mcp_ops_dispatch.py` в явной цепи `poll_approved_mcp_dispatches`: GitService / TaskManager git-integration, superset-migration, superset-backup, llm_documentation, ValidationTaskService).
- [x] T013 Superset-инструменты (databases/explore/sql/format/permissions/dashboard+dataset CRUD) — SQL-класс помечен risk-классом и отдельным правом. Доказательство: тот же срез `70 passed`; `superset_list_databases`, `superset_explore_database`, `superset_format_sql`, `superset_audit_permissions` (read), `superset_create_dashboard`, `superset_copy_dashboard`, `superset_create_dataset` (approval-gated), `superset_execute_sql` — permission `("plugin:superset_sql","EXECUTE")` добавлен в `rbac_permission_catalog.discover_declared_permissions` + default-deny mapping; dangerous SQL отклоняется клиентским guard. Доп. свидетельство (орт. аудит 2026-09-02): guard расширен (`safety.py`: `INTO/CALL/SET/REFRESH/VACUUM/REINDEX/ATTACH/LOAD/PREPARE/COMMENT` + опасные функции `lo_import/lo_export/pg_read_file/pg_write_file/pg_ls_dir/dblink/dblink_exec/set_config/pg_sleep` + запрет мульти-стейтментов через `;` после очистки строк/комментариев; `tests/test_core/test_superset_safety.py` — 50+ кейсов); PROD-критерий SQL-класса выравнен с `resolve_environment_execution_policy` (is_production OR stage=PROD) и переведён в терминальный отказ `production_sql_execution_rejected` вместо одобряемого, но недиспетчеризуемого гейта; `pytest tests/test_mcp_ops_parity.py tests/test_core/test_superset_safety.py -q` → `85 passed`; полный срез аудита `214 passed`; полный бекенд **11202 passed**.
- [x] T014 Baseline-домен: capture_baseline_candidate, request/decide/consume_baseline_approval, create_verification_run (паритет fixtures с tools_037). Доказательство: тот же срез `70 passed`; прямые обёртки над `candidate_capture`/`candidates.request|decide_approval`/`verification_service.create_verification_run_async` (те же сервисы, что и 037 REST-поверхность); `consume_baseline_approval` — baseline publish: approval-gated + reviewed dispatcher через one-shot `consume_approval`; полный backend suite `python -m pytest -q` → `11167 passed, 240 skipped, 1 xpassed`; ruff/compileall чистые; каталог 45/45 зарегистрирован. **Re-verified 2026-09-16:** срез `pytest -q tests/test_mcp_ops_parity.py tests/test_mcp_server.py tests/test_mcp_approvals.py tests/test_mcp_maintenance.py tests/test_mcp_oauth.py tests/api/test_mcp_parity_baseline_037.py` → **84 passed**; каталог вырос до 61 записи (version 2.3.0), baseline-домен на месте; `consume_baseline_approval` дополнен gated `publish_baseline_catalog` (T045, CLOSED 2026-09-11).
- [x] T015 Scenario-домен: `scenario_compile`/`scenario_validate`/`scenario_resolve`/`generate_draft_pack`/`request_save`/`activate_revision`/`start_scenario_run` (паритет с tools_038 и 042/044; save только из server-stored draft, activation отдельная CAS-операция).
- [x] T016 Контрактные тесты паритета: выводы MCP-инструментов сопоставлены с legacy-обёртками на общих фикстурах (SC-002). Доказательство: `pytest tests/api/test_mcp_parity_baseline_037.py -q` → `3 passed` (re-run 2026-09-16 на HEAD `4746af2f` → `3 passed in 1.79s`): request/decide baseline-approval — MCP-gate валидируется моделью `ApprovalGateResponse` и совывает с REST по operation/risk_level/required_permission/status/reason_required/target_paths + actor parity на decide; `create_verification_run` — идентичные overall_status/category statuses/evidence_refs/created_by против REST `/verification-runs`; `superset_format_sql` — идентичная строка против legacy REST `/api/agent/superset/sqllab/format` (единственный дабл — [EXT:Superset] клиент). Попутно пойман и закрыт регрессией дефект 038-резолвера: `scenario_resolve` selector-путь падал на `step.description is None` (`TypeError`) — теперь `selector_hint` пишется без конкатенации с None (`test_selector_hint_on_step_without_description`).
- [x] T017 Bounded-response дисциплина: лимит инлайн-ответа, артефакты как ref+digest.
- [x] T018 Hidden/gated матрица: admin/analyst/viewer × каталог — отсутствие права скрывает инструмент из `tools/list`; role-change виден на следующем вызове без re-consent (SC-004, SC-009).
## Phase 2 — Gates & provenance over MCP
- [x] T020 AgentAction-provenance на каждый вызов (ToolInvocationRecord): principal, tool, digest аргументов, исход, связь с AgentRun.
- [x] T021 `approval_required` конверт для негelegированных действий; durable ActionApprovalGate без side effects до решения (SC-003).
- [x] T022 Gate-инструменты как первичный путь полного цикла в клиенте: `list_pending_approvals(filter)` (+гейты от автоматизации) и `decide_approval(gate_id, confirm|deny, reason)` с CAS, обязательным reason для high-risk confirm, отказом service-principals и typed-ошибками на expired/replayed; web gate cards — равноправный рендер тех же строк (SC-003).
- [x] T023 E2E-walkthrough: внешний клиент создаёт сценарий фикстурного дашборда end-to-end → revision в registry (SC-001). Доказательство: `pytest tests/test_mcp_scenario_e2e.py -q` → `2 passed`: один человеческий принципал с реальным `scenario:EDIT`/`scenario:RUN` (без RBAC-стабов) проходит `tools/list`-видимость → `create_authoring_session` → `propose_graph_revision` (server-derived op) → `get_graph_diff` → `promote_to_scenario` (awaiting_user_review) → `request_save` → `ScenarioRevision(candidate)` в registry → 038 `inspect_scenario`/`scenario_resolve`/`validate_scenario` поверх MCP → `activate_revision` → `entry.current_revision_id` продвинут отдельным CAS; второй тест — принципал без `scenario:EDIT` не видит и не может вызвать save/activate. Транспортный уровень (initialize → mcp-session-id → tools/list → tools/call) покрыт скриптовым клиентом T008.
- [x] T024 Gated-вызовы по контексту: PROD-окружение и baseline publish возвращают `approval_required` при видимом инструменте (hidden-vs-gated, MCPX-FR-018).
- [x] T024a Context revalidation: environment-policy and provider-binding fingerprints are rechecked at dispatcher admission and before provider I/O; target reclassification or binding drift invalidates a prior gate without I/O (SCEX-FR-026).
- [x] T025 Checkpoint-инструменты: `list_checkpoints` + `decide_checkpoint(run_id, disposition, expected_version)` через тот же CAS/аудит, что и монитор 045; user-principal only (service → permission_denied); тест что автоматизация не имеет пути к чекпоинтам (MCPX-FR-019). Доказательство (после closure-review داунгрейда, 2026-09-03): `pytest tests/test_mcp_checkpoints.py -q` → `3 passed`: каталог `("scenario","RUN")` + `service_allowed=False` на оба инструмента; сервис-принципал не видит и не может вызвать; list-проекция pending/decided чекпоинтов рана; decide повторяет REST `/human/decision` (pending-резолв на сервере, CAS `decision_version`, `continue_after_human_decision`, step→passed/`HUMAN_*`, run→queued/executing); stale expected_version → `conflict` без потребления; отсутствие pending/рана → `checkpoint_not_found`. (Closure review 2026-09-03 зафиксировал прежний фиктивный [x]; закрыт реальной реализацией.)
- [x] T026 AgentAuthoringWorkspace operations: `create_authoring_session`, `propose_test_plan`, `start_exploration`, `get_exploration_result`, `propose_graph_revision`, `get_graph_diff`, `promote_to_scenario`; persistent server-owned state, provenance, idempotency and workspace CAS.
- [x] T027 Authoring sandbox contract tests: isolated runtime, origin/API/action allowlists, no shell/credential/filesystem escape, limits, cancellation, receipts, artifact ownership and zero production side effects; retain exploratory traces/screenshots/diagnostics as bounded refs.
- [x] T028 Authoring promotion E2E: sandbox output -> typed proposal -> deterministic 038 compile/validate -> user diff review -> 042 handle-based save -> immutable revision; reject raw code/URLs/cookies/secrets/paths/caller digests and keep code-backed production execution unimplemented. Доказательство: `pytest tests/test_mcp_authoring_promotion_e2e.py -q` → `8 passed`: полный прогон через `tools/call` — `start_exploration` теперь ставит `queued` при зарегистрированном раннере (wiring `provider_available=get_registered_runner() is not None`; регистрация `default_runner`+deployment context в `bootstrap_live_execution_composition`) → реальный `execute_scheduled_exploration_dispatch` из потокового контекста планировщика → `exploration_passed` с observations/proposed_graph/evidence_ref (`draft:exploration-*`) при [EXT:Browser]-дабле; `get_exploration_result` остаётся ограниченной проекцией без утечки наблюдений; значение `observed_dashboard_title` в сохранённой ревизии выводится только из наблюдения; `propose→promote→diff→request_save→activate` завершается `current`-ревизией без продвижения до отдельного CAS-активации. Негативные ветки: typed ops с SQL/`../`/`\\`/`drop` отклоняются до предложения и CAS; exploration-spec с code-токенами и незарегистрированными действиями не персистируется; `GraphRevisionInput`/`PromoteScenarioInput`/`RequestSaveInput`/`ActivateRevisionInput`/`ExplorationInput` структурно отвергают `digest`/`content_hash`. Дополнительно: диспетчерский soak (`test_mcp_ops_parity.py`) — 3 цикла поллера, каждое одобренное действие диспетчеризуется ровно один раз (`14 passed`). Полный срез: `83 passed` (8 файлов MCP-вертикали); полный backend suite **11182 passed**.
## Phase 2b — Initial scenario and automation parity
- [x] T029 Implement `ScenarioRegistry.CreateInitial` and `bootstrap_authoring_scenario`; prove a fresh external MCP client creates a first registry scenario without a seeded base revision. Evidence: `tests/test_mcp_initial_scenario_e2e.py` creates real `AgentRun`/`DraftArtifact` rows and drives `bootstrap_authoring_scenario` without a seeded registry; replay and conflicting idempotency are covered by `tests/test_mcp_t029_bootstrap_automation.py`.
- [ ] T029a Add typed InitialScenarioIntent/TestPackProfile validation and server-owned metric/filter/selector/baseline bindings; unresolved requirements must be `preview_only`, never guessed.
- [x] T029b Expose 046 schedule, trigger-rule, policy and automation-metrics management through curated MCP tools with REST-equivalent RBAC, idempotency and PROD gate behavior. Evidence: curated tools and catalog entries in `src/mcp_server/tools_automation.py`/`rbac_server.py`, idempotency migration `alembic/versions/0018_automation_idempotency.py`, and `tests/test_mcp_t029_bootstrap_automation.py` plus RBAC catalog tests.
- [x] T029c Add fresh-DB MCP E2E for bootstrap → visible registry entry → revision activation → manual run, plus scheduler eligibility/PROD-gate integration evidence. Evidence: `tests/test_mcp_initial_scenario_e2e.py` verifies fresh bootstrap, current revision, refusal to run an un-promoted bootstrap revision (`BOOTSTRAP_REVISION_NOT_RUNNABLE`), a pinned manual run against a genuinely materialized revision, human-step schedule rejection with zero side effects, and an idempotent PROD approval gate; focused MCP regression set: `145 passed` (2026-09-06).
## Phase 2c — Server-owned handle layer (root-cause closure of ADR-0023; contracts: 038 `ScenarioGraph.ServerOwnedPipeline` amendment 2026-09-06, 042 `ScenarioRegistry.DataModel` amendment, 050 stage table status note)
- [x] T029d Persist the 038 handle layer: `CompiledScenarioHandle`/`ValidationResultHandle`/`DraftPackHandle` tables (immutable, append-only, owner_principal+agent_run binding, digest/content_hash columns per 038 amendment) + Alembic migration + server content-store persistence of the canonical `DashboardTestScenario` bytes (`ScenarioGraph.Models.CanonicalBytes`) behind `canonical_bytes_ref`. Minting happens ONLY at the persisted REST boundaries (`api_compile_scenario`/`api_validate_scenario`/`api_resolve_scenario`/`api_draft_pack`); pure compiler functions keep `@SIDE_EFFECT None`. Single-consumption (`consumed_by_revision_id`), cross-binding rejection, GC via 036 draft-retention. Evidence: `src/models/scenario_handles.py`, `src/services/dashboard_testing/scenario/handles.py` (idempotent mint / verify / consume / purge), migration `0019_scenario_handles`, REST minting in `api/routes/dashboard_testing/scenario.py`, authority tests `tests/services/dashboard_testing/scenario/test_handles.py` (3 passed).
- [x] T029e 042 create consumes handles: `create_scenario`/`create_initial` re-verify handle ownership/binding/`save_eligible`, materialize `graph_snapshot` = canonical `DashboardTestScenario` JSON from handle bytes inside the create transaction, and write `OutboxEvent(type=materialize_revision)` + `RevisionMaterialization(pending)`; implement the idempotent reference-artifact worker (`scenario.yaml`/reference `runner.plan.json`) or record an explicit descope decision. `BOOTSTRAP_REVISION_NOT_RUNNABLE` demotes from primary guard to defense-in-depth; `api_create_scenario` stops hardcoding `materialization_status="materialized"`. Evidence: handle-first branch in `registry/create.py` (materializes full canonical graph + server-owned `action_registry_version/hash`), `src/models/scenario_materialization.py` + migration `0020_scenario_materialization`, worker `registry/materialize.py::materialize_pending_revisions` (idempotent, pending→materialized/failed), `api_create_scenario` returns the actual `RevisionMaterialization.status`.
- [x] T029f MCP minting surface: new `register_draft_pack` write tool returns bounded server-issued handle ids; `bootstrap_authoring_scenario` accepts ONLY stored handle ids (transitional `compile:{run_id}:{digest}` string check removed from `create_initial`); catalog bumped `1.0.0 → 2.0.0` (breaking bootstrap handle semantics) with pinned-major ritual `PINNED_CATALOG_MAJOR=2`. Legacy REST `POST /scenarios/{id}/revisions` raw-`graph_snapshot` path retired (returns `410 GONE` `REVISIONS_RAW_GRAPH_RETIRED`; the server-side `create_revision` service remains for the 043 editor save path). Also reclassified `generate_report` out of `_MUTATING_ACTIONS` (local draft report, no external side effect), bumping `ACTION_REGISTRY_VERSION 038.1.0 → 038.2.0` (fingerprint + `scenario_execution/graph.json` fixture updated).
- [x] T029g Full-chain fresh-DB MCP E2E without REST crutches and without hand-seeded revisions: `register_draft_pack`→`bootstrap_authoring_scenario`→direct `start_scenario_run` on a bootstrapped current revision (now materialized) returns `queued`. Evidence: `tests/test_mcp_initial_scenario_e2e.py` rewrote `_pack()` to drive the MCP `register_draft_pack` tool. PostgreSQL concurrency: `verify_handle_chain` uses `SELECT ... FOR UPDATE` + `populate_existing()` to serialize handle consumption, proven by `tests/integration/test_scenario_handle_concurrency.py` (one winner + one `HANDLE_CONSUMED` across two threads on a real PostgreSQL container; `--run-integration` green). *(Amended 2026-09-07, Doc.Adr.ADR0024/T029j: the claim "without REST crutches" did not cover the AgentRun prerequisite — `_pack()` inserted the `AgentRun` row with a raw ORM constructor unreachable for any external MCP client; the field run of 2026-09-07 proved `register_draft_pack` therefore denied every real client. `_pack()` now creates the run through the production `create_agent_run` service boundary with honest test metadata; full external reachability closes at T029i and is pinned by `tests/test_mcp_agent_run_reachability.py` (strict xfail).)*
- [x] T029h Inspect-stage decision: **resolved as hybrid option C (2026-09-06)** — no persisted `InspectionContextHandle`; instead (a) new MCP read tool `inspect_dashboard_context` exposes the existing live resolver (`BaselineEngine.QueryModel.Inspect` via `GET /query-model` service) returning the full authoritative `DashboardQueryModel` + fingerprint for the agent to echo into compile; (b) the register boundary (`register_draft_pack`) evaluates `context_authority` by RECOMPUTING the fingerprint from the client-carried `dashboard_context.query` (claimed fingerprints are ignored — C2) against a live inspection, with sentinel/degraded fingerprints (`""`, `sha256:error`) never verifying (C1), unreachable/unconfigured environments failing OPEN to `unverified` (C3), and falsifiable model-shape claims on live environments rejecting typed (`CONTEXT_FINGERPRINT_MISMATCH`, `CONTEXT_QUERY_MODEL_REQUIRED`, `CONTEXT_ENVIRONMENT_MISMATCH`) — C5 inspect-first; (c) the marker persists on `DraftPackHandle.context_authority` (migration `0021_context_authority`), materializes into `graph_snapshot` at 042 create, and `ScenarioExecution.Runner.Start` refuses PROD dispatch on an explicit non-verified marker (`CONTEXT_AUTHORITY_REQUIRED_FOR_PROD`, zero side effects; missing marker = legacy, allowed); (d) X1 smuggling closure: validator `_check_dashboard_context` recursively rejects `query_context` keys and SQL text inside `dashboard_context`. Catalog minor bump `2.0.0 → 2.1.0` (additive, pinned major untouched). Evidence: `tests/services/dashboard_testing/scenario/test_context_authority.py` (7), validator X1 tests (+2), `test_prod_start_enforces_context_authority_marker`, E2E marker-propagation + PROD-block assertions; broad regression `1338 passed`; `alembic heads` → `0021_context_authority`. Residual: editor promote/save path (`request_save` revisions) does not yet carry the marker — PROD gate treats it as legacy-allowed; tightening requires authority evaluation in the 043 save boundary (follow-up, not scoped by T029h).
*(Orthogonal code review 2026-09-06, [ADR-0023](../../docs/adr/ADR-0023-mcp-scenario-pipeline-handle-gap.md); specs amended the same day and the durable handle layer implemented — 038 `contracts/modules.md`, 042 `data-model.md`/`contracts/modules.md`, 050 `spec.md`): the bootstrap→run happy path is realizable through MCP alone (`register_draft_pack` → `bootstrap_authoring_scenario` → direct `start_scenario_run` on a materialized current revision). Phase 2c residuals now closed: `api_create_scenario` returns real `materialization_status`, legacy REST `POST /scenarios/{id}/revisions` retired (410 GONE), PostgreSQL single-consumption concurrency proven (`SELECT ... FOR UPDATE` + `populate_existing()`; `tests/integration/test_scenario_handle_concurrency.py` green on real PostgreSQL). T029h resolved as the hybrid (see above); follow-up residuals closed 2026-09-07: outbox worker wired into the scheduler poll loop (`scenario_revision_materialization`, 30s, singleton/coalesced, durable-tick test), REST `api_draft_pack` evaluates `context_authority` at parity with the MCP register boundary (async, typed 422 on falsifiable mismatch), and the 043 editor save path provably inherits the server-owned marker (`test_save_proposal_inherits_server_context_authority_marker`). Known-accepted limitation: legacy/NULL markers stay PROD-allowed by design; full-suite regression `3360 passed`.)*
## Phase 2d — Field-run remediation (2026-09-07 sales-prod external MCP run; [ADR-0024](../../docs/adr/ADR-0024-mcp-agent-run-external-boundary.md); spec: MCPX-FR-027/028/029, release-gate rows `E2E-EXT-001/002`, `CAP-001`, `DISP-001`, `TEST-001`)
> Источник: `docs/2026-09-07-sales-prod-mcp-run.md` — первый полевой прогон внешнего MCP-клиента против
> ss-prod Sales Dashboard (ID 11). Все звенья цепи реализованы, но external-клиент упёрся в
> `DRAFT_PACK_ACCESS_DENIED`: `register_draft_pack` требует principal-owned `AgentRun`, а MCP-операции
> создания AgentRun не существует (единственная creation-поверхность `POST /api/agent/runs` — REST/web-session,
> MCP-токены `aud=mcp` на ней не аутентифицируются). Вертикальный E2E маскировал разрыв raw-ORM-seed'ом.
- [x] T029i MCP AgentRun surface (MCPX-FR-027): typed `create_agent_run` + `get_agent_run` tools wrapping the existing `Services.AgentRuns.Service.Create`/snapshot boundary — curated bounded input (UIContext v2: `objectType=dashboard`, numeric `objectId`, `envId`, `route`, `contextVersion=2`, `intent=build_dashboard_test_scenario`; `extra=forbid`; optional `idempotency_key`), permission `("dashboard:testing","EXECUTE")` at REST parity, `service_allowed=False`, idempotent active-run reuse, bounded snapshot projection (`get_agent_run` returns the ownership-scoped `AgentRunSnapshot` fields only). Catalog additive bump `2.1.0 → 2.2.0`; update SC-004 exact-set fixtures (`tests/test_mcp_rbac_visibility.py`) and `tests/api/test_admin_mcp_catalog.py` mirrors. Closure evidence: convert `tests/test_mcp_initial_scenario_e2e.py` to the FULLY external chain (`create_agent_run` → `register_draft_pack` → `bootstrap_authoring_scenario` → `start_scenario_run`), remove the `strict=True` xfail marker in `tests/test_mcp_agent_run_reachability.py` and make it green (`E2E-EXT-001`). Доказательство (2026-09-07): новый seam `src/mcp_server/tools_agent_run.py` (`McpServer.ToolsAgentRun`, 155 LOC) — `create_agent_run` (server-pinned objectType/route/contextVersion/intent; caller владеет только dashboard/environment/name/idempotency_key → conversation-reuse; продуктовая форма run: status RUNNING + `run_started` event) и `get_agent_run` (bounded-проекция + draft_count; чужой/неизвестный id → typed not_found без existence oracle); catalog +2 (`EXECUTE`/`READ`, human-only), `MCP_CATALOG_VERSION=2.2.0` (PINNED_CATALOG_MAJOR=2 не тронут); registration seam в `_build_probe_server` (automation → agent-run → scenario). Тесты: `tests/test_mcp_agent_run_tools.py` (5: продуктовая форма+envelope keys, idempotent replay + env-scoping, structural rejection, ownership read + not_found, RBAC denial by name); `test_mcp_initial_scenario_e2e.py` конвертирован в fully external chain (`create_agent_run` через `tools/call`, ноль не-MCP seeding); xfail-маркер в `test_mcp_agent_run_reachability.py` снят — requirement pin зелёный на живом каталоге; catalog-pin `test_mcp_server.py` +2 имени +4 assertion. RBAC-зеркала правок не потребовали (analyst без EXECUTE/READ —derived-наборы). Gate: 8-файловый срез `70 passed`; полный backend suite `11357 passed`.
- [x] T029j Test-honesty remediation (ADR-0024 §3, binding rule): vertical/E2E tests obtain every prerequisite through a boundary the principal under test can reach; raw-ORM seeding of a chain prerequisite in a test claiming external reachability is forbidden. Executed: `test_mcp_initial_scenario_e2e.py::_pack()` replaced the raw `AgentRun(...)` insert with the production `create_agent_run` service boundary with honest test metadata; new `tests/test_mcp_agent_run_reachability.py` pins (a) the external-reachability REQUIREMENT as `strict=True` xfail (flips the suite red the moment `create_agent_run` lands without spec follow-through) and (b) the current typed zero-side-effect denial (`DRAFT_PACK_ACCESS_DENIED`, no handle rows). Evidence: see `TEST-001` row in spec.md release gates (targeted pytest green 2026-09-07). *(Update 2026-09-07, T029i closure: строгий xfail-пин сработал как спроектирован — конвертирован в обычный requirement-тест (unmarked, green) вместе с конвертацией вертикали в fully external chain; denial-pin сохранён.)*
- [x] T029k Context-authority-derived capability map (MCPX-FR-028; 038 amendment 2026-09-07): server-owned derivation of `capabilities`/`has_dataset_fields` from the authoritative `DashboardQueryModel` (the same live inspection `context_authority` binds), environment policy and provider readiness — exposed through `inspect_dashboard_context` (derived-capability section) and consumed by the persisted compile boundaries (MCP `scenario_compile` + REST `api_compile_scenario`); caller-declared capabilities remain accepted only as an explicit subset narrowing, never as an authority that downgrades verifiable facts into `human_checkpoint`; unresolved facts stay `needs_context`/`needs_selector`/`needs_baseline`; genuinely unsafe PROD mutation contexts keep `human_checkpoint`; HumanStep revisions remain automation-ineligible (SCEX-FR-004a stands). Evidence: `CAP-001` row + capability-derivation unit/E2E tests. Доказательство (2026-09-07): новый `src/services/dashboard_testing/scenario/capability_authority.py` (236 LOC, `ScenarioGraph.CapabilityAuthority`): `derive_capabilities` — только верифицируемые факты (native_filters; text_filter по STRING-фильтру; time_rollover по DATE/TIME/TIME_GRAIN; table_filter+pagination по executable table-viz; xlsx_export из capabilities-флага; dataset_field_read+has_dataset_fields по accessible dataset с колонками; browser — только при readiness-факте из T040-снимка composition-root), `NEVER_DERIVED` = row_edit/bulk_edit/persistence_refresh/safe_test_data/safe_clock_fixture/cross_dashboard (unsafe-mutation автоматизация метаданными невозможна — module @INVARIANT); `merge_capabilities` — derived-wins в ОБЕ стороны (declared-false не понижает проверяемую правду; declared-true не фабрикует) + overrides-аудит; legacy/unparseable payload → дословный caller-declared passthrough (register-time context_authority остаётся жёстким гейтом). Wiring: MCP `inspect_scenario` (+additive `capability_authority` секция), `inspect_dashboard_context` (+`derived_capabilities`), REST `api_compile_scenario` (parity через общий `build_capability_authority` choke point + additive секция). Тесты: фикстура `query_model_sales.json` (форма sales-стенда); unit truth-table `tests/services/dashboard_testing/scenario/test_capability_authority.py` (7, включая **CAP-001 classification-fix через `map_all`**: полевого shape декларации → B01–B04/T01–T03 `automated`, C04–C06 `unsupported`, B05–B09/C01–C03/C02/C07 легитимно `human_checkpoint`); tool-level `tests/test_mcp_capability_authority.py` (3: derived-wins overrides `["text_filter","xlsx_export"]`, legacy caller_declared, derived_capabilities в inspect-ответе); REST-parity pin `test_scenario_routes.py::test_capability_authority_derived_wins`. Браузер-readiness seam запинен monkeypatch (детерминизм против глобального composition-root состояния в full-suite). Gate: 5-файловый срез `67 passed`; полный suite `11357 passed`.
- [x] T029l Disposition vocabulary clarity (MCPX-FR-029; 044/045 amendment 2026-09-07): the 044 lifecycle mapping (`confirm`→`passed`, `false_positive`/`inconclusive`→`inconclusive`, aliases `pass`/`fail`) is immutable and stays; human-facing wording aligns to the persisted outcome — RU labels «Подтвердить соответствие» / «Проблема не подтверждена» / «Недостаточно данных» (+ EN parity), confirm-button restyled from `bg-destructive` to a positive token, `decide_checkpoint`/`decide_approval` MCP tool descriptions and 045 monitor docs carry the outcome table, vitest pins updated (`WaitingForMeView.test.ts`, `HumanCheckpointPanel` tests). Evidence: `DISP-001` row. Доказательство (2026-09-07): `waiting_disposition_confirm/false_positive` в ru/en `dashboard-testing.json` именуют персистентный исход (confirm→passed: «Подтвердить соответствие»/"Confirm conformance"; false_positive: «Проблема не подтверждена»/"Issue not confirmed"; inconclusive без изменений); confirm-кнопки `HumanCheckpointPanel.svelte` и `WaitingForMeView.svelte` переведены `bg-destructive` → `bg-primary` (прецедент approve-кнопки ApprovalDecisionPanel) + @RATIONALE в контрактах компонентов; API-вокабуляр не переименован (continuity аудита/CAS); MCP `decide_checkpoint` docstring (видим в tools/list) несёт immutable outcome-mapping и правило «confirm = проверка пройдена, никогда не подтверждение дефекта»; vitest-пины обновлены (`RunMonitorViews.test.ts` — новый DISP-001 pin лейбла+стиля+dispatch "confirm", `WaitingForMeView.test.ts`, `run.ux.test.ts`). Lifecycle-маппинг не менялся — backend-тесты decision-пути зелёные без правок. Gate: frontend `3507 passed` (206 файлов), lint `0 errors / 364 warnings` (baseline), `npm run build` OK.
- [x] T029m Live-stand replay of the field run: after T029i/T029k/T029l, re-run the 2026-09-07 sales scenario externally against the live stand — `inspect_dashboard_context` → derived-capability compile/validate → `create_agent_run` → `register_draft_pack` → `bootstrap_authoring_scenario` → `start_scenario_run` (PROD gate observed, no unexpected high-risk approvals) → `list_checkpoints`/`decide_checkpoint` human loop with aligned labels; retain the run report beside `docs/2026-09-07-sales-prod-mcp-run.md`. Evidence: `E2E-EXT-002` row. **Status (2026-09-11): CLOSED.** Committed replay client `specs/044-dashboard-scenario-execution/prototype/live_mcp_replay.py` (transport-only adaptation of the proven scripted OAuth/PKCE + tool-chain flows, no new client machinery) replayed the FULL external chain on the live stand with zero non-MCP seeding: inspect (derived capabilities) → `create_agent_run` `a6168c7d…` → compile B01 (capability_authority `derived`) → validate → draft-pack `save_eligible` → `register_draft_pack` (**context_authority verified**) → bootstrap (scenario `e2687f03…`, current revision) → PROD start `pending_approval` (identical retry = one durable gate) → approval → live execution to an honest typed terminal (`capture_screenshot` **passed** with 8 durable refs; `apply_native_filter` typed `BROWSER_ACTION_NOT_SUPPORTED` — no synthesized PASS). Full trace + two fail-closed defects the replay exposed and fixed (binding resolution for identity-less compiled steps; per-step target-identity stamping at bootstrap materialization) + D5 binding-admin live exercise: `docs/2026-09-11-sales-prod-mcp-replay.md`. The human-checkpoint loop was exercised live by the companion `live_mcp_human_loop.py` (compiled B05 HumanCheckpoint → PROD gate → `waiting_human` → MCP `list_checkpoints` decision_version 1 → `decide_checkpoint confirm` decision_version 2 CAS → terminal `passed`), so every element of this task's formulation is covered. The baseline-pinned variant remains blocked on the Gitea PAT (050 T045 / T046-pin).
## Phase 3 — Frontend decommission (flag-driven)
- [ ] T030 Replace historical HandoffSurface/agent entry with ordinary manual scenario editor; remove copy-prompt, agent launch/workspace and proposal-generation UI. Previous handoff implementation is history, not current acceptance.
- [x] T031 Скрыть `/agent`, AssistantChatPanel, кнопку «Ассистент» в TopNavbar за флагом; обновить link-integrity тесты.
- [ ] T032 Retire `/api/assistant/*` и прокси `/api/agent/gradio`; retention-настройки assistant скрыть. *(Closure review 2026-09-03: на HEAD роутеры `/api/assistant` и `agent_*` смонтированы безусловно (app.py), retention-UI в SystemSettings жив, frontend `api/assistant.ts` в поставке — фиктивный [x]. Флаг удалён в T041, поэтому retirement теперь безусловный.)*
- [x] T033 vitest/build/link-integrity зелёные при включённом флаге демонтажа.
## Phase 4 — Removal
- [x] T040 Удалить сервис `agent/` из run.sh и compose-профилей (порт 7860); обновить AGENTS.md/INSTALL.md. Доказательство: `rg "agent|7860" run.sh build.sh docker-compose.yml docker-compose.enterprise-clean.yml` → пусто (кроме исторического секьюрити-комментария); `bash -n`/`yaml.safe_load` чистые; стенд `./run.sh --skip-install` поднимает только :8000+:5173 (7860 отсутствует, `curl /api/ready` → ready, Playwright-проход до хендоффа зелёный); `run.sh`/`build.sh`/`AGENTS.md`/`INSTALL.md` обновлены; из `build.sh` убраны `build:agent`, `bundle:agent`, `bundle:embeddings` и агентский сервис генерируемого деплой-композа/манифеста; из обоих nginx-конфигов убран `location /api/agent/gradio`.
- [x] T041 Удалить код чата: agent/src (app, langgraph_setup, tools*.py, _confirmation, middleware...), frontend agent/assistant компоненты, i18n, типы. Доказательство: `agent/` удалён целиком; удалены `docker/Dockerfile.agent`, `docker/agent.entrypoint.sh`, `backend/tests/test_gradio_proxy_config.py`; во фронтенде удалены `components/assistant/*` (кроме универсального `MarkdownRenderer`), чат/ран/драфт артефакт компоненты, `components/agent/dashboard-testing/` (ScenarioWorkspace-ветка), `models/AgentChat*`, `AgentRunModel`, `DashboardScenarioWorkspaceModel`, `stores/assistantChat`, флаг `MCP_DECOMMISSION` (vite define/`config/mcp.ts`/`global.d.ts`), gradio-прокси из `vite.config.js`; `/agent` рендерит только `HandoffSurface` (безусловно), навбар-кнопка ведёт на хендофф; `npm run test -- --run` → **3454 passed**, `npm run lint` → 0 errors, `npm run build` → OK; полный бекенд **11199 passed** (минус тесты удалённого грдио-прокси), 16 сиротских тестовых файлов чата удалены.
- [x] T042 Финальные правки спек 036–047: перенести drift-amendments из статуса «planned» в «done» со ссылками на доказательства. Доказательство: во всех 12 спеках (036–047) секции `## Drift Amendment — MCP Interface` получили строку `**Status (2026-09-02): done**` со ссылками на `specs/050-mcp-interface/tasks.md` (T012–T028), T030–T033 и чекпоинты `specs/WORKSTATE-043-047.md`. **Re-verified 2026-09-16 (grep):** 11 spec-файлов несут секцию Drift Amendment; 040/041 — дословный маркер `Status (2026-09-02): done`; 036/037/038/042–047 — пост-closure-review формулировка «reported done (2026-09-02); not production-readiness evidence» с теми же ссылками (более строгая, принята как эквивалент).
- [x] T043 Полный прогон backend/frontend suites + стенд без 7860 (SC-005). Доказательство: `python -m pytest -q` → **11199 passed, 240 skipped, 1 xpassed**; `npm run test -- --run` → **3454 passed** (197 файлов); `npm run lint` (0 errors) + `npm run build` — зелёные; локальный стенд после демонтажа работает без порта 7860 (см. T040) и отдаёт рабочий хендофф на `/agent`. **Browser E2E (изолированный стенд):** `docker compose -p ss-tools-e2e --env-file .env.e2e -f docker-compose.e2e.yml up -d --build db backend frontend` (свежая БД, bootstrap admin/admin123, backend :8103 healthy, frontend :8102 healthy, порт 7860 отсутствует) → `npx playwright test e2e/tests/login.e2e.js e2e/tests/agent.e2e.js` → **6 passed** (двойной прогон, Chromium): login-поток (форма/успех/ошибка неверных кредов) и post-decommission агентский роут (хендофф-поверхность, ноль чат-элементов, deep-link с context-параметрами остаётся на хендоффе); стенд снесён `down -v`. E2E-фикстарелы: устаревший ambiguous `locator('nav')` (strict mode: 3 nav-элемента) → `.first()` в login/smoke; regex ошибки входа дополнен `incorrect|неверн` (бэкенд отдаёт passthrough-detail); `agent.e2e.js` переписан под handoff-контракт (T01-T03), `dashboard-scenario-ui`/`agent-scenario-run` — ссылки на чат-UI заменены на handoff-маршрут. Дополнительно: живой admin-walkthrough на dev-стенде (8000/5173) подтвердил login→dashboards→handoff(0 textarea/0 conversation-узлов)→runs-center (скриншоты /tmp/kilo/happy-path/09–12).
## Production readiness — 2026-09-08 (MCPX-FR-030)
Historical [x] rows above retain only their dated local/transport evidence; they do not prove current production readiness. Reopened rows were contradicted by the audited gaps. Removed frontend/agent paths are historical, not implementation prerequisites. New acceptance is **implemented=false / OPEN**.
Contract: [Contract-complete public parity](contracts/modules.md).
- [ ] T044 [P0/P1/P2] REST/MCP lifecycle/read/auth errors and disabled automation validation are identical; service principal cannot decide human gate. Implement at the existing 050 domain boundary; verify with independent hardcoded fixtures and retain command/evidence references in traceability.md. **Status (2026-09-11):** offline parity tests green; live REST-only canary v2 exercised start→gate→approve→dispatch (`4eebfab3`); the live MCP-external replay (T029m CLOSED, `docs/2026-09-11-sales-prod-mcp-replay.md`) exercised the same lifecycle through `/mcp` with the identical typed start/gate/terminal outcomes — remaining: dedicated REST-vs-MCP error-shape parity fixtures for this matrix.
- [x] T045 [P0/P1/P2] consume/publish failures return typed errors/pending state with no legacy fallback; every prerequisite is externally MCP-reachable. Implement at the existing 050 domain boundary; verify with independent hardcoded fixtures and retain command/evidence references in traceability.md. **Status (2026-09-11): CLOSED.** Consume path typed fail-closed (`bbbd4ccf`, live-proven); publication is now externally MCP-reachable through the curated gated tool `publish_baseline_catalog` (human-only, `scenario` RUN_PROD, `requires_approval`) executing the 037 publication-worker contract via `ScenarioExecution.PublicationWorker` + durable `publication_operations` (migration `0023`): idempotency by key, envelope validation (resolver-canonical), branch-head CAS, commit/published receipts, failure retention + reconcile of the same commit; REST parity `POST/GET /api/catalog-publications`. LIVE evidence (`docs/reports/agentic-runtime-live-mcp-publish-t045-2026-09-11.md`): MCP gate `approval_required` → `decide_approval` → dispatch → `published` (commit `abce63db…`, receipt head 3f782574→abce63db) + moved-HEAD canary → typed `PUBLISH_HEAD_MOVED` with a durable `publish_failed` row. Offline: `tests/services/dashboard_testing/execution/test_publication_worker.py`, `tests/api/test_catalog_publications_api.py`, `tests/test_mcp_ops_parity.py`.
- [x] T046 [P0/P1/P2] Fresh external-client chain preserves authoritative context and complete baseline pin without raw ORM/REST repair; no frontend agent controls/routes/requests. Implement at the existing 050 domain boundary; verify with independent hardcoded fixtures and retain command/evidence references in traceability.md. **Status (2026-09-11): CLOSED.** Context authority: fresh REST chain (canary v2 `4eebfab3`) and fresh MCP chain (T029m) preserved the server-resolved binding and verified context (`context_authority=verified`). Complete baseline pin: (a) the MCP-external chain with `--baseline` (`live_mcp_replay.py --baseline`, no raw ORM seeding) compiled the baseline capability, started with the published-catalog selector, and carried a full `runner_plan.baseline_pin` (set/version/digest from the published envelope); (b) the REST canary v4 (`ace916a0…`, then re-run `adeabe63…`) proved the walker stamps that same pin into the persisted `AgentEvaluation.baseline_pin` (`plan pin == record pin`, strict equality asserted) — `docs/reports/agentic-runtime-live-canary-v4-baseline-pin-2026-09-11.md`. No raw ORM/REST pin repair; frontend agent controls remain absent (2026-09-10 evidence).
## Design Amendment implementation tasks — 2026-09-17/18
- [x] T047 [P0] Investigation MCP tools + RBAC/human-only writes + catalog 2.4.0. **CLOSED:** `23414f9a` + `5eea72bc`, 73 focused green.
- [x] T048 [P0] `set_step_inputs` and `set_step_evaluation` through propose_graph_revision/EditOperation parity. **CLOSED:** `534d488f`/`f123210b` + `87a90f6f`.
- [ ] T049 [P1] `propose_baseline_selection` MCP surface for curated coordinates/review handle; observatory non-gating.
- [ ] T050 [P1] `exploration_step` read-only sandbox action with CAS/receipts; reuse 044 browser session; no second Playwright stack.
- [x] T051 [P0] Complete REST/MCP lifecycle/error parity (prior T044 residual), including disabled-action/investigation tools. **CLOSED 2026-09-22:** shared `classify_start_error` in `start_run.py` consumed by REST and MCP (removes the raw-text vs RUN_START_CONFLICT divergence); `tests/test_mcp_rest_error_parity.py` 12+ vectors (env/baseline codes/idempotency/conflict/disabled action/disabled automation/investigation RBAC/unauthenticated + service-principal human-gate denial + MCP no-bypass on the case ACL); 17 green.
Frontend boundary for this package: manual CRUD/editor, human review/approval, monitoring and read-only evidence/evaluation only; all agent interaction is external MCP. No agent chat/prompt/assistant editing/proposal generation/workspace/start/handoff controls. Runtime removal is OPEN, not performed by this spec refresh. Optional approved performance baseline is outside scope.