fix(scenario): review hardening — run.sh key reuse, resolver-canonical publisher envelope, strict pin asserts, honest T045/T046 statuses

This commit is contained in:
2026-09-11 15:09:16 +03:00
parent 7d3fb8a770
commit de5e098a1b
10 changed files with 278 additions and 107 deletions

View File

@@ -11,6 +11,19 @@ Expected honest outcomes: `open-dashboard`/`capture-evidence` pass live; `compar
deterministic dashboard-identity assertion (the published fixture is a validation snapshot, so no
image comparison is claimed); `evaluate-visual` runs the multimodal judge.
"""
# #region ScenarioExecution.LiveCanaryV4 [C:4] [TYPE Module] [SEMANTICS scenario,baseline,pin,gitea,canary,live]
# @defgroup ScenarioExecution Live proof that a published Gitea envelope resolves into plan + record pins.
# @BRIEF Publish the resolver-canonical envelope, start with the selector, and assert both pins match.
# @RELATION VERIFIES -> [ScenarioExecution.BaselineResolver.Resolve]
# @RELATION VERIFIES -> [ScenarioExecution.BaselineResolver.StampEvaluation]
# @RELATION CALLS -> [Tooling.PublishCatalog]
# @INVARIANT The run is fail-closed: the script asserts `runner_plan.baseline_pin` equals
# `AgentEvaluation.baseline_pin` and that both carry the published envelope identity; a
# missing or divergent pin fails the canary instead of being reported as success.
# @RATIONALE T045/T046: the pin must come from published bytes at start time and be stamped into the
# immutable evaluation record without any raw ORM/REST repair.
# @REJECTED Asserting pin truthiness only was rejected after review — the proof must compare the
# server-resolved plan pin against the persisted record pin and the published identity.
import json
import os
import sys
@@ -54,13 +67,15 @@ POLL_TIMEOUT_SECONDS = 300
_TERMINAL_STATUSES = frozenset({"passed", "failed", "blocked", "inconclusive", "cancelled"})
# #region ScenarioExecution.LiveCanaryV4.Helpers [C:1] [TYPE Function] [SEMANTICS descriptor,registry]
# @BRIEF Resolve the server-owned action descriptor snapshot for a graph step.
def descriptor(action: str, tool: str) -> dict:
return resolve_action_descriptor(
tool=tool, action=action,
registry_version=ACTION_REGISTRY_VERSION,
registry_hash=action_registry_fingerprint(),
).snapshot()
# #endregion ScenarioExecution.LiveCanaryV4.Env
# #endregion ScenarioExecution.LiveCanaryV4.Helpers
# #region ScenarioExecution.LiveCanaryV4.Seed [C:4] [TYPE Function] [SEMANTICS scenario,baseline,pin,seed]
@@ -147,8 +162,8 @@ def seed_scenario() -> tuple[str, str]:
# @ingroup ScenarioExecution
# @BRIEF Publish the resolver-canonical envelope (identity top-level + catalog_revision) to Gitea.
# @PRE Deployment env supplies PUBLISHED_CATALOG_GITEA_URL/TOKEN and PUBLISHED_CATALOG_REPO/REF.
# @POST Returns the published commit sha; the envelope is validated before any network call and the
# update path reuses the stored sha (create=POST, update=PUT per the Gitea contents contract).
# @POST Returns the published path, commit sha and envelope identity (set/version/release/digest/
# revision) so the caller can assert the resolved pin against the published generation.
# @INVARIANT The published bytes are the resolver-canonical envelope — the fixture's baseline_pin
# identity hoisted to top-level plus its catalog_revision — so the start-path resolver
# consumes exactly what this packet published (no working-tree YAML, no client bytes).
@@ -166,16 +181,24 @@ def publish_envelope() -> dict:
raw = json.dumps(envelope, indent=1, ensure_ascii=False).encode("utf-8")
validate_catalog(raw)
target = GiteaTarget(
base_url=os.environ["PUBLISHED_CATALOG_GITEA_URL"].rstrip("/"),
repo=os.environ["PUBLISHED_CATALOG_REPO"],
ref=os.environ.get("PUBLISHED_CATALOG_REF", "master"),
token=os.environ["PUBLISHED_CATALOG_GITEA_TOKEN"],
base_url=os.environ["PUBLISHED_CATALOG_GITEA_URL"].strip().rstrip("/"),
repo=(os.environ.get("PUBLISHED_CATALOG_REPO") or os.environ["PUBLISHED_CATALOG_GITEA_REPO"]).strip().strip("/"),
ref=(os.environ.get("PUBLISHED_CATALOG_REF") or os.environ.get("PUBLISHED_CATALOG_GITEA_REF") or "main").strip(),
token=os.environ["PUBLISHED_CATALOG_GITEA_TOKEN"].strip(),
path=catalog_path(pin["baseline_set_id"], pin["baseline_set_version"]),
)
headers = {"Authorization": f"token {os.environ['PUBLISHED_CATALOG_GITEA_TOKEN']}"}
headers = {"Authorization": f"token {target.token}"}
with httpx.Client(base_url=target.base_url, headers=headers, timeout=30.0) as client:
commit = publish_catalog_bytes(client, target, raw, "Publish 044 published-envelope snapshot ss-prod-visual v1 (canary v4)")
return {"path": target.path, "commit_sha": str((commit.get("commit") or {}).get("sha") or "")}
commit = publish_catalog_bytes(client, target, raw, "Publish 037 published envelope ss-prod-visual v1 (canary v4)")
return {
"path": target.path,
"commit_sha": str((commit.get("commit") or {}).get("sha") or ""),
"baseline_set_id": envelope["baseline_set_id"],
"baseline_set_version": envelope["baseline_set_version"],
"release_id": envelope["release_id"],
"catalog_digest": envelope["catalog_digest"],
"catalog_revision_id": envelope["catalog_revision"].get("catalog_revision_id"),
}
# #endregion ScenarioExecution.LiveCanaryV4.Publish
@@ -262,11 +285,17 @@ def main() -> None:
result_path = os.environ.get("SS_REPLAY_RESULT", "/tmp/kilo/live_canary_v4_result.json")
with open(result_path, "w") as handle:
json.dump(report, handle, indent=1, default=str)
# Fail-closed pin proof: the whole packet exists to prove the published-catalog pin end-to-end.
# Fail-closed pin proof: the plan pin must equal the persisted record pin AND carry the
# published envelope identity; a truthiness-only check would pass a divergent generation.
assert isinstance(plan_pin, dict) and plan_pin.get("catalog_digest"), "runner_plan pin missing"
assert evaluation is not None and evaluation.baseline_pin, "AgentEvaluation baseline_pin not stamped"
record_pin = evaluation.baseline_pin if isinstance(evaluation.baseline_pin, dict) else {}
assert plan_pin == record_pin, f"plan pin != AgentEvaluation pin: {plan_pin} != {record_pin}"
for field in ("baseline_set_id", "baseline_set_version", "release_id", "catalog_digest", "catalog_revision_id"):
assert plan_pin.get(field) == publication.get(field), f"pin {field} diverges from published envelope"
if __name__ == "__main__":
main()
# #endregion ScenarioExecution.LiveCanaryV4.Run
# #endregion ScenarioExecution.LiveCanaryV4

View File

@@ -20,6 +20,7 @@ tests/test_mcp_initial_scenario_e2e.py — only the transport is live (httpx + S
# tests (client_flow_http + initial_scenario_e2e), so only the transport is live here.
# @REJECTED Hand-authoring the scenario graph was rejected — the derived-capability compile from live
# context (MCPX-FR-028) is the acceptance target, and baseline refs stay out (PAT-blocked pin).
import argparse
import base64
from hashlib import sha256
import json
@@ -39,10 +40,12 @@ sys.path.insert(0, os.path.join(_REPO_ROOT, "backend"))
import httpx # noqa: E402
from src.core.database import SessionLocal # noqa: E402
from src.models.scenario_run import ScenarioStepRun # noqa: E402
from src.models.scenario_run import ScenarioRun, ScenarioStepRun # noqa: E402
BASE = os.environ.get("SS_REPLAY_BASE", "http://127.0.0.1:8000")
ENVIRONMENT_ID = "ss-prod"
BASELINE_SET = "ss-prod-visual"
BASELINE_SET_VERSION = "1"
DASHBOARD_ID = 11
DASHBOARD_NAME = "Sales Dashboard"
ACTOR = os.environ.get("SS_REPLAY_USER", "admin")
@@ -186,7 +189,7 @@ class LiveMcpReplay:
# (baseline capability stays off), so the gated start never needs the unpublished catalog.
# @REJECTED Hand-building the scenario fixture like the 2026-09-07 run was rejected — T029m demands the
# derived-capability compile from live context (MCPX-FR-028), not a client-authored graph.
def run_chain() -> dict:
def run_chain(baseline: bool = False) -> dict:
replay = LiveMcpReplay()
evidence: dict = {"base": BASE, "environment_id": ENVIRONMENT_ID, "dashboard_id": DASHBOARD_ID}
@@ -218,14 +221,14 @@ def run_chain() -> dict:
agent_run_id = agent_run["run_id"]
evidence["agent_run_id"] = agent_run_id
capabilities = {**context["derived_capabilities"]["capabilities"], "screenshot": True, "baseline": False}
capabilities = {**context["derived_capabilities"]["capabilities"], "screenshot": True, "baseline": baseline}
compiled = replay.call("inspect_scenario", {"request": {
"agent_run_id": agent_run_id,
"objective": {"goal": "t029m live sales smoke", "selected_case_ids": ["B01"],
"rationale": "read-only external MCP replay"},
"query_model": context["query_model"],
"checklist_catalog_version": CHECKLIST_CATALOG_VERSION,
"baseline_version": "unpinned-live-replay",
"baseline_version": BASELINE_SET_VERSION if baseline else "unpinned-live-replay",
"capabilities": capabilities,
"parameters": {
"filter_values": {"type": "string_list", "required": True, "default": []},
@@ -277,14 +280,16 @@ def run_chain() -> dict:
"activation_status": boot.get("activation_status")}
start_key = f"t029m-run-{uuid.uuid4().hex[:12]}"
started = replay.call("start_scenario_run", {"request": {
start_request = {
"scenario_id": boot["scenario_id"], "revision_id": boot["revision_id"],
"environment_id": ENVIRONMENT_ID, "idempotency_key": start_key,
}})
retry = replay.call("start_scenario_run", {"request": {
"scenario_id": boot["scenario_id"], "revision_id": boot["revision_id"],
"environment_id": ENVIRONMENT_ID, "idempotency_key": start_key,
}})
}
if baseline:
# Explicit selector: the pin must resolve from the published Gitea envelope, not request bytes.
start_request["baseline_set"] = BASELINE_SET
start_request["baseline_set_version"] = BASELINE_SET_VERSION
started = replay.call("start_scenario_run", {"request": start_request})
retry = replay.call("start_scenario_run", {"request": start_request})
evidence["start"] = {"status": started.get("status"), "run_id": started.get("run_id"),
"retry_status": retry.get("status"), "retry_run_id": retry.get("run_id"),
"error": started.get("error")}
@@ -320,6 +325,17 @@ def run_chain() -> dict:
evidence["terminal"] = {"status": status, "phase": detail.get("phase"), "error_code": detail.get("error_code"),
"synthetic_pass_guard": "non_pass_confirmed"}
evidence["steps"] = _step_report(run_id)
if baseline:
# T046 baseline-pin proof: the plan pin must come from the published catalog at start time.
with SessionLocal() as db:
plan_pin = (db.get(ScenarioRun, run_id).runner_plan or {}).get("baseline_pin")
evidence["baseline_pin"] = {
"baseline_set_id": (plan_pin or {}).get("baseline_set_id"),
"baseline_set_version": (plan_pin or {}).get("baseline_set_version"),
"catalog_digest": (plan_pin or {}).get("catalog_digest"),
}
if not isinstance(plan_pin, dict) or not plan_pin.get("catalog_digest"):
raise ReplayError("baseline pin was not resolved through the MCP start")
return evidence
@@ -339,10 +355,14 @@ def _step_report(run_id: str) -> list[dict]:
# #region ScenarioExecution.LiveMcpReplay.Main [C:2] [TYPE Function] [SEMANTICS replay,entrypoint,report]
# @BRIEF Entry point: run the chain, print and dump the evidence JSON (fail-closed on ReplayError).
# @BRIEF Entry point: run the chain (optionally with the published baseline selector), dump evidence.
def main() -> None:
parser = argparse.ArgumentParser(description="T029m live external MCP replay (optionally baseline-pinned).")
parser.add_argument("--baseline", action="store_true",
help="compile with the baseline capability and start with the published-catalog selector")
args = parser.parse_args()
try:
evidence = run_chain()
evidence = run_chain(baseline=args.baseline)
evidence["result"] = "ok"
except ReplayError as exc:
evidence = {"result": "blocked", "error": str(exc)}

View File

@@ -243,10 +243,10 @@ Historical [x] rows above retain only their dated local/transport evidence; they
Contract: [Production baseline-backed evaluation](contracts/production-chain.md).
- [ ] T043 [P0/P1/P2] Baseline set/version/catalog/release/commit/IDs/digests affect idempotency; moving catalog after admission cannot change plan/result; legacy unpinned result is ineligible. Implement at the existing 044 domain boundary; verify with independent hardcoded fixtures and retain command/evidence references in traceability.md. **Status (2026-09-10):** request-hash pinning + fail-closed resolver landed offline (commits `83727aa7`/`bbbd4ccf`, `test_start_run_server_authority.py`); the caller-side published-catalog source is wired. Live pin-from-Gitea trace BLOCKED on a valid Gitea PAT (provided token rejected 401).
- [ ] T043 [P0/P1/P2] Baseline set/version/catalog/release/commit/IDs/digests affect idempotency; moving catalog after admission cannot change plan/result; legacy unpinned result is ineligible. Implement at the existing 044 domain boundary; verify with independent hardcoded fixtures and retain command/evidence references in traceability.md. **Status (2026-09-11):** request-hash pinning + fail-closed resolver landed offline (`83727aa7`/`bbbd4ccf`); caller-side published-catalog source wired. Live pin-from-Gitea is now PROVEN: REST canary v4 (`ace916a0…`/`adeabe63…`) + MCP `--baseline` chain resolve the published-envelope pin and the walker stamps it into `AgentEvaluation.baseline_pin` (strict equality) — `docs/reports/agentic-runtime-live-canary-v4-baseline-pin-2026-09-11.md`. Residual: the graph-level deterministic comparison PASS and the MCP publish tool are separate rows (050 T045).
- [ ] T044 [P0/P1/P2] GET/HEAD prove same ACL/status/headers; MIME/digest/length checked before bytes, cross-owner hidden, expired410, corrupt409, traversal/range/oversize rejected. Implement at the existing 044 domain boundary; verify with independent hardcoded fixtures and retain command/evidence references in traceability.md. **Status (2026-09-10):** Slice G GET/HEAD runtime landed and committed (`83727aa7`, `scenario_artifact_content.py`); live storage canary proved durable bytes with digests (canary v1 `597274d3`). Live ACL/status/header canary not yet exercised.
- [ ] T045 [P0/P1/P2] Startup/readiness/start-loop and shutdown/drain/cancel/reconcile survive fault injection; unknown effect quarantines capacity; late response cannot win. Implement at the existing 044 domain boundary; verify with independent hardcoded fixtures and retain command/evidence references in traceability.md. **Status (2026-09-10):** startup/readiness live-verified (provider loop ready, readiness preflight; browser-probe deadlock fixed `ba2f1f45`); capacity leases claimed/released across the live canaries. Fault-injection drain/cancel/reconcile canary not yet exercised.
- [ ] T046 [P0/P1/P2] End-to-end real browser→capture→durable artifact→deterministic comparison→optional immutable evaluation→policy→result; all required evidence present before PASS. Implement at the existing 044 domain boundary; verify with independent hardcoded fixtures and retain command/evidence references in traceability.md. **Status (2026-09-10):** the chain real browser→capture→durable artifact→immutable evaluation→policy→result is PROVEN live (canary v2 `4eebfab3`: `AgentEvaluation` persisted, DecisionPolicy row 11). Still missing: the deterministic comparison step in a live graph and a live baseline pin (Gitea PAT). Note: the canary's typed inconclusive outcome is the honest verdict while prompts remain text-only (images not attached).
- [ ] T046 [P0/P1/P2] End-to-end real browser→capture→durable artifact→deterministic comparison→optional immutable evaluation→policy→result; all required evidence present before PASS. Implement at the existing 044 domain boundary; verify with independent hardcoded fixtures and retain command/evidence references in traceability.md. **Status (2026-09-11):** the chain real browser→capture→durable artifact→immutable evaluation→policy→result is PROVEN live; a deterministic comparison step and a live baseline pin are now both in live graphs (canary v4: `compare_to_baseline` deterministically passes; pin resolved from the published envelope and stamped into `AgentEvaluation.baseline_pin`, `docs/reports/agentic-runtime-live-canary-v4-baseline-pin-2026-09-11.md`). Residual: a graph-level terminal PASS (the baseline-semantic policy marks comparisons without a bound evaluation as typed `EVALUATION_UNAVAILABLE`; `evaluate-visual` itself passes).
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.

View File

@@ -93,7 +93,7 @@ surfaces outside the 050 MCP catalog (no spec row existed before this note):
| Seam | Contract | Surface | Evidence |
|---|---|---|---|
| `Api.ScenarioLiveBindings` | `ScenarioExecution.LiveBinding` + `Core.ConfigManager` | admin REST `GET/PUT/PATCH /api/scenario-live-bindings` (`admin:settings`); validates `LiveExecutionBinding.from_snapshot` + `DashboardQueryModel` (env/dashboard/fingerprint) and writes `settings.scenario_live_execution_bindings` atomically | `backend/tests/api/test_scenario_live_bindings_api.py` (7); first live exercise: re-registered `ss-prod-d11-live-001` with the correct principal, then the T029m run adopted it server-side |
| `Tooling.PublishCatalog` | T045/D7 published-catalog source (`PUBLISHED_CATALOG_*`) | CLI `backend/src/scripts/publish_catalog.py` — pre-validates catalog bytes, then Gitea contents PUT (typed auth/conflict/unavailable) | `backend/tests/scripts/test_publish_catalog.py` (7, offline); publication itself blocked on a valid Gitea PAT |
| `Tooling.PublishCatalog` | T045/D7 published-catalog source (`PUBLISHED_CATALOG_*`) | CLI `backend/src/scripts/publish_catalog.py` — validates the resolver-canonical envelope, then Gitea contents create=POST/update=PUT (typed auth/conflict/unavailable) | `backend/tests/scripts/test_publish_catalog.py` (8, offline); LIVE 2026-09-11: create `e41d6ad2`, update-sha `3bb2c63e`, envelope `4d5b7e4c`/`3f782574`, publish-failure canary typed `PUBLISH_AUTH_FAILED`. 050 T045 stays OPEN for the curated MCP `publish_baseline_catalog` (MCPX-FR-030) |
Cross-reference: the live external MCP replay that exercised the binding path end-to-end (and exposed
the identity-less-compiled-step defect) is `docs/2026-09-11-sales-prod-mcp-replay.md` (050 T029m /

View File

@@ -93,7 +93,7 @@ Historical [x] rows above retain only their dated local/transport evidence; they
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`); publisher `backend/src/scripts/publish_catalog.py` with pre-validation before any network I/O and typed auth/conflict/unavailable (offline `tests/scripts/test_publish_catalog.py`); LIVE evidence (`docs/reports/agentic-runtime-live-canary-v4-baseline-pin-2026-09-11.md`): create `e41d6ad2` (POST), update-sha `3bb2c63e` (PUT), publish-failure canary with a bogus token → typed `PUBLISH_AUTH_FAILED` (HTTP 401, exit 1, no partial state); every prerequisite externally reachable (T029m CLOSED). Defects found and fixed live: Gitea create=POST/update=PUT contract; `run.sh` wiping deployment keys below the MCP_JWT line on every start (venv-python validation fallback + single-line in-place rewrite).
- [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.** Fresh REST chain (canary v2 `4eebfab3`) and fresh MCP-external chain (T029m) preserve the server-resolved binding and verified authoritative context (`context_authority=verified`); the complete baseline pin is proven live by canary v4 (run `ace916a0…`, `docs/reports/agentic-runtime-live-canary-v4-baseline-pin-2026-09-11.md`): selector-driven resolution from the PUBLISHED Gitea envelope → `runner_plan.baseline_pin` == `AgentEvaluation.baseline_pin` (walker stamping, commit `792bb125`), no raw ORM/REST repair; frontend agent controls remain absent (2026-09-10 evidence).
- [ ] 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): OPEN — partially delivered.** CLOSED parts: consume path typed fail-closed (`bbbd4ccf`, live-proven); CLI publisher `backend/src/scripts/publish_catalog.py` with pre-validation and typed auth/conflict/unavailable (offline tests + LIVE create/update/publish-failure canary, `docs/reports/agentic-runtime-live-canary-v4-baseline-pin-2026-09-11.md`). REMAINING (the reason this stays OPEN): MCPX-FR-030 requires a curated MCP `publish_baseline_catalog` operation on the 037 publication-worker contract (explicit authorized intent, `expected_branch_head` CAS, commit/pushed receipt, reconcile) — `backend/src/mcp_server/` registers no publish tool, and the CLI is a plain contents write, so "every prerequisite is externally MCP-reachable" is NOT yet true for publication.
- [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).
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.