Metadata-Version: 2.4
Name: agentcyberrange
Version: 0.1.0
Summary: Async Python SDK for Agent Cyber Range
Project-URL: Homepage, https://github.com/cynicalight/py-agentcyberrange
Project-URL: Repository, https://github.com/cynicalight/py-agentcyberrange
Project-URL: Issues, https://github.com/cynicalight/py-agentcyberrange/issues
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.

The evaluation system is partially open source. You can inspect and test the SDK
locally from this [GitHub repository](https://github.com/cynicalight/py-agentcyberrange)
before requesting an official evaluation.

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. 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.
