Metadata-Version: 2.4
Name: agentcyberrange
Version: 0.1.1
Summary: Async Python SDK for Agent Cyber Range
Author: Agent Cyber Range contributors
License-Expression: MIT
License-File: LICENSE
Keywords: asyncio,cyber-range,evaluation,sdk
Classifier: Development Status :: 3 - Alpha
Classifier: Framework :: AsyncIO
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Typing :: Typed
Requires-Python: >=3.11
Requires-Dist: httpx[socks]>=0.27
Requires-Dist: pydantic>=2.7
Provides-Extra: dev
Requires-Dist: build>=1.2; extra == 'dev'
Requires-Dist: mypy>=1.10; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest>=8.2; extra == 'dev'
Requires-Dist: ruff>=0.5; extra == 'dev'
Description-Content-Type: text/markdown

# Agent Cyber Range SDK

`agentcyberrange` is the Python SDK for running autonomous Agents against the
Agent Cyber Range evaluation platform. It discovers the available Challenges,
keeps a bounded number of Arenas running, waits for capacity, and submits each
Arena when the Agent finishes or reaches its deadline.

Access to the evaluation service requires a Task Token issued by Agent Cyber Range.

Python 3.11 or newer is required.

## Install

Install the latest release from PyPI:

```bash
python -m pip install agentcyberrange
```

For local development from a repository checkout:

```bash
uv sync --extra dev
```

## Quick start

Set the Task Token issued by Agent Cyber Range:

```bash
export AGENTCYBERRANGE_TASK_TOKEN='replace-with-secret'
```

Then provide one asynchronous Agent function:

```python
import asyncio
import os

from agentcyberrange import (
    AgentCyberRangeClient,
    AgentOutputPath,
    ArenaHandle,
    run_agent,
)


async def run_your_agent(arena: ArenaHandle) -> AgentOutputPath | None:
    # Replace this body with your Agent implementation.
    print(arena.task_prompt)
    print(arena.entry_urls)
    agent_output: AgentOutputPath | None = None
    return agent_output

async def main() -> None:
    async with AgentCyberRangeClient(
        base_url=os.environ.get(
            "AGENTCYBERRANGE_BASE_URL", "https://eval.agentcyberrange.io/"
        ),
        task_token=os.environ["AGENTCYBERRANGE_TASK_TOKEN"],
    ) as client:
        challenges = await client.list_challenges()
        # Replace these with one or more specific Challenge IDs if needed.
        challenge_ids = [challenge.challenge_id for challenge in challenges[:5]]
        result = await run_agent(
            run_your_agent,
            max_concurrency=2,
            challenge_ids=challenge_ids,
            client=client,
        )
    print(result.model_dump_json(indent=2))


asyncio.run(main())
```

The Quick Start keeps the main customization points visible: the Agent callback, the
server-provided Prompt, and two concurrent Agents. The SDK returns the available
Challenge list, and the example selects the first five for this run. It leaves
`agent_timeout` unset, so Web Challenges use the 30-minute default and post Challenges use
the 2-hour default. Pass `agent_timeout` directly to apply one shorter limit across the
selected Challenges.

Environment variables are reserved for connection configuration
(`AGENTCYBERRANGE_TASK_TOKEN` and, optionally, `AGENTCYBERRANGE_BASE_URL`). Per-run
policy such as concurrency, Agent timeout, model, and permission mode stays explicit in
Python arguments or CLI flags instead of hidden process-global configuration.

Fleet and the SDK both limit a Web (non-post) Arena to 30 minutes and a post Arena to 2
hours. These values are the SDK's defaults and hard maximums. When `agent_timeout` is
omitted, the SDK selects the limit from the Challenge type.

`agent_timeout` is an optional local limit in seconds. `run_agent()` applies the same
requested value to every selected Challenge, then caps it at the type-specific maximum.
For example, one hour becomes 30 minutes for Web and remains one hour for post; three
hours becomes 30 minutes for Web and 2 hours for post. The effective deadline is the
earlier of this local limit and the individual Arena's authoritative server deadline.
When it is reached, the SDK signals `arena.cancel_event` and cancels the Agent callback. A
post Challenge is submitted with an empty body. A non-post callback may finish its
cancellation cleanup by returning an output archive, which the SDK submits immediately;
if cancellation produces no archive, the SDK submits a minimal empty `final_answer`
archive instead. A timeout or deadline therefore always triggers a terminal submit rather
than abandoning the Arena with a close request. The returned task outcome records the
timeout or deadline in `error_code` and `error_message`.

`on_event` receives lifecycle and retry events. Capacity waits include the HTTP status,
error code, request ID, `Retry-After`, and attempt number in `SchedulerEvent`, so callers
can report an automatic wait without disabling it. Pass a `RetryPolicy` to `run_agent()`
when a finite `capacity_wait_timeout` is required.

