# AgentCyberRange SDK Integration Guide `agentcyberrange` is the asynchronous Python SDK for the AgentCyberRange evaluation platform. Use `run_agent()` for normal integrations. It discovers Challenges, creates Arenas, schedules the tested Agent, and submits when the Agent finishes, times out, or reaches the Arena deadline. The SDK does not issue Task Tokens or implement the Agent itself. ```bash python -m pip install agentcyberrange ``` - Python 3.11 or newer - Dependencies: `httpx` and `pydantic` - Async API only ## Prerequisites The platform issues a Task Token after an evaluation task is created. Configure it before running the SDK: ```bash export AGENTCYBERRANGE_TASK_TOKEN='replace-with-secret' # Override only when connecting to another deployment; this is the default. export AGENTCYBERRANGE_BASE_URL='https://eval.agentcyberrange.io/' ``` ## Quick start For a normal integration, implement `run_your_agent` and pass it to `run_agent()`. After an Arena is ready, `run_agent()` invokes the function and submits as soon as it returns. Pass `arena.task_prompt` to the Agent unchanged; it contains the complete requirements for this run. ```python import asyncio from agentcyberrange import ( AgentOutputPath, ArenaHandle, is_post_challenge, run_agent, ) async def run_your_agent(arena: ArenaHandle) -> AgentOutputPath | None: """Call your Agent here.""" # call_your_existing_agent is your adapter, not an SDK method. agent_output = await call_your_existing_agent( prompt=arena.task_prompt, entry_urls=[endpoint.url for endpoint in arena.entry_urls], cancel_event=arena.cancel_event, ) # POST is scored from live markers and uploads no file. if is_post_challenge(arena.challenge_id): return None # Web must return the path to a .tar.gz or .zip artifact. return agent_output async def main() -> None: result = await run_agent(run_your_agent, max_concurrency=2) print(result.model_dump_json(indent=2)) asyncio.run(main()) ``` By default, `run_agent()` runs every Challenge available to the Task Token. Use `challenge_ids` to select Challenges and `agent_timeout` to request a shorter Agent runtime. ## Default and low-level APIs Use `run_agent()` for normal integrations. Do not replace it with one standalone `create_arena()` call; the two API levels own different work: | Work | `run_agent()` | `AgentCyberRangeClient` | | --- | --- | --- | | Discover Challenges, wait for capacity, and create Arenas | Automatic | Caller invokes each step | | Start the Agent callback | Automatic | Caller | | Watch `agent_timeout` and the Arena deadline | Automatic | Caller | | Submit POST markers or a Web archive | Automatic | Caller invokes `submit()` | | Clean up an Arena that cannot continue | Automatic | Caller invokes `close()` | `create_arena()` waits for capacity, creates one Arena, and waits until it is ready. It does not start an Agent, watch a timeout in the background, or schedule `submit()`. Leaving `async with AgentCyberRangeClient(...)` only closes the HTTP client; it does not submit or close an Arena that was already created. ## Web and POST tasks Use `is_post_challenge(arena.challenge_id)` instead of maintaining a local list of task types. The current SDK classifies a Challenge as POST when its `challenge_id` contains the lowercase substring `post`; all others are Web. Here POST means post-exploitation, not the HTTP method. | Contract | Web | POST | | --- | --- | --- | | Task | Find and reproduce vulnerabilities in a live Web application | Compromise entry hosts, pivot through a multi-host range, and place markers | | Scoring input | The Agent report archive, verified against the live target | User/root markers present on the live target at submit time | | Callback return | Path to a `.tar.gz` or `.zip` file | `None` | | Submit | SDK uploads the archive | SDK uploads no file and starts scoring | ### Web tasks The Agent must verify vulnerabilities through the live endpoints in `task_prompt`. Findings obtained only from source code, Docker, host files, or evaluator data do not count. Create `final_answer/` in the Agent workspace. Use one directory per finding, with at least a report and an executable proof of concept: ```text final_answer/ ├── 001/ │ ├── vulnerability.json │ └── poc.py # or poc.sh ├── 002/ │ ├── vulnerability.json │ └── poc.py └── ... ``` The current `task_prompt` is authoritative for the `vulnerability.json` fields, PoC arguments, and verifier effects. When the Agent finishes, package the top-level `final_answer/` as a non-empty `.tar.gz` or `.zip` and return the file path. Do not return a directory. The archive must not exceed 10 MiB. ### POST tasks A POST task exposes one or more entry points. Compromise an entry host, place the requested user/root markers, then discover and reach deeper hosts from systems already compromised. Follow `task_prompt` for targets, marker locations, and restrictions. The scorer reads the live target state when submit begins. POST does not read `final_answer/` and does not accept an Agent Output file. Return `None` after the Agent finishes; the SDK submits immediately. ## Timeout and submission When a `run_agent()` timeout occurs, it sets `arena.cancel_event`, cancels the Agent callback, and submits promptly: - POST scores the live markers present at that moment. - Web should stop its child process quickly, package the current `final_answer/`, and return that archive from cancellation cleanup. The SDK submits the available output. Do not keep waiting for the Agent after timeout and do not merely call `close()`. If a Web callback returns no archive, the SDK submits an empty `final_answer/` archive and records the timeout or deadline in `TaskOutcome.error_code`. Use this pattern when the callback manages a child process: ```python try: await process.communicate(arena.task_prompt.encode()) except asyncio.CancelledError: await stop_process(process) if not arena.cancel_event.is_set(): raise if is_post_challenge(arena.challenge_id): return None return archive_current_final_answer() ``` Do not call `submit()` or `close()` after normal completion; `run_agent()` handles both after the callback returns. Automatic timeout and submission require the `run_agent()` scheduler to remain alive. Calling only `create_arena()` starts no watcher. If an outer 24-hour timeout kills the Python process, the SDK has no chance to finalize cancellation or submit. ## Common data objects ### `Challenge` Returned by `client.list_challenges()`. | Field | Type | Description | | --- | --- | --- | | `challenge_id` | `str` | Challenge ID used for selection and Arena creation | | `name` | `str` | Display name | | `description` | `str` | Challenge description | | `entry_roles` | `list[str]` | Roles of exposed entry services | ### `ArenaHandle` The runtime object passed to the Agent callback. | Field | Type | Description | | --- | --- | --- | | `arena_id` | `str` | Current Arena ID | | `challenge_id` | `str` | Current Challenge ID | | `task_prompt` | `str` | Complete task prompt; pass it to the Agent unchanged | | `entry_urls` | `list[ArenaEndpoint]` | Live endpoints with `url`, `host`, `port`, `protocol`, and `role` | | `cancel_event` | `asyncio.Event` | Set on timeout, deadline, or external cancellation | ### `ExecutionResult` Returned by `run_agent()`. | Field | Type | Description | | --- | --- | --- | | `execution_id` | `str` | ID for this run | | `status` | `str` | `completed`, `completed_with_errors`, or `cancelled` | | `outcomes` | `list[TaskOutcome]` | Arena, submission, and error data for every Challenge | `TaskOutcome.submission.verdict` contains `score`, `passed`, and `summary`. Per-Challenge failures are normally recorded in `TaskOutcome.error_code` and `error_message` without discarding results from other Challenges. ## Managing one Arena manually Use `AgentCyberRangeClient` only when an existing scheduler must own one Arena or the application needs custom recovery. The caller must complete every step: 1. Select a Challenge with `list_challenges()`. 2. Call `create_arena()` and retain the returned Arena. 3. Pass `arena.task_prompt` and `arena.entry_urls` to the Agent. 4. Watch the local timeout and `arena.lease_expires_at`; stop the Agent and its child processes. 5. For POST, call `submit(arena_id)`. For Web, call `submit(arena_id, agent_output=archive_path)`. 6. Call `close(arena_id)` only when the run is being abandoned and nothing can be scored. The `run_your_agent_with_timeout()` function below belongs to the integrating application, not the SDK. It must manage the Agent process, deadline, and Web archive finalization. ```python import os from agentcyberrange import AgentCyberRangeClient, is_post_challenge async with AgentCyberRangeClient( base_url=os.getenv( "AGENTCYBERRANGE_BASE_URL", "https://eval.agentcyberrange.io/" ), task_token=os.environ["AGENTCYBERRANGE_TASK_TOKEN"], ) as client: challenges = await client.list_challenges() arena = await client.create_arena(challenges[0].challenge_id) try: output_path = await run_your_agent_with_timeout( arena, deadline=arena.lease_expires_at, ) except BaseException: if is_post_challenge(arena.challenge_id): # The Agent may already have placed markers. Try to score them. await client.submit(arena.arena_id) else: partial_output = archive_current_final_answer() if partial_output is not None: await client.submit( arena.arena_id, agent_output=partial_output, ) else: await client.close(arena.arena_id) raise if is_post_challenge(arena.challenge_id): result = await client.submit(arena.arena_id) else: if output_path is None: await client.close(arena.arena_id) raise RuntimeError("Web task did not produce a report archive") result = await client.submit( arena.arena_id, agent_output=output_path, ) ``` `create_arena()` waits and retries on capacity exhaustion by default, but those retries cover only Arena creation. The low-level client does not start a timeout watcher for the caller. `submit()` starts scoring and cleanup. `close()` only cleans up the Arena; it does not score the run and cannot replace `submit()`. A failed POST run may still have placed markers, so submit it when possible. Submit a partial Web archive if the cancellation path preserved one. ## Error handling Every SDK exception inherits from `AgentCyberRangeError`. Common attributes are `message`, `status_code`, `error_code`, `request_id`, `retryable`, and `retry_after`. | Exception | Meaning | | --- | --- | | `AuthenticationError` / `RevokedTokenError` | Task Token is invalid or revoked | | `ChallengeNotFoundError` | Challenge is missing or unavailable to this Token | | `CapacityExhaustedError` | No capacity; high-level scheduling waits automatically | | `InvalidRequestError` | Invalid arguments or a Web archive over 10 MiB | | `ArenaStateError` / `ArenaBusyError` | Current Arena state rejects the operation | | `LaunchFailedError` | Arena did not become ready | | `TransportError` | Network failure or temporarily uncertain submit result | | `ServerError` / `ProtocolError` | Server failure or response outside the SDK contract | ```python from agentcyberrange import AgentCyberRangeError, AuthenticationError, run_agent try: result = await run_agent(run_your_agent) except AuthenticationError: raise RuntimeError("The Task Token is invalid or revoked") except AgentCyberRangeError as exc: print(exc.error_code, exc.request_id, exc.retryable) raise ```