autonoma-ai 0.2.8.dev1784101031__tar.gz → 0.2.9__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. {autonoma_ai-0.2.8.dev1784101031 → autonoma_ai-0.2.9}/PKG-INFO +1 -1
  2. {autonoma_ai-0.2.8.dev1784101031 → autonoma_ai-0.2.9}/pyproject.toml +7 -1
  3. autonoma_ai-0.2.9/src/autonoma/AGENTS.md +21 -0
  4. {autonoma_ai-0.2.8.dev1784101031 → autonoma_ai-0.2.9}/src/autonoma/check.py +0 -1
  5. autonoma_ai-0.2.9/src/autonoma/docs/factories.md +128 -0
  6. autonoma_ai-0.2.9/src/autonoma/docs/implement.md +196 -0
  7. autonoma_ai-0.2.9/src/autonoma/docs/llms.txt +14 -0
  8. autonoma_ai-0.2.9/src/autonoma/docs/overview.md +83 -0
  9. autonoma_ai-0.2.9/src/autonoma/docs/protocol.md +136 -0
  10. autonoma_ai-0.2.9/src/autonoma/docs/scenarios.md +109 -0
  11. autonoma_ai-0.2.9/src/autonoma/docs/validation.md +110 -0
  12. {autonoma_ai-0.2.8.dev1784101031 → autonoma_ai-0.2.9}/src/autonoma/errors.py +1 -0
  13. {autonoma_ai-0.2.8.dev1784101031 → autonoma_ai-0.2.9}/src/autonoma/handler.py +0 -6
  14. {autonoma_ai-0.2.8.dev1784101031 → autonoma_ai-0.2.9}/src/autonoma/types.py +12 -0
  15. {autonoma_ai-0.2.8.dev1784101031 → autonoma_ai-0.2.9}/LICENSE +0 -0
  16. {autonoma_ai-0.2.8.dev1784101031 → autonoma_ai-0.2.9}/README.md +0 -0
  17. {autonoma_ai-0.2.8.dev1784101031 → autonoma_ai-0.2.9}/src/autonoma/__init__.py +0 -0
  18. {autonoma_ai-0.2.8.dev1784101031 → autonoma_ai-0.2.9}/src/autonoma/factory.py +0 -0
  19. {autonoma_ai-0.2.8.dev1784101031 → autonoma_ai-0.2.9}/src/autonoma/fingerprint.py +0 -0
  20. {autonoma_ai-0.2.8.dev1784101031 → autonoma_ai-0.2.9}/src/autonoma/generated/__init__.py +0 -0
  21. {autonoma_ai-0.2.8.dev1784101031 → autonoma_ai-0.2.9}/src/autonoma/generated/sql_queries.py +0 -0
  22. {autonoma_ai-0.2.8.dev1784101031 → autonoma_ai-0.2.9}/src/autonoma/graph.py +0 -0
  23. {autonoma_ai-0.2.8.dev1784101031 → autonoma_ai-0.2.9}/src/autonoma/hmac_util.py +0 -0
  24. {autonoma_ai-0.2.8.dev1784101031 → autonoma_ai-0.2.9}/src/autonoma/payload_topo.py +0 -0
  25. {autonoma_ai-0.2.8.dev1784101031 → autonoma_ai-0.2.9}/src/autonoma/refs.py +0 -0
  26. {autonoma_ai-0.2.8.dev1784101031 → autonoma_ai-0.2.9}/src/autonoma/schema.py +0 -0
  27. {autonoma_ai-0.2.8.dev1784101031 → autonoma_ai-0.2.9}/src/autonoma/serializer.py +0 -0
  28. {autonoma_ai-0.2.8.dev1784101031 → autonoma_ai-0.2.9}/src/autonoma_django/__init__.py +0 -0
  29. {autonoma_ai-0.2.8.dev1784101031 → autonoma_ai-0.2.9}/src/autonoma_django/server.py +0 -0
  30. {autonoma_ai-0.2.8.dev1784101031 → autonoma_ai-0.2.9}/src/autonoma_fastapi/__init__.py +0 -0
  31. {autonoma_ai-0.2.8.dev1784101031 → autonoma_ai-0.2.9}/src/autonoma_fastapi/server.py +0 -0
  32. {autonoma_ai-0.2.8.dev1784101031 → autonoma_ai-0.2.9}/src/autonoma_flask/__init__.py +0 -0
  33. {autonoma_ai-0.2.8.dev1784101031 → autonoma_ai-0.2.9}/src/autonoma_flask/server.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: autonoma-ai
3
- Version: 0.2.8.dev1784101031
3
+ Version: 0.2.9
4
4
  Summary: Autonoma SDK — automate the Autonoma Environment Factory endpoint
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
4
4
 
5
5
  [tool.poetry]
6
6
  name = "autonoma-ai"
7
- version = "0.2.8.dev1784101031"
7
+ version = "0.2.9"
8
8
  description = "Autonoma SDK — automate the Autonoma Environment Factory endpoint"
9
9
  authors = ["Autonoma AI <eng@autonoma.ai>"]
10
10
  license = "MIT"
@@ -27,6 +27,12 @@ packages = [
27
27
  { include = "autonoma_fastapi", from = "src" },
28
28
  { include = "autonoma_django", from = "src" },
29
29
  ]
30
+ # Bundle the agent-facing docs (Markdown) as package data inside `autonoma`, so
31
+ # they land at site-packages/autonoma/docs/ on install.
32
+ include = [
33
+ { path = "src/autonoma/docs/**/*", format = ["sdist", "wheel"] },
34
+ { path = "src/autonoma/AGENTS.md", format = ["sdist", "wheel"] },
35
+ ]
30
36
 
31
37
  [tool.poetry.extras]
32
38
  flask = ["flask"]
