Uh oh!
There was an error while loading. Please reload this page.
fix(agent-challenge): owned CVM teardown, fail-loud list, staging stack - #51
fix(agent-challenge): owned CVM teardown, fail-loud list, staging stack#51echobt wants to merge 568 commits into
Conversation
- _seed_challenge_token: correct the stale ownership comment - the base-master-proxy branch now runs --user root (+ docker.sock) for the in-proxy registry reconciler + challenge-image-updater, so the master reads token files as root regardless of owner. - VALIDATOR_MASTER_URL: make it a REQUIRED --validator-node override (fail fast if unset) instead of defaulting to the node's own advertise address, which is always wrong for a validator (a footgun). - _stage_supervisor_release: export a committed-tree snapshot via 'git archive HEAD' (-> tar) instead of cp -a of the whole working tree (incl .git + uncommitted changes); the release version label derives from committed HEAD. Falls back to cp -a only for a non-git release source. Guard tests updated (validator-node master-url required + not self-referential; staging asserts committed-tree snapshot, no cp -a). bash -n clean; IMAGE_* allowlist unchanged.
…rage - service_image: distinguish an absent service (None) from a transient inspect failure (re-raise DockerOrchestrationError) so a momentary dockerd error on an already-current service no longer triggers a spurious --force redeploy in the challenge-image-updater. - Add a stable public seam (SwarmImageUpdater.targets + image_updater_from_task) and replace the fragile run.__self__._targets reach-ins in three tests. - Add tests: empty targets + validator-agent toggle OFF is a clean no-op; transient inspect failure does not redeploy. - weights.py docstring: acknowledge OnChainWeightSubmitter as a second per-node submit path (default-OFF, never enabled for master); wording only, no behavior change.
_stage_supervisor_release used the git archive HEAD branch whenever the source was a git work tree, but git archive HEAD hard-fails on an unborn HEAD (a repo with no commits). Gate that branch on git rev-parse --verify HEAD succeeding and fall back to cp -a otherwise, so staging never hard-fails.
A transient (non-not-found) docker service inspect failure surfaced as DockerOrchestrationError from running_image and propagated to the per-challenge exception handler, logging a full stack trace every tick during a dockerd hiccup even though the condition is expected and self-correcting (retried next tick). Catch it in _converge_service, log concisely at warning (no stack trace), and skip the roll (action=skipped-inspect-error, no redeploy). Genuinely unexpected errors are not caught here and stay at exception level.
…r-validator on-chain weights
…oute)
Make the master LLM gateway provider-agnostic and config-driven per the
llm-yunwu contract, superseding all deepseek/openrouter/deepseek-v4-pro guidance.
- tokens: add optional source (wire s) + model (wire m) claims to
GatewayTokenClaims/issue/issue_central_gate/verify (backward compatible).
- providers: replace deepseek/openrouter constants with a config-driven
provider registry (build_providers keyed by name; mock+real).
- gateway: single POST /llm/v1/{path} route; resolve provider+model from the
token source via config (default yunwu/claude-opus-4-8); overwrite request
body model + inject provider key; drop _enforce_model/DEEPSEEK_REQUIRED_MODEL;
provider-agnostic build + secret redaction.
- settings: GatewaySettings gains providers registry + default_provider +
default_model + sources; deepseek/openrouter fields removed.
- executor + assignment_coordination: emit BASE_LLM_GATEWAY_URL (={root}/llm/v1)
+ BASE_GATEWAY_TOKEN, stamp source=agent on assignment tokens.
- cli: build service from the yunwu key; mint-central-gate-token gains --source.
- update all platform gateway tests to the new contract (provider_mode=mock).…l-swarm.sh) Render the config-driven, provider-agnostic yunwu gateway block into deploy/swarm/master.yaml + the install-swarm.sh master-config: providers.yunwu (base_url https://yunwu.ai/v1, api_key_file /run/secrets/yunwu_api_key), default_provider/default_model (claude-opus-4-8), and per-source routes (agent/llm_review). install-swarm.sh now creates + mounts the single base_gateway_yunwu_api_key secret at /run/secrets/yunwu_api_key on the master proxy (provider_mode=real), keeps the mandatory HMAC gateway_token_secret, routes both central gates through the master gateway /llm/v1 with the scoped central-gate token (source=llm_review), and drops all deepseek/openrouter secret creation + mounts. Update the swarm/secret/deploy unit tests + runbook to expect the yunwu mount + absence of deepseek/openrouter. No yunwu key value in git. VAL-LLM-CODE-010
Reframe yunwu branding in deploy/swarm/README.md so the gateway is described generically: provider-agnostic, with the active provider + model chosen in master.yaml and injected server-side from the token source claim. Operational secret identifiers (base_gateway_yunwu_api_key / YUNWU_API_KEY / /run/secrets/yunwu_api_key) are unchanged and kept where operators need them, reframed as the configured-provider key. Docs-only; no code/config/prod change.
Remove stale DeepSeek/OpenRouter direct-provider-key prose from README.md and docs/operations/validator.md. The master gateway resolves provider+model server-side from the token 'source' claim (agent / llm_review) per master.yaml; challenge/eval services + validators hold no raw provider key and authenticate with a scoped gateway token (base_gateway_token, source=llm_review, /llm/v1). The single provider key lives only on the master gateway at /run/secrets/yunwu_api_key. Docs-only; no code/config/test changes.
yunwu intermittently closes the connection mid-response (httpx.RemoteProtocolError) or returns a transient 5xx; the gateway had no retry so these collapsed to caller-facing 502s. HttpLLMProvider.forward now wraps the request in a bounded retry loop (default 3 attempts, each on a FRESH client) with exponential backoff (0.25s then 0.5s), retrying the enumerated transient transport exceptions and 502/503/504. A 429 / other 4xx (and 2xx/3xx) returns immediately and is never retried. Retrying is safe because the gateway is fully buffered (no partial bytes reach the caller before an attempt succeeds). Retry logs name only the exception class + attempt counters, so no injected provider key can leak. Retry policy is configurable via ProviderConfig with production-safe defaults.
Allow the frontend origins (joinbase.ai, www, localhost dev ports and platform Vercel previews) to call the single public proxy cross-origin. Public, cookie-less reads only: GET/HEAD/OPTIONS, allow_credentials=False, so token-gated /v1/admin/* and signed uploads stay unchanged. Middleware wraps the whole ASGI app, covering forwarded /challenges/* (incl. SSE) and the included admin/registry routes. Origins overridable via allowed_cors_origins.
The agent-challenge own_runner reads task definitions only from node-local named volumes and fails closed on any digest mismatch; the runner image does not bake the ~89 task trees. Previously the volumes were empty after a fresh deploy, so every terminal-bench evaluation failed with terminal_bench_failed. - Add download-terminal-bench-cache.sh: reads the byte-exact public source (repo + revision + task count) from the frozen golden/dataset-digest.json, clones at the pinned commit, and stages the task dirs (stripping only the task-root .gitignore, as harbor does at packaging time) as a --source dir. - Wire provision_agent_challenge_cache() into install-swarm.sh (STEP 9b, after deploy_master, before deploy_challenges): download then acquire-agent-challenge-cache.sh --apply to copy + digest-verify onto the volumes. Both halves honor the script's dry-run/--apply gate. No secrets are read or required.
Add src/base/compute/ package: - provider.py: ProviderClient protocol, InstanceSpec (mandatory max_lifetime_hours/max_price_per_hour bounds), Offer/Instance, typed ProviderError/CostGuardrailError. - lium.py: LiumClient over httpx (X-API-Key, base https://lium.io/api). list_offers price filtering; provision refuses unbounded/over-priced specs before any network call, always sends bounded termination_hours, and terminates+verifies on any post-rent failure (try/finally); idempotent ensure_ssh_key/ensure_template; idempotent terminate; verify_terminated via GET /pods; stream_logs; watchtower digest; GET /users/me balance. API key never logged/repr'd/in errors. 50 offline respx unit tests (VAL-PROV-001/003/004/005/011/017/018 + secret hygiene). Full base gate green: 1316 passed, coverage 89.1%.
…able log level Batch of tested master/broker hardening (previously staged in the working tree): - Broker capacity: add `broker_max_concurrent_global` (server-wide cap on total concurrent broker jobs across all slugs) and route both run paths through a single `_register_job` choke point that enforces the per-slug + global caps atomically under the workload-ledger lock (over-limit -> WorkloadCapacityError -> HTTP 429 docker_quota_exceeded). Default None = unlimited (behavior-preserving). - WorkloadLedger.register accepts `max_concurrent_global`, checked under the same lock as insertion so check-and-register stays atomic. - Broker output bound: add `broker_log_limit_bytes` (default 5MB) and broker job lifecycle logging (accepted/finished/timed-out/rejected; never logs secrets). - Observability: configurable `log_level` (case-insensitive, falls back to INFO). - Submitter + install-swarm deploy wiring and matching unit tests. Validation: ruff clean, ruff format clean, full unit suite 1234 passed.
…-client secret hygiene
Add build_lium_worker_template (Lium CustomTemplateRequest payload) and build_targon_worker_app (Targon app definition) in src/base/compute/ worker_deployment.py. Both pin the docker image BY DIGEST via a shared pinned_image_reference helper; the image+digest default to the published prism-evaluator digest (M1 placeholder) and are inputs so M2 swaps in docker/Dockerfile.worker with a one-line change. Well-formedness is enforced (required fields, internal ports incl. 22, env plumbing, is_private, no embedded secrets) and covered by offline respx tests that also assert the whole compute suite makes zero real network calls under respx strict mode and needs no provider credentials (VAL-PROV-009/010/016).
…TESTS Add scripts/live_lium_e2e.py driving the M1 provider clients against production: read-only reachability (Lium users/me + executors + watchtower/digest, Targon inventory + apps) plus one batched Lium rental cycle (ensure ssh key + template -> rent cheapest suitable executor -> poll RUNNING -> ssh nvidia-smi -> logs -> DELETE -> verify gone -> record balance delta). Pod is deleted in a finally on every path; opt-in via BASE_LIVE_PROVIDER_TESTS=1 so the default suite stays offline.
…gpu shape - LiumClient.provision rejects sub-1-hour max_lifetime_hours so termination_hours never truncates to 0 (auto-termination stays on) - widen post-rent cleanup guard to key off rent success, not pod-id resolution: a transient GET /pods failure during resolution still best-effort terminates + verifies the just-rented pod - normalize Targon Offer.price_per_hour to per-GPU (cost_per_hour / gpu_count) so the per-GPU max_price cap filters multi-GPU shapes correctly - default WORKER_GPU_SHAPE 'h100' -> live-valid 'h100-small'
Add the miner-funded GPU worker plane registry (architecture.md sec 3.3),
gated behind compute.worker_plane_enabled:
- alembic 0009: worker_registrations + worker_faults + worker_request_nonces
on the existing chain (0008 -> 0009), no legacy table altered.
- WorkerCoordinationService mirroring validator_coordination patterns:
POST /v1/workers/register (sr25519 miner binding verified against the mock
metagraph, binding-nonce replay protection, no silent cross-owner rebind),
POST /v1/workers/{id}/heartbeat, GET /v1/workers (fleet: status/owner/
provider/last-seen/faults, authenticated-but-not-admin), and
GET /v1/workers/active?hotkey= (admission surface).
- Lifecycle pending -> active -> stale -> retired with
compute.worker_heartbeat_ttl_seconds (default 120); retired is terminal
(no heartbeat resurrection); staleness derived on read + a background pass.
- worker_auth: binding message/verify, signed-request verifier + registered-
worker/validator eligibility, worker nonce store.
- Wired into create_proxy_app + the master proxy CLI (flag-off => unmounted).
Unit + Postgres integration tests (15433) cover VAL-MASTER-001/002/015/016/
018/021 and VAL-AGENT-015.…x spurious 502s Broaden RETRYABLE_STATUS_CODES to include Cloudflare 520/522/524, Anthropic 529 (overloaded) and generic 500 so a transient edge/upstream overload is retried instead of collapsed into an opaque 502. Add full-jitter backoff and a 4th attempt. Add a true streaming passthrough for stream=true callers (the eval agent): open the upstream stream with a per-chunk read timeout so a slow large completion no longer trips Cloudflare 524 or the 30s buffered read cap, retrying only pre-first-byte. Log the real upstream status (status+source only, never bodies/keys) so a surfaced failure is no longer an opaque 502. The buffered llm_reviewer safety-gate path response shape is unchanged.
Add the miner-funded WorkerAgent (src/base/worker/): register under a
miner-signed binding, heartbeat to stay active, pull gpu-only replicas,
execute via the AssignmentExecutor seam on its local broker, and post
results that always carry an ExecutionProof envelope (sr25519 over
sha256('{manifest_sha256}:{unit_id}'), pinned identically to prism).
Extract the shared agent-loop primitives into base/coordination/agent_loop
(BackoffPolicy, is_transient_error, AgentCycleSummary, sleep_until,
backoff_sleep); the validator agent now imports them with behavior unchanged.
Add the master worker assignment plane (worker_assignments table + 0010
migration, worker-authenticated pull/result routes gated on registration
and liveness, never a validator permit) and wire it behind
compute.worker_plane_enabled.
Fulfills VAL-AGENT-002/003/004/005/006/007/008/016/017/018.Add a top-level `base worker` Typer app distinct from the legacy `base master worker` Swarm group: - deploy --provider local starts a miner-funded agent against a local master and reports it active within 60s. - deploy --provider lium|targon requires the provider key env (actionable refusal before any network), bounds offer selection by --max-price preferring an exact gpu_count executor (next-cheapest fallback), and never transmits the provider key to the master (pod env excludes it). - status renders the fleet from GET /v1/workers (signed as the worker key). Thread startup_commands through InstanceSpec, LiumClient.ensure_template and build_lium_worker_template (validated metachar-free) per the live-learned Lium rent constraint; keep docker/Dockerfile.worker's exec-form entrypoint metachar-free. Add WorkerSettings, worker/miner keypair resolvers, signed list_workers, config/worker.example.yaml, and unit tests. All behind compute.worker_plane_enabled; full base gate green.
Add WorkerAssignmentEngine that materializes gpu work-unit replicas onto ACTIVE distinct-owner workers behind compute.worker_plane_enabled: R=2 with self-evaluation exclusion (unit waits under sole-capacity scarcity), graceful degradation to R=1 with a recorded warning, per-worker gpu concurrency 1, and per-replica deadline/reassignment bounded by max_attempts. The validator AssignmentService skips worker-plane capabilities when the flag is on, so flag-OFF gpu routing to validators stays byte-identical to legacy.
… behind flag Reconcile replicated gpu worker results (architecture.md 3.3): matching ExecutionProof.manifest_sha256 forwards exactly one result to the challenge; divergent hashes dispute the unit (never forwarded, before or after audit) and create a validator-executor audit unit whose outcome writes worker_faults for the divergent worker(s), visible in fleet status. Single-replica reporting terminates deterministically (accept-after-degrade with warning); late/foreign posts stay rejected with replica state intact. All gated by compute.worker_plane_enabled (reconciler is None when off).
Legacy AssignmentService.reclaim_stale_assignments treated a null assigned_validator_hotkey as 'offline validator => reassignable', so under the worker plane a worker-owned prism PRIMARY (ASSIGNED, null hotkey by design) churned back to PENDING every MasterOrchestrationDriver.run_once pass. _assign_pending_in_session already skipped such units via worker_plane_capabilities; _reclaim_in_session did not. Factor the guard into AssignmentService._worker_plane_owns and apply it in BOTH paths so a worker-owned primary is neither assigned nor reclaimed here, while a genuinely stale validator unit (cpu, or gpu AUDIT with executor_kind= validator) is still reclaimed. Flag OFF keeps reclaim byte-identical to legacy.
The static --static-challenges installer path built the agent-challenge api and worker service env WITHOUT CHALLENGE_TERMINAL_BENCH_LOG_STREAM_URL or CHALLENGE_DOCKER_BROKER_NETWORK, even though both services are already multi-homed onto base_jobs_internal for exactly this purpose. Without them the terminal-bench runner JOB lands on the default bridge network, cannot resolve challenge-agent-challenge by name, and live task.log streaming silently no-ops. Add both to the shared ac_eval_env array (applied to api+worker), mirroring the dynamic seed path (cli_app._agent_challenge_own_runner_env), and lock it in with a parity regression test.
…budget The eval task concurrency was hardcoded (broker_max_concurrent_global=30, CHALLENGE_EVALUATION_CONCURRENCY=15), over-committing a 62 GiB manager. Derive it at install time from system RAM at a 4 GB/task budget: EVAL_TASK_CONCURRENCY = max(4, floor((TOTAL_RAM_GB - reserve) / 4)), reserve 10 (-> 13 on the 62 GiB box), with EVAL_RAM_TOTAL_GB / EVAL_RAM_RESERVE_GB / EVAL_TASK_CONCURRENCY overrides. Render it into both broker_max_concurrent_global and CHALLENGE_EVALUATION_CONCURRENCY, and pin a durable 2g runner (own_runner DooD client) memory ceiling. The 4 GB divisor only sizes concurrency; the inner terminal-bench task container keeps its higher 8 GiB ceiling (low actual usage).
…ollback The master image-updater fired docker service update --detach and never checked convergence, so a broken :latest image silently degraded proxy/broker with no revert, no retry budget, and no alert. Make auto-update durable: - New retry.py: RetryPolicy (bounded exponential backoff + equal jitter) + per-target RetryState. - image_updater.py: record pre-update digest as last-known-good (persisted under the release root, restart-durable), issue the update WITHOUT --detach, poll UpdateStatus.State to convergence; on paused/rolled_back/timeout re-pin the last-known-good digest and schedule exponential backoff; after max_attempts emit an image_update_failed alert and wait for a new digest (which resets). - Operator freeze: supervisor.image_update_hold (global) + per-target hold skip a service entirely so a pinned known-good digest is never overridden. - settings knobs (max_attempts/backoff base+max/hold), new alert kind, alert hook wired into the image-updater task.
…file fix(master): load per-challenge embed.env into isolated child env
…e tests Lock stage defaults (review tdx.small/20GB, eval tdx.xlarge/100GB), disk bounds/billing, lifecycle budget with disk, provision disk_size emission, and frozen eval/review compose hashes before any production sizing change.
…faults Split review (tdx.small/20GB) and eval (tdx.xlarge/100GB) defaults, add disk bounds and billing helpers, and fold optional disk into projected cost while keeping CpuShape free of a disk field.
Charge both stages for compute plus disk against the shared $20 money cap and update the ordered CLI lifecycle fixture for disk-aware totals.
Thread stage disk_size_gb on eval/review deployment plans and send disk_size as a sibling of compose_file without mutating measured compose documents.
… CLI Default eval to tdx.xlarge, add --review-disk-size-gb/--eval-disk-size-gb, pass disks into lifecycle budget checks, and harden offline deploy test doubles.
Record review tdx.small/20GB and eval tdx.xlarge/100GB defaults, disk rate, and unchanged CPU-only $20 money cap in miner and validator self-deploy docs.
Lock DELETE /cvms/{id} allowlist behavior, 204/404 success, unique app_id
resolution, and CLI teardown without a phala binary before the fix lands.Allow DELETE /cvms/{id} (204/404 success) and refuse ambiguous app_id
matches when resolving teardown identity without a phala binary.Route teardown through PhalaCloudClient.delete_cvm, accept optional --cvm-id/--app-id, and resolve unique app_id matches from GET /cvms.
Assert delete_cvm path, ambiguous app_id refusal, and docs no longer require a phala binary in default_phala_teardown.
Describe DELETE /cvms/{id} as the primary path and keep manual phala
cvms list/delete strings for VAL-DEPLOY-020 verification.Check --cvm-id/--app-id before constructing PhalaCloudClient so missing identity fails closed without requiring credentials.
Mirror review prepare recovery: when eval prepare returns token-less secret_delivery, cancel the current run and retry so deploy receives a fresh one-shot capability. Production residual on submission 3 left attempts 1 and 2 stuck after standalone prepare/retry spent the token. Never cancel when the token is already present; never invent tokens.
Assert token-present skips cancel/retry, token-absent recovers via
cancel+retry, sticky absence raises RouteClientError, and the
{env_key,token} secret_delivery shape stays fail-closed. Also pin
review deploy fixtures to the production REVIEW_API_BASE_URL.…UN_TOKEN Cover the production closed loop where eval deploy injects the one-time run token into the CVM but never surfaces it for eval result. Assert --emit-run-token / --token-output handoff, fail-closed without either on live deploy, dry-run exemption, --output hygiene, and prepare/status redaction regressions.
Live eval deploy now requires --token-output and/or --emit-run-token so the host can post via eval result. Always include eval_run_id on success stdout; write the token only to the secure 0600 file or optional stdout key. Never put the token in --output, redacted prepare/status, logs, or exception text. CVM encrypted_env injection is unchanged.
…pends it The handoff check depends only on argv, but ran after _obtain_eval_prepare_with_token had already consumed the single EVAL_RUN_TOKEN delivery. A miner who omitted the flags burnt the token and then hit the error, reproducing the unrecoverable state the handoff exists to prevent. Validate the destination before any remote mutation and cover it with a test asserting eval_prepare is never called.
Lock RED→GREEN contract for plan vs CLI shape mismatch and optional --expected-measurement rtmr0 pin check before Phala create.
…la create Name plan and CLI shapes plus vm_shape/instance_type/rtmr0, warn that a stale allowlist pin only surfaces as a generic key-release denial, and abort before spend. Optional --expected-measurement compares rtmr0 with truncated prefixes only.
Document guest emit-only design, token handoff flags, host scrape of BASE_BENCHMARK_RESULT, eval result posting, teardown, and the shape/pin footgun so miners can finish a run from the docs alone.
…-sizing feat(agent-challenge): CVM sizing, token handoff, and Phala eval deploy hardening
Rebase the safety-critical selfdeploy/staging work onto current main without the older 38-commit stack that conflicted with PR #49. - Fail-loud Phala CVM listing via GET /cvms/paginated + X-Phala-Version 2026-06-23; unknown envelopes raise instead of under-reporting as 0 - Loopback http:// only with SELFDEPLOY_ALLOW_INSECURE_LOOPBACK=1 - Staging compose/scripts/docs with owned-only teardown (never account-sweep) - AGENTS.md points operators at the local staging loop first Dropped changes main already covers (shape mismatch formatter, eval default tdx.xlarge, HTTP delete_cvm). Compose pre-artifact pin matching deferred: main's generator does not yet include artifact envs and cannot reproduce daf0 without a broader compose delta.
📝 WalkthroughWalkthroughAdds an isolated local Agent Challenge staging stack using real Phala TDX CVMs, strict paginated CVM parsing, owned-only teardown planning, loopback HTTP controls, pinned staging configuration, end-to-end review/evaluation orchestration, and production compose upgrade documentation. ChangesAgent Challenge staging
Estimated code review effort: 5 (Critical) | ~120 minutes Sequence Diagram(s)sequenceDiagram
participant Operator
participant run_staging.sh
participant AgentChallenge
participant PhalaCloud
participant KeyRelease
Operator->>run_staging.sh: start local staging run
run_staging.sh->>AgentChallenge: submit miner artifact
AgentChallenge->>PhalaCloud: deploy review CVM
run_staging.sh->>AgentChallenge: poll review outcome
run_staging.sh->>KeyRelease: start local KR service
AgentChallenge->>PhalaCloud: deploy evaluation CVM
PhalaCloud->>KeyRelease: request attested secret release
run_staging.sh->>AgentChallenge: poll guest_artifact_proof
run_staging.sh->>PhalaCloud: delete owned CVMs
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 13
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/challenges/agent-challenge/docs/prod-compose-upgrade.md (1)
1-504: 📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy liftReduce this shipping document to a minimal operational note.
This 500-line runbook embeds incident-specific hashes, submission state, host paths, timestamps, and validation evidence. Keep a short upgrade/rollback pointer in product docs and move measured evidence and change-window details to local untracked evidence or the ops change record.
As per coding guidelines: keep shipping documentation minimal and keep validation logs out of product documentation.🤖 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/docs/prod-compose-upgrade.md` around lines 1 - 504, Replace the oversized incident-specific runbook with a concise product-documentation note covering only the artifact-aware eval pin upgrade, required authorization, high-level upgrade/rollback pointers, and references to the relevant implementation and operational procedures. Remove measured hashes, submission state, host paths, timestamps, validation logs, and other change-window evidence; direct operators to record those details in local untracked evidence or the ops change record instead.Source: Coding guidelines
🧹 Nitpick comments (8)
packages/challenges/agent-challenge/docs/staging.md (1)
311-313: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick winPin the Phala CLI instead of executing
npx phala@latest.This command executes mutable, unreviewed package code and makes teardown verification non-reproducible. Use an approved pinned CLI version or repository-managed toolchain.
As per coding guidelines: treat shell commands and dependency changes as executable surfaces during review.🤖 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/docs/staging.md` around lines 311 - 313, Replace the teardown command that invokes `npx phala@latest` with an approved pinned Phala CLI version or the repository-managed toolchain. Ensure the command resolves a deterministic, reviewed CLI version rather than mutable latest package code.Source: Coding guidelines
packages/challenges/agent-challenge/src/agent_challenge/selfdeploy/cvm_list.py (2)
65-82: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueDeduplicate
_item_id/_extract_cvm_id.Both walk
_CREATE_CVM_ID_FIELDSidentically; only the miss behavior differs.♻️ Proposed dedupe
def _extract_cvm_id(item: Mapping[str, Any]) -> str: - for name in _CREATE_CVM_ID_FIELDS:- if name not in item:- continue- normalized = _normalize_id(item.get(name))- if normalized is not None:- return normalized- raise ValueError("Phala create response does not identify the CVM")+ found = _item_id(item)+ if found is None:+ raise ValueError("Phala create response does not identify the CVM")+ return found🤖 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/selfdeploy/cvm_list.py` around lines 65 - 82, Deduplicate the identical field-scanning logic in _item_id and _extract_cvm_id by reusing one helper or having one function delegate to the other. Preserve _item_id’s None result for missing or invalid identifiers, while keeping _extract_cvm_id’s ValueError behavior when no identifier is found.
235-239: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAmbiguity is signalled by an error message substring.
phala.resolve_cvm_id_from_listdistinguishes this case withif "multiple CVMs match app_id" in msg. A dedicated subclass (e.g.AmbiguousCvmMatchError(CvmListParseError)) would make that contract robust against wording changes.🤖 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/selfdeploy/cvm_list.py` around lines 235 - 239, Introduce a dedicated AmbiguousCvmMatchError subclass of CvmListParseError and raise it in the require_unique branch of the CVM resolution logic when multiple matches are found. Update phala.resolve_cvm_id_from_list to detect this exception type instead of searching the error message for “multiple CVMs match app_id”, preserving existing handling for other CvmListParseError cases.packages/challenges/agent-challenge/scripts/staging/cvm_teardown_policy.py (1)
17-39: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
parse_account_cvms_payloadraisesSystemExitfrom a library-style function.Callers that import this module (the tests do) get process-exit semantics instead of a catchable error. Prefer raising
ValueErrorhere and converting toSystemExitinmain.🤖 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/scripts/staging/cvm_teardown_policy.py` around lines 17 - 39, Update parse_account_cvms_payload to raise ValueError from CvmListParseError instead of SystemExit, preserving the original error message and exception chaining. In main, catch the parsing ValueError and convert it to SystemExit so library callers receive a catchable error while the CLI retains its exit behavior.packages/challenges/agent-challenge/tests/test_phala_cvms_list_parse.py (1)
48-72: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAll three branches of the second
elifassign the same value; the dispatch logic is effectively dead.Lines 61-70 collapse to
self._queue = list(payloads). Also note that a genuine bare-list response body (e.g.[{"id": "cvm_1"}]) is misinterpreted as a one-element queue, so the opener can never serve a list body — worth fixing before someone adds such a test.♻️ Proposed simplification
- if isinstance(payloads, list) and payloads and not isinstance(- payloads[0], dict- ):- # list of sequential response bodies- self._queue = list(payloads)- elif isinstance(payloads, list) and all(- isinstance(p, (dict, list)) for p in payloads- ):- # could be one list-body OR queue of bodies — treat multi as queue- # when first element looks like a full response object with items/total- if (- len(payloads) > 1- and isinstance(payloads[0], dict)- and ("items" in payloads[0] or "total" in payloads[0])- ):- self._queue = list(payloads)- elif len(payloads) == 1:- self._queue = list(payloads)- else:- self._queue = list(payloads)- else:- self._queue = [payloads]+ # ``payloads`` is either a queue of response bodies or a single body.+ self._queue = list(payloads) if isinstance(payloads, list) else [payloads]🤖 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_phala_cvms_list_parse.py` around lines 48 - 72, Fix _CapturingOpener payload classification: remove the redundant branches in the second elif and distinguish a bare list response body from a queue of response bodies, ensuring a payload such as [{"id": "cvm_1"}] is served as one body while genuine sequential bodies remain queued.packages/challenges/agent-challenge/src/agent_challenge/selfdeploy/phala.py (1)
329-347: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winUnreachable fallback branch.
combinedis parsed from a bare list, socombined.total == len(all_items)always. Thereforecombined.total != reported_totalimplieslen(all_items) != reported_total, and the innerifalways raises — lines 336-341 are dead. Collapse to a single check.♻️ Proposed simplification
combined = parse_cvms_list_response(list(all_items)) - if reported_total is not None and combined.total != reported_total:- if len(all_items) != reported_total:- raise CvmListParseError(- "unrecognized CVM list shape: collected items "- f"{len(all_items)} != total {reported_total}"- )- return CvmListSnapshot(- items=combined.items,- total=reported_total,- ids=combined.ids,- source_shape="paginated-merged",- )+ if reported_total is not None and len(all_items) != reported_total:+ raise CvmListParseError(+ "unrecognized CVM list shape: collected items "+ f"{len(all_items)} != total {reported_total}"+ ) return CvmListSnapshot( items=combined.items, total=reported_total if reported_total is not None else combined.total, ids=combined.ids, source_shape="paginated-merged", )🤖 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/selfdeploy/phala.py` around lines 329 - 347, In the paginated response merge logic, simplify the reported_total validation after parse_cvms_list_response(list(all_items)): remove the unreachable inner branch that returns a snapshot, and directly raise CvmListParseError when the merged item count differs from reported_total. Preserve the existing snapshot construction for valid totals, including its total and source_shape values.packages/challenges/agent-challenge/src/agent_challenge/selfdeploy/cli.py (1)
204-212: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winRe-serializing the snapshot into a listing dict re-runs shape validation unnecessarily.
resolve_cvm_id_from_snapshot(snapshot, app_id=identity, require_unique=True)avoids the round-trip through_total_from_mapping, which could reject an otherwise-valid merged snapshot on total/items consistency rules. Note the ambiguity case then raisesCvmListParseErrorrather thanPhalaApiError, so keep that translation in the caller.🤖 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/selfdeploy/cli.py` around lines 204 - 212, The teardown flow should resolve the CVM identity directly from the parsed snapshot instead of rebuilding a listing dict and invoking resolve_cvm_id_from_list. Replace that call with resolve_cvm_id_from_snapshot(snapshot, app_id=identity, require_unique=True), and preserve caller-level translation of ambiguity-related CvmListParseError into the appropriate RouteClientError or API error.packages/challenges/agent-challenge/tests/test_staging_cvm_teardown_policy.py (1)
13-28: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueDuplicated staging-policy module loader across two test files. Both modules define an identical
POLICY_PATHand_load_policy()to importscripts/staging/cvm_teardown_policy.py; a singleconftest.pyfixture would keep the path in one place.
packages/challenges/agent-challenge/tests/test_staging_cvm_teardown_policy.py#L13-L28: movePOLICY_PATHand_load_policyintotests/conftest.pyand keep only thepolicyfixture consumption here.packages/challenges/agent-challenge/tests/test_phala_cvms_list_parse.py#L32-L45: import the sharedPOLICY_PATH/policyfixture instead of redefining the loader.🤖 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_staging_cvm_teardown_policy.py` around lines 13 - 28, Centralize the duplicated cvm_teardown_policy loader by moving POLICY_PATH and _load_policy into tests/conftest.py and exposing the shared policy fixture. In packages/challenges/agent-challenge/tests/test_staging_cvm_teardown_policy.py#L13-L28, remove the local definitions and consume the fixture; in packages/challenges/agent-challenge/tests/test_phala_cvms_list_parse.py#L32-L45, likewise remove the duplicate loader and use the shared POLICY_PATH/policy fixture.
🤖 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 `@AGENTS.md`:
- Around line 96-98: The documentation must consistently define teardown as
owned-CVM-only. In AGENTS.md, replace account-wide zero-count language with
verification that the run-owned CVM count reaches zero, while preserving
paginated verification. In packages/challenges/agent-challenge/docs/staging.md,
update the --down documentation to state that it removes only owned CVMs and
never sweeps account-wide or foreign/production CVMs.
In `@packages/challenges/agent-challenge/docker-compose.staging.yml`:
- Around line 31-32: Remove the unconditional dcap-qvl host bind from the base
staging Compose configuration so the baked /usr/local/bin/dcap-qvl binary
remains available on hosts without the source path. If host overrides are
needed, place this read-only bind in a separate opt-in staging override file
such as docker-compose.staging.dcap.yml.
In
`@packages/challenges/agent-challenge/scripts/staging/config/challenge.env.example`:
- Line 17: Replace the hardcoded value for CHALLENGE_EVAL_KEY_RELEASE_ENDPOINT
with a clearly marked placeholder for the staging host’s externally reachable
IP, preserving port 8701, and add an adjacent comment documenting that operators
must set it to their staging host address.
In
`@packages/challenges/agent-challenge/scripts/staging/config/kr.example/README.md`:
- Around line 9-14: Replace the server-certificate placeholder in the KR staging
README with tested OpenSSL commands that issue a KR server certificate signed by
the generated CA and include the required Subject Alternative Name. Document the
exact generated server key and certificate paths expected by staging, while
preserving the existing CA and golden-key setup.
In
`@packages/challenges/agent-challenge/scripts/staging/config/measurements_source.md`:
- Around line 7-9: Remove the absolute workspace and production-host evidence
paths from the documentation list in measurements_source.md. Retain only
portable provenance such as dates, quote identifiers, and hashes, ensuring the
shipped markdown remains minimal and does not include validation-log locations.
In `@packages/challenges/agent-challenge/scripts/staging/cvm_teardown_policy.py`:
- Around line 250-254: Update the --dry-run argument definition and its handling
in the staging teardown entry point to clearly document that it is accepted for
compatibility and does not change behavior, or remove the flag entirely. Ensure
the help text and surrounding logic do not imply that omitting the flag performs
deletion, and eliminate the no-op unused-variable handling if the flag is
removed.
- Around line 144-162: Propagate the unresolved ids from resolve_delete_ids
through select_teardown_ids and plan_teardown as unresolved_owned, and include
that field in the generated plan output so unverified tracked tokens are
distinguishable from confirmed account ids. Update all callers, including tests
currently using two-value unpacking, to handle the expanded return value while
preserving existing deletion selection.
In `@packages/challenges/agent-challenge/scripts/staging/run_staging.sh`:
- Around line 660-665: Replace the predictable /tmp/hosts.staging.$$ staging in
run_staging.sh lines 660-665 with a mktemp-created file, remove the dead awk
write, and continue filtering and copying the result to /etc/hosts with cleanup.
Apply the same mktemp-based staging fix to the teardown unpin path at lines
506-514; both sites require direct changes using the generated temporary
filename consistently.
- Line 557: Replace the world-readable permissions in run_staging.sh at
packages/challenges/agent-challenge/scripts/staging/run_staging.sh#L557-L557 for
challenge_token and review_evidence_encryption_key with group-scoped read access
shared with container uid 10001, using chgrp or equivalent ownership setup. Also
update
packages/challenges/agent-challenge/scripts/staging/run_staging.sh#L475-L483 to
replace world read/write access on the staging SQLite database and world
read/write/execute access on its volume directory with group-scoped permissions
shared with the KR process; preserve required functionality without exposing
sensitive materials to other local users.
- Around line 370-409: Move the OWNED_CVMS_FILE pruning block out of the
pre-verification path in the teardown function. Compute owned_left from the
original tracking files and final CVM listing first, return failure when any
owned CVM remains, then prune only targeted IDs confirmed absent from the final
listing after the check succeeds; preserve the existing best-effort handling for
the cleanup write.
- Around line 205-214: Remove direct shell interpolation of remote IDs and file
paths into Python source across the identified unquoted heredocs. Pass each
value through environment variables or another data channel, then read it from
os.environ inside the corresponding Python blocks; update the DELETE request
block around cid and the other heredocs at the referenced staging-script
sections consistently.
- Around line 826-841: Remove the eval-based assignment around the Python JSON
parsing block in the staging result handling. Safely transfer reason_code and
retryable from the parsed response using a file or separate command
substitutions with robust shell-safe handling, preserving the existing defaults
for malformed responses and missing items without allowing API-controlled
reason_code content to execute as shell syntax.
In `@packages/challenges/agent-challenge/src/agent_challenge/selfdeploy/phala.py`:
- Around line 305-324: Update the paginated CVM aggregation loop around
parse_cvms_list_response to track each CVM’s unique ID and raise
CvmListParseError when an ID repeats across pages, before extending all_items.
Preserve normal pagination and total-consistency validation, failing closed
instead of returning duplicate rows when the server repeats a page.
---
Outside diff comments:
In `@packages/challenges/agent-challenge/docs/prod-compose-upgrade.md`:
- Around line 1-504: Replace the oversized incident-specific runbook with a
concise product-documentation note covering only the artifact-aware eval pin
upgrade, required authorization, high-level upgrade/rollback pointers, and
references to the relevant implementation and operational procedures. Remove
measured hashes, submission state, host paths, timestamps, validation logs, and
other change-window evidence; direct operators to record those details in local
untracked evidence or the ops change record instead.
---
Nitpick comments:
In `@packages/challenges/agent-challenge/docs/staging.md`:
- Around line 311-313: Replace the teardown command that invokes `npx
phala@latest` with an approved pinned Phala CLI version or the
repository-managed toolchain. Ensure the command resolves a deterministic,
reviewed CLI version rather than mutable latest package code.
In `@packages/challenges/agent-challenge/scripts/staging/cvm_teardown_policy.py`:
- Around line 17-39: Update parse_account_cvms_payload to raise ValueError from
CvmListParseError instead of SystemExit, preserving the original error message
and exception chaining. In main, catch the parsing ValueError and convert it to
SystemExit so library callers receive a catchable error while the CLI retains
its exit behavior.
In `@packages/challenges/agent-challenge/src/agent_challenge/selfdeploy/cli.py`:
- Around line 204-212: The teardown flow should resolve the CVM identity
directly from the parsed snapshot instead of rebuilding a listing dict and
invoking resolve_cvm_id_from_list. Replace that call with
resolve_cvm_id_from_snapshot(snapshot, app_id=identity, require_unique=True),
and preserve caller-level translation of ambiguity-related CvmListParseError
into the appropriate RouteClientError or API error.
In
`@packages/challenges/agent-challenge/src/agent_challenge/selfdeploy/cvm_list.py`:
- Around line 65-82: Deduplicate the identical field-scanning logic in _item_id
and _extract_cvm_id by reusing one helper or having one function delegate to the
other. Preserve _item_id’s None result for missing or invalid identifiers, while
keeping _extract_cvm_id’s ValueError behavior when no identifier is found.
- Around line 235-239: Introduce a dedicated AmbiguousCvmMatchError subclass of
CvmListParseError and raise it in the require_unique branch of the CVM
resolution logic when multiple matches are found. Update
phala.resolve_cvm_id_from_list to detect this exception type instead of
searching the error message for “multiple CVMs match app_id”, preserving
existing handling for other CvmListParseError cases.
In `@packages/challenges/agent-challenge/src/agent_challenge/selfdeploy/phala.py`:
- Around line 329-347: In the paginated response merge logic, simplify the
reported_total validation after parse_cvms_list_response(list(all_items)):
remove the unreachable inner branch that returns a snapshot, and directly raise
CvmListParseError when the merged item count differs from reported_total.
Preserve the existing snapshot construction for valid totals, including its
total and source_shape values.
In `@packages/challenges/agent-challenge/tests/test_phala_cvms_list_parse.py`:
- Around line 48-72: Fix _CapturingOpener payload classification: remove the
redundant branches in the second elif and distinguish a bare list response body
from a queue of response bodies, ensuring a payload such as [{"id": "cvm_1"}] is
served as one body while genuine sequential bodies remain queued.
In
`@packages/challenges/agent-challenge/tests/test_staging_cvm_teardown_policy.py`:
- Around line 13-28: Centralize the duplicated cvm_teardown_policy loader by
moving POLICY_PATH and _load_policy into tests/conftest.py and exposing the
shared policy fixture. In
packages/challenges/agent-challenge/tests/test_staging_cvm_teardown_policy.py#L13-L28,
remove the local definitions and consume the fixture; in
packages/challenges/agent-challenge/tests/test_phala_cvms_list_parse.py#L32-L45,
likewise remove the duplicate loader and use the shared POLICY_PATH/policy
fixture.
🪄 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: 5dc68352-0804-4be8-86b7-594bbffff99f
📒 Files selected for processing (27)
AGENTS.mdpackages/challenges/agent-challenge/docker-compose.staging.ymlpackages/challenges/agent-challenge/docs/README.mdpackages/challenges/agent-challenge/docs/prod-compose-upgrade.mdpackages/challenges/agent-challenge/docs/staging.mdpackages/challenges/agent-challenge/scripts/staging/.gitignorepackages/challenges/agent-challenge/scripts/staging/config/challenge.env.examplepackages/challenges/agent-challenge/scripts/staging/config/challenge_token.examplepackages/challenges/agent-challenge/scripts/staging/config/dstack-client-trust.crt.examplepackages/challenges/agent-challenge/scripts/staging/config/eval_allowlist.jsonpackages/challenges/agent-challenge/scripts/staging/config/kr.example/README.mdpackages/challenges/agent-challenge/scripts/staging/config/kr_allowlist.jsonpackages/challenges/agent-challenge/scripts/staging/config/measurements_source.mdpackages/challenges/agent-challenge/scripts/staging/config/pins.jsonpackages/challenges/agent-challenge/scripts/staging/config/review_allowlist.jsonpackages/challenges/agent-challenge/scripts/staging/config/review_evidence_encryption_key.examplepackages/challenges/agent-challenge/scripts/staging/cvm_teardown_policy.pypackages/challenges/agent-challenge/scripts/staging/run_staging.shpackages/challenges/agent-challenge/src/agent_challenge/selfdeploy/cli.pypackages/challenges/agent-challenge/src/agent_challenge/selfdeploy/client.pypackages/challenges/agent-challenge/src/agent_challenge/selfdeploy/cvm_list.pypackages/challenges/agent-challenge/src/agent_challenge/selfdeploy/phala.pypackages/challenges/agent-challenge/tests/test_phala_create_ack_and_cli_token.pypackages/challenges/agent-challenge/tests/test_phala_cvms_list_parse.pypackages/challenges/agent-challenge/tests/test_selfdeploy_loopback_http_policy.pypackages/challenges/agent-challenge/tests/test_selfdeploy_teardown_http.pypackages/challenges/agent-challenge/tests/test_staging_cvm_teardown_policy.py
| - CVMs are **real** Phala TDX machines (billable). Staging tears down **only CVMs this run owns** (`work/owned_cvms.txt` + per-run track). It never account-sweeps foreign/prod CVMs. Always tear down owned CVMs before you leave. | ||
| Real Phala TDX CVMs + dual attestation flags; always tear down to a verified CVM count of 0 via paginated list (never trust bare `GET /cvms` empty arrays). |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Preserve the owned-CVM teardown boundary consistently.
The documentation mixes owned-only cleanup with account-wide zero-count and sweep language, which could lead to deletion of foreign or production CVMs.
AGENTS.md#L96-L98: verify that only the run-owned CVM count reaches zero; do not require an account-wide zero.packages/challenges/agent-challenge/docs/staging.md#L102-L102: state that--downremoves only owned CVMs and does not sweep the account.
📍 Affects 2 files
AGENTS.md#L96-L98(this comment)packages/challenges/agent-challenge/docs/staging.md#L102-L102
🤖 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 `@AGENTS.md` around lines 96 - 98, The documentation must consistently define
teardown as owned-CVM-only. In AGENTS.md, replace account-wide zero-count
language with verification that the run-owned CVM count reaches zero, while
preserving paginated verification. In
packages/challenges/agent-challenge/docs/staging.md, update the --down
documentation to state that it removes only owned CVMs and never sweeps
account-wide or foreign/production CVMs.
| # dcap-qvl is baked into the runtime image; host bind is optional fallback | ||
| - /root/.cargo/bin/dcap-qvl:/usr/local/bin/dcap-qvl:ro |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Unconditional host bind of dcap-qvl will shadow the baked binary on hosts lacking /root/.cargo/bin/dcap-qvl.
Docker creates an empty directory at a missing bind source, then mounts it over /usr/local/bin/dcap-qvl, turning the baked binary into a directory and failing attestation startup. A "fallback" bind can't be conditional in a compose file — move it to an opt-in override (e.g. docker-compose.staging.dcap.yml) instead.
🔧 Proposed change
- # dcap-qvl is baked into the runtime image; host bind is optional fallback- - /root/.cargo/bin/dcap-qvl:/usr/local/bin/dcap-qvl:ro+ # dcap-qvl is baked into the runtime image. If you need a host override,+ # add it via an extra `-f` override file rather than here — a missing+ # source path would be auto-created as a directory and shadow the binary.📝 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.
| # dcap-qvl is baked into the runtime image; host bind is optional fallback | |
| - /root/.cargo/bin/dcap-qvl:/usr/local/bin/dcap-qvl:ro | |
| # dcap-qvl is baked into the runtime image. If you need a host override, | |
| # add it via an extra `-f` override file rather than here — a missing | |
| # source path would be auto-created as a directory and shadow the binary. |
🤖 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/docker-compose.staging.yml` around lines
31 - 32, Remove the unconditional dcap-qvl host bind from the base staging
Compose configuration so the baked /usr/local/bin/dcap-qvl binary remains
available on hosts without the source path. If host overrides are needed, place
this read-only bind in a separate opt-in staging override file such as
docker-compose.staging.dcap.yml.
| CHALLENGE_EVAL_APP_KMS_PUBLIC_KEY_HEX=8820793b3116a96b7c8c7daf06b104b14cbf9ee5dd3fb65d8ad53b50cefc7809 | ||
| CHALLENGE_EVAL_APP_MEASUREMENT={"mrtd":"f06dfda6dce1cf904d4e2bab1dc370634cf95cefa2ceb2de2eee127c9382698090d7a4a13e14c536ec6c9c3c8fa87077","rtmr0":"68102e7b524af310f7b7d426ce75481e36c40f5d513a9009c046e9d37e31551f0134d954b496a3357fd61d03f07ffe96","rtmr1":"07e6f51aa763abfe75c3ddfbf4f425fe3f0ceff66d807a75e049303dce9addf68e7218729bd419638af63a370f65878c","rtmr2":"df67e467e60edc1737bcf8e682d48131bfb427f523226aa7f197a7608e9b3784783fa759ef5b28191fa12f9ddb36b858","os_image_hash":"5c6d8f757e3adb0563efc809710076a631442db3b4de02ad32d33fe1994721e0","key_provider":"phala","vm_shape":"tdx.small"} | ||
| CHALLENGE_EVAL_APP_MEASUREMENT_ALLOWLIST=[{"mrtd":"f06dfda6dce1cf904d4e2bab1dc370634cf95cefa2ceb2de2eee127c9382698090d7a4a13e14c536ec6c9c3c8fa87077","rtmr0":"68102e7b524af310f7b7d426ce75481e36c40f5d513a9009c046e9d37e31551f0134d954b496a3357fd61d03f07ffe96","rtmr1":"07e6f51aa763abfe75c3ddfbf4f425fe3f0ceff66d807a75e049303dce9addf68e7218729bd419638af63a370f65878c","rtmr2":"df67e467e60edc1737bcf8e682d48131bfb427f523226aa7f197a7608e9b3784783fa759ef5b28191fa12f9ddb36b858","os_image_hash":"5c6d8f757e3adb0563efc809710076a631442db3b4de02ad32d33fe1994721e0","compose_hash":"0647b4d9b1e3d458b7910638ee187c968835840fe2e65f1f332dbe69c518dfd9"}] | ||
| CHALLENGE_EVAL_KEY_RELEASE_ENDPOINT=84.32.70.61:8701 |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Replace the hardcoded host IP with a placeholder.
84.32.70.61:8701 is an operator-specific address baked into a committed template; copying this file verbatim points guest key-release traffic at a foreign host. Use a placeholder and document that it must be the staging host's externally reachable IP.
🔧 Proposed change
-CHALLENGE_EVAL_KEY_RELEASE_ENDPOINT=84.32.70.61:8701+# Externally reachable address of the host running the staging key-release server.+CHALLENGE_EVAL_KEY_RELEASE_ENDPOINT=REPLACE_WITH_STAGING_HOST_IP:8701📝 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.
| CHALLENGE_EVAL_KEY_RELEASE_ENDPOINT=84.32.70.61:8701 | |
| # Externally reachable address of the host running the staging key-release server. | |
| CHALLENGE_EVAL_KEY_RELEASE_ENDPOINT=REPLACE_WITH_STAGING_HOST_IP:8701 |
🤖 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/scripts/staging/config/challenge.env.example`
at line 17, Replace the hardcoded value for CHALLENGE_EVAL_KEY_RELEASE_ENDPOINT
with a clearly marked placeholder for the staging host’s externally reachable
IP, preserving port 8701, and add an adjacent comment documenting that operators
must set it to their staging host address.
| openssl req -x509 -newkey rsa:2048 -nodes \ | ||
| -keyout scripts/staging/config/kr/ca.key \ | ||
| -out scripts/staging/config/kr/ca.crt \ | ||
| -days 365 -subj "/CN=ac-staging-kr-ca" | ||
| # ... issue server cert for your KR host; copy ca.crt to config/kr-server-ca.crt | ||
| openssl rand -out scripts/staging/config/kr/golden.key 32 |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Document the complete KR server-certificate setup.
The command only creates the CA; the # ... issue server cert placeholder leaves the staging setup incomplete. Add tested commands that issue the KR server certificate with the required SAN and document the exact key/certificate paths consumed by staging.
🤖 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/scripts/staging/config/kr.example/README.md`
around lines 9 - 14, Replace the server-certificate placeholder in the KR
staging README with tested OpenSSL commands that issue a KR server certificate
signed by the generated CA and include the required Subject Alternative Name.
Document the exact generated server key and certificate paths expected by
staging, while preserving the existing CA and golden-key setup.
| - Review TEE evidence: `/work/baseintelligence/.omo/evidence/ac-attested-review-20260727/review-tee.json` | ||
| - T8 eval KR allowlist dumps: `/work/baseintelligence/.omo/start-work/T8-e2e/kr-meas-diag-20260725T233258Z.txt` | ||
| - Prod KR file (same core): host `/var/lib/base/keyrelease/eval-allowlist.json` on the prod master |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Remove internal evidence paths from shipped documentation.
The absolute workspace and production-host paths are environment-specific and turn this file into a validation log. Keep portable provenance such as dates, quote identifiers, and hashes, or store raw evidence locations outside the repository.
As per coding guidelines: **/*.md must keep shipping documentation minimal and must not add validation logs to product documentation.
🤖 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/scripts/staging/config/measurements_source.md`
around lines 7 - 9, Remove the absolute workspace and production-host evidence
paths from the documentation list in measurements_source.md. Retain only
portable provenance such as dates, quote identifiers, and hashes, ensuring the
shipped markdown remains minimal and does not include validation-log locations.
Source: Coding guidelines
| # Drop successfully targeted ids from durable owned list (best-effort). | ||
| if [[ -f "${OWNED_CVMS_FILE}" && -n "${will_delete// /}" ]]; then | ||
| local tmp_owned keep d | ||
| tmp_owned="$(mktemp)" | ||
| while read -r id; do | ||
| [[ -n "$id" ]] || continue | ||
| keep=1 | ||
| for d in ${will_delete}; do [[ "$id" == "$d" ]] && keep=0 && break; done | ||
| [[ "$keep" == "1" ]] && echo "$id" | ||
| done <"${OWNED_CVMS_FILE}" >"${tmp_owned}" || true | ||
| mv -f "${tmp_owned}" "${OWNED_CVMS_FILE}" | ||
| fi | ||
| if ! listing="$(phala_get_cvms)"; then | ||
| log "FATAL: post-teardown CVM list failed — count indeterminate (not success)" | ||
| echo '{"count":-1,"ids":[],"indeterminate":true}' | tee "${RUN_DIR}/cvms-final.json" | ||
| return 1 | ||
| fi | ||
| echo "${listing}" | tee "${RUN_DIR}/cvms-final.json" | ||
| local owned_left cnt indeterminate | ||
| cnt="$(python3 -c "import json;print(json.load(open('${RUN_DIR}/cvms-final.json')).get('count',-1))")" | ||
| indeterminate="$(python3 -c "import json;print(bool(json.load(open('${RUN_DIR}/cvms-final.json')).get('indeterminate')))")" | ||
| if [[ "${cnt}" == "-1" || "${indeterminate}" == "True" ]]; then | ||
| log "FATAL: CVM count indeterminate after teardown (count=${cnt})" | ||
| return 1 | ||
| fi | ||
| owned_left="$(python3 -c " | ||
| import json | ||
| from pathlib import Path | ||
| final=set(json.load(open('${RUN_DIR}/cvms-final.json')).get('ids') or []) | ||
| owned=set() | ||
| for p in ('${CVM_TRACK}','${OWNED_CVMS_FILE}'): | ||
| path=Path(p) | ||
| if path.is_file(): | ||
| owned |= {ln.strip() for ln in path.read_text().splitlines() if ln.strip() and not ln.strip().startswith('#')} | ||
| print(' '.join(sorted(owned & final))) | ||
| ")" | ||
| if [[ -n "${owned_left// /}" ]]; then | ||
| log "WARNING: owned CVMs still present after teardown: ${owned_left} (account count=${cnt})" | ||
| return 1 | ||
| fi |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Pruning the owned list before verification defeats the leftover check.
Lines 371-381 drop every targeted id from OWNED_CVMS_FILE, but the owned_left check at 395-405 re-reads that same file. Any CVM whose delete failed (the || true at line 367 swallows it) has already been removed from the owned set, so owned_left is empty and the run reports "teardown OK" while a billable CVM is still alive. Prune only after computing owned_left, and only for ids absent from the final listing.
🔧 Proposed reordering
- # Drop successfully targeted ids from durable owned list (best-effort).- if [[ -f "${OWNED_CVMS_FILE}" && -n "${will_delete// /}" ]]; then- local tmp_owned keep d- tmp_owned="$(mktemp)"- while read -r id; do- [[ -n "$id" ]] || continue- keep=1- for d in ${will_delete}; do [[ "$id" == "$d" ]] && keep=0 && break; done- [[ "$keep" == "1" ]] && echo "$id"- done <"${OWNED_CVMS_FILE}" >"${tmp_owned}" || true- mv -f "${tmp_owned}" "${OWNED_CVMS_FILE}"- fi
if ! listing="$(phala_get_cvms)"; then…then, after owned_left is computed and found empty, drop the targeted ids that are confirmed gone from the final listing.
🤖 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/scripts/staging/run_staging.sh` around
lines 370 - 409, Move the OWNED_CVMS_FILE pruning block out of the
pre-verification path in the teardown function. Compute owned_left from the
original tracking files and final CVM listing first, return failure when any
owned CVM remains, then prune only targeted IDs confirmed absent from the final
listing after the check succeeds; preserve the existing best-effort handling for
the cleanup write.
| log "miner zip ok hash=${zip_hash}" | ||
| if [[ "${SKIP_BUILD}" != "1" ]]; then log "building runtime image"; ${COMPOSE} build agent-challenge; fi | ||
| chmod a+r "${CONFIG_DIR}/challenge_token" "${CONFIG_DIR}/review_evidence_encryption_key" 2>/dev/null || true |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Permissions are widened to other where group/uid ownership would suffice. Both sites bridge the host↔container uid mismatch by granting world access to sensitive material, exposing it to every local user. Use chgrp plus group bits, or chown to the container uid (10001), in both places.
packages/challenges/agent-challenge/scripts/staging/run_staging.sh#L557-L557: replacechmod a+ronchallenge_tokenandreview_evidence_encryption_keywith group-scoped read for the container uid.packages/challenges/agent-challenge/scripts/staging/run_staging.sh#L475-L483: replacechmod a+rwon the staging SQLite DB andchmod a+rwxon its volume directory with group-scoped access shared with the KR process.
As per coding guidelines: "Never expose private keys, wallet mnemonics, API tokens, or full secret values in logs, documentation, or evidence."
📍 Affects 1 file
packages/challenges/agent-challenge/scripts/staging/run_staging.sh#L557-L557(this comment)packages/challenges/agent-challenge/scripts/staging/run_staging.sh#L475-L483
🤖 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/scripts/staging/run_staging.sh` at line
557, Replace the world-readable permissions in run_staging.sh at
packages/challenges/agent-challenge/scripts/staging/run_staging.sh#L557-L557 for
challenge_token and review_evidence_encryption_key with group-scoped read access
shared with container uid 10001, using chgrp or equivalent ownership setup. Also
update
packages/challenges/agent-challenge/scripts/staging/run_staging.sh#L475-L483 to
replace world read/write access on the staging SQLite database and world
read/write/execute access on its volume directory with group-scoped permissions
shared with the KR process; preserve required functionality without exposing
sensitive materials to other local users.
Sources: Coding guidelines, Linters/SAST tools
| awk -v h="${PUB_HOST}" 'index($0, h)==0 || $0 !~ (h "$") {print}' /etc/hosts > /tmp/hosts.staging.$$ || true | ||
| # Safer: drop lines ending with the hostname | ||
| grep -v -E "[[:space:]]${PUB_HOST}$" /etc/hosts > /tmp/hosts.staging.$$ || cp /etc/hosts /tmp/hosts.staging.$$ || true | ||
| echo "${PUB_IP} ${PUB_HOST}" >> /tmp/hosts.staging.$$ | ||
| cp -f /tmp/hosts.staging.$$ /etc/hosts | ||
| rm -f /tmp/hosts.staging.$$ |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Insecure predictable temp file used to rewrite /etc/hosts. Both /etc/hosts mutation paths stage the new file at /tmp/hosts.staging.$$, a guessable name in a world-writable directory, then copy it over /etc/hosts as root — a symlink/pre-creation race. One mktemp-based helper fixes both.
packages/challenges/agent-challenge/scripts/staging/run_staging.sh#L660-L665: drop the deadawkwrite on line 660 and stage the filtered hosts file viatmp="$(mktemp)"before copying to/etc/hosts.packages/challenges/agent-challenge/scripts/staging/run_staging.sh#L506-L514: replace/tmp/hosts.staging.$$in the teardown unpin path with the samemktemp-created file.
🧰 Tools
🪛 ast-grep (0.44.1)
[warning] 661-661: Building a temp file path in a world-writable directory from the PID ($$) or `` is predictable and racy: an attacker can pre-create or guess the name and win a symlink/race attack. Use mktemp (e.g. `f=$(mktemp)` or `f=$(mktemp /tmp/myapp.XXXXXX)`) so the kernel atomically creates a unique, unpredictable file.
Context: /tmp/hosts.staging.$$
Note: [CWE-377] Insecure Temporary File.
(tmp-file-pid-name-bash)
[warning] 661-661: Building a temp file path in a world-writable directory from the PID ($$) or `` is predictable and racy: an attacker can pre-create or guess the name and win a symlink/race attack. Use mktemp (e.g. `f=$(mktemp)` or `f=$(mktemp /tmp/myapp.XXXXXX)`) so the kernel atomically creates a unique, unpredictable file.
Context: /tmp/hosts.staging.$$
Note: [CWE-377] Insecure Temporary File.
(tmp-file-pid-name-bash)
[warning] 662-662: Building a temp file path in a world-writable directory from the PID ($$) or `` is predictable and racy: an attacker can pre-create or guess the name and win a symlink/race attack. Use mktemp (e.g. `f=$(mktemp)` or `f=$(mktemp /tmp/myapp.XXXXXX)`) so the kernel atomically creates a unique, unpredictable file.
Context: /tmp/hosts.staging.$$
Note: [CWE-377] Insecure Temporary File.
(tmp-file-pid-name-bash)
[warning] 663-663: Building a temp file path in a world-writable directory from the PID ($$) or `` is predictable and racy: an attacker can pre-create or guess the name and win a symlink/race attack. Use mktemp (e.g. `f=$(mktemp)` or `f=$(mktemp /tmp/myapp.XXXXXX)`) so the kernel atomically creates a unique, unpredictable file.
Context: /tmp/hosts.staging.$$
Note: [CWE-377] Insecure Temporary File.
(tmp-file-pid-name-bash)
📍 Affects 1 file
packages/challenges/agent-challenge/scripts/staging/run_staging.sh#L660-L665(this comment)packages/challenges/agent-challenge/scripts/staging/run_staging.sh#L506-L514
🤖 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/scripts/staging/run_staging.sh` around
lines 660 - 665, Replace the predictable /tmp/hosts.staging.$$ staging in
run_staging.sh lines 660-665 with a mktemp-created file, remove the dead awk
write, and continue filtering and copying the result to /etc/hosts with cleanup.
Apply the same mktemp-based staging fix to the teardown unpin path at lines
506-514; both sites require direct changes using the generated temporary
filename consistently.
Source: Linters/SAST tools
| eval "$(python3 - <<PY | ||
| import json | ||
| from pathlib import Path | ||
| p=Path("${RUN_DIR}/review-result-a${attempt}-${i}.json") | ||
| try: | ||
| o=json.loads(p.read_text()) | ||
| except Exception: | ||
| o={} | ||
| items=o.get("items") if isinstance(o,dict) else None | ||
| it=items[0] if isinstance(items,list) and items else {} | ||
| rc=str(it.get("reason_code") or "") if isinstance(it,dict) else "" | ||
| rt=it.get("retryable") if isinstance(it,dict) else False | ||
| print(f'reason_code={rc!r}') | ||
| print(f'retryable={"true" if rt else "false"!r}') | ||
| PY | ||
| )" |
There was a problem hiding this comment.
🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win
eval on server-controlled JSON is a command-injection sink.
reason_code comes from the review API response. Python's !r switches to double quotes when the value contains a single quote, so a crafted reason_code breaks out of the shell quoting and executes arbitrary commands in a script that holds the Phala API key and (in this environment) root. Emit the values to a file and read them, or parse with python3 -c into separate command substitutions.
🔧 Proposed change
- eval "$(python3 - <<PY-import json-from pathlib import Path-p=Path("${RUN_DIR}/review-result-a${attempt}-${i}.json")-try:- o=json.loads(p.read_text())-except Exception:- o={}-items=o.get("items") if isinstance(o,dict) else None-it=items[0] if isinstance(items,list) and items else {}-rc=str(it.get("reason_code") or "") if isinstance(it,dict) else ""-rt=it.get("retryable") if isinstance(it,dict) else False-print(f'reason_code={rc!r}')-print(f'retryable={"true" if rt else "false"!r}')-PY-)"+ _hist_json="${RUN_DIR}/review-result-a${attempt}-${i}.json" \+ read -r reason_code retryable < <(HIST_JSON="${RUN_DIR}/review-result-a${attempt}-${i}.json" python3 - <<'PY'+import json, os, re+from pathlib import Path+try:+ o=json.loads(Path(os.environ["HIST_JSON"]).read_text())+except Exception:+ o={}+items=o.get("items") if isinstance(o,dict) else None+it=items[0] if isinstance(items,list) and items else {}+rc=str(it.get("reason_code") or "") if isinstance(it,dict) else ""+rc=re.sub(r"[^A-Za-z0-9_.:-]", "_", rc) or "unknown"+rt=it.get("retryable") if isinstance(it,dict) else False+print(rc, "true" if rt else "false")+PY+)📝 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.
| eval"$(python3 - <<PY | |
| import json | |
| from pathlib import Path | |
| p=Path("${RUN_DIR}/review-result-a${attempt}-${i}.json") | |
| try: | |
| o=json.loads(p.read_text()) | |
| except Exception: | |
| o={} | |
| items=o.get("items") if isinstance(o,dict) else None | |
| it=items[0] if isinstance(items,list) and items else {} | |
| rc=str(it.get("reason_code") or "") if isinstance(it,dict) else"" | |
| rt=it.get("retryable") if isinstance(it,dict) else False | |
| print(f'reason_code={rc!r}') | |
| print(f'retryable={"true" if rt else "false"!r}') | |
| PY | |
| )" | |
| _hist_json="${RUN_DIR}/review-result-a${attempt}-${i}.json" \ | |
| read -r reason_code retryable <<(HIST_JSON="${RUN_DIR}/review-result-a${attempt}-${i}.json" python3 - <<'PY' | |
| import json, os, re | |
| from pathlib import Path | |
| try: | |
| o=json.loads(Path(os.environ["HIST_JSON"]).read_text()) | |
| except Exception: | |
| o={} | |
| items=o.get("items") if isinstance(o,dict) else None | |
| it=items[0] if isinstance(items,list) and items else {} | |
| rc=str(it.get("reason_code") or "") if isinstance(it,dict) else"" | |
| rc=re.sub(r"[^A-Za-z0-9_.:-]", "_", rc) or "unknown" | |
| rt=it.get("retryable") if isinstance(it,dict) else False | |
| print(rc, "true"if rt else"false") | |
| PY | |
| ) |
🤖 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/scripts/staging/run_staging.sh` around
lines 826 - 841, Remove the eval-based assignment around the Python JSON parsing
block in the staging result handling. Safely transfer reason_code and retryable
from the parsed response using a file or separate command substitutions with
robust shell-safe handling, preserving the existing defaults for malformed
responses and missing items without allowing API-controlled reason_code content
to execute as shell syntax.
Source: Linters/SAST tools
| while True: | ||
| path = f"/cvms/paginated?page={page}&page_size={page_size}" | ||
| raw = self.get(path) | ||
| snap = parse_cvms_list_response(raw) | ||
| if reported_total is None: | ||
| reported_total = snap.total | ||
| elif snap.total != reported_total: | ||
| raise CvmListParseError( | ||
| "unrecognized CVM list shape: total changed across pages " | ||
| f"({reported_total} -> {snap.total})" | ||
| ) | ||
| all_items.extend(snap.items) | ||
| if reported_total <= len(all_items): | ||
| break | ||
| if not snap.items: | ||
| raise CvmListParseError( | ||
| "unrecognized CVM list shape: empty page before total reached " | ||
| f"(have={len(all_items)} total={reported_total})" | ||
| ) | ||
| page += 1 |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Pages are merged without identity checks; a server that ignores page yields duplicates.
If the endpoint returns page 1 repeatedly, all_items accumulates duplicate rows until len(all_items) >= reported_total, and the merged snapshot silently reports the same CVM twice — bad input for owned-only teardown planning. Consider tracking seen CVM ids and failing closed on repeats (or asserting the echoed page field matches the requested page).
🤖 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/selfdeploy/phala.py`
around lines 305 - 324, Update the paginated CVM aggregation loop around
parse_cvms_list_response to track each CVM’s unique ID and raise
CvmListParseError when an ID repeats across pages, before extending all_items.
Preserve normal pagination and total-consistency validation, failing closed
instead of returning duplicate rows when the server repeats a page.
Summary
main(post-feat(agent-challenge): CVM sizing, token handoff, and Phala eval deploy hardening #49). Only still-needed selfdeploy/staging safety work; no prism/constation/Lium/master/DB stack.Production safety defects addressed
work/owned_cvms.txt+ per-run track), logs the delete plan first, and never account-sweeps.GET /api/v1/cvmscan return[]while inventory is non-empty. Authority isGET /cvms/paginatedwithX-Phala-Version: 2026-06-23. Unknown envelopes raise; indeterminate count is teardown FAILURE (never success with total=0).Changes
cvm_list.py+PhalaCloudClient.list_cvmsvia/cvms/paginated)http://only withSELFDEPLOY_ALLOW_INSECURE_LOOPBACK=1for 127.0.0.1/localhost/::1scripts/staging/(secrets gitignored;.exampleonly) +docs/staging.md+docs/prod-compose-upgrade.mdExplicitly dropped (main already covers / not portable)
format_eval_shape_mismatch_errortdx.xlargedefaultX-Phala-Versionheader presence2026-01-21; this PR pins CLI2026-06-23+ paginated listdaf0f209…without broader compose deltauv.lockStaging / execution proof honesty
daf0f209…was measured withoutCHALLENGE_PHALA_EVAL_ARTIFACT_{URL,TOKEN}inallowed_envs. Proven on submission 13 whose deployencrypted_env_namescontained neither — a matchingguest_artifact_proofis structurally impossible on that pin.3a81feaf…(current image + artifact envs only).Test plan
origin/main(2bfa110c): 3221 passed, 62 failed, 6 errors, 8 skipped (env/docker/keyrelease pre-existing)test_phala_cvms_list_parse, loopback policy, teardown policy, teardown http, create ack)ruff checkclean on touched Python filesnpx phala@latest cvms list --json→ total 0.exampleconfigs)CI
Notes
Summary by CodeRabbit
New Features
Documentation
Bug Fixes
Tests