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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ 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. -->

## [Unreleased]

### Fixed

- The legacy `register_try()` default no longer attempts anonymous Hosted tenant creation; it directs new buyers to verified Portal signup before any network call. Custom self-hosted Community registration remains available, and the README and `sandbox()` guidance now describe linked API credentials.

## [9.5.0] - 2026-10-21: Decision API approval retry fields

### Added
Expand Down
25 changes: 15 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,22 +68,27 @@ Concurrent executions applies to MAP and WCP executions per tenant. Pending exec

[Get a free Evaluation license](https://getaxonflow.com/evaluation-license?utm_source=readme_sdk_python_eval) · [Run a paid production program](https://getaxonflow.com/design-partner?utm_source=readme_sdk_python_eval) · [Full feature matrix](https://docs.getaxonflow.com/docs/features/community-vs-enterprise?utm_source=readme_sdk_python_eval)

## Try Without Installing
## Hosted without installing the platform

Skip local setup entirely — try AxonFlow instantly at [**try.getaxonflow.com**](https://docs.getaxonflow.com/docs/deployment/community-saas):
Start at the [AxonFlow website](https://getaxonflow.com/) and follow Hosted signup to the [verified Portal](https://portal.try.getaxonflow.com/hosted/start). Verify your email, complete account and plan setup, then link an API client in the Portal. Store its client ID and secret securely. A newly registered unpaid Basic account cannot obtain a linked credential through the Portal.

```bash
# 1. Register (30 seconds)
curl -X POST https://try.getaxonflow.com/api/v1/register \
-H "Content-Type: application/json" -d '{"label":"my-trial"}'

# 2. Set credentials and auto-connect
export AXONFLOW_TRY=1
export AXONFLOW_CLIENT_ID=cs_your-tenant-id
export AXONFLOW_CLIENT_SECRET=your-secret
export AXONFLOW_CLIENT_ID='<linked-client-id>'
export AXONFLOW_CLIENT_SECRET='<linked-client-secret>'
```

```python
import os
from axonflow import AxonFlow

client = AxonFlow(
client_id=os.environ["AXONFLOW_CLIENT_ID"],
client_secret=os.environ["AXONFLOW_CLIENT_SECRET"],
)
```

No Docker, no license, no installation. Rate-limited to 20 req/min. [Learn more](https://docs.getaxonflow.com/docs/deployment/community-saas).
`AXONFLOW_TRY=1` selects the Hosted endpoint; pass the linked credentials to the constructor as shown. Alternatively, set `endpoint="https://try.getaxonflow.com"` explicitly with the variable unset. The legacy `axonflow.community.register_try()` default now directs new Hosted buyers to the Portal before making a request; its custom endpoint form remains available for a self-hosted Community server. For a self-hosted client, unset `AXONFLOW_TRY` so the explicit endpoint is used. [Learn more](https://docs.getaxonflow.com/docs/deployment/community-saas).

## Quick Start

Expand Down
8 changes: 4 additions & 4 deletions axonflow/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -986,10 +986,10 @@ def sandbox(
Start the stack with ``docker compose up`` from
https://github.com/getaxonflow/axonflow before calling this.

For the hosted community sandbox at https://try.getaxonflow.com, use
the registration flow instead: call ``POST /api/v1/register`` to mint
tenant credentials, then construct an ``AxonFlow(...)`` client directly
or set ``AXONFLOW_TRY=1``.
For Hosted, start at the AxonFlow website, verify email, and set up your
account and plan at https://portal.try.getaxonflow.com/hosted/start.
Link an API client and supply its ID and secret to ``AxonFlow(...)``.
``AXONFLOW_TRY=1`` selects Hosted; unset it for this local sandbox method.

Args:
client_id: Optional client ID (defaults to demo-client)
Expand Down
24 changes: 20 additions & 4 deletions axonflow/community.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Community SaaS registration helper for try.getaxonflow.com."""
"""Legacy registration helper for custom self-hosted Community servers."""

from __future__ import annotations

Expand All @@ -7,20 +7,36 @@
import httpx

TRY_ENDPOINT = "https://try.getaxonflow.com"
HOSTED_START_URL = "https://portal.try.getaxonflow.com/hosted/start"


def register_try(label: str = "", endpoint: str = TRY_ENDPOINT) -> dict[str, Any]:
"""Register for a free evaluation tenant on try.getaxonflow.com.
"""Register on a custom self-hosted Community server.

Returns dict with keys: tenant_id, secret, secret_prefix, expires_at, endpoint, note.
Store the secret securely — it is shown only once.

New Hosted buyers must verify their email, set up their account and plan,
then link an API client through the Portal. Existing linked client IDs and
secrets can be supplied explicitly to the SDK.

Args:
label: Optional human-readable name for the registration.
endpoint: Override the default endpoint (for local testing).
endpoint: Custom self-hosted Community endpoint. The legacy Hosted
default raises with Portal setup guidance before any network call.
"""
hostname = httpx.URL(endpoint).host
if hostname and hostname.rstrip(".") in {"try.getaxonflow.com", "try-staging.getaxonflow.com"}:
msg = (
f"New Hosted accounts start at the verified Portal {HOSTED_START_URL}: "
"verify your email, "
"complete account and plan setup, then link an API client in the Portal. "
"Configure that linked client ID and secret explicitly in the SDK."
)
raise ValueError(msg)

response = httpx.post(
f"{endpoint}/api/v1/register",
f"{endpoint.rstrip('/')}/api/v1/register",
json={"label": label} if label else {},
timeout=10.0,
)
Expand Down
13 changes: 13 additions & 0 deletions runtime-e2e/hosted_first_run/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Hosted first-run staging proof

This guarded test requires the exact staging Portal and Agent origins and a client ID and secret linked through a verified, paid staging account. Run it only after the staging stack and public TLS/DNS are restored:

```bash
export AXONFLOW_HOSTED_E2E_PORTAL=https://portal.try-staging.getaxonflow.com
export AXONFLOW_HOSTED_E2E_ENDPOINT=https://try-staging.getaxonflow.com
export AXONFLOW_HOSTED_E2E_CLIENT_ID='<linked-staging-client-id>'
export AXONFLOW_HOSTED_E2E_CLIENT_SECRET='<linked-staging-client-secret>'
python3 runtime-e2e/hosted_first_run/test.py
```

Unset `AXONFLOW_TRY` for this staging run because that legacy switch selects the production Hosted origin. The test verifies that the old Hosted registration helper stops before HTTP, the real Portal serves the Hosted start page, and the linked SDK receives an allow decision for a harmless check-input call from the real Agent. It does not perform signup, start Checkout, charge a card, or issue credentials. Without both exact staging origins it reports SKIP.
74 changes: 74 additions & 0 deletions runtime-e2e/hosted_first_run/test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
"""Guarded website-first proof against a real staging Portal and Agent."""

from __future__ import annotations

import asyncio
import os
import sys
from pathlib import Path

import httpx

ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))

from axonflow import AxonFlow # noqa: E402 - run this checkout's SDK
from axonflow.community import HOSTED_START_URL, register_try # noqa: E402

PORTAL = "https://portal.try-staging.getaxonflow.com"
AGENT = "https://try-staging.getaxonflow.com"


async def main() -> None:
if (
os.getenv("AXONFLOW_HOSTED_E2E_PORTAL") != PORTAL
or os.getenv("AXONFLOW_HOSTED_E2E_ENDPOINT") != AGENT
):
print("SKIP: set both exact staging Portal and Agent origins")
return

client_id = os.getenv("AXONFLOW_HOSTED_E2E_CLIENT_ID")
client_secret = os.getenv("AXONFLOW_HOSTED_E2E_CLIENT_SECRET")
if not client_id or not client_secret:
msg = "Linked staging client ID and secret are required for the real-stack proof"
raise RuntimeError(msg)
if os.getenv("AXONFLOW_TRY") == "1":
msg = "Unset AXONFLOW_TRY so the SDK uses the exact staging Agent endpoint"
raise RuntimeError(msg)

try:
register_try("new Hosted buyer")
except ValueError as error:
if HOSTED_START_URL not in str(error):
msg = "Hosted registration did not direct to the verified Portal"
raise AssertionError(msg) from error
else:
msg = "Anonymous Hosted registration was allowed"
raise AssertionError(msg)

async with httpx.AsyncClient(timeout=10.0) as http:
portal = await http.get(f"{PORTAL}/hosted/start")
portal.raise_for_status()
if not portal.text:
msg = "Verified Portal returned an empty Hosted start page"
raise AssertionError(msg)

async with AxonFlow(endpoint=AGENT, client_id=client_id, client_secret=client_secret) as sdk:
decision = await sdk.mcp_check_input(
connector_type="postgres",
statement="SELECT 1",
operation="query",
)
if decision.allowed is not True:
msg = "Linked staging Agent did not allow the harmless governed probe"
raise AssertionError(msg)

print("PASS: verified Portal start and linked Python SDK governed Agent decision")


if __name__ == "__main__":
try:
asyncio.run(main())
except Exception as error: # noqa: BLE001
print(f"FAIL: {type(error).__name__}: {error}", file=sys.stderr)
raise SystemExit(1) from error
54 changes: 54 additions & 0 deletions tests/test_hosted_first_run.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"""Website-first Hosted registration and legacy Community compatibility."""

from __future__ import annotations

import httpx
import pytest

from axonflow import community


@pytest.mark.parametrize(
"endpoint",
[
community.TRY_ENDPOINT,
"https://try.getaxonflow.com/",
"https://try.getaxonflow.com./",
"https://try.getaxonflow\u3002com",
"https://try.getaxonflow\uff0ecom",
"https://try.getaxonflow\uff61com",
"https://try-staging.getaxonflow.com",
"https://try-staging.getaxonflow.com./",
"https://try-staging.getaxonflow\u3002com",
],
)
def test_hosted_registration_directs_to_verified_portal_before_network(monkeypatch, endpoint):
def unexpected_post(*_args, **_kwargs):
pytest.fail("Hosted registration reached the network")

monkeypatch.setattr(community.httpx, "post", unexpected_post)

with pytest.raises(ValueError, match="verified") as error:
community.register_try("new buyer", endpoint=endpoint)

assert community.HOSTED_START_URL in str(error.value)
assert "link an API client" in str(error.value)


def test_custom_self_hosted_community_registration_still_works(monkeypatch):
seen = []

def post(url, *, json, timeout):
seen.append((url, json, timeout))
return httpx.Response(
201,
json={"tenant_id": "cs_local", "secret": "local-secret"},
request=httpx.Request("POST", url),
)

monkeypatch.setattr(community.httpx, "post", post)

result = community.register_try("local", endpoint="http://localhost:8080/")

assert result == {"tenant_id": "cs_local", "secret": "local-secret"}
assert seen == [("http://localhost:8080/api/v1/register", {"label": "local"}, 10.0)]
Loading