Point ARCP at a Solidity contract; it parses, fans out 8 vulnerability-class checkers in parallel via Kotlin Flow + structured concurrency, runs analyzer hits through a small LLM for triage, and emits a severity-ranked PDF + SARIF audit bundle. The audit's cost ceiling is set on the lease, not discovered after the fact.
Showcases: flatMapMerge for per-vulnerability-class fanout, per-child lease grants as IAM-for-agents (tool.call allowlists scoped per check), cross-language tool calls (Slither/Mythril in a Python sidecar), JSON-schema-constrained triage, SARIF + PDF artifacts.
Four containers:
ollama ◀──http── arcp-runtime ──tool.call──▶ analyzer-pool (Slither/Mythril)
▲
│ ws/arcp
arcp-client (Clikt + Mordant)
The runtime registers a parent contract.audit agent plus 8 children (contract.check_reentrancy, contract.check_integer_overflow, …, contract.check_tx_origin). Each child receives a narrowed lease — a reentrancy check can't accidentally invoke the oracle-manipulation analyzer.
cp .env.example .env
make up # ollama + analyzer-pool + runtime + client
make audit FILE=samples/Reentrant.sol # runs the deliberately-vulnerable sample
make audit FILE=samples/Vault-safe.sol # returns 0 surviving findings| Variable | Default | Effect |
|---|---|---|
DEFAULT_COMPILER |
0.8.20 |
Solidity compiler version passed to solidity.parse_ast. |
DEFAULT_PARALLELISM |
4 |
flatMapMerge concurrency across the 8 vuln-class children. |
SEVERITY_FLOOR |
LOW |
Discard analyzer hits below this severity before triage. |
AUDIT_BUDGET_USD |
1.50 |
Parent cost.budget lease grant. |
PER_CLASS_BUDGET_USD |
0.10 |
Each child's cost.budget grant. |
TRIAGE_PARALLELISM |
4 |
Concurrent triage calls in the finding-level flatMapMerge. |
ANALYZER_POOL_URL |
http://analyzer-pool:9300 |
Where the runtime POSTs /analyze. |
ANALYZER_CACHE_DIR |
/cache/analyzers |
sha256-keyed analyzer result cache (in the pool container). |
ARCP_SDK_VERSION |
latest |
Resolved via Gradle's + dynamic version. |
src/main/kotlin/com/example/arcp/agents/ContractAudit.kt— parent fan-out, triage, artifacts.src/main/kotlin/com/example/arcp/agents/Check*.kt— one file per vulnerability class.src/main/kotlin/dev/arcp/example/audit/— in-process JVM tools (SolidityAstParser,CallGraphBuilder,SeverityMap,EvmDisassembler).src/main/kotlin/dev/arcp/example/audit/AnalyzerPool.kt— HTTP client for the Slither/Mythril sidecar.analyzer-pool/pool.py— Flask app exposingPOST /analyze(slither.*,mythril.simple).samples/—Reentrant.sol(vulnerable DAO clone),Vault-safe.sol(uses anonReentrantmodifier).
Fast in-process smoke tests — no docker, no Slither, no Ollama:
./gradlew --no-daemon testCoverage:
- AST parser surfaces the vulnerable
withdrawfunction inReentrant.sol. CheckReentrancyreturns one finding when wired to a stub analyzer-pool response.ContractAudit.leaseGrantsFor("reentrancy")matches the PROMPT §5 grant set exactly.- Triage step filters
FALSE_POSITIVEverdicts out. SarifBundleemits valid SARIF 2.1.0 JSON with one result for the reentrant sample.
The PROMPT references high-level agent-author APIs (@ArcpAgent, JobContext, LeaseRequest, etc.) that are not yet in dev.arcp:arcp. The example carries a thin local mirror in src/main/kotlin/dev/arcp/example/stubs/ — every type there is marked with a TODO: replace with real SDK API when published comment. The agent code in com/example/arcp/agents/ uses the same names and signatures the PROMPT specifies, so swapping in the real SDK is an import-only change.
make verify