@@ -0,0 +1,21 @@
1
+ <!-- Generated by scripts/build-sdk-docs.mjs from docs/languages/python/AGENTS.md. Edit the source, not this copy. -->
2
+ <!-- BEGIN:autonoma-agent-rules -->
3
+
4
+ # Autonoma SDK: read the bundled docs before wiring the endpoint
5
+
6
+ This package implements the **Autonoma Environment Factory** - a backend endpoint that creates and tears down isolated test data. If you are integrating it, the accurate, version-matched docs ship inside this package. Read them before writing code; your training data may describe an older, adapter-based API that no longer exists.
7
+
8
+ **Start here:** `./docs/implement.md` (in `site-packages/autonoma/docs/implement.md` once installed).
9
+
10
+ Reading order:
11
+
12
+ 1. `docs/overview.md` - what the Environment Factory is and why it is factory-driven.
13
+ 2. `docs/implement.md` - step-by-step setup: install, factories, handler, auth, validate.
14
+ 3. `docs/factories.md` - the `define_factory` contract in Python.
15
+ 4. `docs/scenarios.md` - the `create` data format (`_alias`/`_ref`).
16
+ 5. `docs/protocol.md` - the HTTP wire protocol and error codes.
17
+ 6. `docs/validation.md` - dry-running scenarios with `check_scenario`.
18
+
19
+ Key facts that differ from older docs: the SDK is **factory-driven** (register factories with `define_factory`; there is no `prisma_adapter`/SQLAlchemy adapter and no ORM adapter package). The distribution is `autonoma-ai`; the import package is `autonoma`. Each factory declares a Pydantic v2 `input_model` (there is no database introspection). The main entry is `handle_request`, wrapped by a server adapter (`create_fastapi_handler`, `create_flask_handler`, `create_django_handler`). The auth callback returns a dict with `cookies`, `headers`, and/or `credentials` - there is no `token` field.
20
+
21
+ <!-- END:autonoma-agent-rules -->
@@ -51,7 +51,6 @@ async def check_scenario(
51
51
  signing_secret=signing_secret,
52
52
  auth=options.get("auth", lambda _user, _ctx: {"headers": {"Authorization": "Bearer check-token"}}),
53
53
  factories=factories,
54
- allow_production=True,
55
54
  )
56
55
 
57
56
  up_body = json.dumps({"action": "up", "create": scenario.get("create", {})})
