API Layer¶
Archetype runs as a single archetype serve process. The API layer is a
FastAPI application over one process-owned dispatcher, and the CLI is a thin
HTTP client.
External API operations authenticate an ActorCtx, construct exact family
operation models, and call actor-aware CommandDispatcher methods. Trusted
runtime calls construct the same models and use actor-free dispatcher entry.
Application Factory¶
create_app() builds the FastAPI app with routers for worlds, commands, simulation, and queries:
@asynccontextmanager
async def lifespan(app: FastAPI):
configure_host_observability(service_name="archetype-api")
resources = build_runtime_resources(RuntimeBootstrapConfig.from_env())
app.state.resources = resources
try:
yield
finally:
await resources.aclose()
Imports and create_app() perform no logging or telemetry setup. Each worker
configures its host from the lifespan path, which keeps reload and multi-worker
startup explicit and idempotent. The factory does not automatically invoke
Logfire FastAPI instrumentation; optional Logfire or OTLP export is selected
through the vendor-neutral host adapter.
All worlds live in the server event loop. CLI invocations and remote clients talk to that process over HTTP.
Dependency Injection¶
The FastAPI lifespan owns one RuntimeResources. deps.py exposes only its
dispatcher through get_dispatcher(request).
Routes that expose user-visible operations should inject:
CommandDispatcherfor reads, writes, lifecycle, and simulation control.ActorCtxfrom auth middleware.
Routes do not receive concrete application services or the process wiring graph.
@router.post("/worlds/{world_id}/run")
async def run_world(
world_id: str,
dispatcher: CommandDispatcher = Depends(get_dispatcher),
ctx: ActorCtx = Depends(get_actor_ctx),
):
return await dispatcher.apply_as(
ctx,
Run(world_id=world_id, run_config=RunConfig(num_steps=10)),
)
Authentication context¶
The served API is a single-tenant development surface, and this is the
deliberate v0.6 posture. There is no authentication system: every caller is
the tenant. A request with no Authorization header receives the admin role;
Bearer <role> self-asserts one of admin/operator/player/viewer. Roles
therefore scope cooperating agents — a client instructed to send
Bearer viewer is genuinely viewer-scoped by the command gate — but they are
not a security boundary against an adversarial caller, and binding the server
beyond loopback exposes an unauthenticated admin API. The credentials that
matter are the storage credentials in Daft's IOConfig. Real
authentication/authorization against an external identity provider is a
future hardening slice. The trusted runtime has no actor context. See
Command Gate.
Mission service principals¶
Agent Missions defines a separate authentication seam for the forthcoming
MissionRun control surface. It resolves an opaque bearer credential to a
stable principal id, explicit mission:* capabilities, and an execution-profile
allowlist. Developer role labels are never credentials on that seam. The
provisioning document stores either the name of an environment variable holding
the credential or its SHA-256 verifier; it does not store a plaintext credential.
Missing, malformed, unknown, expired, and revoked credentials fail closed.
This contract does not add a parallel mission-control router. A REST route may
be published only when it can construct the exact Missions operation and enter
the governed actor-aware boundary. Authentication does not mint a run id, and
a successful profile check is not a 202 Accepted MissionRun. The durable
MissionRun owner remains the sole authority for run identity, ownership grants,
lifecycle, and pinned profile identity.
When the Missions library is installed, startup on a non-loopback bind
requires a configured principal directory. An undeclared bind host is treated
as non-loopback: archetype serve declares ARCHETYPE_BIND_HOST from its
--host option, while a bare uvicorn archetype.api.app:create_app --factory
launch declares nothing and therefore fails closed. Loopback development
outside archetype serve opts in explicitly with
ARCHETYPE_BIND_HOST=127.0.0.1. Existing ECS developer routes otherwise
retain the deliberate v0.6 behavior above.
MissionRun control surface¶
The durable MissionRun lifecycle is exposed as a small agent-safe REST
surface under /v1/mission-runs:
| Endpoint | Method | What it does |
|---|---|---|
/v1/mission-runs |
POST | Accept one durable run for a verified principal (202) |
/v1/mission-runs |
GET | Bounded newest-first page of the caller's own runs |
/v1/mission-runs/{run_id} |
GET | Bounded run status projection |
/v1/mission-runs/{run_id}/events |
GET | Ordered durable events, ?after=<cursor>&limit=<n> |
/v1/mission-runs/{run_id}/result |
GET | One immutable terminal result (425 while open) |
/v1/mission-runs/{run_id}/cancel |
POST | Durably record cancellation intent (202, idempotent) |
Submission requires an Idempotency-Key header and a body carrying only
profile_id, repository coordinates, mission name, and an explicit bounded
task DAG with command validators. The same principal, key, and canonical
request digest return the original run; a changed digest under the same key
is a 409 conflict, and both survive an API-process restart. Route handlers
authenticate the verified principal, authorize capability, ownership, and
profile policy, then dispatch the exact registered accept_mission_run,
get_mission_run, get_mission_run_events, and cancel_mission_run
operations. They never open a runtime handle, construct Mission components,
start background tasks, or own recovery — supervision and recovery stay with
the missions-owned MissionRun lifecycle.
Events carry a deterministic (run_id, cursor) identity, a schema version,
a timestamp, a phase/type, and a sanitized bounded payload appended in the
same transaction as the durable transition, so after replay across
reconnects has no gaps, reordering, or duplicated logical events. Cancel
records intent durably before reporting acceptance; cancelling stays
distinct from cancelled, and completion races resolve to the committed
execution fact. Client disconnect is never cancellation.
Executing a REST-accepted run requires host composition: the execution
profile bound through the world_library_configs wiring input resolves the
exact pinned (profile_id, version, digest) to its trusted
AgentMissionConfig factory. An unbound host still accepts, observes, and
cancels runs; supervision records an honest failed run instead of
fabricating provider work. Reason text — provider exception text and
caller-supplied cancel reasons alike — is redacted through the composed
redaction service before it becomes a durable fact, every projection stays
bounded, and terminal task facts cap their commit lists, so provider errors
cannot leak credential-shaped content to any mission:read principal.
Route Structure¶
Routes are thin translators: validate payloads, authenticate, call actor-aware dispatcher entry, and return response models.
Worlds¶
| Endpoint | Method | What it does |
|---|---|---|
/worlds |
POST | Create world through the gate |
/worlds |
GET | List managed worlds / world info |
/worlds/{id} |
GET | Get WorldInfo |
/worlds/{id} |
DELETE | Destroy the live world; persisted data remains |
/worlds/{id}/fork |
POST | Fork a world through the gate |
Lifecycle operations are direct gate calls, not tick-deferred scheduler commands.
Commands¶
| Endpoint | Method | What it does |
|---|---|---|
/worlds/{id}/commands |
POST | Authorized durable deferred admission |
/worlds/{id}/commands/batch |
POST | Tick-deferred batch submit |
/worlds/{id}/commands |
GET | Audit-backed command history |
Command-ledger pending state is an implementation detail. User-facing history is
audit-backed through /worlds/{id}/history and /worlds/{id}/commands.
Simulation¶
| Endpoint | Method | What it does |
|---|---|---|
/worlds/{id}/step |
POST | Execute one tick |
/worlds/{id}/run |
POST | Execute N ticks |
/worlds/{id}/episode |
POST | Run until termination or cap on this world |
/worlds/{id}/rollout |
POST | Fork N episodes and aggregate |
/worlds/{id}/processors |
GET | List processor info |
See Execution Hierarchy.
Query¶
| Endpoint | Method | What it does |
|---|---|---|
/worlds/{id}/state |
GET | Query world state through the gate |
/worlds/{id}/entities/{eid} |
GET | Query one entity projection |
/worlds/{id}/components |
GET | Lazily filter, limit, or count component projections |
/worlds/{id}/history |
GET | Audit history through get_audit_history |
API routes authorize reads through CommandDispatcher.apply_as; trusted
runtime reads use apply. Both construct registered query models. The
dispatcher invokes handlers backed by archetype.world.query for
durable ECS reads and commands-owned AuditLog for GetAuditHistory. Neither
path requires a live world.
Routes may import frozen supported values from archetype.world.models; they
must not import world registry, lifecycle, mutation, simulation, query, or
handler behavior directly.
The component route accepts one inert comparison through where, then applies either the
show row limit or the count terminal. Filtering happens before either terminal and before
row serialization. All three options require at least one component type; show and count are
mutually exclusive. The filter grammar is deliberately small: one component column, one of >,
>=, <, <=, ==, or !=, and one scalar value. Calls, attribute access, arithmetic, and
Boolean composition are rejected rather than evaluated.
See the REST API Reference for generated schemas.
Route Pattern¶
Example create_world route:
@router.post("", response_model=WorldInfo)
async def create_world(
req: CreateWorldRequest,
dispatcher: CommandDispatcher = Depends(get_dispatcher),
ctx: ActorCtx = Depends(get_actor_ctx),
):
info = await dispatcher.apply_as(
ctx,
CreateWorld(
config=req.world_config(),
storage_config=req.storage(),
cache_config=req.cache_config,
),
)
return info
The route does not construct a lifecycle command or bypass the gate.
CLI¶
The CLI (archetype command) is a thin HTTP client.
serve is the sole CLI process-host path: it configures observability before
starting Uvicorn. Every other CLI command remains an HTTP client and performs
no local provider or handler setup.
archetype serve Starts uvicorn with the FastAPI app
archetype world create POST /worlds
archetype world list GET /worlds
archetype world inspect GET /worlds/{id}
archetype world fork POST /worlds/{id}/fork
archetype world destroy DELETE /worlds/{id}
archetype entity spawn POST /worlds/{id}/entities
archetype step POST /worlds/{id}/step
archetype run POST /worlds/{id}/run
archetype episode POST /worlds/{id}/episode
archetype rollout POST /worlds/{id}/rollout
archetype query GET /worlds/{id}/state or /worlds/{id}/components
archetype history GET /worlds/{id}/history
archetype processors list GET /worlds/{id}/processors
archetype hooks list GET /worlds/{id}/hooks
archetype resources list GET /worlds/{id}/resources
The server URL defaults to http://localhost:8000 and can be overridden with
ARCHETYPE_URL or per command with --url. HTTP commands accept the developer
auth shortcut --role / -r and the bearer-token option --token.
Without component types, query returns the world-state projection. Pass comma-separated
component types positionally to use the lazy component-query path:
archetype query <world-id> Agent,Score --where "score__value > 0.5" --show 5
archetype query <world-id> Agent,Score --where "score__value > 0.5" --count
--types remains available as a compatibility spelling for the positional component list.
Source Reference¶
- App factory:
packages/archetype-ecs/src/archetype/api/app.py - Dependency injection:
packages/archetype-ecs/src/archetype/api/deps.py - Request/response models:
packages/archetype-ecs/src/archetype/api/models.py - Routes:
packages/archetype-ecs/src/archetype/api/routes/ - CLI:
packages/archetype-ecs/src/archetype/cli/main.py