Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ jobs:
run: poetry run ruff check --output-format=github .

- name: Typecheck
if: always()
run: poetry run mypy .

- name: Test
if: always()
run: poetry run pytest
11 changes: 9 additions & 2 deletions .secrets.baseline
Original file line number Diff line number Diff line change
Expand Up @@ -151,9 +151,16 @@
"filename": "docs/usage/config.mdx",
"hashed_secret": "3f4f9a14a2d4d72a7074c2969dd34c89f2cbe61a",
"is_verified": false,
"line_number": 23
"line_number": 33
},
{
"type": "Secret Keyword",
"filename": "docs/usage/config.mdx",
"hashed_secret": "01eddf49c6b18f99f87ac7ba45e81d4a227e8d3f",
"is_verified": false,
"line_number": 171
}
]
},
"generated_at": "2025-07-14T09:19:13Z"
"generated_at": "2025-07-24T10:02:58Z"
}
38 changes: 16 additions & 22 deletions docs/intro.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,26 @@ Which means, in order to evaluate Offensive Security agents, we need to develop

## Basic Example

Before you start, ensure you have the `dreadnode` package installed (see [installation](/install)). You can authenticate to a platform using the CLI, which is the recommended way to get started.

```bash
# Authenticate to platform.dreadnode.io
dreadnode login

# For self-hosted platforms, specify the server URL
dreadnode login --server http://self-hosted
```

<Note>
For complete authentication and configuration guidance, see the [Configuration](/usage/config) documentation.
</Note>

The most basic use of Strikes is a run with some logged data:

```python
import asyncio
import dreadnode

# Initialize with default settings
dreadnode.configure()

NAMES = ["Nick", "Will", "Brad", "Brian"]

# Create a new task
Expand All @@ -42,7 +53,7 @@ async def main() -> None:
)

# Log inputs
dn.log_input("names", NAMES)
dreadnode.log_input("names", NAMES)

# Run your tasks
greetings = [
Expand All @@ -51,7 +62,7 @@ async def main() -> None:
]

# Save outputs
dn.log_output("greetings", greetings)
dreadnode.log_output("greetings", greetings)

# Track metrics
dreadnode.log_metric("accuracy", 0.65, step=0)
Expand All @@ -63,19 +74,6 @@ async def main() -> None:
asyncio.run(main())
```

<Note>
We'll assume you have installed the `dreadnode` package and have your environment variables set up. Make sure you have `DREADNODE_API_KEY=...` set to your Platform API key.

For more information on `dreadnode.configure()`, review the [Configuration](/usage/config) topic.

If you call `dreadnode.configure()` without any token and your environment variables are not set, you'll receive a warning in the console, so keep an eye out! You can still run any of your code without sending data to the Dreadnode Platform.
</Note>

<Tip>
**Server Configuration**
By default, the SDK connects to the hosted Dreadnode platform at `https://platform.dreadnode.io`. If you're using a self-hosted instance, you must configure the server URL explicitly in your `dreadnode.configure()` call or via the `DREADNODE_SERVER` environment variable. See the [Configuration](/usage/config) guide for details.
</Tip>

This code should be very familiar if you've used an ML-experimentation library before, and all the functions you're familiar with work exactly like you would expect.

Under the hood, this code did a few things:
Expand Down Expand Up @@ -114,8 +112,6 @@ Runs are the core unit of work in Strikes. They provide the context for all your
```python
import dreadnode

dreadnode.configure()

with dreadnode.run("my-experiment"):
# Everything that happens here is part of the run
# All data collected is associated with this run
Expand Down Expand Up @@ -147,8 +143,6 @@ Tasks are units of work within runs. They help you structure your code and provi
```python
import dreadnode

dreadnode.configure()

@dreadnode.task()
async def say_hello(name: str) -> str:
return f"Hello, {name}!"
Expand Down
178 changes: 164 additions & 14 deletions docs/sdk/api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ ApiClient

```python
ApiClient(
base_url: str, api_key: str, *, debug: bool = False
base_url: str,
*,
api_key: str | None = None,
cookies: dict[str, str] | None = None,
debug: bool = False,
)
```

