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
30 changes: 15 additions & 15 deletions .lint_baselines/falsey_clobber.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,24 +22,24 @@
"axonflow/adapters/tool_wrapper.py:190:20",
"axonflow/adapters/tool_wrapper.py:208:20",
"axonflow/adapters/tool_wrapper.py:220:20",
"axonflow/client.py:1100:16",
"axonflow/client.py:1177:16",
"axonflow/client.py:1649:37",
"axonflow/client.py:1690:18",
"axonflow/client.py:1748:37",
"axonflow/client.py:2266:24",
"axonflow/client.py:2287:33",
"axonflow/client.py:2288:31",
"axonflow/client.py:2300:25",
"axonflow/client.py:2361:28",
"axonflow/client.py:2402:69",
"axonflow/client.py:1098:16",
"axonflow/client.py:1175:16",
"axonflow/client.py:1647:37",
"axonflow/client.py:1688:18",
"axonflow/client.py:1746:37",
"axonflow/client.py:2264:24",
"axonflow/client.py:2285:33",
"axonflow/client.py:2286:31",
"axonflow/client.py:2298:25",
"axonflow/client.py:2359:28",
"axonflow/client.py:2400:69",
"axonflow/client.py:292:14",
"axonflow/client.py:297:24",
"axonflow/client.py:298:20",
"axonflow/client.py:523:44",
"axonflow/client.py:6206:25",
"axonflow/client.py:834:20",
"axonflow/client.py:920:20",
"axonflow/client.py:521:44",
"axonflow/client.py:6204:25",
"axonflow/client.py:832:20",
"axonflow/client.py:918:20",
"axonflow/execution.py:205:19",
"axonflow/interceptors/anthropic.py:134:43",
"axonflow/interceptors/anthropic.py:161:43",
Expand Down
52 changes: 52 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,58 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
and tag v{X.Y.Z}. The release workflow's preflight checks the section
header matches the tag. -->

## [8.0.0] - 2026-05-08 — Decision history API + telemetry simplification

**Major release.** The headline feature is the new decision-history client API:
`list_decisions` for paging through recorded decisions, alongside the
`get_decision_explain` method shipped in v7.4.0 — callers can now both list
and drill in. Bundled into a major because the v8 line also tightens the
telemetry contract — see `Removed` at the bottom of this entry for that.

### Added

- **`client.list_decisions(opts)` method.** Pages over recorded decision
history from the orchestrator, mirroring `GET /api/v1/decisions`.
Companion to the v7.4.0 `get_decision_explain` method — callers can
now both list and drill in. Already shipped on `main` via PR #186 and
graduated into the v8.0 line with this release. See type
`ListDecisionsOptions` and `DecisionListItem` in `axonflow.decisions`.

### Migration guide (v7 → v8)

- **`AxonFlow(...)` no longer accepts the `telemetry` keyword argument.**
Code passing `AxonFlow(..., telemetry=True)` or
`AxonFlow(..., telemetry=False)` will raise `TypeError` at construction
time. Migration:
- If you were using it to disable telemetry, set
`AXONFLOW_TELEMETRY=off` in the environment instead — that's the
sole opt-out lever as of v8.0.
- If you were using it to force-enable, the default is now ON for
every mode so the argument is no longer needed.
- **`AxonFlowConfig.telemetry` field removed.** Code that constructed
the dataclass directly with `AxonFlowConfig(..., telemetry=...)` will
fail to instantiate. Drop the field; rely on the env var.

### Removed