@@ -0,0 +1,128 @@
1
+ <!-- Generated by scripts/build-sdk-docs.mjs from docs/languages/python/factories.md. Edit the source, not this copy. -->
2
+ # Writing factories (Python)
3
+
4
+ A factory tells the SDK how to create and delete one model using your own code. You register one factory per model the platform can create, and pass them all to the handler as `factories`. This page is the exact contract; read it before writing any.
5
+
6
+ ## The shape
7
+
8
+ ```python
9
+ # factories/organization.py
10
+ from pydantic import BaseModel, ConfigDict
11
+ from autonoma import define_factory
12
+ from app.db import db # your app's real database client
13
+
14
+ class OrganizationInput(BaseModel):
15
+ model_config = ConfigDict(extra="ignore")
16
+ name: str
17
+ slug: str
18
+
19
+ def _create(data, ctx):
20
+ org = db.create_organization(name=data.name, slug=data.slug)
21
+ return {"id": str(org.id), "name": org.name, "slug": org.slug}
22
+
23
+ def _teardown(record, ctx):
24
+ db.delete_organization(record["id"])
25
+
26
+ Organization = define_factory(
27
+ create=_create,
28
+ input_model=OrganizationInput,
29
+ teardown=_teardown,
30
+ )
31
+ ```
32
+
33
+ `define_factory(create, input_model, teardown=None, ref_model=None)` validates the shape at startup and returns a `FactoryDefinition`. Import it from the top-level `autonoma` package. `input_model` is a Pydantic v2 class - Pydantic ships as a hard dependency of the SDK, so there is nothing extra to install.
34
+
35
+ ## input_model (required)
36
+
37
+ A Pydantic v2 `BaseModel` describing the fields this model accepts in the create payload. It does two jobs:
38
+
39
+ 1. The SDK validates each incoming record with `input_model.model_validate(...)` before calling `create`.
40
+ 2. The SDK derives the discover schema from it - there is no database introspection, so this model is how the platform learns your model exists and what fields it has.
41
+
42
+ Set `model_config = ConfigDict(extra="ignore")` so recipe-only metadata the platform may attach does not trip validation.
43
+
44
+ **Include every foreign key in the input model, including the scope field.** By the time `create` runs, the SDK has already resolved every `_ref` to the real ID of the referenced record, so a FK arrives as a plain value:
45
+
46
+ ```python
47
+ # factories/user.py
48
+ from pydantic import BaseModel, ConfigDict
49
+ from autonoma import define_factory
50
+ from app.signup import create_user # reuse your real signup code
51
+
52
+ class UserInput(BaseModel):
53
+ model_config = ConfigDict(extra="ignore")
54
+ name: str
55
+ email: str
56
+ organizationId: str # arrives as the real Organization id, not a _ref
57
+
58
+ def _create(data, ctx):
59
+ user = create_user(name=data.name, email=data.email, org_id=data.organizationId)
60
+ return {"id": str(user.id), "email": user.email}
61
+
62
+ User = define_factory(create=_create, input_model=UserInput)
63
+ ```
64
+
65
+ ## create(data, ctx)
66
+
67
+ Creates exactly one record and returns it. It may be a plain `def` or an `async def` - the SDK awaits the result when it is a coroutine.
68
+
69
+ - `data` - a validated **instance** of `input_model`, so read fields as attributes (`data.name`, `data.organizationId`). FK fields are already real IDs.
70
+ - `ctx` - a `FactoryContext` with `refs`, `scenario_name`, and `test_run_id`. `refs` holds every record created so far this run, keyed by model, if you need to look something up.
71
+ - **Return value** - a `dict` that includes at least the primary key `id`, or a Pydantic instance (the SDK calls `model_dump()` on it). If the returned record has no `id`, the SDK fails the request with `FACTORY_MISSING_PK`. Everything you return is stored in `refs`, passed to the auth callback, and later handed to `teardown` - so return whatever teardown or auth will need (typically the id, plus fields like `email`).
72
+
73
+ Reuse your application's real creation path (`create_user`, `create_organization`, a service method). That is the entire point: the test user gets the same password hash, defaults, and side effects a real user would.
74
+
75
+ ## teardown(record, ctx) - optional
76
+
77
+ Deletes one record. The SDK calls it once per created record, in reverse dependency order, during `down`. It may be sync or async.
78
+
79
+ - `record` - exactly what your `create` returned: a `dict` (so `record["id"]`), unless you set `ref_model`, in which case it is a validated instance (so `record.id`).
80
+ - If you omit `teardown`, the model is never deleted on `down` - the SDK has no SQL fallback. Provide it for every model you create, or those rows leak.
81
+
82
+ ```python
83
+ # factories/user.py
84
+ def _teardown(record, ctx):
85
+ db.delete_user(record["id"])
86
+
87
+ User = define_factory(create=_create, input_model=UserInput, teardown=_teardown)
88
+ ```
89
+
90
+ ## ref_model - optional
91
+
92
+ A Pydantic v2 class for the record `create` returns. When set, the SDK validates the stored record through `ref_model.model_validate(record)` before `teardown`, and `record` arrives as an instance (attribute access, no dict lookups):
93
+
94
+ ```python
95
+ # factories/user.py
96
+ class UserRef(BaseModel):
97
+ id: str
98
+ email: str
99
+
100
+ def _teardown(record, ctx):
101
+ db.delete_user(record.id) # record is a UserRef instance
102
+
103
+ User = define_factory(
104
+ create=_create,
105
+ input_model=UserInput,
106
+ teardown=_teardown,
107
+ ref_model=UserRef,
108
+ )
109
+ ```
110
+
111
+ ## Registering factories
112
+
113
+ Collect every factory into one `dict` keyed by model name - the key must match the model name the platform sends in `create`:
114
+
115
+ ```python
116
+ # factories/__init__.py
117
+ from factories.organization import Organization
118
+ from factories.user import User
119
+ from factories.member import Member
120
+
121
+ factories = {
122
+ "Organization": Organization,
123
+ "User": User,
124
+ "Member": Member,
125
+ }
126
+ ```
127
+
128
+ Pass that dict as `factories` when you create the handler (see `implement.md`). Every model that appears in a scenario must have an entry here, or the request fails with `INVALID_BODY` ("no factory registered for model ...").
@@ -0,0 +1,196 @@
1
+ <!-- Generated by scripts/build-sdk-docs.mjs from docs/languages/python/implement.md. Edit the source, not this copy. -->
2
+ # Implement the endpoint (Python)
3
+
4
+ Follow these steps to stand up a working Environment Factory endpoint. This is written for a coding agent doing the integration; do the steps in order and do not skip the validation step.
5
+
6
+ ## Prerequisites
7
+
8
+ - A Python 3.10+ backend on FastAPI, Flask, or Django.
9
+ - The database client your app already uses (SQLAlchemy, Django ORM, raw `psycopg` - it does not matter; your factories call it).
10
+ - Pydantic v2. It ships as a hard dependency of the SDK, so it installs automatically.
11
+
12
+ ## Step 1 - Detect the stack and pick packages
13
+
14
+ Install the core distribution `autonoma-ai` with the extra that matches the app's HTTP framework. There is no ORM adapter package to install - the SDK is factory-driven.
15
+
16
+ | Framework | Install command | Handler module | Handler function |
17
+ |-----------|-----------------|----------------|------------------|
18
+ | FastAPI | `pip install "autonoma-ai[fastapi]"` | `autonoma_fastapi` | `create_fastapi_handler` |
19
+ | Flask | `pip install "autonoma-ai[flask]"` | `autonoma_flask` | `create_flask_handler` |
20
+ | Django | `pip install "autonoma-ai[django]"` | `autonoma_django` | `create_django_handler` |
21
+
22
+ ```bash
23
+ # terminal - example: FastAPI
24
+ pip install "autonoma-ai[fastapi]"
25
+ ```
26
+
27
+ The import package is `autonoma` (plus the framework module), even though the distribution is named `autonoma-ai`. Use the project's package manager (pip, poetry, uv) as appropriate; `pip install "autonoma-ai[all]"` pulls every adapter.
28
+
29
+ ## Step 2 - Generate the two secrets
30
+
31
+ ```bash
32
+ # terminal
33
+ openssl rand -hex 32 # AUTONOMA_SHARED_SECRET
34
+ openssl rand -hex 32 # AUTONOMA_SIGNING_SECRET (must be different)
35
+ ```
36
+
37
+ Add both to your environment (and placeholders to `.env.example` if it exists). The SDK raises `SAME_SECRETS` if they match.
38
+
39
+ ```bash
40
+ # .env
41
+ AUTONOMA_SHARED_SECRET=... # shared with Autonoma
42
+ AUTONOMA_SIGNING_SECRET=... # private, never shared
43
+ ```
44
+
45
+ ## Step 3 - Find the scope field
46
+
47
+ Read the database schema. Find the foreign key that appears on the most models and points at a single root entity - commonly `organizationId`, `orgId`, `tenantId`, or `workspaceId`. That is the scope field. The root model itself (e.g. `Organization`) does not carry it.
48
+
49
+ Confirm the field, the endpoint path, and the app's auth mechanism with the user before writing code.
50
+
51
+ ## Step 4 - Write a factory per model
52
+
53
+ Write one factory for each model the platform will create, calling your app's real creation code. See `factories.md` for the full contract. Collect them into one registry keyed by model name:
54
+
55
+ ```python
56
+ # factories/__init__.py
57
+ from factories.organization import Organization
58
+ from factories.user import User
59
+ from factories.member import Member
60
+
61
+ factories = {"Organization": Organization, "User": User, "Member": Member}
62
+ ```
63
+
64
+ ## Step 5 - Wire the handler
65
+
66
+ Create one `HandlerConfig` and pass it to your adapter's handler function. The config carries the scope field, both secrets, the factory registry, and the auth callback. All config fields are `snake_case`.
67
+
68
+ ```python
69
+ # app/autonoma_endpoint.py (FastAPI)
70
+ import os
71
+ from autonoma import HandlerConfig
72
+ from autonoma_fastapi import create_fastapi_handler
73
+ from factories import factories
74
+ from app.auth import create_session # your app's real session code
75
+
76
+ config = HandlerConfig(
77
+ scope_field="organizationId",
78
+ shared_secret=os.environ["AUTONOMA_SHARED_SECRET"],
79
+ signing_secret=os.environ["AUTONOMA_SIGNING_SECRET"],
80
+ factories=factories,
81
+ auth=lambda user, ctx: {
82
+ "cookies": [
83
+ {"name": "session", "value": create_session(user["id"]),
84
+ "httpOnly": True, "sameSite": "lax", "path": "/"}
85
+ ],
86
+ },
87
+ )
88
+
89
+ router = create_fastapi_handler(config)
90
+ app.include_router(router, prefix="/api/autonoma")
91
+ ```
92
+
93
+ Other frameworks use the same `config` object; only the mounting differs:
94
+
95
+ ```python
96
+ # app/autonoma_endpoint.py (Flask)
97
+ from autonoma_flask import create_flask_handler
98
+
99
+ bp = create_flask_handler(config)
100
+ app.register_blueprint(bp, url_prefix="/api/autonoma")
101
+ ```
102
+
103
+ ```python
104
+ # urls.py (Django)
105
+ from django.urls import path
106
+ from autonoma_django import create_django_handler
107
+ from app.autonoma_endpoint import config
108
+
109
+ urlpatterns = [
110
+ path("api/autonoma", create_django_handler(config)),
111
+ ]
112
+ ```
113
+
114
+ The Flask and FastAPI adapters register their route at `/`, so with a `/api/autonoma` prefix the live path is `/api/autonoma/` (trailing slash). With Django you control the exact path in `urls.py`.
115
+
116
+ FastAPI also accepts `create_fastapi_handler(config_factory=make_config)` - a zero-arg callable that returns a fresh `HandlerConfig` per request, useful when a factory needs per-request state such as a database session.
117
+
118
+ ## Step 6 - Implement the auth callback
119
+
120
+ This is the part that most often breaks tests, so get it right. The callback receives the first created `User` record as a `dict` (or `None` if the scenario made none) and an `AuthContext` with `scope_value` and `refs`. It must return **real, working credentials** using the app's actual auth mechanism. If it returns a fake or hardcoded token, every test fails at login. The callback may be sync or async (`async def`).
121
+
122
+ The return type is a dict with any of `cookies`, `headers`, `credentials` - there is no top-level `token` field. Pick the shape that matches how your app authenticates:
123
+
124
+ ```python
125
+ # app/autonoma_endpoint.py
126
+ # Session cookie (most web apps)
127
+ def auth(user, ctx):
128
+ token = create_session(user["id"])
129
+ return {"cookies": [
130
+ {"name": "session", "value": token, "httpOnly": True, "sameSite": "lax", "path": "/"}
131
+ ]}
132
+
133
+ # JWT bearer token (APIs, SPAs) - the token goes in a header
134
+ def auth(user, ctx):
135
+ token = jwt.encode({"sub": user["id"]}, os.environ["JWT_SECRET"], algorithm="HS256")
136
+ return {"headers": {"Authorization": f"Bearer {token}"}}
137
+
138
+ # Email + password (the runner logs in through the UI, e.g. mobile)
139
+ def auth(user, ctx):
140
+ return {"credentials": {"email": user["email"], "password": "test-password-123"}}
141
+ ```
142
+
143
+ For the email/password shape, the `User` factory must create the record with a matching password hash, so a real login succeeds.
144
+
145
+ ## Step 7 - The endpoint is always enabled
146
+
147
+ There is no on/off switch: HMAC signing is the gate, so unsigned or tampered requests get `401` no matter where the endpoint runs. On Autonoma preview environments (`AUTONOMA_PREVIEWKIT` is set) no extra guard is needed - previews are isolated and never production. If you deploy the factory in your own environments and want it dark in production anyway, gate it in your handler with your own condition:
148
+
149
+ ```python
150
+ # app/autonoma_endpoint.py (your route, before delegating to the SDK)
151
+ if os.environ.get("APP_ENV") == "production":
152
+ raise HTTPException(status_code=404) # or your framework's 404
153
+ ```
154
+
155
+ The old `allow_production` config option is deprecated and ignored - passing it changes nothing.
156
+
157
+ ## Step 8 - Validate before deploying
158
+
159
+ Dry-run your scenarios against a real database with `check_scenario` and iterate until they pass. See `validation.md`. Never ship a scenario you have not validated.
160
+
161
+ ## Step 9 - Smoke-test with curl
162
+
163
+ ```bash
164
+ # terminal
165
+ SECRET="your-shared-secret"
166
+ BODY='{"action":"discover"}'
167
+ SIG=$(echo -n "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | sed 's/.*= //')
168
+ curl -s -X POST http://localhost:8000/api/autonoma/ \
169
+ -H "Content-Type: application/json" -H "x-signature: $SIG" -d "$BODY" | jq .
170
+ ```
171
+
172
+ Expected: a JSON schema listing your models and `scopeField`. A `404` means the route is not mounted; a `401` means the secret does not match.
173
+
174
+ ## Step 10 - Report and connect
175
+
176
+ Tell the user the endpoint path, confirm all scenarios pass, and hand off:
177
+
178
+ 1. Set `AUTONOMA_SHARED_SECRET` and `AUTONOMA_SIGNING_SECRET` in staging/production env.
179
+ 2. Deploy the endpoint.
180
+ 3. Paste `AUTONOMA_SHARED_SECRET` into the Autonoma dashboard when connecting the app.
181
+
182
+ ## Rules
183
+
184
+ **Do:**
185
+ - Reuse the app's existing DB client and real creation code inside factories.
186
+ - Return real credentials from `auth` using the app's own session/JWT logic.
187
+ - Register a factory (with a `teardown`) for every model any scenario creates.
188
+ - Match the project's conventions: import style, file layout, naming.
189
+ - Validate every scenario with `check_scenario` before deploying.
190
+
191
+ **Do not:**
192
+ - Implement HMAC, token signing, or teardown ordering yourself - the SDK owns all of it.
193
+ - Return a hardcoded token like `"test-token"` from `auth`.
194
+ - Use the same value for `shared_secret` and `signing_secret`.
195
+ - Set `id`, defaulted fields, or auto timestamps in scenario data.
196
+ - Expect the SDK to inject the scope field or wire any FK - you set every FK as a `_ref`.
@@ -0,0 +1,14 @@
1
+ # Autonoma Environment Factory SDK
2
+
3
+ > This package implements the Autonoma Environment Factory: a single backend endpoint that creates isolated, factory-driven test data before an end-to-end test run and tears it down afterward. These docs are bundled inside the installed package so a coding agent can read the API that matches this exact version, instead of relying on training data.
4
+
5
+ If you are setting up the endpoint, read `implement.md` and follow it step by step. The other files are reference.
6
+
7
+ ## Reading order
8
+
9
+ 1. overview.md - What the Environment Factory is, the three actions (discover/up/down), the scope field, the two secrets, and why it is factory-driven. Start here.
10
+ 2. implement.md - Step-by-step setup for this language: install, register factories, wire the handler, implement the auth callback, enable the endpoint, and validate. This is the doc to follow when integrating.
11
+ 3. factories.md - How to write a factory in this language: the `create`/`teardown` contract, the input schema, and what `create` receives and must return.
12
+ 4. scenarios.md - The `create` data format: the flat map, `_alias`/`_ref` links, the scope field, and the three standard scenarios. Language-agnostic.
13
+ 5. protocol.md - The HTTP wire protocol: authentication, the three actions, the refs token, the safety model, and every error code. Language-agnostic. Reach for this when debugging.
14
+ 6. validation.md - How to dry-run a scenario against a real database in this language before deploying.
@@ -0,0 +1,83 @@
1
+ <!-- Generated by scripts/build-sdk-docs.mjs from docs/shared/overview.md. Edit the source, not this copy. -->
2
+ # Overview
3
+
4
+ The Autonoma Environment Factory is a single endpoint in your backend that creates fresh, isolated test data before an end-to-end test run and deletes it afterward. This SDK implements that endpoint for you.
5
+
6
+ ## Why it exists
7
+
8
+ Every end-to-end test needs data. A test for "user adds an item to the cart" needs a user, some products, and a cart. A test for "admin views analytics" needs an organization with users, runs, and history.
9
+
10
+ Tests cannot share data. If one test deletes a product, another test that expects ten products fails. Running tests in parallel makes this worse. The Environment Factory gives each test run its own organization, its own users, and its own records, then tears all of it down when the run finishes. No interference, no leftover rows.
11
+
12
+ ## How it works
13
+
14
+ The platform talks to one HTTP POST endpoint that you mount in your app (you choose the path, e.g. `/api/autonoma`). The endpoint handles three actions:
15
+
16
+ | Action | What the platform asks | What the SDK does |
17
+ |--------|------------------------|-------------------|
18
+ | `discover` | "What models can you create?" | Returns the model + field list, derived from your registered factories. |
19
+ | `up` | "Create this data." | Runs each record through its factory in dependency order, returns login credentials and a signed teardown token. |
20
+ | `down` | "Delete what you created." | Verifies the token and calls each factory's teardown in reverse order. |
21
+
22
+ You do not write routing, signature checks, dependency sorting, or teardown logic. You write factories and an auth callback, and the SDK does the rest.
23
+
24
+ ## Factory-driven: you own every write
25
+
26
+ This SDK never runs SQL and never touches your database directly. Every model it creates goes through a factory you register - a small function that calls your own code to make one record. This is the core design decision, and it has two consequences worth internalizing before you write anything:
27
+
28
+ - **Test data is created the way production data is.** If your app hashes passwords in `createUser`, your factory calls `createUser`, so the test user has a real password hash. A raw database seed could never reproduce that. Factories reuse your real creation logic - password hashing, external-service calls, state-machine defaults - instead of guessing at column values.
29
+ - **The endpoint is safe by construction.** `up` can only insert (through your factories). `down` can only delete records that `up` created (proven by a signed token). The SDK has no code path that can `UPDATE`, `DROP`, `TRUNCATE`, or run an arbitrary query. See [protocol.md](protocol.md) for the full safety model.
30
+
31
+ There is no ORM adapter, no schema introspection, and no SQL fallback. A model with no registered factory simply cannot be created.
32
+
33
+ ## What you provide
34
+
35
+ Four things, passed as configuration when you create the handler:
36
+
37
+ | Config | What it is |
38
+ |--------|------------|
39
+ | `scopeField` | The single column name that isolates test data (e.g. `organizationId`). |
40
+ | `factories` | One factory per model the platform can create. |
41
+ | `sharedSecret` | Shared with Autonoma. Verifies that incoming requests are authentic (HMAC). |
42
+ | `signingSecret` | Known only to you. Signs the teardown token so it cannot be forged. |
43
+ | `auth` | A callback that returns real login credentials for the created test user. |
44
+
45
+ ## The scope field
46
+
47
+ Most multi-tenant apps have one foreign key on nearly every model - `organizationId`, `orgId`, `tenantId`, `workspaceId`. That column is your scope field: the boundary that keeps one test run's data separate from another's.
48
+
49
+ Two things to know:
50
+
51
+ - **You set it; the SDK does not.** The SDK never injects the scope field into a record. Every model that has it must set it explicitly in the create payload, normally as a `_ref` pointing at the scope record. Your factory writes it. The SDK only *reads* the scope field after creation, to derive the value it hands your auth callback.
52
+ - **It must be a single column.** Compound scope keys (e.g. `organizationId` + `countryId`) are not supported. If your app scopes by several fields, use one root entity whose ID the child models reference. If your app is not multi-tenant at all, add a dedicated field like `testRunId` to every model and use that.
53
+
54
+ ## The two secrets
55
+
56
+ You need two different secret values. The SDK throws `SAME_SECRETS` at startup if they match.
57
+
58
+ - **Shared secret** - both you and Autonoma hold it. Autonoma signs every request with it (HMAC-SHA256); your endpoint verifies the signature. This stops anyone else from calling your endpoint.
59
+ - **Signing secret** - only you hold it. During `up` the SDK signs a token listing every created record's ID. During `down` it verifies that token before deleting. Autonoma stores the token opaquely and passes it back; it can neither read nor forge it.
60
+
61
+ Generate them as two distinct random values:
62
+
63
+ ```bash
64
+ openssl rand -hex 32 # AUTONOMA_SHARED_SECRET
65
+ openssl rand -hex 32 # AUTONOMA_SIGNING_SECRET (must differ)
66
+ ```
67
+
68
+ ## Language availability
69
+
70
+ The SDK ships for eight languages, each an independent implementation that passes the same conformance suite. The protocol behavior is identical everywhere; only package names and syntax differ.
71
+
72
+ | Language | Server adapters |
73
+ |----------|-----------------|
74
+ | TypeScript | Web standard (Next.js App Router, Bun, Deno), Express, Hono, Node http |
75
+ | Python | FastAPI, Flask, Django |
76
+ | Elixir | Plug (Phoenix) |
77
+ | PHP | Laravel |
78
+ | Java | Spring Boot |
79
+ | Ruby | Rails / Rack |
80
+ | Rust | Actix, Axum |
81
+ | Go | Gin |
82
+
83
+ See `implement.md` for the step-by-step setup in your language, `factories.md` for how to write factories, `scenarios.md` for the data format, and `protocol.md` for the wire protocol.
@@ -0,0 +1,136 @@
1
+ <!-- Generated by scripts/build-sdk-docs.mjs from docs/shared/protocol.md. Edit the source, not this copy. -->
2
+ # Protocol
3
+
4
+ The Environment Factory protocol is a request-response protocol over a single HTTP POST endpoint, authenticated with HMAC-SHA256. It is identical across every language SDK. This page is the wire contract; you rarely need it to integrate, but it is the source of truth when debugging.
5
+
6
+ Protocol version: `1.0`
7
+
8
+ ## Transport and authentication
9
+
10
+ All communication is HTTP POST to one URL that you choose. Requests and responses are JSON.
11
+
12
+ Every request carries an `x-signature` header: the HMAC-SHA256 of the raw request body, keyed with the shared secret, as a hex digest. The SDK recomputes the HMAC over the exact request bytes (before any JSON parsing) and compares in constant time. A missing or wrong signature is rejected with `401`.
13
+
14
+ ```
15
+ x-signature: HMAC-SHA256(raw_request_body_bytes, sharedSecret) -> hex
16
+ ```
17
+
18
+ Because the signature is over the raw bytes, a server adapter must read the body as an untouched string before framework middleware reparses it. This is the one subtle requirement server adapters exist to satisfy.
19
+
20
+ ## Response metadata
21
+
22
+ Every successful response includes:
23
+
24
+ ```json
25
+ {
26
+ "version": "1.0",
27
+ "sdk": { "language": "typescript", "orm": "unknown", "server": "web" }
28
+ }
29
+ ```
30
+
31
+ `orm` is `"unknown"` in factory-driven mode - the SDK does not talk to an ORM. Error responses replace the payload with `{ "error": "...", "code": "..." }`.
32
+
33
+ ## Actions
34
+
35
+ ### discover
36
+
37
+ ```json
38
+ // request
39
+ { "action": "discover" }
40
+ ```
41
+
42
+ ```json
43
+ // response 200
44
+ {
45
+ "version": "1.0",
46
+ "sdk": { "language": "typescript", "orm": "unknown", "server": "web" },
47
+ "schema": {
48
+ "models": [
49
+ { "name": "Organization", "tableName": "organization", "fields": [
50
+ { "name": "name", "type": "string", "isRequired": true, "isId": false, "hasDefault": false }
51
+ ] }
52
+ ],
53
+ "edges": [],
54
+ "relations": [],
55
+ "scopeField": "organizationId"
56
+ }
57
+ }
58
+ ```
59
+
60
+ The schema is built from your factories' input schemas, not from database introspection. `models` and `scopeField` are the meaningful parts. `edges` and `relations` are always empty arrays - the SDK does not introspect foreign keys, so it emits nothing there. Dependency order is not carried in the schema; it is derived per request from the `_alias`/`_ref` graph in the `up` payload.
61
+
62
+ ### up
63
+
64
+ ```json
65
+ // request
66
+ {
67
+ "action": "up",
68
+ "testRunId": "optional-uuid",
69
+ "create": {
70
+ "Organization": [{ "_alias": "org", "name": "Acme", "slug": "acme" }],
71
+ "User": [{ "_alias": "alice", "email": "alice@test.com", "organizationId": { "_ref": "org" } }],
72
+ "Member": [{ "role": "owner", "organizationId": { "_ref": "org" }, "userId": { "_ref": "alice" } }]
73
+ }
74
+ }
75
+ ```
76
+
77
+ `create` is a flat map keyed by model name; each value is an array of records. Cross-model links use `_alias`/`_ref` (see [scenarios.md](scenarios.md)). If `testRunId` is omitted the SDK generates a UUID.
78
+
79
+ ```json
80
+ // response 200
81
+ {
82
+ "version": "1.0",
83
+ "sdk": { "language": "typescript", "orm": "unknown", "server": "web" },
84
+ "auth": { "cookies": [], "headers": {}, "credentials": {} },
85
+ "refs": { "Organization": [{ "id": "..." }], "User": [{ "id": "..." }] },
86
+ "refsToken": "header.payload.signature"
87
+ }
88
+ ```
89
+
90
+ - `auth` - whatever your auth callback returned: `cookies`, `headers`, and/or `credentials`. There is no top-level `token` field; a bearer token goes in `headers` (e.g. `{ "Authorization": "Bearer ..." }`).
91
+ - `refs` - every created record, keyed by model, exactly as each factory returned it.
92
+ - `refsToken` - a signed token encoding the created IDs, passed back on `down`.
93
+
94
+ ### down
95
+
96
+ ```json
97
+ // request
98
+ { "action": "down", "refsToken": "header.payload.signature" }
99
+ ```
100
+
101
+ ```json
102
+ // response 200
103
+ { "version": "1.0", "sdk": { ... }, "ok": true }
104
+ ```
105
+
106
+ The SDK verifies the token, then calls each factory's `teardown` in reverse dependency order. A model whose factory defines no teardown is skipped.
107
+
108
+ ## The refs token
109
+
110
+ The refs token is a JWT-like structure (`header.payload.signature`) signed with your signing secret. It carries the scope value, every created record ID keyed by model, and the alias dependency graph needed to reverse the create order. Because only your backend knows the signing secret, neither the platform nor any third party can forge or alter it. This is what guarantees `down` can only delete what `up` created.
111
+
112
+ ## Safety model
113
+
114
+ The protocol enforces five hard constraints:
115
+
116
+ 1. **The production guard is yours.** The SDK has no on/off switch - on Autonoma preview environments (`AUTONOMA_PREVIEWKIT` is set) no guard is needed, previews are isolated and never production. In your own deployments, mount the route only outside production with a condition you own (for example, register the endpoint only when `NODE_ENV` is not `production`). The old `allowProduction` option is deprecated and ignored.
117
+ 2. **Up can only create.** Every record routes through a factory's `create`. The SDK never updates, deletes, drops, truncates, or runs SQL of its own.
118
+ 3. **Down can only delete what up created.** The signed token names the exact record IDs. `down` verifies it before deleting and touches nothing else.
119
+ 4. **Requests are authenticated.** Every request is HMAC-signed with the shared secret. Unsigned or tampered requests get `401` - the endpoint serves no unauthenticated caller.
120
+ 5. **Factory-driven writes.** There is no executor and no SQL fallback. A factory body may run a raw insert internally, but that code is yours, not the SDK's.
121
+
122
+ ## Error codes
123
+
124
+ | Code | HTTP | Meaning |
125
+ |------|------|---------|
126
+ | `INVALID_SIGNATURE` | 401 | HMAC signature missing or does not match the shared secret. |
127
+ | `INVALID_BODY` | 400 | Body is not valid JSON, is missing `action`/`create`, or references an unknown alias. |
128
+ | `UNKNOWN_ACTION` | 400 | `action` is not `discover`, `up`, or `down`. |
129
+ | `UNKNOWN_ENVIRONMENT` | 400 | The requested environment name does not exist. |
130
+ | `INVALID_REFS_TOKEN` | 403 | The refs token is missing, malformed, or failed signature verification. |
131
+ | `UNRESOLVED_TOKEN` | 400 | A literal `{{...}}` placeholder reached the SDK unresolved. |
132
+ | `FACTORY_MISSING_PK` | 500 | A factory's `create` returned a record without its primary key. |
133
+ | `SAME_SECRETS` | 500 | `sharedSecret` and `signingSecret` are the same value. |
134
+ | `INTERNAL_ERROR` | 500 | Unexpected error, or a create payload failed a factory's input schema. |
135
+
136
+ These are the shared codes. A given language may surface an extra, more specific code for a failure another language folds into `INTERNAL_ERROR` (for example, some SDKs emit `FACTORY_TEARDOWN_ERROR` when a factory's teardown throws). See your language's `validation.md` for any language-specific codes.
@@ -0,0 +1,109 @@
1
+ <!-- Generated by scripts/build-sdk-docs.mjs from docs/shared/scenarios.md. Edit the source, not this copy. -->
2
+ # Scenario data
3
+
4
+ Scenario data describes the records a test run needs. The platform generates it from your discover schema, but you write it yourself for integration tests and when debugging. The format is identical across every language SDK.
5
+
6
+ ## The shape: a flat map
7
+
8
+ Data is a flat map. Top-level keys are model names; each value is an array of records to create for that model.
9
+
10
+ ```json
11
+ {
12
+ "create": {
13
+ "Organization": [{ "_alias": "org", "name": "Test Org", "slug": "test-org" }],
14
+ "User": [{ "_alias": "admin", "name": "Admin", "email": "admin@test.com", "organizationId": { "_ref": "org" } }],
15
+ "Member": [{ "role": "owner", "organizationId": { "_ref": "org" }, "userId": { "_ref": "admin" } }]
16
+ }
17
+ }
18
+ ```
19
+
20
+ Two rules cover almost everything:
21
+
22
+ - **A model is created only when it is a top-level key.** There is no nesting. You never place one model's records inside another model's fields. (If you do, the SDK treats that array as opaque field data and hands it to the factory verbatim - it is not created as separate records.)
23
+ - **Every cross-model link is an `_alias`/`_ref` pair.** Name a record with `_alias`, then point a foreign key at it with `{ "_ref": "alias" }`. This is the only way to link records.
24
+
25
+ ## How ordering works
26
+
27
+ The SDK builds a dependency graph purely from the `_alias`/`_ref` edges in the payload and topologically sorts it: a record that another record `_ref`s is created first. Key order in the JSON is irrelevant. There is no relation introspection - the graph comes entirely from the `_ref` edges you write.
28
+
29
+ By the time your factory's `create` runs, every `_ref` has already been replaced with the real ID of the referenced record. Your factory receives a plain FK value (a string or number), never a `_ref` object and never a temporary ID.
30
+
31
+ A `_ref` to an alias that no record in the same payload declares fails with `INVALID_BODY` ("references unknown alias(es)").
32
+
33
+ ## Setting foreign keys and the scope field
34
+
35
+ You set every foreign key yourself, including the scope field. There is no "FK direction" to reason about - every link, up or across, is just a `_ref` from the record holding the column to the record it depends on. Use the exact column name from your schema, whatever you called it (`organizationId`, `orgId`, `tenant_fk`).
36
+
37
+ ```json
38
+ {
39
+ "create": {
40
+ "Organization": [{ "_alias": "org", "name": "Acme", "slug": "acme" }],
41
+ "Application": [{ "name": "Marketing Site", "architecture": "WEB", "organizationId": { "_ref": "org" } }],
42
+ "Member": [
43
+ { "role": "owner", "organizationId": { "_ref": "org" }, "userId": { "_ref": "alice" } }
44
+ ],
45
+ "User": [{ "_alias": "alice", "name": "Alice", "email": "alice@test.com", "organizationId": { "_ref": "org" } }]
46
+ }
47
+ }
48
+ ```
49
+
50
+ The scope field is a foreign key like any other. The SDK does not inject it - set it explicitly on every model that has it.
51
+
52
+ ## What to include and omit in a record
53
+
54
+ Include every field your factory's input schema marks required and does not default:
55
+
56
+ - Required fields with no default value.
57
+ - Every foreign key, as a `{ "_ref": "alias" }`.
58
+ - The scope field, as a `_ref`.
59
+ - Unique fields, with values distinct across records so parallel runs do not collide.
60
+
61
+ Omit anything the factory or database fills in:
62
+
63
+ - Primary keys / IDs - generated by your creation code.
64
+ - Fields with default values.
65
+ - Auto-managed timestamps (`createdAt`, `updatedAt`).
66
+
67
+ ## Built-in placeholder tokens
68
+
69
+ The platform normally resolves all values before calling `up`, but the SDK also understands three placeholder tokens inside string values, as defense in depth:
70
+
71
+ | Token | Resolves to |
72
+ |-------|-------------|
73
+ | `{{testRunId}}` | The current run's ID - useful for making unique values. |
74
+ | `{{index}}` | The record's zero-based position within its model array. |
75
+ | `{{cycle(a,b,c)}}` | `a`, `b`, `c` in turn, cycling by record index. |
76
+
77
+ Any other `{{...}}` that reaches the SDK fails loudly with `UNRESOLVED_TOKEN` rather than being inserted as a literal string.
78
+
79
+ ## More link examples
80
+
81
+ Join table (each row references two aliases):
82
+
83
+ ```json
84
+ {
85
+ "create": {
86
+ "Tag": [
87
+ { "_alias": "tag1", "name": "Critical", "color": "#DC2626", "organizationId": { "_ref": "org" } },
88
+ { "_alias": "tag2", "name": "Flaky", "color": "#EAB308", "organizationId": { "_ref": "org" } }
89
+ ],
90
+ "Test": [{ "_alias": "t1", "name": "Homepage", "testGenerationId": { "_ref": "gen1" } }],
91
+ "TestTag": [
92
+ { "testId": { "_ref": "t1" }, "tagId": { "_ref": "tag1" } },
93
+ { "testId": { "_ref": "t1" }, "tagId": { "_ref": "tag2" } }
94
+ ]
95
+ }
96
+ }
97
+ ```
98
+
99
+ Distinct users for distinct members (the common "one member per user" case): give each `User` its own `_alias`, then point each `Member.userId` at a different one. A shared alias would try to make the same user a member twice and hit a unique constraint.
100
+
101
+ ## The three standard scenarios
102
+
103
+ When setting up a project, create three scenarios as a baseline. Name files after the shape, not the test.
104
+
105
+ - **empty** - organization + one user + their membership. Tests empty states, onboarding, and "no data yet" screens.
106
+ - **standard** - realistic variety: several users with different roles, at least one record per enum value, descriptive names ("Marketing Website", not "App 1"), enough data to exercise every filter and status.
107
+ - **large** - high volume: many explicitly listed records so pagination, sort stability, and filter performance get tested.
108
+
109
+ Add more targeted scenarios later, only when a specific test needs a data shape these three do not cover. Do not generate dozens up front.
@@ -0,0 +1,110 @@
1
+ <!-- Generated by scripts/build-sdk-docs.mjs from docs/languages/python/validation.md. Edit the source, not this copy. -->
2
+ # Validating scenarios (Python)
3
+
4
+ Scenario data is often generated by an LLM, so validate it against a real database before it reaches production. `check_scenario` runs the full `up` then `down` cycle through the same handler the platform hits, using your real factories, and reports exactly where it failed.
5
+
6
+ ## check_scenario(factories, scenario, options=None)
7
+
8
+ `check_scenario` is an async coroutine - `await` it (or run it under pytest with `pytest-asyncio`).
9
+
10
+ ```python
11
+ # tests/test_scenarios.py
12
+ import pytest
13
+ from autonoma import check_scenario
14
+ from factories import factories
15
+
16
+ @pytest.mark.asyncio
17
+ async def test_standard_scenario():
18
+ result = await check_scenario(factories, {
19
+ "create": {
20
+ "Organization": [{"_alias": "org", "name": "Test Org", "slug": "test-org"}],
21
+ "User": [{"_alias": "admin", "name": "Admin", "email": "admin@test.com",
22
+ "organizationId": {"_ref": "org"}}],
23
+ "Member": [{"role": "owner", "organizationId": {"_ref": "org"},
24
+ "userId": {"_ref": "admin"}}],
25
+ },
26
+ }, {"scopeField": "organizationId"})
27
+
28
+ if not result.valid:
29
+ for err in result.errors:
30
+ print(f"[{err.phase}] {err.message} - {err.fix}")
31
+ assert result.valid
32
+ ```
33
+
34
+ The first argument is your **factory registry**, not an adapter - `check_scenario` calls the real factories, so they must point at a real (test) database. The third argument is an options `dict` whose keys are **camelCase**: `scopeField`, `auth`, `sharedSecret`, `signingSecret`. All have safe defaults for a dry run, so `{"scopeField": "organizationId"}` is usually all you need.
35
+
36
+ ### The result
37
+
38
+ `check_scenario` returns a `CheckResult` dataclass:
39
+
40
+ | Field | Meaning |
41
+ |-------|---------|
42
+ | `valid` | `True` if the full create + teardown cycle succeeded. |
43
+ | `phase` | `"ok"` if it passed, else `"up"` (create rejected) or `"down"` (teardown failed). |
44
+ | `errors` | List of `CheckError`, each with `phase`, `message`, and a suggested `fix` string. |
45
+ | `timing` | `dict` with `"upMs"` and `"downMs"` in milliseconds. |
46
+
47
+ `message` is the underlying database or SDK error; `fix` is a heuristic hint the SDK derives from common failures (unique constraint, foreign key, null column).
48
+
49
+ ## The testcontainers harness
50
+
51
+ The reliable way to validate locally is a real Postgres in Docker. Point your factories' database client at the container, drop every scenario file into a folder, and parametrize one test over all of them.
52
+
53
+ ```python
54
+ # tests/test_validate_scenarios.py
55
+ import json
56
+ from pathlib import Path
57
+ import pytest
58
+ from testcontainers.postgres import PostgresContainer
59
+ from autonoma import check_scenario
60
+
61
+ SCENARIOS_DIR = Path(__file__).parent.parent / "scenarios"
62
+
63
+ @pytest.fixture(scope="session")
64
+ def database_url():
65
+ with PostgresContainer("postgres:16-alpine") as pg:
66
+ url = pg.get_connection_url()
67
+ # run your migrations against `url` here (alembic upgrade head,
68
+ # django migrate, create_all, ...)
69
+ yield url
70
+
71
+ @pytest.mark.asyncio
72
+ @pytest.mark.parametrize(
73
+ "scenario_file", sorted(SCENARIOS_DIR.glob("*.json")), ids=lambda p: p.name
74
+ )
75
+ async def test_scenario(scenario_file, database_url):
76
+ from factories import build_factories # build factories bound to database_url
77
+ factories = build_factories(database_url)
78
+ scenario = json.loads(scenario_file.read_text())
79
+ result = await check_scenario(factories, scenario, {"scopeField": "organizationId"})
80
+ for err in result.errors:
81
+ print(f"[{err.phase}] {err.message} - {err.fix}")
82
+ assert result.valid
83
+ ```
84
+
85
+ ```bash
86
+ # terminal
87
+ pip install pytest pytest-asyncio "testcontainers[postgres]"
88
+ pytest tests/test_validate_scenarios.py
89
+ ```
90
+
91
+ There is no `check_all_scenarios` helper in the Python SDK - loop or `parametrize` over your scenario files yourself and call `check_scenario` once per file, as above.
92
+
93
+ ## The fix loop
94
+
95
+ Validation is iterative, especially for LLM-generated data:
96
+
97
+ 1. Run `check_scenario`.
98
+ 2. If it fails, read `result.errors[i].message` (and the `fix` hint).
99
+ 3. Edit the scenario JSON and re-run.
100
+ 4. Repeat until `valid` is `True`.
101
+
102
+ Common failures and fixes:
103
+
104
+ | Message contains | Cause | Fix |
105
+ |------------------|-------|-----|
106
+ | `unique constraint ... slug` | Two records share a unique value | Make each unique field distinct across records (add `{{index}}` or `{{testRunId}}`). |
107
+ | `references unknown alias(es)` | A `_ref` targets an alias no record declares | Add the target record with that `_alias`, or fix the name. |
108
+ | `validation error` / `field required` | A required field is missing or the wrong type | Add the field; check it is declared on the factory's `input_model`. |
109
+ | `no factory registered for model "X"` | Model in `create` has no factory | Register a factory for `X` and add it to the registry. |
110
+ | `must return a record dict with "id"` | A factory's `create` returned no primary key | Return `{"id": ...}` (or a Pydantic instance with `id`). |
@@ -24,6 +24,7 @@ def unknown_action(action: str) -> AutonomaError:
24
24
 
25
25
 
26
26
  def production_blocked(detail: Optional[str] = None) -> AutonomaError:
27
+ """Deprecated - the SDK no longer gates on production; this is never raised."""
27
28
  return AutonomaError(
28
29
  "Environment factory is disabled"
29
30
  + ("" if detail is None else f". {detail}"),
@@ -22,7 +22,6 @@ from autonoma.errors import (
22
22
  invalid_body,
23
23
  invalid_refs_token,
24
24
  invalid_signature,
25
- production_blocked,
26
25
  same_secrets,
27
26
  unknown_action,
28
27
  )
@@ -113,11 +112,6 @@ async def handle_request(config: HandlerConfig, req: HandlerRequest) -> HandlerR
113
112
  if config.shared_secret == config.signing_secret:
114
113
  raise same_secrets()
115
114
 
116
- if not config.allow_production:
117
- raise production_blocked(
118
- "Set allow_production=True to enable the endpoint."
119
- )
120
-
121
115
  signature: str = (
122
116
  req.headers.get("x-signature") or req.headers.get("X-Signature") or ""
123
117
  )
@@ -19,6 +19,7 @@ The types in this module fall into two groups:
19
19
 
20
20
  from __future__ import annotations
21
21
 
22
+ import warnings
22
23
  from dataclasses import dataclass, field
23
24
  from typing import Any, Callable, Union
24
25
 
@@ -138,11 +139,22 @@ class HandlerConfig:
138
139
  Callable[[dict[str, Any] | None, AuthContext], Any], # async callables
139
140
  ]
140
141
  factories: FactoryRegistry = field(default_factory=dict)
142
+ # Deprecated - ignored; the endpoint is always enabled and HMAC signing is
143
+ # the gate. On Autonoma previews (AUTONOMA_PREVIEWKIT set) no guard is
144
+ # needed; gate manually in your handler for your own production deployments.
141
145
  allow_production: bool = False
142
146
  sdk: dict[str, str] | None = None
143
147
  before_down: Callable[[HookContext], Any] | None = None
144
148
  after_up: Callable[[HookContext, dict[str, Any]], dict[str, Any]] | None = None
145
149
 
150
+ def __post_init__(self) -> None:
151
+ if self.allow_production:
152
+ warnings.warn(
153
+ "allow_production is deprecated and ignored - the endpoint is always enabled",
154
+ DeprecationWarning,
155
+ stacklevel=2,
156
+ )
157
+
146
158
 
147
159
  @dataclass
148
160
  class HandlerRequest: