Skip to content

feat(agent-challenge): NO_PHALA host pipeline with unattested weight push - #52

Merged
echobt merged 4 commits into
mainfrom
feat/agent-challenge-no-phala
Jul 28, 2026
Merged

feat(agent-challenge): NO_PHALA host pipeline with unattested weight push#52
echobt merged 4 commits into
mainfrom
feat/agent-challenge-no-phala

Conversation

@echobt

@echobt echobt commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Summary

Temporary NO_PHALA mode lets the master validator run the full agent-challenge product loop on the host while Phala CVMs are disabled. This PR completes that path: dual-flag-off analysis (AST + LLM) → host own_runner benchmark → scores → authenticated raw-weight push, with unforgeable unattested provenance everywhere scores/weights are recorded.

What NO_PHALA is (and is not)

Is: opt-in host-local execution (NO_PHALA / CHALLENGE_NO_PHALA, default OFF). Master runs jobs via existing own_runner Docker path. Phala client/deploy refuse. Results hard-marked attested:false / attestation_status:unattested / execution_mode:no_phala_host. ZIP hash provenance (expected == download == executed) still enforced.

Is not / does not prove: TEE / Intel TDX, DCAP quote, RTMR / compose_hash / KMS bind, measurement allowlist, or host integrity. Scores and weights from this mode are host-trust only — not TEE-attested.

Full pipeline now supported

With attestation flags off (required by NO_PHALA contradiction) the pre-Phala offline chain runs:

submit → analysis_queued → AST + LLM → analysis_allowed
      → waiting_miner_env → tb_queued → tb_running (own_runner)
      → tb_completed + score → get_weights → raw-weight push loop
Stage Status
Offline analysis path (flags off) Already worked — submit forks to queue_submission_analysis
AST + LLM via worker Already workedrun_worker_oncerun_next_analysis
Host TB via own_runner_backend Already worked — default backend; NO_PHALA marks emit unattested
Scores on EvaluationJob Already worked when phala_attestation_enabled=false
Unattested on tb_completed metadata Fixed — runner writes provenance fields under NO_PHALA
Authenticated raw-weight push Added (port of 61651c2f) + CRITICAL log when NO_PHALA pushes
Real-time task logs Already worked — SSE / task-events (needs stream URL for live channels)

Attested path files have zero intentional diff for gates: attestation.py, plan_scoring.py, direct_result.py untouched.

Safety design

  • Default OFF; never inferred from missing Phala key
  • Startup fails closed if NO_PHALA + either attestation flag
  • PhalaCloudClient / eval+review deploy() refuse in-mode
  • mark_result_unattested hard-codes attested=False and strips proof-looking keys
  • Weight push does not silently strip attestation preconditions on the attested path; under valid NO_PHALA, attestation flags are already off, and every push logs CRITICAL host-trust provenance
  • ZIP provenance retained

Enable / disable on master

# /var/lib/base/challenges/agent-challenge/embed.env
# Prefer CHALLENGE_ prefix (entrypoint forwards CHALLENGE_* only)
CHALLENGE_NO_PHALA=true
CHALLENGE_PHALA_ATTESTATION_ENABLED=false
CHALLENGE_ATTESTED_REVIEW_ENABLED=false
CHALLENGE_COMBINED_WORKER=true   # or run agent-challenge-worker
# LLM: CHALLENGE_LLM_GATEWAY_BASE_URL + CHALLENGE_LLM_GATEWAY_TOKEN (BASE /llm/v1 shape)
# Optional push: CHALLENGE_MASTER_BASE_URL + shared token

Confirm: curl -sS http://127.0.0.1:18081/health"no_phala":true,"attestation_mode":"no_phala_host".

Validated output (local)

Health + mode banner + weight push loop (real):

GET /health → {"status":"ok",...,"no_phala":true,"attestation_mode":"no_phala_host"}
CRITICAL NO_PHALA MODE ACTIVE — host-local unattested execution
INFO raw weight push loop started
INFO raw weight push attempt

Submit → analysis chain (real):

[submit] HTTP 201 — submission_id=1 ...
[status] queued (phase=analysis)
[status] LLM standby (phase=llm_standby)

Live LLM review needs the BASE master LLM gateway ({root}/llm/v1 + X-Gateway-Token), not raw OpenRouter. Without it the worker correctly enters llm_standby / fail-closed pause. Full AST→LLM→TB→score→push is covered by automated tests with a static reviewer + mock HTTP.

Phala CVMs: npx phala@latest cvms list --jsontotal: 0.

Tests

cd packages/challenges/agent-challenge && uv run pytest tests -q
  • Before (this branch tip prior): 3245 passed / 62 failed / 6 errors
  • After: 3262 passed / 62 failed / 6 errors (+17; same failure/error baseline)
  • New: test_no_phala_pipeline.py, test_ac_raw_weight_push.py, test_raw_weight_push_lifespan.py
  • ruff clean on touched files

Test plan

  • Unit: NO_PHALA analysis allow → tb_queued + AST/LLM rows
  • Unit: job complete → tb_completed metadata unattested
  • Unit: scores enter get_weights with flags off
  • Unit: weight push HTTP + CRITICAL unattested log; mode-off no banner; empty skip
  • Package pytest count non-regressing
  • Local /health + push loop + submit analysis phase
  • Production master: enable embed.env, confirm gateway + Docker + push to master ingress

CI

  • Waiting for required checks to go green on this head SHA.

Notes

  • Does not touch keyrelease/.
  • Unrelated dirty files on the worktree (staging/cli handoff) are not in this PR.
  • Weights derived under NO_PHALA must never be described as TEE-attested.

Summary by CodeRabbit

  • New Features
    • Added NO_PHALA host-local execution mode with explicit unattested result labeling and expanded /health + /version visibility.
    • Added durable “raw weight push” from challenge to master, including background pushing and cursor checkpointing.
    • Enabled OpenRouter-backed LLM reviewing in NO_PHALA mode and optionally injected OpenRouter keys for measured in-CVM evaluation.
  • Bug Fixes
    • Improved background task lifecycle handling and shutdown to prevent pending-task issues.
  • Documentation
    • Added operator-facing guidance for enabling, verifying, and understanding NO_PHALA behavior and limitations.
  • Tests
    • Added comprehensive NO_PHALA and raw-weight push coverage, including fail-closed validation and emission correctness.

echobt added 2 commits July 28, 2026 13:08
Opt-in host execution while Phala CVMs are disabled. Results are
explicitly marked unattested and cannot be forged as attested. Attested
path stays byte-identical when the flag is off. Contradiction with
attestation flags fails closed at startup.
… push

Drive the dual-flag-off analysis+own_runner path under NO_PHALA through
scores and authenticated raw-weight push, with unattested provenance on
completion metadata and CRITICAL push logs. Attested gates stay untouched.
@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds an explicit NO_PHALA host-local unattested evaluation mode with environmental resolution, fail-closed validation, result provenance marking, durable HMAC-signed raw-weight delivery, background task lifecycle integration, OpenRouter alternative LLM provider support, comprehensive operational documentation, and extensive unit and integration test coverage.

Changes

NO_PHALA execution contract

Layer / File(s) Summary
NO_PHALA contract and refusal gates
src/agent_challenge/evaluation/no_phala.py, src/agent_challenge/sdk/config.py, src/agent_challenge/sdk/schemas.py, src/agent_challenge/selfdeploy/phala.py, src/agent_challenge/selfdeploy/eval.py, src/agent_challenge/selfdeploy/review.py, docs/no-phala-mode.md, tests/test_no_phala_mode.py
Defines challenge-prefixed environment precedence, fail-closed attestation compatibility validation, unattested envelope rules with explicit marking and proof stripping, guest artifact provenance with triple-hash matching, Phala client/deploy refusal gates, operator-visible health/version fields, and comprehensive operational documentation.

