feat(staging-check): ORCH-048 B6 reads registry via GET /projects, not local import
B6 built the project registry by importing src.projects locally (host-path hack
+ importlib.reload), so it evaluated ORCH_PROJECTS_JSON from the launcher's
process-env. On the deployer's canonical host run that var is unset → built-in
default (ET+ORCH) → false FAIL even when staging isolation is healthy.
- Add read-only additive endpoint GET /projects (src/main.py) returning
known_plane_project_ids + {plane_project_id, repo, work_item_prefix, name}
of the live process; no secrets. Existing routes unchanged.
- Rewrite B6 to fetch GET {base}/projects via the same stdlib _get helper as
A/B4/B5/C; drop the host-path hack and importlib.reload (launch-invariant).
- Isolate the verdict in pure _evaluate_b6(known) -> (passed, detail); contract
unchanged (PASS iff SANDBOX in known and prod ET/ORCH absent). Endpoint
degradation (non-200 / missing key / bad body / network) → deterministic FAIL.
- src/projects.py and .env* untouched.
Docs (golden source): API table + staging-gate B6 mechanic in
docs/architecture/README.md; B6 description + isolation row in
docs/operations/STAGING_CHECK.md; CHANGELOG entry.
Tests: tests/test_staging_check_b6.py (TC-01..TC-07), tests/test_projects_endpoint.py.
Refs: ORCH-048
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -214,8 +214,74 @@ SANDBOX_PROJECT_ID = "8c5a3025-4f9d-4190-b79f-fa06276bb27e"
|
||||
PROD_ET_PROJECT_ID = "7a79f0a9-5278-49cd-9007-9a338f238f9c"
|
||||
PROD_ORCH_PROJECT_ID = "8da6aa25-a60e-44d6-a1e2-d8ae59aa7d6a"
|
||||
|
||||
B6_LABEL = "B6 Registry: sandbox present, prod ET/ORCH absent"
|
||||
|
||||
def block_b(results: Results):
|
||||
|
||||
def _evaluate_b6(known: set[str]) -> tuple[bool, str]:
|
||||
"""Pure B6 verdict (ORCH-048, 02-trz §9 / TR-2, TR-3).
|
||||
|
||||
PASS ⟺ sandbox project is registered AND neither prod project (ET / ORCH) is.
|
||||
Isolated from any I/O so the registry-isolation contract is unit-testable on
|
||||
both outcomes (clean → PASS, polluted → FAIL) without a live staging instance
|
||||
or docker. ``detail`` keeps the human-readable format the suite already emits.
|
||||
"""
|
||||
sandbox_present = SANDBOX_PROJECT_ID in known
|
||||
et_absent = PROD_ET_PROJECT_ID not in known
|
||||
orch_absent = PROD_ORCH_PROJECT_ID not in known
|
||||
ok = sandbox_present and et_absent and orch_absent
|
||||
detail = (
|
||||
f"sandbox={'YES' if sandbox_present else 'NO'}, "
|
||||
f"prod-ET={'NO(good)' if et_absent else 'YES(BAD!)'}, "
|
||||
f"prod-ORCH={'NO(good)' if orch_absent else 'YES(BAD!)'}"
|
||||
)
|
||||
return ok, detail
|
||||
|
||||
|
||||
def _check_b6(base: str, results: Results) -> None:
|
||||
"""B6 — registry isolation, read over HTTP from the LIVE instance (ORCH-048).
|
||||
|
||||
Fetches ``GET {base}/projects`` (the read-only endpoint added in ADR-001) and
|
||||
evaluates ``known_plane_project_ids`` with :func:`_evaluate_b6`. The endpoint
|
||||
reflects the registry of the very process that serves webhooks (port 8501),
|
||||
independent of how this suite was launched (host / ``docker exec``).
|
||||
|
||||
Replaces the old local-import hack (a hardcoded host-path insert plus a forced
|
||||
module reload of the registry, TR-6) which evaluated the registry of whoever ran
|
||||
the script — on a host run with no ``ORCH_PROJECTS_JSON`` that fell back to the
|
||||
built-in default (ET+ORCH) → false FAIL.
|
||||
|
||||
Any source degradation (non-200 / missing or malformed key / network error)
|
||||
yields a **deterministic FAIL** with a readable detail (TR-4) — never a false
|
||||
PASS and never an unhandled exception.
|
||||
"""
|
||||
try:
|
||||
status, data = _get(f"{base}/projects")
|
||||
except Exception as e:
|
||||
results.add(B6_LABEL, False, f"GET /projects failed: {e}")
|
||||
return
|
||||
|
||||
if status != 200:
|
||||
results.add(B6_LABEL, False, f"GET /projects → HTTP {status}")
|
||||
return
|
||||
if not isinstance(data, dict) or "known_plane_project_ids" not in data:
|
||||
keys = list(data.keys()) if isinstance(data, dict) else type(data).__name__
|
||||
results.add(B6_LABEL, False,
|
||||
f"GET /projects → 200 but no 'known_plane_project_ids' (got {keys})")
|
||||
return
|
||||
|
||||
raw = data.get("known_plane_project_ids")
|
||||
if not isinstance(raw, (list, tuple, set)):
|
||||
results.add(B6_LABEL, False,
|
||||
f"GET /projects → 'known_plane_project_ids' not a list "
|
||||
f"(got {type(raw).__name__})")
|
||||
return
|
||||
|
||||
known = {str(x) for x in raw}
|
||||
ok, detail = _evaluate_b6(known)
|
||||
results.add(B6_LABEL, ok, detail)
|
||||
|
||||
|
||||
def block_b(base: str, results: Results):
|
||||
print(f"\n{_BOLD}[Block B] ACCESS{_RESET}")
|
||||
|
||||
plane_token = os.environ.get("ORCH_PLANE_API_TOKEN", "")
|
||||
@@ -260,28 +326,9 @@ def block_b(results: Results):
|
||||
except Exception as e:
|
||||
results.add("B5 Gitea: orchestrator-sandbox accessible, push=true", False, str(e))
|
||||
|
||||
# B6 — Registry: sandbox in known IDs, prod ET/ORCH NOT in known IDs
|
||||
try:
|
||||
# Import from inside the container (script runs in /repos/orchestrator context)
|
||||
sys.path.insert(0, "/repos/orchestrator")
|
||||
# Force reload to pick up container env
|
||||
import importlib
|
||||
if "src.projects" in sys.modules:
|
||||
importlib.reload(sys.modules["src.projects"])
|
||||
from src.projects import known_plane_project_ids
|
||||
known = known_plane_project_ids()
|
||||
sandbox_present = SANDBOX_PROJECT_ID in known
|
||||
et_absent = PROD_ET_PROJECT_ID not in known
|
||||
orch_absent = PROD_ORCH_PROJECT_ID not in known
|
||||
ok = sandbox_present and et_absent and orch_absent
|
||||
detail = (
|
||||
f"sandbox={'YES' if sandbox_present else 'NO'}, "
|
||||
f"prod-ET={'NO(good)' if et_absent else 'YES(BAD!)'}, "
|
||||
f"prod-ORCH={'NO(good)' if orch_absent else 'YES(BAD!)'}"
|
||||
)
|
||||
results.add("B6 Registry: sandbox present, prod ET/ORCH absent", ok, detail)
|
||||
except Exception as e:
|
||||
results.add("B6 Registry: sandbox present, prod ET/ORCH absent", False, str(e))
|
||||
# B6 — Registry: sandbox in known IDs, prod ET/ORCH NOT in known IDs.
|
||||
# Read over HTTP from the live instance (GET /projects) — ORCH-048 / ADR-001.
|
||||
_check_b6(base, results)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -628,7 +675,7 @@ def main():
|
||||
results = Results()
|
||||
|
||||
block_a(base, results)
|
||||
block_b(results)
|
||||
block_b(base, results)
|
||||
block_c(base, results, args.mode)
|
||||
|
||||
all_ok = results.summary()
|
||||
|
||||
Reference in New Issue
Block a user