The LLM exploit-analysis stage (scanner/exploit.py, commit bce6238, live-verified against a real claude -p) was handed off to the @deploy cross-team agent for VM runtime provisioning on the polymarket-infra box, and @deploy accepted. This note records the agreed runtime shape, the technical facts that shaped it, and the operator-gated blockers. Negotiation transcript: /Users/levander/coding/polymarket_fetch/AGENT_HANDOFF.md.
For Agents
The exploit stage runs as a systemd oneshot + timer on the shared
polymarket-infrae2-small box under a dedicated unprivileged userbsc-exploit. It is slither-free, writes nothing to disk, reads exactly 3 env vars, and upserts verdicts straight to Postgres. Status (2026-06-03): live-validated against prod (one clean local run, exit 0) and theBSC_EXPLOIT_ENVsecret is now seeded with all 3 keys — @deploy is unblocked, awaiting role write+dispatch (see Live validation — 2026-06-03 and Pending — blocked on operator).
Live validation — 2026-06-03
First production run: clean pass, verdicts grounded
scanner.exploit(commitbce6238) was run once locally against the day’s batchbatch-2026-06-03, writing to the prod Supabasebsc_contract_analysistable after the operator minted the 1-yearCLAUDE_CODE_OAUTH_TOKEN. Result: 16 High-finding contracts → 14 analyzed (allfalse_positive), 2needs_review, 0 failures, exit 0.
- Verdicts are high-quality and grounded, not rubber-stamped. Spot-checked 4 rationales — each correctly identified contract provenance and cited specific functions/lines: DooarSwapV2Pair weak-prng = standard UniV2 TWAP timestamp truncation; stake_pool
emergencyWithdraw=onlyRole(ADMIN_ROLE)+ the insight thatdeposit()forwards funds tomasterAddressso the contract custodies ~zero drainable balance; Vault = GMX/Gambit-derived with a shared OZnonReentrantguard, thereentrancy-ethflag mislabeled (only ERC20 outflow); VaultSupervisor’s Soladyincorrect-shift+ OZMath.mulDivincorrect-exp= well-known library-detector FPs. Allinjection_observed=false,per_findingcorrectly populated with finding_keys. 14/14 false_positive is the honest, correct outcome for a day’s batch of forked / library-heavy unaudited BSC contracts — the LLM triage layer working as designed (vulnerable ≠ exploitable), not a defaulting bug. - Source-size budget defers large proxy implementations (a real limitation). Both
needs_reviewwere large proxy→implementation contracts wherebuild_sourcecould not retain the finding’s code snippet within the source budget, so the stage deliberately marked themneeds_review(rationale “source too large to retain finding code”) rather than analyze truncated/mutilated code. This is by design (the findings-aware truncation guard) — big contracts get deferred and retried, not verdicted. It produces a small steady trickle ofneeds_reviewrows that do NOT fail the run or affect exit codes. Future tuning lever: raise the source budget or improve per-file selection so large implementations can still be analyzed.
Handoff outcome
- What moved:
scanner.exploit(LLM exploit-analysis stage), pinned at commitbce62380a5bdf759facb512b0ba809180c63f79e. - Who owns runtime: @deploy is writing the deploy role (Ansible-driven).
- Trigger model: runs after the existing GitHub-Actions daily scan+load populates the day’s
bsc_findingbatch (the 04:17 UTC scan/load → the 06:00 UTC exploit pass).
Agreed VM runtime shape
On the polymarket-infra e2-small box.
User + filesystem
- Dedicated unprivileged user
bsc-exploit, home/opt/bsc-exploit, no sudo. /opt/bsc-exploit/repo/= shallow clone ofgithub.com/wowjeeez/bsc-vuln-scannerpinned to a specific commit (bce62380a5bdf759facb512b0ba809180c63f79e), NOT trackingmain. Deliberate deploys only (Ansiblegit fetch+checkout <SHA>).uvinstalled;.venvmaterialized viauv sync --frozen --no-dev.requires-python = ">=3.12"→ pin Python 3.12 on the VM.
Claude CLI
claudeCLI installed user-local at~bsc-exploit/.local/bin.- Authenticated via the 1-year
CLAUDE_CODE_OAUTH_TOKENfromclaude setup-token. - The only writable-dir need is the CLI’s own config/cache under
$HOME(~/.claude; optionally pin viaCLAUDE_CONFIG_DIR). The stage code itself writes nothing.
Secrets
- A new GitHub Actions secret
BSC_EXPLOIT_ENVinwowjeeez/terraform— separate fromPOLYMARKET_FETCH_ENV, least-privilege sobsc-exploitcannot see Telegram / polymarket tokens. - CD materializes
/opt/bsc-exploit/.envmode 0600. Three keys only:CLAUDE_CODE_OAUTH_TOKENDATABASE_URLETHERSCAN_API_KEY
- Per “gotcha #25”: the GH secret is the only source of truth — local
secrets/*.envare gitignored and ignored by CD.
systemd unit
- oneshot service + timer at
*-*-* 06:00:00 UTCwithPersistent=true(catches up if the box was down). Runs after the 04:17 UTC GH-Actions scan+load has populated that day’sbsc_findingbatch. User=bsc-exploitWorkingDirectory=/opt/bsc-exploit/repoEnvironmentFile=/opt/bsc-exploit/.envRuntimeMaxSec=7200ExecStart=/opt/bsc-exploit/.venv/bin/python -m scanner.exploit- Output → journald.
OnFailure=alert on exit code 1 or 2 (exit 0 = silent).
Technical facts that shaped the spec
The exploit stage is slither-free
Even though
slither-analyzer+solc-selectare in the main dependency closure (souv sync --no-devstill installs them),scanner.exploitnever imports or invokes them — it uses a proxy-following Etherscan source fetch, no compilation. The VM therefore needs NOsolc-select installstep (that belongs to GH-Actions / the scan stage only). The two deps are harmless dead weight in the venv.
Stage writes nothing to disk
No scratch / output / cache files. Source is held in memory, the prompt is piped to
claudevia stdin, JSON is captured from stdout, and verdicts are upserted straight to Postgres. The only writable path required is theclaudeCLI’s own$HOMEconfig/cache.
Exactly 3 env vars
ETHERSCAN_API_KEY, DATABASE_URL, CLAUDE_CODE_OAUTH_TOKEN.
DATABASE_URL must be the Session Pooler (:5432), NOT the :6543 transaction pooler
The stage upserts and connects with
prepare_threshold=None; the transaction pooler breaks prepared-statement-less upserts. Use the same DSN the GH scan/load uses (Session Pooler,:5432). This matches the existing findings-loader / supabase-findings-store connection rule.
Exit codes (alert contract)
| Code | Meaning | Alert? |
|---|---|---|
0 | success / nothing-to-do | no (silent) |
1 | DB error / rate-limit-stop / all-contracts-failed | yes |
2 | misconfig (missing secret) | yes |
A Claude rate-limit stops the batch cleanly and self-heals on the next run via the unchanged-source cache (re-analysis is skipped for contracts whose source hasn’t changed).
Future-update protocol
- Phase (a) — now: @bscvuln pings @deploy with a new SHA → @deploy bumps the pin + redeploys.
- Phase (b) — later: graduate to
workflow_dispatch.
Pending — blocked on operator
CLEARED (2026-06-03) —
BSC_EXPLOIT_ENVis now seeded with all 3 keysThe three out-of-band items have all landed in the GH-Actions secret on
wowjeeez/terraform:
- ✅ 1-year
CLAUDE_CODE_OAUTH_TOKEN(minted viaclaude setup-token).- ✅ Session-pooler (
:5432) DSN forDATABASE_URL.- ✅
ETHERSCAN_API_KEY.@deploy’s dispatch gate is cleared — remaining work is @deploy writing and dispatching the VM role.
Stood down — not an open item
"Rotate the Supabase DB password" — RESOLVED, do not resurface
The previously-flagged item to rotate the Supabase DB password was stood down. The rogue DB writer in the Round-53 incident turned out to be the operator’s own forgotten macOS LaunchAgent
com.levander.polymarket-bot, not a third party — resolved in Round 54. No credential rotation is needed.
Related
- adhoc-analysis-and-queue — on-demand analysis by address + the
bsc_analysis_requestqueue (commit272a224) that extends this stage; itsbuild_source(text, truncated)fix supersedes the 2026-06-03 source-budget explanation of the twoneeds_reviewproxy contracts above - contract-scanner — the 5-stage pipeline; the exploit stage is the LLM analysis layer downstream of triage
- supabase-findings-store — the
public.bsc_findingstore the exploit stage reads from and upserts verdicts to - findings-loader — the GH-Actions scan/load that populates the day’s batch (04:17 UTC) before this stage runs (06:00 UTC); shares the Session-Pooler DSN rule
- LOG
- TOPICS