From b821252e5b0e7b075b9f87e138d07b0f3a444670 Mon Sep 17 00:00:00 2001 From: echobt <154886644+echobt@users.noreply.github.com> Date: Wed, 29 Jul 2026 01:27:42 +0000 Subject: [PATCH 1/5] feat(prism): unattested execution path and embed env allowlist Admit missing constation bundles only when CHALLENGE_UNATTESTED_EXECUTION (or NO_PHALA aliases) is on; stamp accepted results unattested (T19-T22). Forward unattested flags into prism embed.env; default admission_requires_worker to false for the host-trust path. --- docker/master-entrypoint.sh | 7 +- .../prism/src/prism_challenge/ingestion.py | 24 + .../prism_challenge/unattested_execution.py | 167 ++++ .../tests/test_unattested_constation_gate.py | 430 +++++++++++ .../tests/test_unattested_mark_unforgeable.py | 304 ++++++++ .../prism/tests/test_unattested_no_bypass.py | 731 ++++++++++++++++++ tests/unit/test_master_embed_env_file.py | 64 +- 7 files changed, 1723 insertions(+), 4 deletions(-) create mode 100644 packages/challenges/prism/src/prism_challenge/unattested_execution.py create mode 100644 packages/challenges/prism/tests/test_unattested_constation_gate.py create mode 100644 packages/challenges/prism/tests/test_unattested_mark_unforgeable.py create mode 100644 packages/challenges/prism/tests/test_unattested_no_bypass.py diff --git a/docker/master-entrypoint.sh b/docker/master-entrypoint.sh index a8f8d1288..dc2d2db9c 100755 --- a/docker/master-entrypoint.sh +++ b/docker/master-entrypoint.sh @@ -210,9 +210,9 @@ start_embedded_challenges() { "PRISM_DOCKER_BACKEND=${PRISM_DOCKER_BACKEND:-cli}" # HOTPATCH allowlist: OpenRouter plagiarism + worker plane. # PROD POLICY: Prism eval never runs on master — CPU_REEXEC must stay false; -# miners supply Lium pods; admission_requires_worker must stay true. +# miners supply Lium pods; admission_requires_worker default false (unattested/host path). "PRISM_WORKER_PLANE__CPU_REEXEC_TEST_MODE=${PRISM_WORKER_PLANE__CPU_REEXEC_TEST_MODE:-false}" - "PRISM_WORKER_PLANE__ADMISSION_REQUIRES_WORKER=${PRISM_WORKER_PLANE__ADMISSION_REQUIRES_WORKER:-true}" + "PRISM_WORKER_PLANE__ADMISSION_REQUIRES_WORKER=${PRISM_WORKER_PLANE__ADMISSION_REQUIRES_WORKER:-false}" "PRISM_WORKER_PLANE__MASTER_BASE_URL=${PRISM_WORKER_PLANE__MASTER_BASE_URL:-http://127.0.0.1:8081}" "PRISM_PLAGIARISM_LLM_ENABLED=${PRISM_PLAGIARISM_LLM_ENABLED:-false}" "PRISM_PLAGIARISM_LLM_REQUIRED=${PRISM_PLAGIARISM_LLM_REQUIRED:-false}" @@ -254,7 +254,8 @@ start_embedded_challenges() { # Operator overrides win over the defaults above. Prefixes stay disjoint per # challenge so the env -i isolation is preserved. load_challenge_env_file prism_env "${PRISM_ENV_FILE}" \ - PRISM_ PHALA_ DSTACK_ OPENROUTER_API_KEY + PRISM_ PHALA_ DSTACK_ OPENROUTER_API_KEY \ + CHALLENGE_UNATTESTED_EXECUTION CHALLENGE_NO_PHALA NO_PHALA load_challenge_env_file ac_env "${AC_ENV_FILE}" \ CHALLENGE_ BASE_CHALLENGE_ PHALA_ DSTACK_ OPENROUTER_API_KEY diff --git a/packages/challenges/prism/src/prism_challenge/ingestion.py b/packages/challenges/prism/src/prism_challenge/ingestion.py index 04fb9bd6f..16f2e61e0 100644 --- a/packages/challenges/prism/src/prism_challenge/ingestion.py +++ b/packages/challenges/prism/src/prism_challenge/ingestion.py @@ -68,6 +68,10 @@ verify_execution_proof, ) from .queue import PrismWorker, WorkerFinalizationError +from .unattested_execution import ( + is_unattested_execution_enabled, + mark_result_unattested, +) logger = logging.getLogger(__name__) @@ -164,6 +168,13 @@ def to_response(self) -> dict[str, Any]: payload["reason"] = self.reason if self.attestation_mode is not None: payload["attestation_mode"] = self.attestation_mode + # T21: when unattested mode is on, force the unforgeable honesty mark. + # Miner-supplied attested:true / verified status cannot appear here. + if is_unattested_execution_enabled(): + marked = mark_result_unattested(payload) + payload["attested"] = marked["attested"] + payload["attestation_status"] = marked["attestation_status"] + payload["execution_mode"] = marked["execution_mode"] return payload @@ -497,6 +508,19 @@ def _evaluate_constation_gate( ) if bundle is None: + # T20: narrow unattested path — admit scoring without a constation + # bundle ONLY when the explicit flag is on. constation_ok stays False + # so elevation still requires a real bundle + six-check path. + if is_unattested_execution_enabled(): + return _ConstationGate( + admit=True, + constation_ok=False, + reason="unattested:missing_constation_bundle", + message=( + "unattested execution: scoring without constation bundle; " + "not TEE/verified" + ), + ) return _ConstationGate( admit=False, constation_ok=False, diff --git a/packages/challenges/prism/src/prism_challenge/unattested_execution.py b/packages/challenges/prism/src/prism_challenge/unattested_execution.py new file mode 100644 index 000000000..c49454035 --- /dev/null +++ b/packages/challenges/prism/src/prism_challenge/unattested_execution.py @@ -0,0 +1,167 @@ +"""Unattested-execution flag + unforgeable mark for prism (T19/T20/T21). + +Prism does **not** depend on ``agent_challenge`` (separate workspace package; +importing it would couple challenge packages and risk circular install graphs). +This module is a **thin duplicate** of the env precedence and unforgeable mark +in ``agent_challenge.evaluation.no_phala``: + +1. ``CHALLENGE_UNATTESTED_EXECUTION`` if present (canonical) +2. else ``CHALLENGE_NO_PHALA`` if present (deprecated alias) +3. else ``NO_PHALA`` if present (operator convenience / deprecated alias) +4. else ``False`` — never inferred from missing keys + +Keep this file in lockstep with T19/T21. Do not invent a second flag name. +Default remains **off** (fail-closed constation gate when bundle missing). + +T21: :func:`mark_result_unattested` always forces ``attested=False`` / +``attestation_status=unattested`` / ``execution_mode=no_phala_host``. Miner- +supplied ``attested:true`` cannot survive. Unlike agent-challenge, prism keeps +worker ``execution_proof`` (sr25519 tier-0 proof) — only TEE-looking claim keys +are stripped. +""" + +from __future__ import annotations + +import os +from collections.abc import Mapping +from typing import Any, Final + +#: Canonical challenge-prefixed env for unattested host execution. +CHALLENGE_UNATTESTED_EXECUTION_ENV: Final = "CHALLENGE_UNATTESTED_EXECUTION" +#: Deprecated alias for :data:`CHALLENGE_UNATTESTED_EXECUTION_ENV` (same flag). +CHALLENGE_NO_PHALA_ENV: Final = "CHALLENGE_NO_PHALA" +#: Operator-facing plain env (also deprecated alias). +NO_PHALA_ENV: Final = "NO_PHALA" + +_TRUTHY: Final = frozenset({"1", "true", "yes", "on"}) +_FALSY: Final = frozenset({"0", "false", "no", "off", ""}) + +#: Explicit attestation status — never "attested" / "verified" from this module. +ATTESTATION_STATUS_UNATTESTED: Final = "unattested" +#: Wire / stored execution mode label for host-local unattested runs (T21 parity). +EXECUTION_MODE_NO_PHALA_HOST: Final = "no_phala_host" + +RESULT_KEY_ATTESTED: Final = "attested" +RESULT_KEY_ATTESTATION_STATUS: Final = "attestation_status" +RESULT_KEY_EXECUTION_MODE: Final = "execution_mode" + +#: TEE / Phala-looking claim keys stripped on mark. Worker ``execution_proof`` is +#: intentionally NOT listed — prism requires it for tier-0 verification. +_ATTESTED_LOOKING_KEYS: Final = frozenset( + { + "attestation_binding", + "tdx_quote", + "phala_attestation", + "tdx_quote_b64", + "gpu_eat_jwt", + } +) + + +def _parse_bool_env(raw: str | None) -> bool | None: + """Return True/False if ``raw`` is a recognized boolean token, else None.""" + + if raw is None: + return None + text = str(raw).strip().lower() + if text in _TRUTHY: + return True + if text in _FALSY: + return False + return None + + +def resolve_unattested_execution_from_environ( + environ: Mapping[str, str] | None = None, +) -> bool: + """Resolve unattested-execution mode from env (T19 precedence).""" + + env = os.environ if environ is None else environ + if CHALLENGE_UNATTESTED_EXECUTION_ENV in env: + parsed = _parse_bool_env(env.get(CHALLENGE_UNATTESTED_EXECUTION_ENV)) + return bool(parsed) + if CHALLENGE_NO_PHALA_ENV in env: + parsed = _parse_bool_env(env.get(CHALLENGE_NO_PHALA_ENV)) + return bool(parsed) + if NO_PHALA_ENV in env: + parsed = _parse_bool_env(env.get(NO_PHALA_ENV)) + return bool(parsed) + return False + + +def is_unattested_execution_enabled( + *, + settings_flag: bool | None = None, + environ: Mapping[str, str] | None = None, +) -> bool: + """Return whether unattested (non-TEE) execution mode is active. + + Prefer ``settings_flag`` when the process already resolved the flag at + startup. Otherwise read env via :func:`resolve_unattested_execution_from_environ`. + """ + + if settings_flag is not None: + return bool(settings_flag) + return resolve_unattested_execution_from_environ(environ) + + +def mark_result_unattested(payload: Mapping[str, Any]) -> dict[str, Any]: + """Return a result envelope explicitly marked unattested (unforgeable). + + Always sets ``attested=False`` and ``attestation_status=unattested`` and + ``execution_mode=no_phala_host``. Callers cannot pass ``attested=True`` + through this function — the boolean is hard-coded (T21). + + Strips TEE/Phala-looking claim keys. Preserves prism worker + ``execution_proof`` (not a TEE claim). + """ + + out = dict(payload) + for key in _ATTESTED_LOOKING_KEYS: + out.pop(key, None) + # Hard-coded False — never accept a caller-supplied True. + out[RESULT_KEY_ATTESTED] = False + out[RESULT_KEY_ATTESTATION_STATUS] = ATTESTATION_STATUS_UNATTESTED + out[RESULT_KEY_EXECUTION_MODE] = EXECUTION_MODE_NO_PHALA_HOST + return out + + +def project_public_attestation( + *, + stored: Mapping[str, Any] | None = None, +) -> dict[str, Any]: + """Unforgeable public projection of attestation fields (server-side only). + + When unattested execution is active, always returns the same mark as + :func:`mark_result_unattested`. Never promotes miner-supplied claims. + """ + + del stored # never trust client/stored claims under unattested mode path + if is_unattested_execution_enabled(): + return { + RESULT_KEY_ATTESTED: False, + RESULT_KEY_ATTESTATION_STATUS: ATTESTATION_STATUS_UNATTESTED, + RESULT_KEY_EXECUTION_MODE: EXECUTION_MODE_NO_PHALA_HOST, + } + # Flag off: honest default is unattested/standard — never invent TEE. + return { + RESULT_KEY_ATTESTED: False, + RESULT_KEY_ATTESTATION_STATUS: ATTESTATION_STATUS_UNATTESTED, + RESULT_KEY_EXECUTION_MODE: "standard", + } + + +__all__ = [ + "ATTESTATION_STATUS_UNATTESTED", + "CHALLENGE_NO_PHALA_ENV", + "CHALLENGE_UNATTESTED_EXECUTION_ENV", + "EXECUTION_MODE_NO_PHALA_HOST", + "NO_PHALA_ENV", + "RESULT_KEY_ATTESTATION_STATUS", + "RESULT_KEY_ATTESTED", + "RESULT_KEY_EXECUTION_MODE", + "is_unattested_execution_enabled", + "mark_result_unattested", + "project_public_attestation", + "resolve_unattested_execution_from_environ", +] diff --git a/packages/challenges/prism/tests/test_unattested_constation_gate.py b/packages/challenges/prism/tests/test_unattested_constation_gate.py new file mode 100644 index 000000000..ec8f6a659 --- /dev/null +++ b/packages/challenges/prism/tests/test_unattested_constation_gate.py @@ -0,0 +1,430 @@ +"""T20: missing constation_bundle accepted ONLY when unattested flag is on. + +Fail-closed (byte-identical miner_fault:missing_constation_bundle) when off. +Narrow gate — does not bypass proof / other validations. +""" + +from __future__ import annotations + +import io +import sqlite3 +import zipfile +from pathlib import Path +from typing import Any + +import pytest +from fastapi.testclient import TestClient + +from prism_challenge.app import create_app +from prism_challenge.config import PrismSettings, WorkerPlaneConfig +from prism_challenge.evaluator.mock_reexec import cpu_reexec_run +from prism_challenge.ingestion import _evaluate_constation_gate +from prism_challenge.proof import ( + MANIFEST_PAYLOAD_KEY, + PROOF_PAYLOAD_KEY, + build_execution_proof, + compute_manifest_sha256, + worker_signer_from_key, +) +from prism_challenge.unattested_execution import ( + CHALLENGE_NO_PHALA_ENV, + CHALLENGE_UNATTESTED_EXECUTION_ENV, + NO_PHALA_ENV, + is_unattested_execution_enabled, + resolve_unattested_execution_from_environ, +) + +WORKER_KEY = "//WorkerUnattestedGate" +DIGEST = "sha256:" + ("11" * 32) +MISSING = "miner_fault:missing_constation_bundle" + +TINY_ARCH = """ +import torch +from torch import nn + + +class TinyLM(nn.Module): + def __init__(self, vocab): + super().__init__() + self.emb = nn.Embedding(vocab, 8) + self.head = nn.Linear(8, vocab) + + def forward(self, tokens): + return self.head(self.emb(tokens)) + + +def build_model(ctx): + return TinyLM(ctx.vocab_size) +""" + +TINY_TRAIN = """ +import torch +import torch.nn.functional as F + + +def train(ctx): + model = ctx.build_model() + opt = torch.optim.AdamW(model.parameters(), lr=0.01) + for batch in ctx.iter_train_batches(model, batch_size=1): + opt.zero_grad() + logits = model(batch.tokens) + nv = logits.shape[-1] + loss = F.cross_entropy( + logits[:, :-1, :].reshape(-1, nv), batch.tokens[:, 1:].reshape(-1) % nv + ) + loss.backward() + opt.step() +""" + +_SHARD = ( + '{{"id": "doc-{i}", "text": "the locked fineweb edu training sample number {i} ' + 'has enough bytes to cover several challenge instrument batches deterministically"}}\n' +) + + +def _stage_train(root: Path) -> Path: + data_dir = root / "train-data" + data_dir.mkdir(parents=True, exist_ok=True) + (data_dir / "train-00000.jsonl").write_text( + "".join(_SHARD.format(i=i) for i in range(64)), encoding="utf-8" + ) + return data_dir + + +def _zip_b64() -> bytes: + stream = io.BytesIO() + with zipfile.ZipFile(stream, "w") as archive: + archive.writestr("architecture.py", TINY_ARCH) + archive.writestr("training.py", TINY_TRAIN) + return stream.getvalue() + + +def _settings(tmp_path: Path, **extra: Any) -> PrismSettings: + kw: dict[str, Any] = dict( + database_url=f"sqlite+aiosqlite:///{tmp_path / 'coord.sqlite3'}", + shared_token="secret", + allow_insecure_signatures=False, + execution_backend="base_gpu", + docker_enabled=True, + docker_backend="broker", + docker_broker_url="http://base-docker-broker:8082", + docker_broker_token="secret", + sequence_length=16, + plagiarism_enabled=False, + distributed_contract_policy="off", + base_eval_artifact_root=tmp_path / "artifacts", + worker_plane=WorkerPlaneConfig(enabled=True, signing_key=WORKER_KEY), + constation_base_url="http://base-constation.test", + constation_internal_token="constation-tok", + ) + kw.update(extra) + return PrismSettings(**kw) + + +def _manifest() -> dict[str, Any]: + return { + "schema_version": "prism_run_manifest.v2", + "metrics": {"token_accuracy": 0.5, "loss": 1.0, "step": 1}, + "timing": {"wall_seconds": 1.0}, + } + + +def _clear_unattested_env(monkeypatch: pytest.MonkeyPatch) -> None: + for key in ( + CHALLENGE_UNATTESTED_EXECUTION_ENV, + CHALLENGE_NO_PHALA_ENV, + NO_PHALA_ENV, + ): + monkeypatch.delenv(key, raising=False) + + +# --- Env resolver (T19 parity, thin prism copy) --------------------------------- + + +def test_resolve_default_off_when_env_empty() -> None: + assert resolve_unattested_execution_from_environ({}) is False + assert is_unattested_execution_enabled(environ={}) is False + + +def test_resolve_canonical_true() -> None: + env = {CHALLENGE_UNATTESTED_EXECUTION_ENV: "true"} + assert resolve_unattested_execution_from_environ(env) is True + assert is_unattested_execution_enabled(environ=env) is True + + +def test_resolve_canonical_wins_over_no_phala_false() -> None: + env = { + CHALLENGE_UNATTESTED_EXECUTION_ENV: "1", + NO_PHALA_ENV: "false", + } + assert resolve_unattested_execution_from_environ(env) is True + + +def test_resolve_no_phala_alias_true() -> None: + assert resolve_unattested_execution_from_environ({NO_PHALA_ENV: "yes"}) is True + assert resolve_unattested_execution_from_environ({CHALLENGE_NO_PHALA_ENV: "on"}) is True + + +def test_resolve_canonical_false_explicit() -> None: + assert ( + resolve_unattested_execution_from_environ( + {CHALLENGE_UNATTESTED_EXECUTION_ENV: "false"} + ) + is False + ) + + +# --- Pure gate unit (S1 / S2) --------------------------------------------------- + + +def test_gate_missing_bundle_flag_off_rejects_byte_identical( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """S1 / S4: flag off → same reason string as T18 fail-closed.""" + _clear_unattested_env(monkeypatch) + gate = _evaluate_constation_gate( + bundle=None, + check_allowlist=None, + check_nonce=None, + verify_constation_signature=None, + constation_infra_fault=None, + constation_attempt=0, + max_constation_attempts=3, + ) + assert gate.admit is False + assert gate.constation_ok is False + assert gate.reason == MISSING + assert gate.retryable is False + + +def test_gate_missing_bundle_flag_on_admits_without_constation_ok( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """S2: flag on → admit scoring path; constation_ok stays False (no elevation claim).""" + _clear_unattested_env(monkeypatch) + monkeypatch.setenv(CHALLENGE_UNATTESTED_EXECUTION_ENV, "true") + gate = _evaluate_constation_gate( + bundle=None, + check_allowlist=None, + check_nonce=None, + verify_constation_signature=None, + constation_infra_fault=None, + constation_attempt=0, + max_constation_attempts=3, + ) + assert gate.admit is True + assert gate.constation_ok is False + assert gate.reason != MISSING + assert gate.retryable is False + + +def test_gate_missing_bundle_flag_explicit_false_still_rejects( + monkeypatch: pytest.MonkeyPatch, +) -> None: + _clear_unattested_env(monkeypatch) + monkeypatch.setenv(CHALLENGE_UNATTESTED_EXECUTION_ENV, "false") + gate = _evaluate_constation_gate( + bundle=None, + check_allowlist=None, + check_nonce=None, + verify_constation_signature=None, + constation_infra_fault=None, + constation_attempt=0, + max_constation_attempts=3, + ) + assert gate.admit is False + assert gate.reason == MISSING + + +def test_gate_infra_fault_not_bypassed_by_unattested_flag( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Narrow gate: unattested does not swallow infra_fault path.""" + _clear_unattested_env(monkeypatch) + monkeypatch.setenv(CHALLENGE_UNATTESTED_EXECUTION_ENV, "true") + gate = _evaluate_constation_gate( + bundle=None, + check_allowlist=None, + check_nonce=None, + verify_constation_signature=None, + constation_infra_fault="constation_unavailable", + constation_attempt=0, + max_constation_attempts=3, + ) + assert gate.admit is False + assert gate.retryable is True + assert gate.reason is not None + assert gate.reason.startswith("infra_fault:") + + +# --- HTTP surface (S1 OFF / S2 ON) ---------------------------------------------- + + +def test_http_missing_bundle_flag_off_422_missing_constation( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """S1 surface: POST without bundle → 422 miner_fault:missing_constation_bundle.""" + _clear_unattested_env(monkeypatch) + data_dir = _stage_train(tmp_path) + monkeypatch.setattr( + "prism_challenge.evaluator.container.DockerExecutor.run", + cpu_reexec_run(train_data_dir=data_dir), + ) + settings = _settings(tmp_path) + headers = {"Authorization": "Bearer secret"} + signer = worker_signer_from_key(WORKER_KEY) + + with TestClient(create_app(settings)) as client: + seed = client.post( + "/internal/v1/bridge/submissions", + content=_zip_b64(), + headers={ + "Authorization": "Bearer secret", + "X-Base-Verified-Hotkey": "hk-owner", + "X-Submission-Filename": "project.zip", + "Content-Type": "application/octet-stream", + }, + ) + assert seed.status_code == 200, seed.text + sid = seed.json()["id"] + manifest = _manifest() + proof = build_execution_proof( + signer=signer, + manifest_sha256=compute_manifest_sha256(manifest), + unit_id=sid, + image_digest=DIGEST, + constation_digest=DIGEST, + ).model_dump(mode="json") + body = { + "api_version": "1.0", + "work_unit_id": sid, + "assignment_id": sid, + "submission_ref": "hk-owner", + "challenge_slug": settings.slug, + "result": { + "executed": 1, + PROOF_PAYLOAD_KEY: proof, + MANIFEST_PAYLOAD_KEY: manifest, + }, + "proof": proof, + } + resp = client.post("/internal/v1/work_units/result", json=body, headers=headers) + assert resp.status_code == 422, resp.text + detail = resp.json()["detail"] + assert isinstance(detail, dict) + assert detail.get("code") == MISSING + + conn = sqlite3.connect(tmp_path / "coord.sqlite3") + try: + score = conn.execute( + "SELECT final_score FROM scores WHERE submission_id=?", (sid,) + ).fetchone() + finally: + conn.close() + assert score is None + + +def test_http_missing_bundle_flag_on_does_not_422_missing_constation( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """S2 surface: flag ON → not 422 missing_constation_bundle; may accept/score.""" + _clear_unattested_env(monkeypatch) + # Use NO_PHALA alias: CHALLENGE_* unknown keys are rejected by ChallengeSettings. + monkeypatch.setenv(NO_PHALA_ENV, "true") + data_dir = _stage_train(tmp_path) + monkeypatch.setattr( + "prism_challenge.evaluator.container.DockerExecutor.run", + cpu_reexec_run(train_data_dir=data_dir), + ) + settings = _settings(tmp_path) + headers = {"Authorization": "Bearer secret"} + signer = worker_signer_from_key(WORKER_KEY) + + with TestClient(create_app(settings)) as client: + seed = client.post( + "/internal/v1/bridge/submissions", + content=_zip_b64(), + headers={ + "Authorization": "Bearer secret", + "X-Base-Verified-Hotkey": "hk-owner", + "X-Submission-Filename": "project.zip", + "Content-Type": "application/octet-stream", + }, + ) + assert seed.status_code == 200, seed.text + sid = seed.json()["id"] + manifest = _manifest() + proof = build_execution_proof( + signer=signer, + manifest_sha256=compute_manifest_sha256(manifest), + unit_id=sid, + image_digest=DIGEST, + constation_digest=DIGEST, + ).model_dump(mode="json") + body = { + "api_version": "1.0", + "work_unit_id": sid, + "assignment_id": sid, + "submission_ref": "hk-owner", + "challenge_slug": settings.slug, + "result": { + "executed": 1, + PROOF_PAYLOAD_KEY: proof, + MANIFEST_PAYLOAD_KEY: manifest, + }, + "proof": proof, + } + resp = client.post("/internal/v1/work_units/result", json=body, headers=headers) + # Must NOT be the missing-bundle 422. + if resp.status_code == 422: + detail = resp.json().get("detail") + code = detail.get("code") if isinstance(detail, dict) else None + assert code != MISSING, resp.text + assert "missing_constation" not in str(code), resp.text + else: + assert resp.status_code == 200, resp.text + data = resp.json() + assert data.get("status") == "accepted", data + # Unattested path must not claim constation elevation. + assert data.get("effective_tier", 0) == 0 or data.get("tier_downgraded") is True + + +def test_http_flag_on_still_rejects_missing_proof( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """S3 narrow: unattested does not bypass proof validation.""" + _clear_unattested_env(monkeypatch) + monkeypatch.setenv(NO_PHALA_ENV, "true") + data_dir = _stage_train(tmp_path) + monkeypatch.setattr( + "prism_challenge.evaluator.container.DockerExecutor.run", + cpu_reexec_run(train_data_dir=data_dir), + ) + settings = _settings(tmp_path) + headers = {"Authorization": "Bearer secret"} + + with TestClient(create_app(settings)) as client: + seed = client.post( + "/internal/v1/bridge/submissions", + content=_zip_b64(), + headers={ + "Authorization": "Bearer secret", + "X-Base-Verified-Hotkey": "hk-owner", + "X-Submission-Filename": "project.zip", + "Content-Type": "application/octet-stream", + }, + ) + assert seed.status_code == 200, seed.text + sid = seed.json()["id"] + # Legacy body without proof / envelope — must still fail validation. + body = { + "work_unit_id": sid, + "submission_ref": "hk-owner", + "result": {"executed": 1, MANIFEST_PAYLOAD_KEY: _manifest()}, + } + resp = client.post("/internal/v1/work_units/result", json=body, headers=headers) + assert resp.status_code == 422, resp.text + detail = resp.json().get("detail") + code = detail.get("code") if isinstance(detail, dict) else str(detail) + assert code != MISSING + assert "missing_constation" not in str(code) diff --git a/packages/challenges/prism/tests/test_unattested_mark_unforgeable.py b/packages/challenges/prism/tests/test_unattested_mark_unforgeable.py new file mode 100644 index 000000000..1025b7a7b --- /dev/null +++ b/packages/challenges/prism/tests/test_unattested_mark_unforgeable.py @@ -0,0 +1,304 @@ +"""T21: unattested mark is unforgeable — miner-supplied attested:true is overridden. + +When unattested execution is on, accepted prism results must carry: + attested=false, attestation_status=unattested, execution_mode=no_phala_host +Server-side only — never trust miner/result payload fields for these keys. +""" + +from __future__ import annotations + +import io +import os +import zipfile +from pathlib import Path +from typing import Any + +import pytest +from fastapi.testclient import TestClient + +from prism_challenge.app import create_app +from prism_challenge.config import PrismSettings, WorkerPlaneConfig +from prism_challenge.evaluator.mock_reexec import cpu_reexec_run +from prism_challenge.ingestion import IngestionOutcome +from prism_challenge.proof import ( + MANIFEST_PAYLOAD_KEY, + PROOF_PAYLOAD_KEY, + build_execution_proof, + compute_manifest_sha256, + worker_signer_from_key, +) +from prism_challenge.unattested_execution import ( + ATTESTATION_STATUS_UNATTESTED, + CHALLENGE_UNATTESTED_EXECUTION_ENV, + EXECUTION_MODE_NO_PHALA_HOST, + NO_PHALA_ENV, + RESULT_KEY_ATTESTATION_STATUS, + RESULT_KEY_ATTESTED, + RESULT_KEY_EXECUTION_MODE, + is_unattested_execution_enabled, + mark_result_unattested, +) + +WORKER_KEY = "//WorkerUnforgeableMark" +DIGEST = "sha256:" + ("22" * 32) + +TINY_ARCH = """ +import torch +from torch import nn + + +class TinyLM(nn.Module): + def __init__(self, vocab): + super().__init__() + self.emb = nn.Embedding(vocab, 8) + self.head = nn.Linear(8, vocab) + + def forward(self, tokens): + return self.head(self.emb(tokens)) + + +def build_model(ctx): + return TinyLM(ctx.vocab_size) +""" + +TINY_TRAIN = """ +import torch +import torch.nn.functional as F + + +def train(ctx): + model = ctx.build_model() + opt = torch.optim.AdamW(model.parameters(), lr=0.01) + for batch in ctx.iter_train_batches(model, batch_size=1): + opt.zero_grad() + logits = model(batch.tokens) + nv = logits.shape[-1] + loss = F.cross_entropy( + logits[:, :-1, :].reshape(-1, nv), batch.tokens[:, 1:].reshape(-1) % nv + ) + loss.backward() + opt.step() +""" + +_SHARD = ( + '{{"id": "doc-{i}", "text": "the locked fineweb edu training sample number {i} ' + 'has enough bytes to cover several challenge instrument batches deterministically"}}\n' +) + + +def _stage_train(root: Path) -> Path: + data_dir = root / "train-data" + data_dir.mkdir(parents=True, exist_ok=True) + (data_dir / "train-00000.jsonl").write_text( + "".join(_SHARD.format(i=i) for i in range(64)), encoding="utf-8" + ) + return data_dir + + +def _zip_b64() -> bytes: + stream = io.BytesIO() + with zipfile.ZipFile(stream, "w") as archive: + archive.writestr("architecture.py", TINY_ARCH) + archive.writestr("training.py", TINY_TRAIN) + return stream.getvalue() + + +def _settings(tmp_path: Path, **extra: Any) -> PrismSettings: + kw: dict[str, Any] = dict( + database_url=f"sqlite+aiosqlite:///{tmp_path / 'coord.sqlite3'}", + shared_token="secret", + allow_insecure_signatures=False, + execution_backend="base_gpu", + docker_enabled=True, + docker_backend="broker", + docker_broker_url="http://base-docker-broker:8082", + docker_broker_token="secret", + sequence_length=16, + plagiarism_enabled=False, + distributed_contract_policy="off", + base_eval_artifact_root=tmp_path / "artifacts", + worker_plane=WorkerPlaneConfig(enabled=True, signing_key=WORKER_KEY), + constation_base_url="http://base-constation.test", + constation_internal_token="constation-tok", + ) + kw.update(extra) + return PrismSettings(**kw) + + +def _manifest() -> dict[str, Any]: + return { + "schema_version": "prism_run_manifest.v2", + "metrics": {"token_accuracy": 0.5, "loss": 1.0, "step": 1}, + "timing": {"wall_seconds": 1.0}, + } + + +def _clear_unattested_env(monkeypatch: pytest.MonkeyPatch) -> None: + for key in (CHALLENGE_UNATTESTED_EXECUTION_ENV, "CHALLENGE_NO_PHALA", NO_PHALA_ENV): + monkeypatch.delenv(key, raising=False) + # PrismSettings (via DockerExecutorSettings) forbids unknown CHALLENGE_* keys. + # Agent-challenge conftest setdefaults (e.g. REVIEW_EVIDENCE_ENCRYPTION_KEY) and + # host/prod env must not leak into create_app / PrismSettings construction. + known = {f"CHALLENGE_{name.upper()}" for name in PrismSettings.model_fields} + known.add("CHALLENGE_ENV_FILE") + for key in list(os.environ): + if key.startswith("CHALLENGE_") and key not in known: + monkeypatch.delenv(key, raising=False) + +# --- Unit: mark_result_unattested (agent-challenge pattern) --------------------- + + +def test_mark_result_unattested_overrides_miner_attested_true() -> None: + """S2: miner-supplied attested:true / verified status cannot survive the mark.""" + forged = { + "score": 0.99, + "attested": True, + "attestation_status": "attested", + "execution_mode": "phala_tee", + "tdx_quote": "ab" * 40, + "phala_attestation": {"quote": "x"}, + "attestation_binding": {"agent_hash": "a" * 64}, + # Worker proof must remain for prism verification path. + "execution_proof": {"version": 1, "manifest_sha256": "ab" * 32}, + } + out = mark_result_unattested(forged) + assert out[RESULT_KEY_ATTESTED] is False + assert out[RESULT_KEY_ATTESTATION_STATUS] == ATTESTATION_STATUS_UNATTESTED + assert out[RESULT_KEY_EXECUTION_MODE] == EXECUTION_MODE_NO_PHALA_HOST + assert out["score"] == 0.99 + # TEE-looking claim keys stripped; worker execution_proof kept. + assert "tdx_quote" not in out + assert "phala_attestation" not in out + assert "attestation_binding" not in out + assert "execution_proof" in out + + +def test_mark_result_unattested_hardcodes_false_even_on_empty() -> None: + """S1: empty payload still gets the honest unattested triple.""" + out = mark_result_unattested({}) + assert out[RESULT_KEY_ATTESTED] is False + assert out[RESULT_KEY_ATTESTATION_STATUS] == ATTESTATION_STATUS_UNATTESTED + assert out[RESULT_KEY_EXECUTION_MODE] == EXECUTION_MODE_NO_PHALA_HOST + + +def test_ingestion_outcome_to_response_flag_on_overrides_miner_fields( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """S2 surface: IngestionOutcome response never echoes miner attested:true when flag on.""" + _clear_unattested_env(monkeypatch) + monkeypatch.setenv(CHALLENGE_UNATTESTED_EXECUTION_ENV, "true") + assert is_unattested_execution_enabled() is True + + outcome = IngestionOutcome( + status="accepted", + work_unit_id="wu-1", + submission_id="sub-1", + claimed_tier=1, + effective_tier=0, + tier_downgraded=True, + idempotent=False, + finalized=True, + submission_status="completed", + score_written=True, + # Even if a caller tried to smuggle via reason/attestation_mode, honesty fields + # come only from mark_result_unattested when flag is on. + attestation_mode="miner_rent_image_pin_evidence_v1", + ) + payload = outcome.to_response() + assert payload[RESULT_KEY_ATTESTED] is False + assert payload[RESULT_KEY_ATTESTATION_STATUS] == ATTESTATION_STATUS_UNATTESTED + assert payload[RESULT_KEY_EXECUTION_MODE] == EXECUTION_MODE_NO_PHALA_HOST + # Miner cannot force verified via any residual field. + assert payload.get("attested") is not True + assert payload.get("attestation_status") != "attested" + + +def test_ingestion_outcome_to_response_flag_off_does_not_claim_tee( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """S3 adjacent: flag off — no forged TEE claim; honesty fields absent or unattested.""" + _clear_unattested_env(monkeypatch) + outcome = IngestionOutcome( + status="accepted", + work_unit_id="wu-2", + submission_id="sub-2", + claimed_tier=0, + effective_tier=0, + tier_downgraded=False, + idempotent=False, + finalized=True, + score_written=True, + ) + payload = outcome.to_response() + # Must never claim verified TEE when flag is off either. + assert payload.get("attested") is not True + assert payload.get("attestation_status") != "attested" + assert payload.get("attestation_status") != "verified" + + +# --- HTTP: miner body attested:true overridden --------------------------------- + + +def test_http_miner_attested_true_overridden_when_unattested_flag_on( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """S4: POST result with miner attested:true → response attested:false / unattested.""" + _clear_unattested_env(monkeypatch) + # NO_PHALA alias: ChallengeSettings rejects unknown CHALLENGE_* keys. + monkeypatch.setenv(NO_PHALA_ENV, "true") + data_dir = _stage_train(tmp_path) + monkeypatch.setattr( + "prism_challenge.evaluator.container.DockerExecutor.run", + cpu_reexec_run(train_data_dir=data_dir), + ) + settings = _settings(tmp_path) + headers = {"Authorization": "Bearer secret"} + signer = worker_signer_from_key(WORKER_KEY) + + with TestClient(create_app(settings)) as client: + seed = client.post( + "/internal/v1/bridge/submissions", + content=_zip_b64(), + headers={ + "Authorization": "Bearer secret", + "X-Base-Verified-Hotkey": "hk-owner-forge", + "X-Submission-Filename": "project.zip", + "Content-Type": "application/octet-stream", + }, + ) + assert seed.status_code == 200, seed.text + sid = seed.json()["id"] + manifest = _manifest() + proof = build_execution_proof( + signer=signer, + manifest_sha256=compute_manifest_sha256(manifest), + unit_id=sid, + image_digest=DIGEST, + constation_digest=DIGEST, + ).model_dump(mode="json") + body = { + "api_version": "1.0", + "work_unit_id": sid, + "assignment_id": sid, + "submission_ref": "hk-owner-forge", + "challenge_slug": settings.slug, + "result": { + "executed": 1, + PROOF_PAYLOAD_KEY: proof, + MANIFEST_PAYLOAD_KEY: manifest, + # Miner forge attempt inside result payload (top-level extras + # are rejected by ExternalResultEnvelope; result is open dict): + "attested": True, + "attestation_status": "attested", + "execution_mode": "phala_tee", + }, + "proof": proof, + } + resp = client.post("/internal/v1/work_units/result", json=body, headers=headers) + assert resp.status_code == 200, resp.text + data = resp.json() + assert data.get("status") == "accepted", data + # Server-stamped honesty mark — miner attested:true did not win. + assert data[RESULT_KEY_ATTESTED] is False + assert data[RESULT_KEY_ATTESTATION_STATUS] == ATTESTATION_STATUS_UNATTESTED + assert data[RESULT_KEY_EXECUTION_MODE] == EXECUTION_MODE_NO_PHALA_HOST diff --git a/packages/challenges/prism/tests/test_unattested_no_bypass.py b/packages/challenges/prism/tests/test_unattested_no_bypass.py new file mode 100644 index 000000000..cb30b1e1a --- /dev/null +++ b/packages/challenges/prism/tests/test_unattested_no_bypass.py @@ -0,0 +1,731 @@ +"""T22: unattested flag ON must NOT become a generic bypass of prism validations. + +Complements T20 (narrow missing-bundle admit). Every case below runs with the +unattested / NO_PHALA flag explicitly ON and asserts fail-closed behavior for +a distinct validation other than missing_constation_bundle. + +Matrix (validation | still enforced when unattested ON) is asserted by the +test ids and summarized in evidence T22-no-bypass/MATRIX.md. +""" + +from __future__ import annotations + +import asyncio +import io +import sqlite3 +import zipfile +from pathlib import Path +from typing import Any + +import pytest +from fastapi.testclient import TestClient + +from prism_challenge.app import create_app +from prism_challenge.config import PrismSettings, WorkerPlaneConfig +from prism_challenge.constation import CheckOutcome, ConstationBundle, miner_fault_reason +from prism_challenge.evaluator.mock_reexec import cpu_reexec_run +from prism_challenge.ingestion import ( + ResultIngestionError, + _evaluate_constation_gate, + ingest_work_unit_result, + parse_execution_proof, + verify_proof_integrity, +) +from prism_challenge.proof import ( + MANIFEST_PAYLOAD_KEY, + PROOF_PAYLOAD_KEY, + build_execution_proof, + compute_manifest_sha256, + worker_signer_from_key, +) +from prism_challenge.unattested_execution import ( + CHALLENGE_NO_PHALA_ENV, + CHALLENGE_UNATTESTED_EXECUTION_ENV, + NO_PHALA_ENV, + is_unattested_execution_enabled, +) + +WORKER_KEY = "//WorkerUnattestedNoBypass" +DIGEST = "sha256:" + ("22" * 32) +MISSING = "miner_fault:missing_constation_bundle" + +TINY_ARCH = """ +import torch +from torch import nn + + +class TinyLM(nn.Module): + def __init__(self, vocab): + super().__init__() + self.emb = nn.Embedding(vocab, 8) + self.head = nn.Linear(8, vocab) + + def forward(self, tokens): + return self.head(self.emb(tokens)) + + +def build_model(ctx): + return TinyLM(ctx.vocab_size) +""" + +TINY_TRAIN = """ +import torch +import torch.nn.functional as F + + +def train(ctx): + model = ctx.build_model() + opt = torch.optim.AdamW(model.parameters(), lr=0.01) + for batch in ctx.iter_train_batches(model, batch_size=1): + opt.zero_grad() + logits = model(batch.tokens) + nv = logits.shape[-1] + loss = F.cross_entropy( + logits[:, :-1, :].reshape(-1, nv), batch.tokens[:, 1:].reshape(-1) % nv + ) + loss.backward() + opt.step() +""" + +_SHARD = ( + '{{"id": "doc-{i}", "text": "the locked fineweb edu training sample number {i} ' + 'has enough bytes to cover several challenge instrument batches deterministically"}}\n' +) + + +def _stage_train(root: Path) -> Path: + data_dir = root / "train-data" + data_dir.mkdir(parents=True, exist_ok=True) + (data_dir / "train-00000.jsonl").write_text( + "".join(_SHARD.format(i=i) for i in range(64)), encoding="utf-8" + ) + return data_dir + + +def _zip_bytes() -> bytes: + stream = io.BytesIO() + with zipfile.ZipFile(stream, "w") as archive: + archive.writestr("architecture.py", TINY_ARCH) + archive.writestr("training.py", TINY_TRAIN) + return stream.getvalue() + + +def _settings(tmp_path: Path, **extra: Any) -> PrismSettings: + kw: dict[str, Any] = dict( + database_url=f"sqlite+aiosqlite:///{tmp_path / 'coord.sqlite3'}", + shared_token="secret", + allow_insecure_signatures=False, + execution_backend="base_gpu", + docker_enabled=True, + docker_backend="broker", + docker_broker_url="http://base-docker-broker:8082", + docker_broker_token="secret", + sequence_length=16, + plagiarism_enabled=False, + distributed_contract_policy="off", + base_eval_artifact_root=tmp_path / "artifacts", + worker_plane=WorkerPlaneConfig(enabled=True, signing_key=WORKER_KEY), + constation_base_url="http://base-constation.test", + constation_internal_token="constation-tok", + ) + kw.update(extra) + return PrismSettings(**kw) + + +def _manifest(marker: str = "ok") -> dict[str, Any]: + return { + "schema_version": "prism_run_manifest.v2", + "metrics": { + "token_accuracy": 0.5, + "loss": 1.0, + "step": 1, + "marker": marker, + }, + "timing": {"wall_seconds": 1.0}, + } + + +def _clear_unattested_env(monkeypatch: pytest.MonkeyPatch) -> None: + for key in ( + CHALLENGE_UNATTESTED_EXECUTION_ENV, + CHALLENGE_NO_PHALA_ENV, + NO_PHALA_ENV, + ): + monkeypatch.delenv(key, raising=False) + + +def _enable_unattested(monkeypatch: pytest.MonkeyPatch) -> None: + """Turn unattested ON via NO_PHALA alias (ChallengeSettings-safe for HTTP).""" + _clear_unattested_env(monkeypatch) + monkeypatch.setenv(NO_PHALA_ENV, "true") + assert is_unattested_execution_enabled() is True + + +def _seed_client(client: TestClient) -> str: + seed = client.post( + "/internal/v1/bridge/submissions", + content=_zip_bytes(), + headers={ + "Authorization": "Bearer secret", + "X-Base-Verified-Hotkey": "hk-owner", + "X-Submission-Filename": "project.zip", + "Content-Type": "application/octet-stream", + }, + ) + assert seed.status_code == 200, seed.text + return str(seed.json()["id"]) + + +def _score_row(db_path: Path, submission_id: str) -> Any: + conn = sqlite3.connect(db_path) + try: + return conn.execute( + "SELECT final_score FROM scores WHERE submission_id=?", (submission_id,) + ).fetchone() + finally: + conn.close() + + +def _http_post_result( + client: TestClient, + *, + sid: str, + result: dict[str, Any], + proof: dict[str, Any] | None = None, + slug: str, +) -> Any: + body: dict[str, Any] = { + "api_version": "1.0", + "work_unit_id": sid, + "assignment_id": sid, + "submission_ref": "hk-owner", + "challenge_slug": slug, + "result": result, + } + if proof is not None: + body["proof"] = proof + return client.post( + "/internal/v1/work_units/result", + json=body, + headers={"Authorization": "Bearer secret"}, + ) + + +def _assert_not_missing_bundle_bypass(resp: Any, *, expected_code: str | None = None) -> str: + """422 fail-closed with a code that is NOT the missing-bundle unattested path.""" + assert resp.status_code == 422, resp.text + detail = resp.json().get("detail") + code = detail.get("code") if isinstance(detail, dict) else str(detail) + assert code != MISSING, f"must not collapse to missing-bundle: {resp.text}" + assert "missing_constation" not in str(code), resp.text + if expected_code is not None: + assert code == expected_code, resp.text + return str(code) + + +def _failing_bundle() -> ConstationBundle: + """Minimal bundle that fails allowlist (six-check path, not missing-bundle).""" + return ConstationBundle( + commit_sha="a" * 40, + tree_sha="b" * 40, + variant="default", + digest=DIGEST, + work_unit_id="wu-t22", + miner_hotkey="hk", + pod_id="pod-1", + nonce="nonce-1", + signed_attestation="sig-bytes", + expected_sealed_manifest_hashes={"a": "1" * 64}, + reported_sealed_manifest_hashes={"a": "1" * 64}, + lium_declared_digest=None, + constation_gap_budget_seconds=60.0, + constation_observed_max_gap_seconds=1.0, + ) + + +# --- Unit: proof shape / integrity still enforced with flag ON -------------------- + + +def test_flag_on_parse_still_rejects_proof_missing( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """V1 proof_missing — unattested does not skip ExecutionProof presence.""" + _enable_unattested(monkeypatch) + with pytest.raises(ResultIngestionError) as exc: + parse_execution_proof({"executed": 1}) + assert exc.value.reason == "proof_missing" + + +def test_flag_on_parse_still_rejects_proof_bad_version( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """V2 proof_bad_version.""" + _enable_unattested(monkeypatch) + signer = worker_signer_from_key(WORKER_KEY) + manifest = _manifest() + proof = build_execution_proof( + signer=signer, + manifest_sha256=compute_manifest_sha256(manifest), + unit_id="u1", + image_digest=DIGEST, + constation_digest=DIGEST, + ).model_dump(mode="json") + proof["version"] = 99 + with pytest.raises(ResultIngestionError) as exc: + parse_execution_proof({PROOF_PAYLOAD_KEY: proof, MANIFEST_PAYLOAD_KEY: manifest}) + assert exc.value.reason == "proof_bad_version" + + +def test_flag_on_parse_still_rejects_proof_bad_manifest_hash( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """V3 proof_bad_manifest_hash.""" + _enable_unattested(monkeypatch) + signer = worker_signer_from_key(WORKER_KEY) + manifest = _manifest() + proof = build_execution_proof( + signer=signer, + manifest_sha256=compute_manifest_sha256(manifest), + unit_id="u1", + image_digest=DIGEST, + constation_digest=DIGEST, + ).model_dump(mode="json") + proof["manifest_sha256"] = "not-a-valid-hex-digest" + with pytest.raises(ResultIngestionError) as exc: + parse_execution_proof({PROOF_PAYLOAD_KEY: proof, MANIFEST_PAYLOAD_KEY: manifest}) + assert exc.value.reason == "proof_bad_manifest_hash" + + +def test_flag_on_parse_still_rejects_proof_missing_signature( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """V4 proof_missing_signature.""" + _enable_unattested(monkeypatch) + signer = worker_signer_from_key(WORKER_KEY) + manifest = _manifest() + proof = build_execution_proof( + signer=signer, + manifest_sha256=compute_manifest_sha256(manifest), + unit_id="u1", + image_digest=DIGEST, + constation_digest=DIGEST, + ).model_dump(mode="json") + proof["worker_signature"] = {"worker_pubkey": "only-pubkey"} + with pytest.raises(ResultIngestionError) as exc: + parse_execution_proof({PROOF_PAYLOAD_KEY: proof, MANIFEST_PAYLOAD_KEY: manifest}) + assert exc.value.reason == "proof_missing_signature" + + +def test_flag_on_verify_still_rejects_manifest_tampered( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """V5 manifest_tampered — hash mismatch after sign.""" + _enable_unattested(monkeypatch) + signer = worker_signer_from_key(WORKER_KEY) + unit_id = "u-tamper" + manifest = _manifest("orig") + proof = build_execution_proof( + signer=signer, + manifest_sha256=compute_manifest_sha256(manifest), + unit_id=unit_id, + image_digest=DIGEST, + constation_digest=DIGEST, + ) + tampered = _manifest("mutated") + with pytest.raises(ResultIngestionError) as exc: + verify_proof_integrity(proof, unit_id=unit_id, manifest=tampered) + assert exc.value.reason == "manifest_tampered" + + +def test_flag_on_verify_still_rejects_signature_invalid( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """V6 signature_invalid — corrupt worker sig bytes.""" + _enable_unattested(monkeypatch) + signer = worker_signer_from_key(WORKER_KEY) + unit_id = "u-sig" + manifest = _manifest() + proof = build_execution_proof( + signer=signer, + manifest_sha256=compute_manifest_sha256(manifest), + unit_id=unit_id, + image_digest=DIGEST, + constation_digest=DIGEST, + ) + corrupt = proof.model_copy( + update={"worker_signature": proof.worker_signature.model_copy(update={"sig": "0x00"})} + ) + with pytest.raises(ResultIngestionError) as exc: + verify_proof_integrity(corrupt, unit_id=unit_id, manifest=manifest) + assert exc.value.reason == "signature_invalid" + + +def test_flag_on_result_malformed_still_raises( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """V7 result_malformed — non-Mapping result rejected at ingest boundary.""" + _enable_unattested(monkeypatch) + + class _NoWorker: + settings = type("S", (), {"worker_plane": type("W", (), {"enabled": False})()})() + repository = None + + async def _run() -> None: + with pytest.raises(ResultIngestionError) as exc: + await ingest_work_unit_result( + worker=_NoWorker(), # type: ignore[arg-type] + work_unit_id="wu", + submission_ref="hk", + result="not-an-object", # type: ignore[arg-type] + ) + assert exc.value.reason == "result_malformed" + + asyncio.run(_run()) + + +# --- Unit: constation gate narrowness with flag ON -------------------------------- + + +def test_flag_on_infra_fault_still_not_admitted( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """V8 infra_fault — unattested does not swallow infra path (T20 expand).""" + _enable_unattested(monkeypatch) + gate = _evaluate_constation_gate( + bundle=None, + check_allowlist=None, + check_nonce=None, + verify_constation_signature=None, + constation_infra_fault="constation_unavailable", + constation_attempt=0, + max_constation_attempts=3, + ) + assert gate.admit is False + assert gate.retryable is True + assert gate.reason is not None + assert gate.reason.startswith("infra_fault:") + assert gate.reason != MISSING + + +def test_flag_on_infra_fault_retry_exhausted_still_rejects( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """V9 infra_fault retry exhausted — still no admit under unattested.""" + _enable_unattested(monkeypatch) + gate = _evaluate_constation_gate( + bundle=None, + check_allowlist=None, + check_nonce=None, + verify_constation_signature=None, + constation_infra_fault="constation_unavailable", + constation_attempt=3, + max_constation_attempts=3, + ) + assert gate.admit is False + assert gate.retryable is False + assert gate.reason is not None + assert gate.reason.startswith("infra_fault:") + + +def test_flag_on_bundle_present_failed_six_check_still_rejects( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """V10 six-check fail — unattested only applies when bundle is None. + + A present but invalid bundle must still fail closed (miner_fault), not + fall through to the unattested missing-bundle admit branch. + """ + _enable_unattested(monkeypatch) + bundle = _failing_bundle() + + def _deny_allowlist(**_kwargs: Any) -> CheckOutcome: + return CheckOutcome(ok=False, reason="unknown_digest") + + def _ok_nonce(**_kwargs: Any) -> CheckOutcome: + return CheckOutcome(ok=True, reason="ok") + + def _ok_sig(_att: object) -> CheckOutcome: + return CheckOutcome(ok=True, reason="ok") + + gate = _evaluate_constation_gate( + bundle=bundle, + check_allowlist=_deny_allowlist, + check_nonce=_ok_nonce, + verify_constation_signature=_ok_sig, + constation_infra_fault=None, + constation_attempt=0, + max_constation_attempts=3, + ) + assert gate.admit is False + assert gate.constation_ok is False + assert gate.reason is not None + assert gate.reason.startswith("miner_fault:") + assert gate.reason != MISSING + assert "unattested" not in (gate.reason or "") + assert gate.reason == miner_fault_reason("unknown_digest") + + +def test_flag_on_bundle_present_checkers_unavailable_is_infra( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """V11 checkers unavailable with bundle present → infra_fault, not unattested admit.""" + _enable_unattested(monkeypatch) + gate = _evaluate_constation_gate( + bundle=_failing_bundle(), + check_allowlist=None, + check_nonce=None, + verify_constation_signature=None, + constation_infra_fault=None, + constation_attempt=0, + max_constation_attempts=3, + ) + assert gate.admit is False + assert gate.retryable is True + assert gate.reason is not None + assert gate.reason.startswith("infra_fault:") + + +# --- HTTP surface: flag ON still 422 on non-bundle faults ------------------------- + + +def test_http_flag_on_still_rejects_missing_proof( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """H1 missing proof → result_envelope_invalid (expands T20 S3).""" + _enable_unattested(monkeypatch) + data_dir = _stage_train(tmp_path) + monkeypatch.setattr( + "prism_challenge.evaluator.container.DockerExecutor.run", + cpu_reexec_run(train_data_dir=data_dir), + ) + settings = _settings(tmp_path) + with TestClient(create_app(settings)) as client: + sid = _seed_client(client) + resp = _http_post_result( + client, + sid=sid, + slug=settings.slug, + result={"executed": 1, MANIFEST_PAYLOAD_KEY: _manifest()}, + ) + # Envelope requires proof: ExecutionProof — fails closed before ingest. + code = _assert_not_missing_bundle_bypass( + resp, expected_code="result_envelope_invalid" + ) + assert code == "result_envelope_invalid" + assert _score_row(tmp_path / "coord.sqlite3", sid) is None + + +def test_http_flag_on_still_rejects_bad_proof_version( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """H2 bad proof version → result_envelope_invalid via HTTP.""" + _enable_unattested(monkeypatch) + data_dir = _stage_train(tmp_path) + monkeypatch.setattr( + "prism_challenge.evaluator.container.DockerExecutor.run", + cpu_reexec_run(train_data_dir=data_dir), + ) + settings = _settings(tmp_path) + signer = worker_signer_from_key(WORKER_KEY) + with TestClient(create_app(settings)) as client: + sid = _seed_client(client) + manifest = _manifest() + proof = build_execution_proof( + signer=signer, + manifest_sha256=compute_manifest_sha256(manifest), + unit_id=sid, + image_digest=DIGEST, + constation_digest=DIGEST, + ).model_dump(mode="json") + proof["version"] = 7 + resp = _http_post_result( + client, + sid=sid, + slug=settings.slug, + result={ + "executed": 1, + PROOF_PAYLOAD_KEY: proof, + MANIFEST_PAYLOAD_KEY: manifest, + }, + proof=proof, + ) + # SDK ExecutionProof.version is Literal[1] — envelope rejects before ingest. + _assert_not_missing_bundle_bypass(resp, expected_code="result_envelope_invalid") + assert _score_row(tmp_path / "coord.sqlite3", sid) is None + + +def test_http_flag_on_still_rejects_manifest_tampered( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """H3 manifest_tampered via HTTP.""" + _enable_unattested(monkeypatch) + data_dir = _stage_train(tmp_path) + monkeypatch.setattr( + "prism_challenge.evaluator.container.DockerExecutor.run", + cpu_reexec_run(train_data_dir=data_dir), + ) + settings = _settings(tmp_path) + signer = worker_signer_from_key(WORKER_KEY) + with TestClient(create_app(settings)) as client: + sid = _seed_client(client) + signed_manifest = _manifest("signed") + proof = build_execution_proof( + signer=signer, + manifest_sha256=compute_manifest_sha256(signed_manifest), + unit_id=sid, + image_digest=DIGEST, + constation_digest=DIGEST, + ).model_dump(mode="json") + forwarded = _manifest("tampered-after-sign") + resp = _http_post_result( + client, + sid=sid, + slug=settings.slug, + result={ + "executed": 1, + PROOF_PAYLOAD_KEY: proof, + MANIFEST_PAYLOAD_KEY: forwarded, + }, + proof=proof, + ) + _assert_not_missing_bundle_bypass(resp, expected_code="manifest_tampered") + assert _score_row(tmp_path / "coord.sqlite3", sid) is None + + +def test_http_flag_on_still_rejects_signature_invalid( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """H4 signature_invalid via HTTP (corrupt sig).""" + _enable_unattested(monkeypatch) + data_dir = _stage_train(tmp_path) + monkeypatch.setattr( + "prism_challenge.evaluator.container.DockerExecutor.run", + cpu_reexec_run(train_data_dir=data_dir), + ) + settings = _settings(tmp_path) + signer = worker_signer_from_key(WORKER_KEY) + with TestClient(create_app(settings)) as client: + sid = _seed_client(client) + manifest = _manifest() + proof = build_execution_proof( + signer=signer, + manifest_sha256=compute_manifest_sha256(manifest), + unit_id=sid, + image_digest=DIGEST, + constation_digest=DIGEST, + ).model_dump(mode="json") + proof["worker_signature"] = { + **proof["worker_signature"], + "sig": "0xdeadbeef", + } + resp = _http_post_result( + client, + sid=sid, + slug=settings.slug, + result={ + "executed": 1, + PROOF_PAYLOAD_KEY: proof, + MANIFEST_PAYLOAD_KEY: manifest, + }, + proof=proof, + ) + _assert_not_missing_bundle_bypass(resp, expected_code="signature_invalid") + assert _score_row(tmp_path / "coord.sqlite3", sid) is None + + +def test_http_flag_on_still_rejects_proof_bad_manifest_hash( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """H5 bad manifest hash shape → result_envelope_invalid via HTTP.""" + _enable_unattested(monkeypatch) + data_dir = _stage_train(tmp_path) + monkeypatch.setattr( + "prism_challenge.evaluator.container.DockerExecutor.run", + cpu_reexec_run(train_data_dir=data_dir), + ) + settings = _settings(tmp_path) + signer = worker_signer_from_key(WORKER_KEY) + with TestClient(create_app(settings)) as client: + sid = _seed_client(client) + manifest = _manifest() + proof = build_execution_proof( + signer=signer, + manifest_sha256=compute_manifest_sha256(manifest), + unit_id=sid, + image_digest=DIGEST, + constation_digest=DIGEST, + ).model_dump(mode="json") + proof["manifest_sha256"] = "gg" * 32 # non-hex + resp = _http_post_result( + client, + sid=sid, + slug=settings.slug, + result={ + "executed": 1, + PROOF_PAYLOAD_KEY: proof, + MANIFEST_PAYLOAD_KEY: manifest, + }, + proof=proof, + ) + # SDK ExecutionProof.manifest_sha256 pattern — envelope rejects before ingest. + _assert_not_missing_bundle_bypass(resp, expected_code="result_envelope_invalid") + assert _score_row(tmp_path / "coord.sqlite3", sid) is None + + +def test_http_flag_on_still_rejects_challenge_mismatch( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """H6 result_challenge_mismatch via HTTP — binding still enforced.""" + _enable_unattested(monkeypatch) + data_dir = _stage_train(tmp_path) + monkeypatch.setattr( + "prism_challenge.evaluator.container.DockerExecutor.run", + cpu_reexec_run(train_data_dir=data_dir), + ) + settings = _settings(tmp_path) + signer = worker_signer_from_key(WORKER_KEY) + with TestClient(create_app(settings)) as client: + sid = _seed_client(client) + manifest = _manifest() + proof = build_execution_proof( + signer=signer, + manifest_sha256=compute_manifest_sha256(manifest), + unit_id=sid, + image_digest=DIGEST, + constation_digest=DIGEST, + ).model_dump(mode="json") + resp = _http_post_result( + client, + sid=sid, + slug="not-the-prism-slug", + result={ + "executed": 1, + PROOF_PAYLOAD_KEY: proof, + MANIFEST_PAYLOAD_KEY: manifest, + }, + proof=proof, + ) + _assert_not_missing_bundle_bypass(resp, expected_code="result_challenge_mismatch") + assert _score_row(tmp_path / "coord.sqlite3", sid) is None + + +def test_matrix_documents_enforced_validations() -> None: + """Structural lock: T22 covers ≥4 distinct non-missing-bundle validations.""" + enforced = frozenset( + { + "proof_missing", + "proof_bad_version", + "proof_bad_manifest_hash", + "proof_missing_signature", + "manifest_tampered", + "signature_invalid", + "result_malformed", + "result_envelope_invalid", + "result_challenge_mismatch", + "infra_fault", + "miner_fault:unknown_digest", + } + ) + assert MISSING not in enforced + assert miner_fault_reason("missing_constation_bundle") == MISSING + assert len(enforced) >= 4 diff --git a/tests/unit/test_master_embed_env_file.py b/tests/unit/test_master_embed_env_file.py index f4af4f6dd..9243ab58d 100644 --- a/tests/unit/test_master_embed_env_file.py +++ b/tests/unit/test_master_embed_env_file.py @@ -47,7 +47,11 @@ def _write_exec(path: Path, body: str) -> None: path.chmod(0o755) -def _run_entrypoint(tmp_path: Path, ac_env_file_body: str | None) -> dict[str, str]: +def _run_entrypoint( + tmp_path: Path, + ac_env_file_body: str | None, + prism_env_file_body: str | None = None, +) -> dict[str, str]: """Run the entrypoint with stubbed uvicorn/python; return child env dumps.""" bin_dir = tmp_path / "bin" @@ -67,6 +71,8 @@ def _run_entrypoint(tmp_path: Path, ac_env_file_body: str | None) -> dict[str, s if ac_env_file_body is not None: (ac_dir / "embed.env").write_text(ac_env_file_body, encoding="utf-8") + if prism_env_file_body is not None: + (prism_dir / "embed.env").write_text(prism_env_file_body, encoding="utf-8") env = { "PATH": f"{bin_dir}:{os.environ.get('PATH', '/usr/bin:/bin')}", @@ -172,3 +178,59 @@ def test_env_file_ignores_comments_blanks_and_malformed_keys(tmp_path: Path) -> assert "CHALLENGE_EVAL_APP_IDENTITY=app-id" in ac_env assert "RANDOM_UNRELATED" not in ac_env assert "not-a-valid-key" not in ac_env + + +def test_prism_env_file_forwards_unattested_flags(tmp_path: Path) -> None: + """Prism embed.env must forward unattested / NO_PHALA flags under env -i.""" + + dumps = _run_entrypoint( + tmp_path, + ac_env_file_body=None, + prism_env_file_body="\n".join( + [ + "# prism host-trust / unattested (T19-T22)", + "CHALLENGE_UNATTESTED_EXECUTION=true", + "CHALLENGE_NO_PHALA=true", + "NO_PHALA=true", + "PRISM_RAW_WEIGHT_PUSH_ENABLED=true", + "RANDOM_LEAK=nope", + "", + ] + ), + ) + + prism_env = dumps["prism"] + assert "CHALLENGE_UNATTESTED_EXECUTION=true" in prism_env + assert "CHALLENGE_NO_PHALA=true" in prism_env + assert "NO_PHALA=true" in prism_env + assert "PRISM_RAW_WEIGHT_PUSH_ENABLED=true" in prism_env + assert "RANDOM_LEAK" not in prism_env + + +def test_prism_admission_requires_worker_defaults_false(tmp_path: Path) -> None: + """Built-in default for admission_requires_worker is false (unattested path).""" + + dumps = _run_entrypoint(tmp_path, ac_env_file_body=None) + assert ( + "PRISM_WORKER_PLANE__ADMISSION_REQUIRES_WORKER=false" in dumps["prism"] + ) + + +def test_prism_unattested_flags_do_not_leak_into_ac(tmp_path: Path) -> None: + """Prism-only unattested keys must not appear in the AC child env.""" + + dumps = _run_entrypoint( + tmp_path, + ac_env_file_body=None, + prism_env_file_body=( + "CHALLENGE_UNATTESTED_EXECUTION=true\n" + "CHALLENGE_NO_PHALA=true\n" + "NO_PHALA=true\n" + ), + ) + ac_env = dumps["ac"] + assert "CHALLENGE_UNATTESTED_EXECUTION" not in ac_env + # AC may still get CHALLENGE_* from its own defaults/file — but not from prism file. + # NO_PHALA from prism file must not leak: + assert "NO_PHALA=true" not in ac_env + From 2c1065e0ae06a81a9250c175078f70707d542af5 Mon Sep 17 00:00:00 2001 From: echobt <154886644+echobt@users.noreply.github.com> Date: Wed, 29 Jul 2026 02:09:52 +0000 Subject: [PATCH 2/5] fix(prism): align raw-weight push epoch with master seal identity Use master epoch interval (default 360s) for seal identity instead of challenge scoring epoch_seconds, and advance past ledger last_epoch when wall clock lags force-advanced open epochs so continuous push stays eligible for master ingress. --- .../src/prism_challenge/raw_weight_push.py | 33 +++++- .../prism/tests/test_raw_weight_push.py | 106 ++++++++++++++++++ 2 files changed, 137 insertions(+), 2 deletions(-) diff --git a/packages/challenges/prism/src/prism_challenge/raw_weight_push.py b/packages/challenges/prism/src/prism_challenge/raw_weight_push.py index 24955f797..410cbddb3 100644 --- a/packages/challenges/prism/src/prism_challenge/raw_weight_push.py +++ b/packages/challenges/prism/src/prism_challenge/raw_weight_push.py @@ -426,7 +426,7 @@ async def push_once( if epoch is not None else int(self.epoch_fn()) if self.epoch_fn is not None - else int(now.timestamp()) // 3600 + else int(now.timestamp()) // 360 ) revision = ( int(force_revision) @@ -625,14 +625,43 @@ def maybe_build_push_client_from_settings( token = str(shared) if shared else None if not token: return None + # Challenge scoring epoch (architecture crowns) stays on settings.epoch_seconds. epoch_seconds = int(getattr(settings, "epoch_seconds", 3600) or 3600) + # Master weight-seal identity uses BASE_MASTER epoch interval (default 360s), + # NOT the challenge scoring epoch. Wall-clock can lag force-advanced open + # epochs, so advance past last acknowledged push when behind. + master_epoch_seconds = int( + getattr(settings, "raw_weight_master_epoch_seconds", 0) + or __import__("os").environ.get("PRISM_RAW_WEIGHT_MASTER_EPOCH_SECONDS", 0) + or __import__("os").environ.get("BASE_MASTER__EPOCH_INTERVAL_SECONDS", 0) + or 360 + ) # VAL-RESLAB-008: raw_weight_push settings defaults match get_weights 0.50/0.50. arch = float(getattr(settings, "architecture_reward_weight", 0.50)) train = float(getattr(settings, "training_reward_weight", 0.50)) interval_hint = float(getattr(settings, "raw_weight_push_interval_seconds", 30.0)) + db_path = str(getattr(database, "path", "") or "") def _epoch() -> int: - return int(datetime.now(UTC).timestamp()) // max(epoch_seconds, 1) + import sqlite3 + wall = int(datetime.now(UTC).timestamp()) // max(int(master_epoch_seconds), 1) + last = None + try: + if db_path: + con = sqlite3.connect(db_path) + try: + row = con.execute( + "SELECT last_epoch FROM raw_weight_push_ledger WHERE id = 1" + ).fetchone() + if row and row[0] is not None: + last = int(row[0]) + finally: + con.close() + except Exception: + last = None + if last is not None and wall <= last: + return last + 1 + return wall client = RawWeightPushClient( database=database, diff --git a/packages/challenges/prism/tests/test_raw_weight_push.py b/packages/challenges/prism/tests/test_raw_weight_push.py index 631deb160..93648f644 100644 --- a/packages/challenges/prism/tests/test_raw_weight_push.py +++ b/packages/challenges/prism/tests/test_raw_weight_push.py @@ -259,3 +259,109 @@ class _Enabled(_Settings): ) assert client is not None assert client.master_base_url == "http://master.test" + + +def test_maybe_build_push_client_uses_master_epoch_interval( + database: Database, monkeypatch: pytest.MonkeyPatch +) -> None: + """Seal identity uses master 360s interval, not challenge scoring epoch.""" + + class _Settings: + raw_weight_push_enabled = True + master_base_url = "http://master.test" + worker_plane = type("WP", (), {"master_base_url": None})() + slug = SLUG + epoch_seconds = 21600 # challenge scoring — must NOT drive seal epoch + architecture_reward_weight = 0.5 + training_reward_weight = 0.5 + raw_weight_push_interval_seconds = 5.0 + raw_weight_master_epoch_seconds = 360 + + def internal_token(self) -> str: + return TOKEN + + monkeypatch.delenv("PRISM_RAW_WEIGHT_MASTER_EPOCH_SECONDS", raising=False) + monkeypatch.delenv("BASE_MASTER__EPOCH_INTERVAL_SECONDS", raising=False) + + fixed_now = datetime(2026, 7, 29, 12, 0, 0, tzinfo=UTC) + monkeypatch.setattr( + "prism_challenge.raw_weight_push.datetime", + type( + "DT", + (), + { + "now": staticmethod(lambda tz=None: fixed_now), + "UTC": UTC, + }, + ), + ) + + client = maybe_build_push_client_from_settings( + settings=_Settings(), database=database, repository=object() + ) + assert client is not None + assert client.epoch_fn is not None + expected = int(fixed_now.timestamp()) // 360 + # Challenge 21600-scale would be far smaller — prove we use 360. + challenge_scale = int(fixed_now.timestamp()) // 21600 + assert expected != challenge_scale + assert client.epoch_fn() == expected + + +@pytest.mark.asyncio +async def test_maybe_build_push_client_advances_past_ledger_epoch( + database: Database, monkeypatch: pytest.MonkeyPatch +) -> None: + """When sealer force-advances ahead of wall clock, push past last ack.""" + + class _Settings: + raw_weight_push_enabled = True + master_base_url = "http://master.test" + worker_plane = type("WP", (), {"master_base_url": None})() + slug = SLUG + epoch_seconds = 21600 + architecture_reward_weight = 0.5 + training_reward_weight = 0.5 + raw_weight_push_interval_seconds = 5.0 + raw_weight_master_epoch_seconds = 360 + + def internal_token(self) -> str: + return TOKEN + + monkeypatch.delenv("PRISM_RAW_WEIGHT_MASTER_EPOCH_SECONDS", raising=False) + monkeypatch.delenv("BASE_MASTER__EPOCH_INTERVAL_SECONDS", raising=False) + + fixed_now = datetime(2026, 7, 29, 12, 0, 0, tzinfo=UTC) + monkeypatch.setattr( + "prism_challenge.raw_weight_push.datetime", + type( + "DT", + (), + { + "now": staticmethod(lambda tz=None: fixed_now), + "UTC": UTC, + }, + ), + ) + + store = RawWeightPushStore(database, challenge_slug=SLUG) + await store.init() + wall = int(fixed_now.timestamp()) // 360 + last_epoch = wall + 50 + await store.acknowledge( + epoch=last_epoch, + revision=1, + payload_digest="a" * 64, + snapshot_id="snap-ledger", + canonical_payload="{}", + nonce="n-ledger", + acknowledged_at=fixed_now.isoformat(), + ) + + client = maybe_build_push_client_from_settings( + settings=_Settings(), database=database, repository=object() + ) + assert client is not None + assert client.epoch_fn is not None + assert client.epoch_fn() == last_epoch + 1 + From b4282df822cfaa3e2ecce6e6d0da888c06aa6356 Mon Sep 17 00:00:00 2001 From: echobt <154886644+echobt@users.noreply.github.com> Date: Wed, 29 Jul 2026 02:15:45 +0000 Subject: [PATCH 3/5] style(prism): ruff format for unattested and epoch-align --- packages/challenges/prism/src/prism_challenge/ingestion.py | 3 +-- .../challenges/prism/src/prism_challenge/raw_weight_push.py | 1 + packages/challenges/prism/tests/test_raw_weight_push.py | 1 - tests/unit/test_master_embed_env_file.py | 5 +---- 4 files changed, 3 insertions(+), 7 deletions(-) diff --git a/packages/challenges/prism/src/prism_challenge/ingestion.py b/packages/challenges/prism/src/prism_challenge/ingestion.py index 16f2e61e0..085241ed4 100644 --- a/packages/challenges/prism/src/prism_challenge/ingestion.py +++ b/packages/challenges/prism/src/prism_challenge/ingestion.py @@ -517,8 +517,7 @@ def _evaluate_constation_gate( constation_ok=False, reason="unattested:missing_constation_bundle", message=( - "unattested execution: scoring without constation bundle; " - "not TEE/verified" + "unattested execution: scoring without constation bundle; not TEE/verified" ), ) return _ConstationGate( diff --git a/packages/challenges/prism/src/prism_challenge/raw_weight_push.py b/packages/challenges/prism/src/prism_challenge/raw_weight_push.py index 410cbddb3..f838ac2d2 100644 --- a/packages/challenges/prism/src/prism_challenge/raw_weight_push.py +++ b/packages/challenges/prism/src/prism_challenge/raw_weight_push.py @@ -644,6 +644,7 @@ def maybe_build_push_client_from_settings( def _epoch() -> int: import sqlite3 + wall = int(datetime.now(UTC).timestamp()) // max(int(master_epoch_seconds), 1) last = None try: diff --git a/packages/challenges/prism/tests/test_raw_weight_push.py b/packages/challenges/prism/tests/test_raw_weight_push.py index 93648f644..c78d8569e 100644 --- a/packages/challenges/prism/tests/test_raw_weight_push.py +++ b/packages/challenges/prism/tests/test_raw_weight_push.py @@ -364,4 +364,3 @@ def internal_token(self) -> str: assert client is not None assert client.epoch_fn is not None assert client.epoch_fn() == last_epoch + 1 - diff --git a/tests/unit/test_master_embed_env_file.py b/tests/unit/test_master_embed_env_file.py index 9243ab58d..668e9492c 100644 --- a/tests/unit/test_master_embed_env_file.py +++ b/tests/unit/test_master_embed_env_file.py @@ -211,9 +211,7 @@ def test_prism_admission_requires_worker_defaults_false(tmp_path: Path) -> None: """Built-in default for admission_requires_worker is false (unattested path).""" dumps = _run_entrypoint(tmp_path, ac_env_file_body=None) - assert ( - "PRISM_WORKER_PLANE__ADMISSION_REQUIRES_WORKER=false" in dumps["prism"] - ) + assert "PRISM_WORKER_PLANE__ADMISSION_REQUIRES_WORKER=false" in dumps["prism"] def test_prism_unattested_flags_do_not_leak_into_ac(tmp_path: Path) -> None: @@ -233,4 +231,3 @@ def test_prism_unattested_flags_do_not_leak_into_ac(tmp_path: Path) -> None: # AC may still get CHALLENGE_* from its own defaults/file — but not from prism file. # NO_PHALA from prism file must not leak: assert "NO_PHALA=true" not in ac_env - From a3d50dc95ab37a771cfd0f6546cbd8ecb6cab73b Mon Sep 17 00:00:00 2001 From: echobt <154886644+echobt@users.noreply.github.com> Date: Wed, 29 Jul 2026 02:18:10 +0000 Subject: [PATCH 4/5] style(prism): ruff format unattested and epoch-align --- .../challenges/prism/tests/test_unattested_constation_gate.py | 4 +--- .../prism/tests/test_unattested_mark_unforgeable.py | 1 + packages/challenges/prism/tests/test_unattested_no_bypass.py | 4 +--- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/challenges/prism/tests/test_unattested_constation_gate.py b/packages/challenges/prism/tests/test_unattested_constation_gate.py index ec8f6a659..8d95501c0 100644 --- a/packages/challenges/prism/tests/test_unattested_constation_gate.py +++ b/packages/challenges/prism/tests/test_unattested_constation_gate.py @@ -167,9 +167,7 @@ def test_resolve_no_phala_alias_true() -> None: def test_resolve_canonical_false_explicit() -> None: assert ( - resolve_unattested_execution_from_environ( - {CHALLENGE_UNATTESTED_EXECUTION_ENV: "false"} - ) + resolve_unattested_execution_from_environ({CHALLENGE_UNATTESTED_EXECUTION_ENV: "false"}) is False ) diff --git a/packages/challenges/prism/tests/test_unattested_mark_unforgeable.py b/packages/challenges/prism/tests/test_unattested_mark_unforgeable.py index 1025b7a7b..70107d50a 100644 --- a/packages/challenges/prism/tests/test_unattested_mark_unforgeable.py +++ b/packages/challenges/prism/tests/test_unattested_mark_unforgeable.py @@ -145,6 +145,7 @@ def _clear_unattested_env(monkeypatch: pytest.MonkeyPatch) -> None: if key.startswith("CHALLENGE_") and key not in known: monkeypatch.delenv(key, raising=False) + # --- Unit: mark_result_unattested (agent-challenge pattern) --------------------- diff --git a/packages/challenges/prism/tests/test_unattested_no_bypass.py b/packages/challenges/prism/tests/test_unattested_no_bypass.py index cb30b1e1a..0ebc838f6 100644 --- a/packages/challenges/prism/tests/test_unattested_no_bypass.py +++ b/packages/challenges/prism/tests/test_unattested_no_bypass.py @@ -508,9 +508,7 @@ def test_http_flag_on_still_rejects_missing_proof( result={"executed": 1, MANIFEST_PAYLOAD_KEY: _manifest()}, ) # Envelope requires proof: ExecutionProof — fails closed before ingest. - code = _assert_not_missing_bundle_bypass( - resp, expected_code="result_envelope_invalid" - ) + code = _assert_not_missing_bundle_bypass(resp, expected_code="result_envelope_invalid") assert code == "result_envelope_invalid" assert _score_row(tmp_path / "coord.sqlite3", sid) is None From 024a4bd637a719fad0df81de7307ead823309510 Mon Sep 17 00:00:00 2001 From: echobt <154886644+echobt@users.noreply.github.com> Date: Wed, 29 Jul 2026 02:23:22 +0000 Subject: [PATCH 5/5] test(master): expect admission_requires_worker default false Unattested ship sets entrypoint default to :-false; align the worker-plane policy landmine with docker/master-entrypoint.sh. --- tests/unit/test_master_entrypoint_prism_policy.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_master_entrypoint_prism_policy.py b/tests/unit/test_master_entrypoint_prism_policy.py index 00ad20053..94cde4984 100644 --- a/tests/unit/test_master_entrypoint_prism_policy.py +++ b/tests/unit/test_master_entrypoint_prism_policy.py @@ -2,7 +2,7 @@ PROD POLICY baked into docker/master-entrypoint.sh: - CPU_REEXEC_TEST_MODE defaults false (eval never on master) -- ADMISSION_REQUIRES_WORKER defaults true +- ADMISSION_REQUIRES_WORKER defaults false - plagiarism LLM defaults off until secrets present """ @@ -23,7 +23,7 @@ def test_entrypoint_embeds_worker_plane_prod_defaults() -> None: ) assert ( "PRISM_WORKER_PLANE__ADMISSION_REQUIRES_WORKER=" - "${PRISM_WORKER_PLANE__ADMISSION_REQUIRES_WORKER:-true}" + "${PRISM_WORKER_PLANE__ADMISSION_REQUIRES_WORKER:-false}" ) in text assert "PRISM_WORKER_PLANE__MASTER_BASE_URL=" in text assert "PRISM_PLAGIARISM_LLM_ENABLED=${PRISM_PLAGIARISM_LLM_ENABLED:-false}" in text