Result marking and unattested evaluation

Layer / File(s) Summary
Unattested evaluation result pipeline
src/agent_challenge/evaluation/own_runner_backend.py, src/agent_challenge/evaluation/runner.py, tests/test_no_phala_pipeline.py
Marks legacy evaluation results and tb_completed completion metadata as unattested when NO_PHALA is enabled, computes and attaches optional guest artifact provenance from hashed local agent ZIP, and validates the offline analysis, evaluation, job completion, scoring, and weight-generation flow end-to-end.

Raw-weight delivery

Layer / File(s) Summary
Raw-weight push implementation
src/agent_challenge/evaluation/raw_weight_push.py, tests/test_ac_raw_weight_push.py
Implements durable signed raw-weight snapshot delivery via canonical request construction, HMAC-SHA256 signing, SQLAlchemy-backed pending/acknowledged cursor checkpointing, retry-safe pending reuse, exact acknowledgement identity matching without cursor advancement on mismatch, and delivery contract tests covering WTA weight shaping, empty-weight skipping, successful ACK, digest mismatch, and token non-logging.

Configuration and database

Layer / File(s) Summary
Settings, schema, and database updates
src/agent_challenge/sdk/config.py, src/agent_challenge/sdk/db.py, src/agent_challenge/core/models.py, src/agent_challenge/sdk/schemas.py, tests/test_raw_weight_push_lifespan.py
Extends ChallengeSettings with raw-weight-push fields (enabled, base_url with alias choices, interval, freshness, timeout, epoch_seconds) and NO_PHALA boolean with environment-precedence re-resolution, initializes database to register raw-weight ledger models, imports ledger on model init, and extends health/version response schemas with NO_PHALA mode fields and attestation status.

Application lifecycle

Layer / File(s) Summary
Application background task lifecycle
src/agent_challenge/api/app.py, src/agent_challenge/sdk/app_factory.py, tests/test_raw_weight_push_lifespan.py, tests/test_health.py, tests/conftest.py
Wires raw-weight push loop into FastAPI app startup/shutdown via optional background task factory parameter, stores constructed client on app state, registers done-callbacks for unexpected task exits, conditionally logs NO_PHALA startup banner, exposes mode fields in health/version responses, provides create_app() factory with optional settings/database overrides, and validates task cancellation and database initialization.

OpenRouter LLM provider

Layer / File(s) Summary
OpenRouter review provider for NO_PHALA
src/agent_challenge/analyzer/openrouter_review_provider.py, src/agent_challenge/analyzer/lifecycle.py, src/agent_challenge/sdk/config.py, src/agent_challenge/selfdeploy/cli.py, tests/test_openrouter_review_provider.py, tests/test_eval_deploy_token_handoff.py
Implements OpenRouter-backed review provider with API-key resolution (explicit, environment variables, local opencode auth JSON), cost-limit enforcement, fail-closed error handling for HTTP/timeout/rate-limit issues, lifecycle integration that builds OpenRouter provider when NO_PHALA is enabled and gateway is not explicitly set, extends ChallengeSettings with LLM fields, injects optional OpenRouter key into eval-deploy encrypted environment, and validates provider selection and cost-limit behavior.

Estimated code review effort: 4 (Complex) | ~90 minutes

Sequence Diagram(s)

sequenceDiagram
  participant AgentChallenge as Agent Challenge App
  participant RawWeightPushLoop as Push Loop Task
  participant RawWeightStore as Durable Store
  participant Master as Master Endpoint
  
  AgentChallenge->>AgentChallenge: startup: create_app()
  AgentChallenge->>RawWeightPushLoop: start background task
  loop Every 30s or configured interval
    RawWeightPushLoop->>RawWeightStore: get pending or fresh epoch
    RawWeightPushLoop->>RawWeightStore: record_pending(payload, epoch, digest)
    RawWeightPushLoop->>Master: POST /raw-weights (signed)
    alt Exact match acknowledged
      Master-->>RawWeightPushLoop: {accepted:true, epoch, revision, digest, snapshot_id}
      RawWeightPushLoop->>RawWeightStore: acknowledge() → advance cursor
    else Mismatch or error
      Master-->>RawWeightPushLoop: error or digest mismatch
      RawWeightPushLoop->>RawWeightStore: keep pending for retry
    end
  end
  AgentChallenge->>RawWeightPushLoop: cancel during shutdown
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 26.52% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the PR’s main change: adding NO_PHALA host pipeline support with unattested weight pushing.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/agent-challenge-no-phala

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (4)
packages/challenges/agent-challenge/tests/conftest.py (1)

68-72: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Drop the blanket try/except now that the ledger table is always created.

Database.init() imports agent_challenge.evaluation.raw_weight_push (and core/models.py re-exports the model), so raw_weight_push_ledger is always in Base.metadata before create_all. Swallowing the error here also masks a worse failure mode: a statement error inside the open engine.begin() transaction leaves the connection needing a rollback, so the following delete(...) calls would fail with a confusing PendingRollbackError rather than the original cause.

♻️ Proposed simplification
-        # Optional table (registered when raw_weight_push is imported).
-        try:
-            await connection.exec_driver_sql("DELETE FROM raw_weight_push_ledger")
-        except Exception:
-            pass  # table created on first Database.init after raw_weight_push import
+        await connection.exec_driver_sql("DELETE FROM raw_weight_push_ledger")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/challenges/agent-challenge/tests/conftest.py` around lines 68 - 72,
Remove the blanket try/except around the raw_weight_push_ledger deletion in the
database cleanup fixture. Since Database.init() always registers the model
before schema creation, execute the DELETE directly and allow any statement or
transaction error to propagate; leave the subsequent cleanup deletes unchanged.
packages/challenges/agent-challenge/src/agent_challenge/sdk/app_factory.py (1)

178-184: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Task naming hardcodes a caller's private closure name.

_background_task_name matches the literal _run_raw_weight_push, the name of a nested closure in agent_challenge/api/app.py. Renaming that closure silently downgrades the task name to challenge-background-task and breaks test_raw_weight_push_lifespan.py, which asserts on raw-weight-push-loop. Prefer letting callers supply the name (e.g. accept (factory, name) pairs, or read an explicit factory.task_name attribute) instead of reverse-coupling the SDK to a caller's internal identifier.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/challenges/agent-challenge/src/agent_challenge/sdk/app_factory.py`
around lines 178 - 184, The _background_task_name function is coupled to the
caller’s private _run_raw_weight_push closure name. Remove this literal-name
matching and update the task-registration flow to accept or read an explicit
task name from the caller, preserving raw-weight-push-loop for that task while
retaining the default challenge-background-task name when no name is supplied.
packages/challenges/agent-challenge/tests/test_ac_raw_weight_push.py (1)

189-230: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for the durable-retry path.