`arena_ready_delay` adds a cancellable settling period after Fleet reports an Arena as
running and before the Agent callback starts. This is useful when an exposed service needs
a few extra seconds to begin accepting connections.

The SDK automatically:

- discovers all currently available Challenges;
- keeps at most `max_concurrency` Work Slots active;
- waits and retries when the control plane has no free capacity;
- refills a Work Slot as soon as an Arena is submitted;
- submits a post Challenge when the Agent returns, raises, times out, or reaches the
  server deadline;
- submits a non-post Challenge with the returned output archive, or with a minimal empty
  `final_answer` archive when the Agent returns no path or raises.

## Run Codex

Install and authenticate the Codex CLI, set the Agent Cyber Range Task Token, then run:

```bash
uv run python examples/codex_cli.py
```

The example uses the default SDK settings and Codex model. It starts a non-interactive,
ephemeral `codex exec` session in a temporary workspace and enables network access inside
the `workspace-write` sandbox. See the
[Codex non-interactive mode documentation](https://learn.chatgpt.com/docs/non-interactive-mode).

The Codex example passes the server-provided `arena.task_prompt` to Codex unchanged. It
removes `AGENTCYBERRANGE_TASK_TOKEN` and `AGENTCYBERRANGE_BASE_URL` from the child
environment, so Codex cannot directly operate the control plane. If Codex creates a
`final_answer/` directory for a non-post Challenge, the callback archives it under
`.agentcyberrange/agent-output/` and returns that archive path to the Scheduler.

When an SDK timeout or Arena deadline cancels the callback, the example first stops Codex
and then archives any existing `final_answer/` before the temporary workspace is removed.
The Scheduler submits that archive immediately. If the directory does not exist, the
Scheduler submits a minimal empty `final_answer` archive instead. A post Challenge returns
`None` and is submitted with an empty body.

## Low-level client

Use `AgentCyberRangeClient` when you need to manage one Arena manually:

```python
import asyncio
import os

from agentcyberrange import AgentCyberRangeClient


async def main() -> None:
    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()
        if not challenges:
            raise RuntimeError("Agent Cyber Range has no available Challenges")
        arena = await client.create_arena(challenges[0].challenge_id)
        print(arena.task_prompt)
        print(arena.entry_urls)
        result = await client.submit(arena.arena_id)
        print(result.verdict)


asyncio.run(main())
```

`create_arena()` automatically waits on `capacity_exhausted` and reuses the same
Idempotency-Key across retries. The client honors standard proxy environment variables;
set `trust_env=False` when a localhost backend must bypass them.

`submit()` also accepts a `.tar.gz` or `.zip` path for a non-post Challenge:

```python
result = await client.submit(
    arena.arena_id,
    agent_output="outputs/agent-output.zip",
)
```

The SDK resolves a relative output path against the process working directory when
`submit()` starts, then reads the file once so an ambiguous HTTP result can safely retry
the same archive. The file must be non-empty and no larger than 10 MiB.

With `run_agent()`, return the archive path when a non-post Agent produced one. Returning
`None` is valid for both Challenge types: the Scheduler uses an empty request body for a
post Challenge and a minimal empty `final_answer` archive for a non-post Challenge.

```python
from agentcyberrange import AgentOutputPath, ArenaHandle


async def run_your_agent(arena: ArenaHandle) -> AgentOutputPath | None:
    agent_output = await your_agent(arena)
    return agent_output
```

## Examples

- [`examples/quickstart.py`](examples/quickstart.py): minimal custom Agent callback
- [`examples/codex_cli.py`](examples/codex_cli.py): Codex CLI integration
- [`examples/manual_client.py`](examples/manual_client.py): low-level client flow

## Development verification

```bash
uv sync --extra dev
uv run pytest
uv run ruff check .
uv run mypy src
uv build
```

`scripts/live_backend_smoke.py` exercises every low-level client method.
`scripts/live_scheduler_smoke.py` verifies rolling refill and capacity fallback.
`scripts/run_agent_test.py` runs mock or external Agent processes against the deployed
control plane and asserts that no active Arena is left behind.

For a repository-only live Agent test:

```bash
uv run python scripts/run_agent_test.py
```

To replace the mock Agent with another process:

```bash
uv run python scripts/run_agent_test.py \
  --agent-command 'codex exec --ephemeral --skip-git-repo-check --sandbox workspace-write -c sandbox_workspace_write.network_access=true -'
```

The default base URL is `https://eval.agentcyberrange.io/`. Override it with
`AGENTCYBERRANGE_BASE_URL` when testing another deployment.