Expand All @@ -29,7 +33,9 @@ Initializes the API client.
(`str`)
–The base URL of the Dreadnode API.
* **`api_key`**
(`str`)
(`str`, default:
`None`
)
–The API key for authentication.
* **`debug`**
(`bool`, default:
Expand All @@ -42,11 +48,13 @@ Initializes the API client.
def __init__(
self,
base_url: str,
api_key: str,
*,
api_key: str | None = None,
cookies: dict[str, str] | None = None,
debug: bool = False,
):
"""Initializes the API client.
"""
Initializes the API client.

Args:
base_url (str): The base URL of the Dreadnode API.
Expand All @@ -57,12 +65,28 @@ def __init__(
if not self._base_url.endswith("/api"):
self._base_url += "/api"

_cookies = httpx.Cookies()
cookie_domain = urlparse(base_url).hostname
if cookie_domain is None:
raise ValueError(f"Invalid URL: {base_url}")

if cookie_domain == "localhost":
cookie_domain = "localhost.local"

for key, value in (cookies or {}).items():
_cookies.set(key, value, domain=cookie_domain)

headers = {
"User-Agent": f"dreadnode-sdk/{VERSION}",
"Accept": "application/json",
}

if api_key:
headers["X-Api-Key"] = api_key

self._client = httpx.Client(
headers={
"User-Agent": f"dreadnode-sdk/{VERSION}",
"Accept": "application/json",
"X-API-Key": api_key,
},
headers=headers,
cookies=_cookies,
base_url=self._base_url,
timeout=30,
)
Expand Down Expand Up @@ -133,7 +157,8 @@ def export_metrics(
metrics: list[str] | None = None,
aggregations: list[MetricAggregationType] | None = None,
) -> pd.DataFrame:
"""Exports metric data for a specific project.
"""
Exports metric data for a specific project.

Args:
project: The project identifier.
Expand Down Expand Up @@ -224,7 +249,8 @@ def export_parameters(
metrics: list[str] | None = None,
aggregations: list[MetricAggregationType] | None = None,
) -> pd.DataFrame:
"""Exports parameter data for a specific project.
"""
Exports parameter data for a specific project.

Args:
project: The project identifier.
Expand Down Expand Up @@ -306,7 +332,8 @@ def export_runs(
status: StatusFilter = "completed",
aggregations: list[MetricAggregationType] | None = None,
) -> pd.DataFrame:
"""Exports run data for a specific project.
"""
Exports run data for a specific project.

Args:
project: The project identifier.
Expand Down Expand Up @@ -398,7 +425,8 @@ def export_timeseries(
time_axis: TimeAxisType = "relative",
aggregations: list[TimeAggregationType] | None = None,
) -> pd.DataFrame:
"""Exports timeseries data for a specific project.
"""
Exports timeseries data for a specific project.

Args:
project: The project identifier.
Expand Down Expand Up @@ -427,6 +455,47 @@ def export_timeseries(
```


</Accordion>

### get\_device\_codes

```python
get_device_codes() -> DeviceCodeResponse
```

Start the authentication flow by requesting user and device codes.

<Accordion title="Source code in dreadnode/api/client.py" icon="code">
```python
def get_device_codes(self) -> DeviceCodeResponse:
"""Start the authentication flow by requesting user and device codes."""

response = self.request("POST", "/auth/device/code")
return DeviceCodeResponse(**response.json())
```


</Accordion>

### get\_github\_access\_token

```python
get_github_access_token(
repos: list[str],
) -> GithubTokenResponse
```

Try to get a GitHub access token for the given repositories.

<Accordion title="Source code in dreadnode/api/client.py" icon="code">
```python
def get_github_access_token(self, repos: list[str]) -> GithubTokenResponse:
"""Try to get a GitHub access token for the given repositories."""
response = self.request("POST", "/github/token", json_data={"repos": repos})
return GithubTokenResponse(**response.json())
```


</Accordion>

### get\_project
Expand Down Expand Up @@ -634,6 +703,26 @@ def get_run_trace(
```


</Accordion>

### get\_user

```python
get_user() -> UserResponse
```

Get the user email and username.

<Accordion title="Source code in dreadnode/api/client.py" icon="code">
```python
def get_user(self) -> UserResponse:
"""Get the user email and username."""

response = self.request("GET", "/user")
return UserResponse(**response.json())
```


</Accordion>

### get\_user\_data\_credentials
Expand Down Expand Up @@ -729,6 +818,47 @@ def list_runs(self, project: str) -> list[RunSummary]:
```


</Accordion>

### poll\_for\_token

```python
poll_for_token(
device_code: str,
interval: int = DEFAULT_POLL_INTERVAL,
max_poll_time: int = DEFAULT_MAX_POLL_TIME,
) -> AccessRefreshTokenResponse
```

Poll for the access token with the given device code.

<Accordion title="Source code in dreadnode/api/client.py" icon="code">
```python
def poll_for_token(
self,
device_code: str,
interval: int = DEFAULT_POLL_INTERVAL,
max_poll_time: int = DEFAULT_MAX_POLL_TIME,
) -> AccessRefreshTokenResponse:
"""Poll for the access token with the given device code."""

start_time = datetime.now(timezone.utc)
while (datetime.now(timezone.utc) - start_time).total_seconds() < max_poll_time:
response = self._request(
"POST", "/auth/device/token", json_data={"device_code": device_code}
)

if response.status_code == 200: # noqa: PLR2004
return AccessRefreshTokenResponse(**response.json())
if response.status_code != 401: # noqa: PLR2004
raise RuntimeError(self._get_error_message(response))

time.sleep(interval)

raise RuntimeError("Polling for token timed out")
```


</Accordion>

### request
Expand Down Expand Up @@ -782,7 +912,8 @@ def request(
params: dict[str, t.Any] | None = None,
json_data: dict[str, t.Any] | None = None,
) -> httpx.Response:
"""Makes an HTTP request to the API and raises exceptions for errors.
"""
Makes an HTTP request to the API and raises exceptions for errors.

Args:
method (str): The HTTP method (e.g., "GET", "POST").
Expand All @@ -808,6 +939,25 @@ def request(
```


</Accordion>

### url\_for\_user\_code

```python
url_for_user_code(user_code: str) -> str
```

Get the URL to verify the user code.

<Accordion title="Source code in dreadnode/api/client.py" icon="code">
```python
def url_for_user_code(self, user_code: str) -> str:
"""Get the URL to verify the user code."""

return f"{self._base_url.removesuffix('/api')}/account/device?code={user_code}"
```


</Accordion>
ExportFormat
------------
Expand Down
Loading