The suite verifies ack matching and mismatch, but not the headline promise in the module docstring: after a timeout/transport error, the pending row is retained and the next push_once re-sends the exact same bytes (same epoch/revision/digest) rather than minting a new snapshot. Worth one test with a transport that raises httpx.ConnectTimeout first, then acks.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/challenges/agent-challenge/tests/test_ac_raw_weight_push.py` around
lines 189 - 230, Add an async test alongside
test_mismatched_ack_digest_does_not_advance_cursor covering the durable retry
path: configure the transport to raise httpx.ConnectTimeout on the first
request, then return a valid matching acknowledgment; assert the pending row is
retained after the failure and that the next push_once succeeds while reusing
the exact same serialized payload, epoch, revision, and payload digest rather
than creating a new snapshot.
packages/challenges/agent-challenge/src/agent_challenge/evaluation/no_phala.py (1)

85-96: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Unrecognized boolean tokens silently resolve to "off" with no warning.

_parse_bool_env returns None for unrecognized tokens, and callers do bool(parsed)False. An operator typo (e.g. NO_PHALA=tru) silently disables the mode instead of surfacing a misconfiguration. The direction is safe (fails to "off"), but a warning would help operators notice the typo instead of assuming NO_PHALA is active when it silently isn't.

♻️ Proposed enhancement
 def resolve_no_phala_from_environ(
     environ: Mapping[str, str] | None = None,
 ) -> bool:
     env = os.environ if environ is None else environ
     if CHALLENGE_NO_PHALA_ENV in env:
         parsed = _parse_bool_env(env.get(CHALLENGE_NO_PHALA_ENV))
+        if parsed is None:
+            logger.warning(
+                "%s=%r is not a recognized boolean token; treating as off",
+                CHALLENGE_NO_PHALA_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))
+        if parsed is None:
+            logger.warning(
+                "%s=%r is not a recognized boolean token; treating as off",
+                NO_PHALA_ENV, env.get(NO_PHALA_ENV),
+            )
         return bool(parsed)
     return False
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/challenges/agent-challenge/src/agent_challenge/evaluation/no_phala.py`
around lines 85 - 96, Update _parse_bool_env and its caller so unrecognized
non-empty boolean tokens emit a warning identifying the invalid value before
preserving the current safe false/off behavior. Keep recognized tokens and unset
values unchanged, and reuse the module’s existing logging mechanism rather than
adding a new one.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@packages/challenges/agent-challenge/src/agent_challenge/evaluation/own_runner_backend.py`:
- Around line 1207-1217: Update the NO_PHALA branch in _emit_job_result to catch
ArtifactProvenanceError from _maybe_no_phala_artifact_proof or
mark_result_unattested, then use the existing fail-closed result emission path
(emit_failclosed_result) so a genuine ZIP hash mismatch still emits a result
line instead of propagating. Preserve the current successful artifact-proof flow
and mirror the attested path’s error handling.
- Around line 626-661: Update _maybe_no_phala_artifact_proof so it returns None
when PHALA_AGENT_HASH_ENV is unset or blank, instead of assigning executed as
expected. Only call build_guest_artifact_proof when an independently declared
expected hash is available; preserve the existing artifact and digest validation
behavior otherwise.

In
`@packages/challenges/agent-challenge/src/agent_challenge/evaluation/raw_weight_push.py`:
- Around line 374-384: Update the pending-payload handling around
RawWeightPushRequest validation to inspect the restored payload’s freshness
timestamps before assigning it to raw_bytes. If the pending payload is expired,
discard it by clearing pending, payload, and raw_bytes so the normal path
rebuilds a fresh revision; otherwise preserve the existing byte-for-byte retry
behavior for still-fresh pending payloads.
- Around line 418-422: Update the revision selection in the rebuild path around
_next_revision so that, when rebuilding a discarded or corrupted pending entry
in the same epoch, the next revision is seeded from the latest outstanding
revision. Use pending.revision together with cursor.revision (taking the
maximum) before recording the new pending payload, while preserving
force_revision behavior.

---

Nitpick comments:
In
`@packages/challenges/agent-challenge/src/agent_challenge/evaluation/no_phala.py`:
- Around line 85-96: Update _parse_bool_env and its caller so unrecognized
non-empty boolean tokens emit a warning identifying the invalid value before
preserving the current safe false/off behavior. Keep recognized tokens and unset
values unchanged, and reuse the module’s existing logging mechanism rather than
adding a new one.

In `@packages/challenges/agent-challenge/src/agent_challenge/sdk/app_factory.py`:
- Around line 178-184: The _background_task_name function is coupled to the
caller’s private _run_raw_weight_push closure name. Remove this literal-name
matching and update the task-registration flow to accept or read an explicit
task name from the caller, preserving raw-weight-push-loop for that task while
retaining the default challenge-background-task name when no name is supplied.

In `@packages/challenges/agent-challenge/tests/conftest.py`:
- Around line 68-72: Remove the blanket try/except around the
raw_weight_push_ledger deletion in the database cleanup fixture. Since
Database.init() always registers the model before schema creation, execute the
DELETE directly and allow any statement or transaction error to propagate; leave
the subsequent cleanup deletes unchanged.

In `@packages/challenges/agent-challenge/tests/test_ac_raw_weight_push.py`:
- Around line 189-230: Add an async test alongside
test_mismatched_ack_digest_does_not_advance_cursor covering the durable retry
path: configure the transport to raise httpx.ConnectTimeout on the first
request, then return a valid matching acknowledgment; assert the pending row is
retained after the failure and that the next push_once succeeds while reusing
the exact same serialized payload, epoch, revision, and payload digest rather
than creating a new snapshot.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 07a166ef-7110-4050-b3ff-3e9497c11489

📥 Commits

Reviewing files that changed from the base of the PR and between 2bfa110 and a3447f2.

📒 Files selected for processing (20)
  • packages/challenges/agent-challenge/docs/no-phala-mode.md
  • packages/challenges/agent-challenge/src/agent_challenge/api/app.py
  • packages/challenges/agent-challenge/src/agent_challenge/core/models.py
  • packages/challenges/agent-challenge/src/agent_challenge/evaluation/no_phala.py
  • packages/challenges/agent-challenge/src/agent_challenge/evaluation/own_runner_backend.py
  • packages/challenges/agent-challenge/src/agent_challenge/evaluation/raw_weight_push.py
  • packages/challenges/agent-challenge/src/agent_challenge/evaluation/runner.py
  • packages/challenges/agent-challenge/src/agent_challenge/sdk/app_factory.py
  • packages/challenges/agent-challenge/src/agent_challenge/sdk/config.py
  • packages/challenges/agent-challenge/src/agent_challenge/sdk/db.py
  • packages/challenges/agent-challenge/src/agent_challenge/sdk/schemas.py
  • packages/challenges/agent-challenge/src/agent_challenge/selfdeploy/eval.py
  • packages/challenges/agent-challenge/src/agent_challenge/selfdeploy/phala.py
  • packages/challenges/agent-challenge/src/agent_challenge/selfdeploy/review.py
  • packages/challenges/agent-challenge/tests/conftest.py
  • packages/challenges/agent-challenge/tests/test_ac_raw_weight_push.py
  • packages/challenges/agent-challenge/tests/test_health.py
  • packages/challenges/agent-challenge/tests/test_no_phala_mode.py
  • packages/challenges/agent-challenge/tests/test_no_phala_pipeline.py
  • packages/challenges/agent-challenge/tests/test_raw_weight_push_lifespan.py

Comment on lines +626 to +661
def _maybe_no_phala_artifact_proof() -> dict[str, str | bool] | None:
"""Build guest_artifact_proof for NO_PHALA host runs when ZIP is available.

Returns None when the artifact path is missing (caller still marks
unattested without the triple). When the path is present, enforces
expected == download == executed against the plan/declared agent hash.
"""

from agent_challenge.evaluation.no_phala import (
ArtifactProvenanceError,
build_guest_artifact_proof,
)

artifact_path = resolve_agent_artifact_path()
if artifact_path is None or not Path(artifact_path).is_file():
return None
try:
executed = agent_artifact_sha256(artifact_path)
except ValueError:
return None
expected = (os.environ.get(PHALA_AGENT_HASH_ENV) or "").strip().lower()
if not expected:
# No plan hash declared — use the executed digest as the expected pin
# so provenance still records download==executed for the host ZIP.
expected = executed
download = executed # host-local: download path is the same file we execute
try:
return build_guest_artifact_proof(
expected_hash=expected,
download_hash=download,
executed_hash=executed,
)
except ArtifactProvenanceError:
raise


Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Trivial guest_artifact_proof when no declared hash is available.

When CHALLENGE_PHALA_AGENT_HASH is unset, expected/download/executed are all set to the same locally-computed digest, so the built proof always shows match: true without checking against anything independent. This could misrepresent an unverified artifact as provenance-checked. Consider omitting guest_artifact_proof entirely (return None) when no independently-declared hash exists, rather than emitting a self-referential trivial match.

🛡️ Proposed fix
     expected = (os.environ.get(PHALA_AGENT_HASH_ENV) or "").strip().lower()
     if not expected:
-        # No plan hash declared — use the executed digest as the expected pin
-        # so provenance still records download==executed for the host ZIP.
-        expected = executed
+        # No independently-declared plan hash — there is nothing to verify
+        # against, so omit the proof rather than emit a trivially-true match.
+        return None
     download = executed  # host-local: download path is the same file we execute
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def _maybe_no_phala_artifact_proof() -> dict[str, str | bool] | None:
"""Build guest_artifact_proof for NO_PHALA host runs when ZIP is available.
Returns None when the artifact path is missing (caller still marks
unattested without the triple). When the path is present, enforces
expected == download == executed against the plan/declared agent hash.
"""
from agent_challenge.evaluation.no_phala import (
ArtifactProvenanceError,
build_guest_artifact_proof,
)
artifact_path = resolve_agent_artifact_path()
if artifact_path is None or not Path(artifact_path).is_file():
return None
try:
executed = agent_artifact_sha256(artifact_path)
except ValueError:
return None
expected = (os.environ.get(PHALA_AGENT_HASH_ENV) or "").strip().lower()
if not expected:
# No plan hash declared — use the executed digest as the expected pin
# so provenance still records download==executed for the host ZIP.
expected = executed
download = executed # host-local: download path is the same file we execute
try:
return build_guest_artifact_proof(
expected_hash=expected,
download_hash=download,
executed_hash=executed,
)
except ArtifactProvenanceError:
raise
def _maybe_no_phala_artifact_proof() -> dict[str, str | bool] | None:
"""Build guest_artifact_proof for NO_PHALA host runs when ZIP is available.
Returns None when the artifact path is missing (caller still marks
unattested without the triple). When the path is present, enforces
expected == download == executed against the plan/declared agent hash.
"""
from agent_challenge.evaluation.no_phala import (
ArtifactProvenanceError,
build_guest_artifact_proof,
)
artifact_path = resolve_agent_artifact_path()
if artifact_path is None or not Path(artifact_path).is_file():
return None
try:
executed = agent_artifact_sha256(artifact_path)
except ValueError:
return None
expected = (os.environ.get(PHALA_AGENT_HASH_ENV) or "").strip().lower()
if not expected:
# No independently-declared plan hash — there is nothing to verify
# against, so omit the proof rather than emit a trivially-true match.
return None
download = executed # host-local: download path is the same file we execute
try:
return build_guest_artifact_proof(
expected_hash=expected,
download_hash=download,
executed_hash=executed,
)
except ArtifactProvenanceError:
raise
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/challenges/agent-challenge/src/agent_challenge/evaluation/own_runner_backend.py`
around lines 626 - 661, Update _maybe_no_phala_artifact_proof so it returns None
when PHALA_AGENT_HASH_ENV is unset or blank, instead of assigning executed as
expected. Only call build_guest_artifact_proof when an independently declared
expected hash is available; preserve the existing artifact and digest validation
behavior otherwise.