- **`AxonFlow(..., telemetry=...)` keyword argument** and the
corresponding `AxonFlowConfig.telemetry: bool | None` field.
`AXONFLOW_TELEMETRY=off` is now the sole opt-out path. Tests that
need to defend against contaminated dev environments should clear
the env var explicitly via `monkeypatch.setenv("AXONFLOW_TELEMETRY", "")`.
- **Sandbox-mode silent telemetry suppression.** Sandbox-mode clients
(constructed via `AxonFlow.sandbox()` or `mode=Mode.SANDBOX`) now fire
telemetry on the same heartbeat schedule as production-mode clients.
Pings are tagged `stream="sandbox"` so analytics can distinguish dev
pings from production heartbeat — see the checkpoint-service
`IsValidIncomingStream` allowlist for the wire-side gate.
- **`send_telemetry_ping` signature change.** The internal helper
`axonflow.telemetry.send_telemetry_ping` no longer accepts
`telemetry_enabled` or `has_credentials` parameters; `_is_telemetry_enabled`
takes no arguments. Callers should not have been depending on these
internals (they're underscore-prefixed), but the change is recorded
here for completeness.

## [7.1.0] - 2026-05-06 — X-Axonflow-Client header + scope-aware license validation

**Companion release to platform v7.7.0.** The Python SDK now sends an
Expand Down
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -441,11 +441,34 @@ If you are evaluating AxonFlow in a company setting and cannot open a public iss

No email required. Optional contact if you want a response.

## Sandbox Mode

```python
# Quick sandbox client for local testing — defaults to http://localhost:8080.
from axonflow import AxonFlow

client = AxonFlow.sandbox()
```

> Sandbox-mode clients fire telemetry like every other client — anonymous SDK
> heartbeat, classification-only payload, opt-out via `AXONFLOW_TELEMETRY=off`.
> Pings are tagged `stream="sandbox"` server-side so dev/test usage is
> distinguishable from production heartbeat. (Pre-v8.0 sandbox-mode pings
> were silently suppressed; the suppression was removed in v8.0 to give a
> single ops-controlled opt-out lever.)

## Telemetry

This SDK sends anonymous usage telemetry (SDK version, OS, enabled features) to help improve AxonFlow.
No prompts, payloads, or PII are ever collected. Opt out: `AXONFLOW_TELEMETRY=off`.

`AXONFLOW_TELEMETRY=off` is the **sole opt-out lever** as of v8.0. The
v7.x `telemetry` keyword argument on `AxonFlow(...)` and the
corresponding `AxonFlowConfig.telemetry` field have been removed; the
previous silent suppression of sandbox-mode pings has also been removed
(sandbox-mode pings now fire and are tagged `stream="sandbox"` so
they're distinguishable from production heartbeat).

### Scope of `AXONFLOW_TELEMETRY=off`

`AXONFLOW_TELEMETRY=off` disables the anonymous SDK heartbeat (version, OS, architecture). On **self-hosted** and **in-VPC** deployments, that heartbeat is the only data the SDK sends to AxonFlow, so setting `=off` means we receive nothing. On **Community SaaS** (`try.getaxonflow.com`) the hosted service also processes operational data — registrations, audit logs, policy enforcement records, workflow state, plan data, and request-header metadata aggregated for usage analytics — as part of running the platform; that operational data flow is governed by the [Privacy Policy](https://getaxonflow.com/privacy/), not by `AXONFLOW_TELEMETRY`.
Expand Down
2 changes: 1 addition & 1 deletion axonflow/_version.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""Single source of truth for the AxonFlow SDK version."""

__version__ = "7.1.0"
__version__ = "8.0.0"
14 changes: 6 additions & 8 deletions axonflow/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -477,7 +477,6 @@ def __init__(
*,
mode: Mode | str = Mode.PRODUCTION,
debug: bool = False,
telemetry: bool | None = None,
timeout: float = 60.0,
map_timeout: float = 120.0,
insecure_skip_verify: bool = False,
Expand All @@ -492,11 +491,10 @@ def __init__(
endpoint: AxonFlow endpoint URL. Can also be set via AXONFLOW_AGENT_URL env var.
client_id: Client ID (optional for community/self-hosted mode)
client_secret: Client secret (optional for community/self-hosted mode)
mode: Operation mode (production or sandbox)
mode: Operation mode (production or sandbox). Sandbox-mode no longer
suppresses telemetry as of v8.0 — pings fire and are tagged
``stream="sandbox"`` server-side.
debug: Enable debug logging
telemetry: Enable/disable anonymous telemetry. ``None`` uses mode default
(ON for production, OFF for sandbox). Set ``AXONFLOW_TELEMETRY=off``
to opt out via environment.
timeout: Request timeout in seconds
map_timeout: Timeout for MAP operations in seconds (default: 120s)
MAP operations involve multiple LLM calls and need longer timeouts
Expand Down Expand Up @@ -614,11 +612,12 @@ def __init__(
# (CLI, serverless cold-starts) still deliver the ping. Subsequent
# gate runs happen async via ``_pre_request_hook`` on every
# public HTTP request. See axonflow/heartbeat.py for the contract
# and stamp-on-DELIVERY semantics.
# and stamp-on-DELIVERY semantics. The v7.x ``telemetry_enabled``
# programmatic override was removed in v8.0; AXONFLOW_TELEMETRY=off
# is now the sole opt-out lever.
maybe_send_heartbeat(
mode=self._config.mode.value,
endpoint=self._config.endpoint,
telemetry_enabled=telemetry,
debug=debug,
)

Expand Down Expand Up @@ -774,7 +773,6 @@ def _pre_request_hook(self) -> None:
maybe_send_heartbeat(
mode=self._config.mode.value,
endpoint=self._config.endpoint,
telemetry_enabled=self._config.telemetry,
debug=self._config.debug,
)

Expand Down
14 changes: 10 additions & 4 deletions axonflow/heartbeat.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@
The gate is consulted both at client construction and at every public
HTTP request site (via ``_pre_request_hook``). Each gate run:

1. Re-evaluates ``AXONFLOW_TELEMETRY=off`` / mode-disabled cheaply
(lock-free) so a mid-process opt-out toggle takes effect immediately.
1. Re-evaluates ``AXONFLOW_TELEMETRY=off`` cheaply (lock-free) so a
mid-process opt-out toggle takes effect immediately. As of v8.0 the
env var is the sole opt-out path — sandbox-mode is no longer
silently suppressed; sandbox pings fire and carry stream="sandbox"
in the payload.
2. Checks an in-memory 1-hour cache to bound stat() syscall frequency on
hot request paths.
3. Reads the stamp file mtime as the source of truth for last successful
Expand Down Expand Up @@ -237,20 +240,23 @@ def _register_thread(t: threading.Thread) -> None:
def maybe_send_heartbeat(
mode: str,
endpoint: str,
telemetry_enabled: bool | None,
debug: bool = False,
) -> None:
"""Central gate for telemetry pings.

Called from ``AxonFlow.__init__`` and ``AxonFlow._pre_request_hook``.
Implements the contract documented at the top of this module. Never
raises — heartbeat failures must not surface to the caller.

The v7.x ``telemetry_enabled`` parameter was removed in v8.0 along
with the corresponding config field. ``AXONFLOW_TELEMETRY=off`` in
the environment is now the SOLE opt-out lever — see CHANGELOG.
"""
# Lazy imports break the heartbeat → telemetry → heartbeat cycle that
# would otherwise occur if these were top-level imports.
from axonflow.telemetry import _is_telemetry_enabled, _send_telemetry_ping_now # noqa: PLC0415

if not _is_telemetry_enabled(mode, telemetry_enabled, has_credentials=False):
if not _is_telemetry_enabled():
return

h = _state
Expand Down
66 changes: 36 additions & 30 deletions axonflow/telemetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,34 +64,27 @@ def _flush_pending_telemetry() -> None:
t.join(timeout=_TIMEOUT_SECONDS)


def _is_telemetry_enabled(
mode: str,
telemetry_enabled: bool | None,
has_credentials: bool, # noqa: ARG001 kept for API compat
) -> bool:
def _is_telemetry_enabled() -> bool:
"""Determine whether telemetry should fire.

Priority (highest to lowest):
1. ``AXONFLOW_TELEMETRY=off`` environment variable -> disabled
(canonical AxonFlow-specific opt-out)
2. Explicit config value (``telemetry_enabled``) -> use that
3. Default: ON for all modes except sandbox
``AXONFLOW_TELEMETRY=off`` in the environment is the SOLE opt-out path.
Telemetry is otherwise ON by default, regardless of mode (sandbox /
production / anything else). Sandbox-mode pings are tagged
``stream="sandbox"`` in the payload so analytics can still distinguish
them — see ``_build_payload``.

Historical context: v7.x supported a ``telemetry_enabled: bool | None``
config field and a ``mode != "sandbox"`` default-suppression rule.
Both were removed in v8.0 to leave a single, ops-controlled opt-out
lever and avoid silent suppression that masks real adoption signal.
See CHANGELOG v8.0.0.

``DO_NOT_TRACK`` is intentionally NOT honored. It is commonly inherited
from host tools and developer environments (CLIs like Codex and Claude
Code inject it unconditionally), which makes it an unreliable expression
of user intent for AxonFlow telemetry.
"""
# Environment-level opt-out always wins.
if os.environ.get("AXONFLOW_TELEMETRY", "").strip().lower() == "off":
return False

# Explicit config override.
if telemetry_enabled is not None:
return telemetry_enabled

# Default: ON everywhere except sandbox mode.
return mode != "sandbox"
return os.environ.get("AXONFLOW_TELEMETRY", "").strip().lower() != "off"


def _detect_platform_version(endpoint: str, timeout: float = 2.0) -> str | None:
Expand Down Expand Up @@ -179,8 +172,17 @@ def _build_payload(
platform_version: str | None = None,
endpoint_type: str = "unknown",
) -> dict[str, object]:
"""Build the JSON payload for the checkpoint ping."""
return {
"""Build the JSON payload for the checkpoint ping.

The ``stream`` field classifies the heartbeat sub-stream. Sandbox-mode
clients emit ``"sandbox"`` so analytics can distinguish dev/test pings
from production heartbeat without conflating them; production-mode and
other modes omit the field entirely (we drop None-valued entries before
JSON-encoding) and the server defaults to ``"heartbeat"``. The
wire-allowlist is enforced server-side — see checkpoint-service
``IsValidIncomingStream``.
"""
payload: dict[str, object] = {
"sdk": "python",
"sdk_version": _SDK_VERSION,
"platform_version": platform_version,
Expand All @@ -192,6 +194,9 @@ def _build_payload(
"features": [],
"instance_id": str(uuid.uuid4()),
}
if mode == "sandbox":
payload["stream"] = "sandbox"
return payload


def _send_telemetry_ping_now(url: str, mode: str, endpoint: str, debug: bool) -> bool:
Expand Down Expand Up @@ -271,24 +276,25 @@ def _do_ping(url: str, mode: str, endpoint: str, debug: bool) -> None:
def send_telemetry_ping(
mode: str,
endpoint: str,
telemetry_enabled: bool | None,
has_credentials: bool = False,
debug: bool = False,
) -> None:
"""Fire-and-forget telemetry ping. Runs in a daemon thread.

Args:
mode: SDK operation mode (``"production"`` or ``"sandbox"``).
Sandbox-mode pings fire on the same schedule as production-mode
pings as of v8.0; the payload is tagged ``stream="sandbox"`` so
analytics can distinguish them server-side.
endpoint: The AxonFlow agent endpoint, used to detect the platform
version via ``/health``.
telemetry_enabled: Explicit config override. ``None`` means use the
mode-based default.
has_credentials: Whether the client was initialized with credentials
(clientId + clientSecret). Used to distinguish managed cloud from
self-hosted/community deployments for the default behavior.
debug: When ``True``, log debug-level messages about the ping.

Note:
``AXONFLOW_TELEMETRY=off`` is the SOLE opt-out path. The v7.x
``telemetry_enabled`` parameter and ``has_credentials`` parameter
were removed in v8.0 — see CHANGELOG.
"""
if not _is_telemetry_enabled(mode, telemetry_enabled, has_credentials):
if not _is_telemetry_enabled():
return

logger.info(
Expand Down
13 changes: 9 additions & 4 deletions axonflow/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ class AxonFlowConfig(BaseModel):
The SDK will work without authentication headers in this mode.

As of v1.0.0, all routes go through a single endpoint (ADR-026).

As of v8.0, the legacy ``telemetry`` field has been removed. To
opt out of the anonymous heartbeat, set ``AXONFLOW_TELEMETRY=off``
in the environment — it is now the sole opt-out lever.
"""

model_config = ConfigDict(frozen=True)
Expand All @@ -74,10 +78,11 @@ class AxonFlowConfig(BaseModel):
client_secret: str | None = Field(default=None, description="Client secret (optional)")
mode: Mode = Field(default=Mode.PRODUCTION, description="Operation mode")
debug: bool = Field(default=False, description="Enable debug logging")
telemetry: bool | None = Field(
default=None,
description="Enable/disable anonymous telemetry (None = mode default)",
)
# `telemetry` field removed in v8.0. AXONFLOW_TELEMETRY=off is now the
# SOLE opt-out path for the SDK heartbeat. Sandbox-mode pings are no
# longer suppressed; they fire and carry stream="sandbox" in the
# payload so analytics can distinguish dev/test pings server-side.
# See CHANGELOG v8.0.0 and axonflow.telemetry._is_telemetry_enabled.
timeout: float = Field(default=60.0, gt=0, description="Request timeout (seconds)")
map_timeout: float = Field(default=120.0, gt=0, description="MAP operations timeout (seconds)")
insecure_skip_verify: bool = Field(default=False, description="Skip TLS verify")
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "axonflow"
version = "7.1.0"
version = "8.0.0"
description = "AxonFlow Python SDK - Enterprise AI Governance in 3 Lines of Code"
readme = "README.md"
license = {text = "MIT"}
Expand Down
Loading
Loading