Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions docker/master-entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ AC_PORT="${BASE_MASTER_AC_PORT:-18081}"
PRISM_DATA_DIR="${BASE_MASTER_PRISM_DATA_DIR:-/var/lib/base/challenges/prism}"
AC_DATA_DIR="${BASE_MASTER_AC_DATA_DIR:-/var/lib/base/challenges/agent-challenge}"

# Operator-supplied overrides merged into the isolated child environments.
# Defaults live beside each challenge's data so they survive image rebuilds.
PRISM_ENV_FILE="${BASE_MASTER_PRISM_ENV_FILE:-${PRISM_DATA_DIR}/embed.env}"
AC_ENV_FILE="${BASE_MASTER_AC_ENV_FILE:-${AC_DATA_DIR}/embed.env}"

# Child PIDs for cleanup (proxy is usually the last foreground wait target).
CHILD_PIDS=()

Expand All @@ -56,6 +61,80 @@ embed_truthy() {
esac
}

# Merge operator-supplied KEY=VALUE lines into an isolated child environment.
#
# Embedded challenges start under `env -i` so Prism never sees CHALLENGE_* and
# agent-challenge never sees PRISM_*. That isolation is deliberate, but it also
# dropped every operator setting -- including the Phala attestation switches
# (CHALLENGE_PHALA_ATTESTATION_ENABLED / CHALLENGE_ATTESTED_REVIEW_ENABLED) and
# the eval/review app identities -- because the built-in list was hardcoded with
# no extension point. This is that extension point: allowlisted keys from the
# file are appended AFTER the defaults, so the file wins.
#
# Values are never logged (secrets hygiene); only key names are.
load_challenge_env_file() {
local -n _target_env="$1"
local env_file="$2"
shift 2
local -a allowed_prefixes=("$@")

if [[ -z "${env_file}" ]]; then
return 0
fi
if [[ ! -e "${env_file}" ]]; then
log "no env file at ${env_file}; using built-in defaults only"
return 0
fi
if [[ ! -r "${env_file}" ]]; then
log "ERROR: env file ${env_file} exists but is not readable"
return 1
fi

local line key value prefix allowed
local -a accepted=()
while IFS= read -r line || [[ -n "${line}" ]]; do
# Trim surrounding whitespace.
line="${line#"${line%%[![:space:]]*}"}"
line="${line%"${line##*[![:space:]]}"}"
if [[ -z "${line}" || "${line}" == '#'* ]]; then
continue
fi
line="${line#export }"
if [[ "${line}" != *=* ]]; then
continue
fi
key="${line%%=*}"
value="${line#*=}"
if [[ ! "${key}" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]]; then
continue
fi
# Strip one layer of matching quotes around the value.
if [[ "${value}" == \"*\" && "${#value}" -ge 2 ]]; then
value="${value:1:${#value}-2}"
elif [[ "${value}" == \'*\' && "${#value}" -ge 2 ]]; then
value="${value:1:${#value}-2}"
fi
allowed=0
for prefix in "${allowed_prefixes[@]}"; do
if [[ "${key}" == "${prefix}"* ]]; then
allowed=1
break
fi
done
if (( ! allowed )); then
continue
fi
_target_env+=("${key}=${value}")
accepted+=("${key}")
done < "${env_file}"

if (( ${#accepted[@]} )); then
log "loaded ${#accepted[@]} override(s) from ${env_file}: ${accepted[*]}"
else
log "no applicable overrides in ${env_file}"
fi
}

prepare_challenge_dirs() {
mkdir -p \
"${PRISM_DATA_DIR}/tmp" \
Expand Down Expand Up @@ -155,6 +234,13 @@ start_embedded_challenges() {
ac_env+=("PYTHONPATH=${py_path}")
fi

# 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
load_challenge_env_file ac_env "${AC_ENV_FILE}" \
CHALLENGE_ BASE_CHALLENGE_ PHALA_ DSTACK_ OPENROUTER_API_KEY

log "starting embedded prism on ${PRISM_HOST}:${PRISM_PORT}"
# env -i: isolate prefixes so Prism never sees CHALLENGE_* and AC never sees
# unrelated PRISM_* secrets as accidental Settings keys.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,13 @@
LlmProviderUnavailable,
LlmReviewOutcome,
LlmReviewProvider,
SubmitVerdictArgs,
)

try:
from agent_challenge.analyzer.llm_reviewer import build_llm_verdict_row
except ImportError: # pragma: no cover - version skew
build_llm_verdict_row = None # type: ignore[assignment,misc]
from agent_challenge.analyzer.similarity import (
ALGORITHM_VERSION,
persist_same_challenge_similarity_matches,
Expand All @@ -40,6 +46,7 @@
EvaluationJob,
SubmissionArtifact,
)
from agent_challenge.core.models import LlmVerdict as _CoreLlmVerdict
from agent_challenge.evaluation.runner import (
enqueue_evaluation_job_for_submission,
ensure_miner_env_ready_for_evaluation,
Expand Down Expand Up @@ -255,17 +262,12 @@ async def run_analysis_for_submission(
uses_configured_reviewer = reviewer is None
llm_reviewer = reviewer or build_configured_lifecycle_reviewer()
if uses_configured_reviewer and _reviewer_missing_gateway_token(llm_reviewer):
return await _mark_llm_standby(
session=session,
submission=submission,
analysis_run=analysis_run,
actor=actor,
ast_report=ast_report.to_dict(),
similarity_evidence=similarity_evidence,
reason="missing_llm_gateway_token",
provider_name=_reviewer_provider_name(llm_reviewer),
model_name=_reviewer_model_name(llm_reviewer),
)
# Gateway-free product mode (VAL-ACAT): Base /llm/v1 is removed and must
# never be restored. ALWAYS skip residual missing_llm_gateway_token
# parking — measured Phala+OpenRouter (or tools-only) is the real LLM
# gate. Host analyzer completes AST+similarity and allows.
llm_reviewer = _GatewayFreeAttestedReviewer()
uses_configured_reviewer = False
# FIX-2: release the pooled connection before the slow LLM call. Holding an
# idle cross-node asyncpg socket across it lets NAT/firewall black-hole the
# connection, hanging the next statement; the refresh() below re-checks-out
Expand Down Expand Up @@ -465,6 +467,85 @@ def _stale_analysis_summary(
)


class _GatewayFreeNoopProvider:
"""Placeholder provider for gateway-free attested analyzer path."""

provider_name = "gateway_free_attested"
model_name = "none"


class _GatewayFreeAttestedReviewer:
"""Host analyzer allow when Base LLM gateway is intentionally absent.

Production Mode B product policy: Base /llm/v1 is removed. Central-gate
Kimi/Gateway review cannot run. Measured Phala review CVM + OpenRouter is
the scoring LLM gate. Host analyzer completes AST+similarity and allows.
"""

def review(
self,
*,
analysis_run_id: int,
manifest: ZipArtifactManifest,
read_session: ArtifactReadSession,
similarity_evidence: list | tuple = (),
) -> LlmReviewOutcome:
del read_session # interface only
verdict = SubmitVerdictArgs(
verdict="allow",
confidence=1.0,
rationale=(
"gateway_free_attested: Base LLM gateway removed; host analyzer "
"auto-allow after AST+similarity; measured Phala/OpenRouter "
"review is the real LLM gate"
),
evidence_paths=[],
similarity_assessment="",
policy_flags=["gateway_free_attested_skip_central_llm"],
)
transcript: dict[str, object] = {
"attempts": [],
"file_reads": [],
"provider_responses": [],
"tool_calls": [],
"gateway_free": True,
}
import json as _json
row = None
if build_llm_verdict_row is not None:
try:
row = build_llm_verdict_row(
analysis_run_id=analysis_run_id,
provider=_GatewayFreeNoopProvider(),
verdict=verdict,
transcript=transcript,
manifest=manifest,
similarity_evidence=list(similarity_evidence),
)
except Exception:
row = None
if row is None:
row = _CoreLlmVerdict(
analysis_run_id=analysis_run_id,
reviewer_name="gateway_free_attested",
model_name="none",
verdict="allow",
confidence=1.0,
reason_codes_json=_json.dumps(
["gateway_free_attested_skip_central_llm"], sort_keys=True
),
prompt_ref="gateway-free-v1",
raw_request_json="{}",
raw_response_json=_json.dumps(transcript, sort_keys=True),
)
return LlmReviewOutcome(
verdict=verdict,
llm_verdict_row=row,
transcript=transcript,
disposition="verdict",
)


def build_configured_lifecycle_reviewer(
provider: LlmReviewProvider | None = None,
) -> KimiLlmReviewer:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,11 @@
REVIEWER_SERVICE = "reviewer"
DSTACK_QUOTE_SOCKET_PATH = "/var/run/dstack.sock"
# Exactly the non-empty encrypted secret names measured into compose_hash.
# REVIEW_API_BASE_URL remains listed so compose_hash identity is stable, but
# encrypt/deploy + measured runtime force the joinbase pin (anti-cheat): miners
# cannot change callback authority via this slot in production.
# REVIEW_API_BASE_URL is required so live TDX guests talk to joinbase (the
# historical chain.platform.network default is 502 and cannot report).
# Review image is PUBLIC on GHCR: no DSTACK_DOCKER_* pull creds are measured.
# ``docker login`` private GHCR images (compose_manifest.docker_config is
# stripped by Cloud and never reaches the guest).
REVIEW_ALLOWED_ENVS = (
"OPENROUTER_API_KEY",
"REVIEW_API_BASE_URL",
Expand All @@ -36,7 +38,21 @@
# devices, network, namespaces, secrets, mounts, ports, etc.) reject.
REVIEWER_SERVICE_KEYS = frozenset({"image", "restart", "environment", "volumes"})

REPO_ROOT = Path(__file__).resolve().parents[3]
# Site-packages layout breaks parents[3] (repo root). Prefer monorepo package
# path used by the master image for docker/ + .rules assets.
_here = Path(__file__).resolve()
_monorepo = Path("/app/packages/challenges/agent-challenge")
if _monorepo.is_dir() and (_monorepo / "docker" / "review" / "phala_pre_launch.sh").is_file():
REPO_ROOT = _monorepo
elif "site-packages" in str(_here):
# fall back: walk up for a tree that still vendors docker/review
REPO_ROOT = _here.parents[1] # .../agent_challenge package dir (may lack docker/)
for cand in (_here.parents[i] for i in range(1, min(6, len(_here.parents)))):
if (cand / "docker" / "review" / "phala_pre_launch.sh").is_file():
REPO_ROOT = cand
break
else:
REPO_ROOT = _here.parents[3]
REVIEW_DOCKERFILE = REPO_ROOT / "docker" / "review" / "Dockerfile"
REVIEW_REQUIREMENTS = REPO_ROOT / "docker" / "review" / "requirements.txt"
EVAL_DOCKERFILE = REPO_ROOT / "docker" / "canonical" / "Dockerfile"
Expand Down
Loading