Comment on lines +1207 to +1217
# Temporary NO_PHALA host mode: mark the legacy line unattested and
# attach guest_artifact_proof when the agent ZIP provenance is known.
# Attested path below is untouched when NO_PHALA is off.
from agent_challenge.evaluation.no_phala import (
is_no_phala_enabled,
mark_result_unattested,
)

if is_no_phala_enabled():
artifact_proof = _maybe_no_phala_artifact_proof()
payload = mark_result_unattested(payload, artifact_proof=artifact_proof)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Unhandled ArtifactProvenanceError crashes the NO_PHALA emit path on a genuine hash mismatch.

Unlike the attested path below (which catches AttestationEmissionError and calls emit_failclosed_result(...) for a graceful fail-closed result), a genuine ZIP hash mismatch here propagates an uncaught ArtifactProvenanceError out of _emit_job_result, so no result line is ever emitted for the exact case this mechanism is meant to catch.

🐛 Proposed fix
+        from agent_challenge.evaluation.no_phala import ArtifactProvenanceError
         from agent_challenge.evaluation.no_phala import (
             is_no_phala_enabled,
             mark_result_unattested,
         )
 
         if is_no_phala_enabled():
-            artifact_proof = _maybe_no_phala_artifact_proof()
-            payload = mark_result_unattested(payload, artifact_proof=artifact_proof)
+            try:
+                artifact_proof = _maybe_no_phala_artifact_proof()
+            except ArtifactProvenanceError as exc:
+                logger.critical("NO_PHALA artifact provenance mismatch: %s", exc)
+                emit_failclosed_result(total=len(list(task_ids)))
+                return 1
+            payload = mark_result_unattested(payload, artifact_proof=artifact_proof)
         emit_benchmark_result_line(payload)
         return 0
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Temporary NO_PHALA host mode: mark the legacy line unattested and
# attach guest_artifact_proof when the agent ZIP provenance is known.
# Attested path below is untouched when NO_PHALA is off.
from agent_challenge.evaluation.no_phala import (
is_no_phala_enabled,
mark_result_unattested,
)
if is_no_phala_enabled():
artifact_proof = _maybe_no_phala_artifact_proof()
payload = mark_result_unattested(payload, artifact_proof=artifact_proof)
from agent_challenge.evaluation.no_phala import ArtifactProvenanceError
from agent_challenge.evaluation.no_phala import (
is_no_phala_enabled,
mark_result_unattested,
)
if is_no_phala_enabled():
try:
artifact_proof = _maybe_no_phala_artifact_proof()
except ArtifactProvenanceError as exc:
logger.critical("NO_PHALA artifact provenance mismatch: %s", exc)
emit_failclosed_result(total=len(list(task_ids)))
return 1
payload = mark_result_unattested(payload, artifact_proof=artifact_proof)
emit_benchmark_result_line(payload)
return 0
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/challenges/agent-challenge/src/agent_challenge/evaluation/own_runner_backend.py`
around lines 1207 - 1217, Update the NO_PHALA branch in _emit_job_result to
catch ArtifactProvenanceError from _maybe_no_phala_artifact_proof or
mark_result_unattested, then use the existing fail-closed result emission path
(emit_failclosed_result) so a genuine ZIP hash mismatch still emits a result
line instead of propagating. Preserve the current successful artifact-proof flow
and mirror the attested path’s error handling.

Comment on lines +374 to +384
if pending is not None:
# Retry exact previous bytes after timeout/restart (no new revision).
try:
pending_bytes = str(pending["canonical_payload"]).encode("utf-8")
payload = RawWeightPushRequest.model_validate_json(pending_bytes)
raw_bytes = payload.canonical_bytes()
except Exception: # noqa: BLE001 - corrupt pending is rebuilt
pending = None
payload = None
raw_bytes = None

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Stale pending payload is retried forever after its freshness window expires.

The pending payload is replayed byte-for-byte, so computed_at/expires_at stay at the original values. Once more than freshness_seconds (default 300s) of failures/downtime elapse, every retry posts an already-expired snapshot that master will reject on freshness, and pending is only cleared on a successful ack — delivery is wedged permanently. Add a staleness escape so an expired pending is discarded and rebuilt with a fresh revision.

🛠️ Proposed staleness escape
         if pending is not None:
             # Retry exact previous bytes after timeout/restart (no new revision).
             try:
                 pending_bytes = str(pending["canonical_payload"]).encode("utf-8")
                 payload = RawWeightPushRequest.model_validate_json(pending_bytes)
+                expires_at = datetime.fromisoformat(
+                    str(payload.expires_at).replace("Z", "+00:00")
+                )
+                if expires_at <= now:
+                    # Expired snapshot would be rejected on freshness forever.
+                    raise ValueError("pending payload expired")
                 raw_bytes = payload.canonical_bytes()
-            except Exception:  # noqa: BLE001 - corrupt pending is rebuilt
+            except Exception:  # noqa: BLE001 - corrupt/expired pending is rebuilt
                 pending = None
                 payload = None
                 raw_bytes = None
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if pending is not None:
# Retry exact previous bytes after timeout/restart (no new revision).
try:
pending_bytes = str(pending["canonical_payload"]).encode("utf-8")
payload = RawWeightPushRequest.model_validate_json(pending_bytes)
raw_bytes = payload.canonical_bytes()
except Exception: # noqa: BLE001 - corrupt pending is rebuilt
pending = None
payload = None
raw_bytes = None
if pending is not None:
# Retry exact previous bytes after timeout/restart (no new revision).
try:
pending_bytes = str(pending["canonical_payload"]).encode("utf-8")
payload = RawWeightPushRequest.model_validate_json(pending_bytes)
expires_at = datetime.fromisoformat(
str(payload.expires_at).replace("Z", "+00:00")
)
if expires_at <= now:
# Expired snapshot would be rejected on freshness forever.
raise ValueError("pending payload expired")
raw_bytes = payload.canonical_bytes()
except Exception: # noqa: BLE001 - corrupt/expired pending is rebuilt
pending = None
payload = None
raw_bytes = None
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/challenges/agent-challenge/src/agent_challenge/evaluation/raw_weight_push.py`
around lines 374 - 384, Update the pending-payload handling around
RawWeightPushRequest validation to inspect the restored payload’s freshness
timestamps before assigning it to raw_bytes. If the pending payload is expired,
discard it by clearing pending, payload, and raw_bytes so the normal path
rebuilds a fresh revision; otherwise preserve the existing byte-for-byte retry
behavior for still-fresh pending payloads.

Comment on lines +418 to +422
revision = (
int(force_revision)
if force_revision is not None
else self._next_revision(cursor, resolved_epoch)
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check Prism's equivalent revision logic for how pending revisions are handled.
fd -t f 'raw_weight_push*.py' packages/challenges/prism --exec cat -n {}

Repository: BaseIntelligence/base

Length of output: 39895


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and inspect the Agent Challenge client implementation plus its revision/pending tests.
fd -t f 'raw_weight_push.*\.py|test.*raw.*weight|.*weight.*test.*\.py' packages/challenges/agent-challenge -x sh -c '
  echo "===== FILE: $1 ====="
  wc -l "$1"
  rg -n "_next_revision|record_pending|get_pending|push_once|pending" "$1"
' sh {}

echo "===== candidate files ====="
fd -t f 'raw_weight_push.*\.py|test.*raw.*weight|.*weight.*test.*\.py' packages/challenges/agent-challenge

echo "===== focused agent file outline/sections ====="
agent_file=$(fd -t f 'raw_weight_push.py' packages/challenges/agent-challenge | head -n1)
if [ -n "${agent_file:-}" ]; then
  ast-grep outline "$agent_file" || true
  sed -n '240,445p' "$agent_file" | cat -n
fi

Repository: BaseIntelligence/base

Length of output: 15126


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
class PushCursor:
    def __init__(self, epoch, revision):
        self.epoch = epoch
        self.revision = revision

def next_revision(cursor, epoch):
    if cursor is None:
        return 1
    if cursor.epoch == epoch:
        return cursor.revision + 1
    return 1

cursor = PushCursor(epoch=10, revision=5)
pending = {"epoch": 10, "revision": 6, "payload_digest": "aaa"}

revision = next_revision(cursor, 10)
# A rejected/discard-like rebuild that does not persist pending would record:
rebuild = {"epoch": 10, "revision": revision, "payload_digest": "bbb"}

# A later retry with unchanged persisted pending state reuses pending if not rebuilt.
retry = pending

print({"cursor": (cursor.epoch, cursor.revision), "pending_before_rebuild": (pending["epoch"], pending["revision"], pending["payload_digest"]), "rebuilt": (rebuild["epoch"], rebuild["revision"], rebuild["payload_digest"]), "retry_reuses": (retry["epoch"], retry["revision"])})
PY

Repository: BaseIntelligence/base

Length of output: 273


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
for p in Path("packages/challenges/agent-challenge").rglob("test_ac_raw_weight_push.py"):
    text = p.read_text()
    print(f"===== {p} =====")
    for needle in ["build_corrupt_pending", "discard_pending_identity", "corrupt pending", "_next_revision", "revision"]:
        idx = text.find(needle)
        print(f"{needle}: {idx}")
    start = text.find("build_corrupt_pending")
    if start == -1:
        start = text.find("discard_pending_identity")
    if start != -1:
        end = text.find("\n    def ", start + 1)
        print(text[start: end if end != -1 else start + 2000])
PY

Repository: BaseIntelligence/base

Length of output: 349


Seed rebuilt revisions from the latest outstanding revision.

When pending is discarded/corrupted and rebuilt in the same epoch, _next_revision only sees the acknowledged cursor, so it reuses the same revision number with a different digest. Seed the rebuilt revision from pending.revision/max(cursor.revision, pending.revision) before recording the new pending payload.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/challenges/agent-challenge/src/agent_challenge/evaluation/raw_weight_push.py`
around lines 418 - 422, Update the revision selection in the rebuild path around
_next_revision so that, when rebuilding a discarded or corrupted pending entry
in the same epoch, the next revision is seeded from the latest outstanding
revision. Use pending.revision together with cursor.revision (taking the
maximum) before recording the new pending payload, while preserving
force_revision behavior.

echobt added 2 commits July 28, 2026 13:47
Add OpenRouterReviewProvider for analyzer LLM review when NO_PHALA is on,
with key resolution (env then opencode auth.json), operator config keys,
and fail-closed cost/error handling. Gateway path stays default when mode
is off. Document embed.env keys and cover selection/parsing with unit tests.
Production mounts the challenge token via shared_token_file; raw-weight
push now resolves it the same way auth does so the loop does not skip.
NO_PHALA + cli docker backend runs own_runner in-process to avoid nested
DooD path mismatch on master embed. Default epoch_seconds matches master
sealer (360s).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (2)
packages/challenges/agent-challenge/tests/test_raw_weight_push_lifespan.py (1)

208-208: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Use an HTTPS fixture URL for the token-bearing client.

Although this test does not send a request today, RawWeightPushClient stores the shared token with this base URL. Using https://master.test prevents future push-capable tests from transmitting the token over cleartext.

Proposed fix
-        master_base_url="http://master.test",
+        master_base_url="https://master.test",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/challenges/agent-challenge/tests/test_raw_weight_push_lifespan.py`
at line 208, Update the master_base_url fixture used to construct the
token-bearing RawWeightPushClient to use the HTTPS scheme while preserving the
existing host and test behavior.

Source: Linters/SAST tools

packages/challenges/agent-challenge/src/agent_challenge/analyzer/openrouter_review_provider.py (1)

98-122: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Scavenging unrelated local credential stores widens the secret surface.

~/.factory/*.json (and the recursive key hunt) makes the challenge service pick up any developer credential file it happens to find, with no operator intent and no audit trail of which source was used. Recommend limiting resolution to the explicit setting, the two env vars, and at most the documented opencode/auth.json, and logging only the source name (never the value).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/challenges/agent-challenge/src/agent_challenge/analyzer/openrouter_review_provider.py`
around lines 98 - 122, Restrict credential resolution to the explicit setting,
the two supported environment variables, and the documented opencode/auth.json
source; remove the factory-root candidate scanning and recursive JSON key
discovery used by _key_from_factory_dir and _key_from_json_file_shallow. Track
the selected source and log only its source name, never the credential value,
while preserving the existing precedence order.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@packages/challenges/agent-challenge/src/agent_challenge/analyzer/openrouter_review_provider.py`:
- Around line 242-250: Update the response parsing around choices and message so
the first choices element must be a mapping before calling .get or passing it to
_parse_provider_tool_calls. Treat malformed elements such as strings as an empty
message and preserve the existing LlmProviderResponse construction and
fail-closed behavior.

In
`@packages/challenges/agent-challenge/src/agent_challenge/evaluation/runner.py`:
- Around line 1401-1412: Update the NO_PHALA execution path in the runner around
_run_no_phala_host_terminal_bench to forward
_terminal_bench_stream_env(plan.attempt_id), matching the Docker branch’s
per-attempt streaming environment injection and preserving real-time SSE log
ingestion.
- Around line 2078-2097: Update the host-local subprocess setup around env and
cwd construction to prevent validator environment and workspace code from
reaching the host process. Build env from an explicit allowlist of required
control variables plus _terminal_bench_env(miner_env, gateway), without
inheriting all of os.environ; retain BASE_AGENT_PATH, remove the
workspace-derived PYTHONPATH, and run subprocess.run from a neutral directory
while allowing own_runner to import the agent inside its sandbox.

In `@packages/challenges/agent-challenge/src/agent_challenge/sdk/config.py`:
- Around line 372-381: Update the validation_alias for llm_cost_limit_usd to
include the documented CHALLENGE_LLM_COST_LIMIT_USD and CHALLENGE_LLM_COST_LIMIT
environment names, while preserving the existing aliases and default behavior.

In
`@packages/challenges/agent-challenge/tests/test_openrouter_review_provider.py`:
- Around line 37-59: Replace all plaintext secret-like test fixtures with
identifier-only placeholders, preserving each test’s assertions and precedence
behavior. Update
packages/challenges/agent-challenge/tests/test_openrouter_review_provider.py
ranges 37-59, 62-123, 126-189, and 192-254, plus
packages/challenges/agent-challenge/tests/test_eval_deploy_token_handoff.py
range 555-575; use names, digests, or SHAs consistently for environment values,
provider/header fixtures, failure-path keys, gateway tokens, and OpenRouter
tokens.

---

Nitpick comments:
In
`@packages/challenges/agent-challenge/src/agent_challenge/analyzer/openrouter_review_provider.py`:
- Around line 98-122: Restrict credential resolution to the explicit setting,
the two supported environment variables, and the documented opencode/auth.json
source; remove the factory-root candidate scanning and recursive JSON key
discovery used by _key_from_factory_dir and _key_from_json_file_shallow. Track
the selected source and log only its source name, never the credential value,
while preserving the existing precedence order.

In `@packages/challenges/agent-challenge/tests/test_raw_weight_push_lifespan.py`:
- Line 208: Update the master_base_url fixture used to construct the
token-bearing RawWeightPushClient to use the HTTPS scheme while preserving the
existing host and test behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e61a0dab-a80b-4e6a-9369-278ade34910e

📥 Commits

Reviewing files that changed from the base of the PR and between a3447f2 and 0b785f2.

📒 Files selected for processing (10)
  • packages/challenges/agent-challenge/docs/no-phala-mode.md
  • packages/challenges/agent-challenge/src/agent_challenge/analyzer/lifecycle.py
  • packages/challenges/agent-challenge/src/agent_challenge/analyzer/openrouter_review_provider.py
  • packages/challenges/agent-challenge/src/agent_challenge/evaluation/raw_weight_push.py
  • packages/challenges/agent-challenge/src/agent_challenge/evaluation/runner.py
  • packages/challenges/agent-challenge/src/agent_challenge/sdk/config.py
  • packages/challenges/agent-challenge/src/agent_challenge/selfdeploy/cli.py
  • packages/challenges/agent-challenge/tests/test_eval_deploy_token_handoff.py
  • packages/challenges/agent-challenge/tests/test_openrouter_review_provider.py
  • packages/challenges/agent-challenge/tests/test_raw_weight_push_lifespan.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/challenges/agent-challenge/docs/no-phala-mode.md
  • packages/challenges/agent-challenge/src/agent_challenge/evaluation/raw_weight_push.py

Comment on lines +242 to +250
choices = data.get("choices")
message = choices[0].get("message", {}) if isinstance(choices, list) and choices else {}
return LlmProviderResponse(
content=str(message.get("content") or ""),
tool_calls=_parse_provider_tool_calls(message),
raw_response=_redacted_response(data),
usage=data.get("usage") if isinstance(data.get("usage"), Mapping) else None,
cost=data.get("cost") if isinstance(data.get("cost"), Mapping) else None,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Malformed choices raises AttributeError instead of failing closed.

choices[0] is only checked for list-ness, not element type; a response like {"choices": ["x"]} escapes as AttributeError, bypassing the LlmProvider* classification the reviewer relies on.

🛡️ Proposed fix
-        choices = data.get("choices")
-        message = choices[0].get("message", {}) if isinstance(choices, list) and choices else {}
+        choices = data.get("choices")
+        first = choices[0] if isinstance(choices, list) and choices else None
+        raw_message = first.get("message") if isinstance(first, Mapping) else None
+        message: Mapping[str, Any] = raw_message if isinstance(raw_message, Mapping) else {}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
choices = data.get("choices")
message = choices[0].get("message", {}) if isinstance(choices, list) and choices else {}
return LlmProviderResponse(
content=str(message.get("content") or ""),
tool_calls=_parse_provider_tool_calls(message),
raw_response=_redacted_response(data),
usage=data.get("usage") if isinstance(data.get("usage"), Mapping) else None,
cost=data.get("cost") if isinstance(data.get("cost"), Mapping) else None,
)
choices = data.get("choices")
first = choices[0] if isinstance(choices, list) and choices else None
raw_message = first.get("message") if isinstance(first, Mapping) else None
message: Mapping[str, Any] = raw_message if isinstance(raw_message, Mapping) else {}
return LlmProviderResponse(
content=str(message.get("content") or ""),
tool_calls=_parse_provider_tool_calls(message),
raw_response=_redacted_response(data),
usage=data.get("usage") if isinstance(data.get("usage"), Mapping) else None,
cost=data.get("cost") if isinstance(data.get("cost"), Mapping) else None,
)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/challenges/agent-challenge/src/agent_challenge/analyzer/openrouter_review_provider.py`
around lines 242 - 250, Update the response parsing around choices and message
so the first choices element must be a mapping before calling .get or passing it
to _parse_provider_tool_calls. Treat malformed elements such as strings as an
empty message and preserve the existing LlmProviderResponse construction and
fail-closed behavior.

Comment on lines +1401 to +1412
if _np() and settings.docker_backend in {"cli", "docker"}:
run = await asyncio.to_thread(
_run_no_phala_host_terminal_bench,
job=job,
task=task,
plan=plan,
agent_workspace=agent_workspace,
miner_env=miner_env,
gateway=gateway,
execution_backend=execution_backend,
timeout_seconds=settings.evaluation_timeout_seconds,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Host path drops _terminal_bench_stream_env(plan.attempt_id).

The Docker branch injects the per-attempt log-stream env; the NO_PHALA branch does not, so real-time SSE log ingest silently stops in this mode. Forward it via _run_no_phala_host_terminal_bench if streaming is expected to keep working.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/challenges/agent-challenge/src/agent_challenge/evaluation/runner.py`
around lines 1401 - 1412, Update the NO_PHALA execution path in the runner
around _run_no_phala_host_terminal_bench to forward
_terminal_bench_stream_env(plan.attempt_id), matching the Docker branch’s
per-attempt streaming environment injection and preserving real-time SSE log
ingestion.

Comment on lines +2078 to +2097
env = {
**dict(os.environ),
**_terminal_bench_env(miner_env, gateway),
"PYTHONPATH": f"{agent_workspace}:{os.environ.get('PYTHONPATH', '')}".rstrip(":"),
"DOCKER_HOST": os.environ.get("DOCKER_HOST") or "unix:///var/run/docker.sock",
"BASE_AGENT_PATH": str(agent_workspace),
}
if env["PYTHONPATH"].endswith(":"):
env["PYTHONPATH"] = env["PYTHONPATH"][:-1]

try:
proc = subprocess.run(
cmd,
cwd=str(agent_workspace),
capture_output=True,
text=True,
timeout=timeout_seconds,
check=False,
env=env,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🔴 Critical | 🏗️ Heavy lift

Host-local path leaks the validator's whole environment and puts the miner workspace on sys.path of a host process.

Two distinct hazards in this env/cwd construction:

  1. **dict(os.environ) hands the validator process environment (shared token, database_url, eval_result_signer_mnemonic, gateway tokens, OpenRouter key, …) to own_runner and everything it spawns. The Docker path deliberately passes only _terminal_bench_env(miner_env, gateway); this path reverses that containment.
  2. cwd=agent_workspace plus PYTHONPATH=agent_workspace:... means miner-supplied files in the extracted ZIP precede stdlib/site-packages for python -m agent_challenge... — a submitted subprocess.py/json.py executes as the validator host user, outside any sandbox.

Pass an explicit allowlisted env (control vars + _terminal_bench_env), and keep the agent workspace off the interpreter path (use BASE_AGENT_PATH only, run from a neutral cwd, and let own_runner import the agent inside its own sandbox).

As per coding guidelines: "Do not accept code that exfiltrates secrets, tokens, SSH keys, cookies, environment variables, or private files" and "Do not accept code that disables sandboxing, escapes containers, changes host permissions, or bypasses resource limits."

🧰 Tools
🪛 ast-grep (0.45.0)

[error] 2088-2096: Command coming from incoming request
Context: subprocess.run(
cmd,
cwd=str(agent_workspace),
capture_output=True,
text=True,
timeout=timeout_seconds,
check=False,
env=env,
)
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(subprocess-from-request)


[error] 2088-2096: Use of unsanitized data to create processes
Context: subprocess.run(
cmd,
cwd=str(agent_workspace),
capture_output=True,
text=True,
timeout=timeout_seconds,
check=False,
env=env,
)
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(os-system-unsanitized-data)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/challenges/agent-challenge/src/agent_challenge/evaluation/runner.py`
around lines 2078 - 2097, Update the host-local subprocess setup around env and
cwd construction to prevent validator environment and workspace code from
reaching the host process. Build env from an explicit allowlist of required
control variables plus _terminal_bench_env(miner_env, gateway), without
inheriting all of os.environ; retain BASE_AGENT_PATH, remove the
workspace-derived PYTHONPATH, and run subprocess.run from a neutral directory
while allowing own_runner to import the agent inside its sandbox.

Source: Coding guidelines

Comment on lines +372 to +381
# Optional USD spend ceiling for OpenRouter analyzer calls; fail closed when
# exceeded. Env: CHALLENGE_LLM_COST_LIMIT / CHALLENGE_LLM_COST_LIMIT_USD
llm_cost_limit_usd: float | None = Field(
default=None,
validation_alias=AliasChoices(
"llm_cost_limit_usd",
"LLM_COST_LIMIT",
"llm_cost_limit",
),
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

pydantic-settings validation_alias env_prefix not applied AliasChoices

💡 Result:

In Pydantic Settings, the env_prefix configuration is generally ignored for fields that have a validation_alias (or alias) explicitly defined [1][2][3][4]. This behavior is intentional, as the explicit alias is treated as the source of truth for the environment variable name, overriding the default prefixing logic [3]. Regarding AliasChoices and env_prefix: 1. The env_prefix does not automatically apply to the strings defined inside AliasChoices [5][3]. If you define a field with validation_alias=AliasChoices('a', 'b'), Pydantic will look for environment variables named exactly a and b, not PREFIX_a or PREFIX_b [5][3]. 2. If you need the prefix to apply to your aliases, you must manually include the prefix within the alias strings (e.g., AliasChoices('MY_PREFIX_a', 'MY_PREFIX_b')) or use the env_prefix_target configuration setting if you are on a recent version of Pydantic Settings [6][1][4]. 3. The env_prefix_target setting, available in newer versions, allows you to control how the prefix is applied. By setting env_prefix_target='all', you can attempt to apply the prefix to both variable names and aliases, though its effectiveness with complex AliasChoices remains a known point of discussion in the community [6][1][4]. If you find that your aliases are not picking up the expected environment variables, it is because Pydantic prioritizes the defined alias over the env_prefix [1][3]. To achieve your desired configuration, you may need to explicitly define the full variable names including the prefix within the AliasChoices list, or implement a custom EnvSettingsSource by overriding the get_field_value method to manually handle the prefixing logic [7].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate config.py =="
fd -a 'config\.py$' . | sed 's#^\./##'

echo "== relevant config.py section =="
config_path="$(fd 'config\.py$' packages/challenges/agent-challenge/src/agent_challenge | head -n1 || true)"
if [ -n "$config_path" ]; then
  sed -n '330,390p' "$config_path" | cat -n
  echo "== env_prefix references =="
  rg -n "env_prefix|CHALLENGE_LLM_COST_LIMIT|CHALLENGE_LLM_COST_LIMIT_USD|LLM_COST_LIMIT|master_base_url" "$config_path" packages/challenges/agent-challenge/src/agent_challenge || true
fi

echo "== pydantic dependency versions =="
for f in pyproject.toml pnpm-lock.yaml package-lock.json poetry.lock requirements.txt; do
  [ -f "$f" ] && { echo "--- $f"; sed -n '1,220p' "$f" | rg -n "pydantic|pydantic-settings|version|\\$\\s*$" | head -n 120; }
done

Repository: BaseIntelligence/base

Length of output: 4633


🏁 Script executed:

#!/bin/bash
set -euo pipefail

config_path="packages/challenges/agent-challenge/src/agent_challenge/sdk/config.py"
echo "== config.py llm_cost_limit section =="
sed -n '35,110p' "$config_path" | cat -n
echo "== llm_cost_limit section =="
sed -n '360,385p' "$config_path" | cat -n

echo "== tests mentioning llm cost/env names =="
rg -n "llm_cost_limit|LLM_COST_LIMIT|CHALLENGE_LLM_COST_LIMIT|llm_cost_limit_usd|CHALLENGE_LLM_COST_LIMIT_USD|env_prefix" packages/challenges/agent-challenge/tests packages/challenges/agent-challenge/src/agent_challenge -g '*.py' || true

echo "== pyproject pydantic versions =="
sed -n '1,110p' pyproject.toml | cat -n

Repository: BaseIntelligence/base

Length of output: 17039


Add the documented CHALLENGE_* aliases for llm_cost_limit_usd.

With an explicit validation_alias, env_prefix is not applied to these choices, so llm_cost_limit_usd only reads llm_cost_limit_usd, LLM_COST_LIMIT, and llm_cost_limit. Setting CHALLENGE_LLM_COST_LIMIT_USD / CHALLENGE_LLM_COST_LIMIT will be ignored, leaving the documented spend-ceiling configuration ineffective.

🛠️ Proposed fix
     llm_cost_limit_usd: float | None = Field(
         default=None,
         validation_alias=AliasChoices(
             "llm_cost_limit_usd",
+            "CHALLENGE_LLM_COST_LIMIT_USD",
+            "CHALLENGE_LLM_COST_LIMIT",
             "LLM_COST_LIMIT",
             "llm_cost_limit",
         ),
     )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Optional USD spend ceiling for OpenRouter analyzer calls; fail closed when
# exceeded. Env: CHALLENGE_LLM_COST_LIMIT / CHALLENGE_LLM_COST_LIMIT_USD
llm_cost_limit_usd: float | None = Field(
default=None,
validation_alias=AliasChoices(
"llm_cost_limit_usd",
"LLM_COST_LIMIT",
"llm_cost_limit",
),
)
# Optional USD spend ceiling for OpenRouter analyzer calls; fail closed when
# exceeded. Env: CHALLENGE_LLM_COST_LIMIT / CHALLENGE_LLM_COST_LIMIT_USD
llm_cost_limit_usd: float | None = Field(
default=None,
validation_alias=AliasChoices(
"llm_cost_limit_usd",
"CHALLENGE_LLM_COST_LIMIT_USD",
"CHALLENGE_LLM_COST_LIMIT",
"LLM_COST_LIMIT",
"llm_cost_limit",
),
)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/challenges/agent-challenge/src/agent_challenge/sdk/config.py` around
lines 372 - 381, Update the validation_alias for llm_cost_limit_usd to include
the documented CHALLENGE_LLM_COST_LIMIT_USD and CHALLENGE_LLM_COST_LIMIT
environment names, while preserving the existing aliases and default behavior.

Comment on lines +37 to +59
def test_resolve_key_order_explicit_then_env_then_opencode(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.delenv("CHALLENGE_OPENROUTER_API_KEY", raising=False)
monkeypatch.delenv("OPENROUTER_API_KEY", raising=False)

assert resolve_openrouter_api_key(explicit=" explicit-key ", home=tmp_path) == "explicit-key"

monkeypatch.setenv("CHALLENGE_OPENROUTER_API_KEY", "challenge-key")
monkeypatch.setenv("OPENROUTER_API_KEY", "plain-key")
env = dict(**__import__("os").environ)
assert resolve_openrouter_api_key(explicit=None, environ=env, home=tmp_path) == "challenge-key"

monkeypatch.delenv("CHALLENGE_OPENROUTER_API_KEY", raising=False)
env = dict(**__import__("os").environ)
assert resolve_openrouter_api_key(explicit=None, environ=env, home=tmp_path) == "plain-key"

monkeypatch.delenv("OPENROUTER_API_KEY", raising=False)
auth = tmp_path / ".local" / "share" / "opencode" / "auth.json"
auth.parent.mkdir(parents=True)
payload = {"openrouter": {"type": "api", "key": "file-key"}}
auth.write_text(json.dumps(payload), encoding="utf-8")
assert resolve_openrouter_api_key(explicit=None, environ={}, home=tmp_path) == "file-key"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Replace plaintext secret fixtures with identifier-only placeholders.

The tests embed complete key/token-like values. Even when synthetic, this violates the repository rule and risks normalizing secrets in source or triggering secret scanners.

  • packages/challenges/agent-challenge/tests/test_openrouter_review_provider.py#L37-L59: use identifier-style placeholders for precedence assertions.
  • packages/challenges/agent-challenge/tests/test_openrouter_review_provider.py#L62-L123: replace the provider/header fixture with an identifier-only placeholder.
  • packages/challenges/agent-challenge/tests/test_openrouter_review_provider.py#L126-L189: replace failure-path key fixtures likewise.
  • packages/challenges/agent-challenge/tests/test_openrouter_review_provider.py#L192-L254: replace gateway/OpenRouter token fixtures with names or digests.
  • packages/challenges/agent-challenge/tests/test_eval_deploy_token_handoff.py#L555-L575: replace the OpenRouter environment fixture with an identifier-only placeholder.

As per coding guidelines, “Never log, document, or include evidence containing private keys, wallet mnemonics, API tokens, or full secret values; only names, digests, and SHAs are permitted.”

🧰 Tools
🪛 ast-grep (0.45.0)

[info] 57-57: use jsonify instead of json.dumps for JSON output
Context: json.dumps(payload)
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)

📍 Affects 2 files
  • packages/challenges/agent-challenge/tests/test_openrouter_review_provider.py#L37-L59 (this comment)
  • packages/challenges/agent-challenge/tests/test_openrouter_review_provider.py#L62-L123
  • packages/challenges/agent-challenge/tests/test_openrouter_review_provider.py#L126-L189
  • packages/challenges/agent-challenge/tests/test_openrouter_review_provider.py#L192-L254
  • packages/challenges/agent-challenge/tests/test_eval_deploy_token_handoff.py#L555-L575
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/challenges/agent-challenge/tests/test_openrouter_review_provider.py`
around lines 37 - 59, Replace all plaintext secret-like test fixtures with
identifier-only placeholders, preserving each test’s assertions and precedence
behavior. Update
packages/challenges/agent-challenge/tests/test_openrouter_review_provider.py
ranges 37-59, 62-123, 126-189, and 192-254, plus
packages/challenges/agent-challenge/tests/test_eval_deploy_token_handoff.py
range 555-575; use names, digests, or SHAs consistently for environment values,
provider/header fixtures, failure-path keys, gateway tokens, and OpenRouter
tokens.

Source: Coding guidelines

@echobt
echobt merged commit 9b395a8 into main Jul 28, 2026
45 checks passed
@echobt
echobt deleted the feat/agent-challenge-no-phala branch July 28, 2026 20:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant