Applications

The mountable application classes: OpenAPI, MCP, and the automatic _server application with its sections.

OpenAPI

OpenApiApplication: a RoutedApplication that exposes REST + OpenAPI + docs.

OpenApiApplication wraps an API surface — either the app’s own @route methods (direct mode) or an external RoutingClass attached under api_name (mounted mode) — and adds a _meta sub-tree with three introspection endpoints:

  • _meta/schema_json — the OpenAPI 3.1 document of the API;

  • _meta/docs — a Swagger-UI page pointing at _meta/schema_json;

  • _meta/index — an HTML splash linking to the docs.

The schema is built STANDALONE from the app’s own router (router_openapi(app.route)): there is no dependency on a _server application (that surface belongs to a later macro). The mounted routing class is linked as an eager instance branch, so it inherits the app router’s plugins — pydantic among them — and its handler signatures are captured into the neutral params/result blocks the OpenAPITranslator reads; in direct mode the same plugins reach the app’s own router through the server’s plugin arming (PluginMixin config).

The docs and splash HTML live in dedicated resource files next to this module and are read at USE time (never at import): a swap of the template file takes effect without re-importing the package.

Kwargs peeled by the cooperative __init__ (D16): routing_class (a RoutingClass instance to mount), module ("pkg.mod:ClassName" import path, an alternative to routing_class), docs (documentation style — "swagger" or "off") and api_name (the segment the mounted class is attached under, default "api"). The rest flows down the chain (db_name to RoutedApplication, mount_name to BaseApplication).

class genro_asgi.applications.openapi.OpenApiApplication(**kwargs)[source]

Bases: RoutedApplication

Expose an API surface as REST + OpenAPI 3.1 with a Swagger docs page.

Two ways to supply the API:

  • direct mode — subclass and write @route methods on the app itself; endpoints sit at the app root (/{app}/endpoint);

  • mounted mode — pass routing_class= (or module=); the class is attached under api_name (/{app}/{api_name}/endpoint).

Meta endpoints are attached under _meta in both modes.

Parameters:

kwargs (Any)

property docs_style: str

Documentation style — "swagger" or "off".

property api_name: str

Path segment the mounted routing class is attached under.

property api_info: dict[str, Any]

the mounted class’s, else the app’s, else empty.

Type:

OpenAPI info dict

schema_filters()[source]

Node filters forwarded to router_openapi when building the schema.

Empty by default (the whole router). A subclass whose router carries the channel plugin overrides this to select the REST-facing channel so the schema stays visible (the McpOpenApiApplication bridge).

Return type:

dict[str, Any]

MCP

MCP applications: the stateless Streamable HTTP transport over McpEngine.

Two ready-to-mount apps expose a genro-routes router as MCP tools over JSON-RPC 2.0. Both delegate the HTTP shell to a McpTransport — the helper that owns everything the transport-agnostic McpEngine does not: method/header/Origin gating, the JSON-RPC envelope, the 202-for-notifications rule, and the sync/async invoke callback (async handlers stay on the loop, sync handlers go through the server pool via run_sync — the Macro 1 protocol, replacing the old smartasync). The transport holds its owning application as self.application (dual-parent) and reaches its spread_over_params and server through it.

  • McpApplication — the whole app is one MCP endpoint. It holds an engine over an EXTERNAL router (routing_class= or module=); every request is a JSON-RPC message. Without a router initialize still answers and tools/list is empty.

  • McpOpenApiApplication — one router, two faces: it inherits the whole OpenApiApplication machinery (_meta docs, pydantic plug, REST dispatch) and adds an MCP face under mcp_name_segment (default "mcp"). The channel plugin drives per-face visibility: a method is an MCP tool only on channel "mcp" (@route(channel_channels="mcp,rest") for a dual method); undeclared methods default to REST-only (channels=rest_channel).

Transport conformance (MCP Streamable HTTP): a JSON-RPC POST answers with a JSON response; a notification (no id) answers HTTP 202 with an empty body; a GET opens the SSE push stream (below); any other method answers 405; an MCP-Protocol-Version header that is present but unsupported answers 400 (an absent header is assumed 2025-03-26 per the spec’s backwards-compat rule); an Origin header present and not in allowed_origins answers 403 (the allowed_origins option defaults to None — no restriction, a dev-mode default: production fronting owns the Origin gate). The transport gates raise core HTTP exceptions answered by the server’s ErrorMiddleware.

The push half (core 1e, the option-B commitment honored): a GET opens a text/event-stream keyed by Mcp-Session-Id — echoed when the client supplies one, minted otherwise (secrets.token_urlsafe, decoupled from the cookie session: MCP clients carry no cookie) — and follows the server’s task hub live (server.tasks.hub, the A<->C bridge). A Last-Event-ID header replays the session’s current progress.json snapshots first (snapshot-baseline resumability — no durable event log, ratified). A server composed without tasks answers GET with 405, the 1c stateless behavior. The engine stays untouched apart from advertising the capability.

class genro_asgi.applications.mcp.McpApplication(**kwargs)[source]

Bases: RoutedApplication

Standalone MCP transport: the whole app is one JSON-RPC endpoint.

Supply the tool surface as an external RoutingClass (routing_class= instance, or module="pkg.mod:Class" imported and instantiated with no arguments); every route on it becomes a tool. The channel filter only bites when the external router plugs the channel plugin, so a plain router exposes all its routes. Without a router the app still answers initialize and lists no tools. Class attributes carry the MCP identity defaults so a subclass can set them declaratively.

Parameters:

kwargs (Any)

property mcp_engine: McpEngine | None

The MCP engine driving this app’s tool surface (None if unbuilt).

class genro_asgi.applications.mcp.McpOpenApiApplication(**kwargs)[source]

Bases: OpenApiApplication

OpenApiApplication that also exposes its API router as MCP tools.

The REST/OpenAPI faces work exactly as in OpenApiApplication; the MCP face answers under mcp_name_segment (default "mcp") via the same engine, pointed at the API router (the app’s own in direct mode, the mounted class’s in mounted mode). Visibility is the channel plugin’s job: the MCP face lists only channel-"mcp" entries; undeclared methods default to REST-only (channels=rest_channel).

Parameters:

kwargs (Any)

property mcp_engine: McpEngine | None

The MCP engine driving this app’s tool surface (None if unbuilt).

property mcp_name_segment: str

Path segment under which the MCP JSON-RPC face is served.

auth_filters(scope)[source]

Node-resolution filters: the base auth tags plus the REST channel.

The API router carries the channel plugin (a method is an MCP tool only on channel "mcp"), so the REST face must resolve on the REST channel; the MCP face passes "mcp" through the engine. The filter is harmless when no router on the path plugs channel (it is ignored).

Return type:

dict[str, str]

Parameters:

scope (MutableMapping[str, Any])

schema_filters()[source]

Build the OpenAPI schema over the REST channel (the channel plugin is armed).

Return type:

dict[str, Any]

McpEngine — MCP (JSON-RPC 2.0) core over a genro-routes Router.

The engine turns a Router’s @route entries into MCP tools and serves the protocol methods initialize / tools/list / tools/call. It is transport- and app-agnostic: it holds a Router and a channel to filter on and never touches HTTP concerns (headers, Origin, 202-for-notifications belong to the host application). dispatch receives the parsed JSON-RPC message and returns the RESULT object — envelope bookkeeping (id, jsonrpc) stays with the transport; protocol failures raise McpError carrying the JSON-RPC code for the transport to render. A list payload is rejected with -32600: JSON-RPC batching entered the MCP spec in 2025-03-26 and was removed in 2025-06-18.

Protocol (2025-11-25, the current revision):

  • initialize negotiates the version: the client’s requested version is echoed when it appears in SUPPORTED_VERSIONS, anything else is answered with the latest supported revision.

  • tools/list walks router.nodes(forbidden=False, channel_channel=...) so only the entries reachable on the engine’s channel are advertised. Tool names join the router path with tool_separator (default ".", a character illegal in Python identifiers, so sub.ping <-> sub/ping round-trips losslessly). Descriptors read ONLY the neutral blocks cached by genro-routes’ pydantic plugin at decoration time: inputSchema from the entry’s params.schema (aggregate request_schema; fallback: an object schema assembled from params.fields), outputSchema from result.schema (response_schema). Nothing is derived from the callable and pydantic is never imported here.

  • tools/call resolves the tool through router.node(path, ...) and reads the node.error STRING CODE (the stable genro-routes contract — resolution never raises): not_found/not_available -> -32601, not_authorized/not_authenticated -> -32000, any other code -> -32603. Execution is delegated to the invoke callback so a host app can interpose parameter adaptation and pool dispatch; the default calls the node directly and the engine awaits an awaitable result (async handlers), nothing more. Input-validation failures are TOOL EXECUTION errors — {"isError": true, "content": [...]} results, not JSON-RPC protocol errors (SEP-1303, enables model self-correction). Validation runs INSIDE genro-routes (the pydantic plugin validates at call time; the engine never re-validates). Since genro-routes 0.28.0 every bad-argument error — a pydantic.ValidationError or an unbindable-argument TypeError alike — is channelled through the node’s errors={"validation_error": ...} seam to a local marker, so this module needs no pydantic import; the bare TypeError catch remains for an async handler body raising at await time (a sync body’s TypeError is already folded into the marker upstream). Both become isError results. ping answers an empty result (spec MUST). A dict result is returned BOTH as structuredContent and as its JSON text rendering (the unstructured content SHOULD match the declared outputSchema); any other result stays text-only.

class genro_asgi.mcp.engine.McpEngine(router=None, *, name='genro-mcp', version='1.0.0', tool_separator='.', channel='mcp', invoke=None)[source]

Bases: object

MCP JSON-RPC core over a router.

Parameters:
  • router (Router | None) – The genro-routes Router whose entries are exposed as tools.

  • version (str) – server identity returned by initialize.

  • tool_separator (str) – joins router/method segments into a flat tool name.

  • channel (str) – channel to filter entries on (visibility per channel).

  • invoke (Callable[[Any, dict], Any] | None) – callback (node, arguments) -> result running a resolved node; the engine awaits an awaitable result. Host applications pass their own to interpose parameter adaptation (e.g. spread_over_params) and pool dispatch for sync handlers; the default calls the node directly.

  • name (str)

async dispatch(payload, auth_tags=None)[source]

Route a parsed JSON-RPC message to its MCP handler.

Returns the JSON-RPC RESULT object; the transport owns the envelope.

Raises:

McpError – invalid message shape (-32600, batching included) or unknown method (-32601); tools/call resolution failures bubble up from handle_tools_call().

Return type:

dict

Parameters:
  • payload (Any)

  • auth_tags (Any)

handle_initialize(params)[source]

Negotiate the protocol version and return the server capabilities.

The client’s requested version is echoed when supported; any other request is answered with the latest supported revision (spec negotiation rule).

Return type:

dict

Parameters:

params (dict)

handle_tools_list()[source]

Enumerate the tools visible on this channel.

forbidden=False excludes entries the channel does not expose, so the tool list carries only what is reachable on this channel.

Return type:

dict

async handle_tools_call(params, auth_tags=None)[source]

Resolve a tool name to its router node, invoke it, wrap the result.

Bad tool arguments come back as isError results — genro-routes 0.28.0 folds validation failures AND unbindable arguments into the validation_error mapping, while the TypeError catch covers an async handler body raising at await time; resolution failures read node.error and raise McpError.

Raises:

McpError – no router configured (-32603), unknown/unavailable tool (-32601), not authorized/authenticated (-32000), any other resolution code (-32603).

Return type:

dict

Parameters:
exception genro_asgi.mcp.engine.McpError(code, message)[source]

Bases: Exception

Carries a JSON-RPC error code + message for the transport to render.

Parameters:
Return type:

None

Server application

ServerApplication: the automatic _server system app (D4).

ServerApplication is the server’s own application — the system surface every server exposes under /_server without configuring it (D4: “automatic, not configured”). AsgiServer mounts one at the end of its __init__ (_mount_server_app), so a hand-built AsgiServer(primary=...) gets it exactly like a config-materialized one; ConfigurationHandler.materialize never special-cases it. The demux finds it through the ordinary mount table — there is no dedicated demux logic.

It extends OpenApiApplication (REST + OpenAPI; the MCP face on _server is out of this wave), so /_server/_meta/ carries the usual schema/docs/index endpoints, and adds:

  • index — the /_server/ descriptor: title and the attached section names (JSON — no HTML in code);

  • sections / attach_section(section, name) — the registry of system sections: attach_section links a RoutingClass under name (endpoints at /_server/<name>/...) and records it so introspection surfaces (the index today, monitors later) can enumerate them;

  • the PASSWORD login surface (core 1d wave 1): login (JSON POST → UserStore.verifyAvatarrequest.session.attach_avatar), login_page (HTML GET, the descriptor-driven resources/login.html read at USE time), logout and the public login_methods — dual-mode by TWO routes, never in-handler Accept sniffing. The methods live in an AuthSection attached under auth (ensure_auth_section / register_auth_method); PasswordMethod is registered at construction. login enforces the store-backed lockout (REVIEW #9): the per-identity failure counter (failed_attempts/last_failed_at) rides the UserStore record with exponential backoff; the policy comes from the config’s login() element (login_policy, defaults 5 attempts / 30s base).

Handlers stay PURE: they return values and never touch cookies or an ambient request/response (the old self.server.request idiom must never be reintroduced). Login attaches the avatar to the existing session in place — the id never changes, so no login-time cookie exists. A handler that needs the live request DECLARES an UNANNOTATED _request parameter: bind_kwargs injects the per-dispatch Request for that name — the same declarative convention body_data follows — and the handler reaches the server through _request.server. Leaving it unannotated keeps it out of the pydantic model (and thus the public OpenAPI schema); the pydantic wrapper, seeing no type hint, passes it straight through instead of routing it into validation. The _ prefix is the injected-name convention bind_kwargs matches in the neutral fields block. pydantic and openapi are fixed server structure (armed on every router by PluginMixin), so the handler signatures are always captured and per-entry OpenAPI controls (openapi_method) always take effect.

The future internal server (a D8 orchestration concern) is a SUBCLASS that overrides what it needs — not a profile flag on this class: no code exists for a consumer that does not exist yet.

Kwargs peeled by the cooperative __init__ (D16): mount_name is FIXED to "_server" — the system mount is a D4 invariant, not a preference, and three cross-file references hardcode /_server/... (PasswordMethod’s action, LOGIN_PAGE_URL, login.html’s fetch). A non-default value raises rather than silently 404-ing those references. login and oidc are the values the config-class lift carries (the server_app= server kwarg, forwarded by _mount_server_app): the lockout policy dict and the per-code OIDC provider dicts, stored as login_policy/oidc_providers (consumed by the lockout check and the OidcMethod registration). The rest flows down the chain. A hand-built AsgiServer(primary=...) passes nothing, so the defaults (empty dicts) keep today’s bare app.

class genro_asgi.applications.server_app.ServerAppConfig(name=None)[source]

Bases: BuilderBase, ServerAppConfigElements

The _server app’s config-class: the base IS the default.

main() declares nothing, so materializing the base yields today’s bare ServerApplication() — no configuration, no regression. A site personalizes _server by subclassing this in its config.py and passing the subclass to ConfigurationHandler alongside the site recipe; the handler claims it by class identity (ServerApplication.config_class), no name registry.

Parameters:

name (str | None)

main(root)[source]

Default _server configuration: nothing to declare.

Return type:

None

Parameters:

root (Any)

class genro_asgi.applications.server_app.ServerAppConfigElements[source]

Bases: object

Config grammar of ServerApplication — the dialect of its config-class.

NOT composed into the site dialect: an application’s configuration is a SEPARATE builder (distributed config), and _server’s is the one the core knows how to mount by construction. Declares the identity surface: admin_password (a ^pointer node value) plus users()/tokens() store descriptors. All three are LIFTED to the SERVER constructor kwargs (AuthMixin peels them): the stores live on the server (Phase 3) — the config keeps the app’s shape, the runtime stays where it is. The login surface (login() policy, oidc() providers) lifts as the single server_app= server kwarg instead: those values belong to THIS app, which peels them when AsgiServer forwards them at mount time.

admin_password = <genro_builders.builder._decorators._DeclarativeMarker object>
users = <genro_builders.builder._decorators._DeclarativeMarker object>
tokens = <genro_builders.builder._decorators._DeclarativeMarker object>
login = <genro_builders.builder._decorators._DeclarativeMarker object>
oidc = <genro_builders.builder._decorators._DeclarativeMarker object>
class genro_asgi.applications.server_app.ServerApplication(**kwargs)[source]

Bases: OpenApiApplication

System endpoints of a server, auto-mounted under /_server (D4).

Carries the public server’s system surface: the password login surface and the sections attached through attach_section, listed by the index descriptor. The future internal server (a D8 orchestration concern) will be a SUBCLASS overriding what it needs — not a profile flag on this class.

Parameters:

kwargs (Any)

config_class

alias of ServerAppConfig

property login_policy: dict[str, Any]

The lockout policy from the config’s login() element (may be empty).

property oidc_providers: dict[str, dict[str, Any]]

OIDC provider configs from the oidc() elements, keyed by code.

property sections: dict[str, RoutingClass]

Attached system sections keyed by their mount segment (may be empty).

property auth_section: AuthSection | None

The auth section carrying the login methods, or None.

attach_section(section, name)[source]

Attach section under name and record it in sections.

Links the section’s router into this app (endpoints at /_server/<name>/...) and keeps it enumerable for the introspection surfaces (the index descriptor today).

Return type:

None

Parameters:
  • section (RoutingClass)

  • name (str)

ensure_auth_section()[source]

The auth section, attached under auth on first use.

Return type:

AuthSection

register_auth_method(method)[source]

Register a login method in the auth section (created on demand).

Return type:

None

Parameters:

method (AuthMethod)

bind_kwargs(node, request)[source]

Inject the live Request into handlers that declare _request.

Extends the base reconciliation with the declarative seam the login surface needs: when the node’s neutral params block declares a _request parameter, the per-dispatch Request is bound to it (overriding any same-named wire value). Handlers leave _request unannotated so it stays out of the pydantic model — and therefore out of the public OpenAPI schema — while still being visible in the neutral fields this method reads. No ambient state — the request travels as an ordinary argument, exactly like body_data.

Return type:

dict[str, Any]

Parameters:
  • node (RouterNode)

  • request (Request)

index()[source]

The /_server/ descriptor: title and section names.

Return type:

dict[str, Any]

login(identity='', password='', _request=None)[source]

Authenticate against the server’s UserStore and attach the identity.

The JSON convergence point of every form method: verifies the credentials (UserStore.verify — the record key is identity), builds the Avatar and attaches it to the request’s session in place (_request.session.attach_avatar) — the session id never changes at login, so the client’s cookie stays valid and no Set-Cookie is involved. The server’s user_store is wired in the next wave (Macro 5b): until then a server without one answers the error shape.

The next return path is NOT a login parameter: the challenge redirects to login_page?next=... and the page script owns the post-success redirect — login itself never sees it and posts carry only the credentials.

Enforces the server-side lockout (REVIEW #9): the failure counter lives ON the user’s store record (failed_attempts / last_failed_at), so it survives restarts and is shared across processes on a shared store. After max_attempts consecutive failures the identity is refused until the exponential-backoff window (_lock_seconds_remaining) has passed; refused attempts never touch the counter — an attacker hammering a locked identity cannot extend a legitimate user’s lock — and a success resets it. Known-identity failures surface the server-computed remaining_attempts; unknown identities have no record, hence no counter and no such field. Per-IP rate limiting is a future middleware concern, not this handler’s.

The method is POST by declaration (openapi_method="post"): with _request hidden from the schema (see below) the remaining fields are all scalar, so the guesser would otherwise pick GET.

Parameters:
  • identity (str) – The record key to verify (NOT the old username).

  • password (str) – The password to verify.

  • _request – The live Request, injected by bind_kwargs. Left unannotated so it stays out of the pydantic model — and thus out of the public OpenAPI request body — while the _ prefix is the injected-name convention bind_kwargs matches.

Return type:

dict[str, Any]

Returns:

{session_id, identity, tags} on success; {"error": ...} on missing/invalid credentials, active lockout, or when no user store is wired — with remaining_attempts when the identity has a record.

Note

Route: POST /_server/login

login_page(next='')[source]

Serve the descriptor-driven HTML login page (GET, dual-mode twin of login).

The page builds itself from login_methods and posts credentials to the method’s action (/_server/login). Read at USE time so a template swap needs no re-import. next is accepted so the challenge redirect’s query binds; the page script consumes it client-side.

Note

Route: GET /_server/login_page

Return type:

str

Parameters:

next (str)

logout(session_id='')[source]

Destroy a session.

Deletes the session from the store. No error if the session is unknown.

Parameters:

session_id (str) – Session token to invalidate.

Return type:

dict[str, Any]

Returns:

{"status": "ok"} (always succeeds).

Note

Route: POST /_server/logout

login_methods()[source]

Public descriptors of the active auth methods (NO auth_rule).

The login page builds itself from this: register a method, its descriptor (and therefore its button/form) appears. Deliberately public — a caller must see the methods before it can authenticate. Empty list when no login surface is active.

Return type:

dict[str, Any]

Returns:

{"methods": [descriptor, ...]} in registration order.

Note

Route: GET /_server/login_methods

Server sections

The _server/auth container: the mount that holds the auth-method sections.

When the login surface is active the ServerApplication attaches ONE AuthSection under the auth name, so it lives at /_server/auth/. A registered auth method (AuthMethod) is attached to this section under its method_id ONLY when it owns routes, so those routes live at /_server/auth/<method_id>/ (e.g. a future OIDC start and callback at /_server/auth/oidc:google/start). A route-less method — the password one — is recorded in the registry but never attached: zero-route nodes never enter the routing tree (Invariant 10).

The section is a thin router node: it holds no routes of its own, it only carries the routed method children and keeps the ordered registry the login surface reads to build login_methods. Routing is dispatch; the registry is this dict.

class genro_asgi.applications.server_sections.auth_section.AuthSection(application)[source]

Bases: RoutingClass

The _server/auth mount that carries the registered auth methods.

Note

Parent (dual relationship): the ServerApplication, stored as self.application. The AsgiServer is reached via self.application.server.

Parameters:

application (ServerApplication)

__init__(application)[source]

Bind the section to its ServerApplication and start an empty registry.

Parameters:

application (ServerApplication) – The ServerApplication this section belongs to (dual relationship). The AsgiServer is application.server.

Return type:

None

property server: Any

The AsgiServer, reached through the parent ServerApplication.

property methods: dict[str, AuthMethod]

The registered methods, keyed by method_id (insertion order).

register(method)[source]

Record a method; mount its routes only when it owns some.

Every method enters the ordered registry the login surface reads to build login_methods. Only a method that OWNS routes is also linked into this section’s router under method_id (so its routes live at /_server/auth/<method_id>/); a route-less method (the password one) stays registry-only — zero-route nodes are never attached to the routing tree (Invariant 10: routing is dispatch, never a registry).

Parameters:

method (AuthMethod) – The AuthMethod to register. Its method_id must be unique.

Raises:

ValueError – If a method with the same method_id is already registered (method ids are unique by contract, so a clash is a configuration bug).

Return type:

None

descriptors()[source]

The descriptor of every registered method, in registration order.

Return type:

list[dict[str, Any]]

The _server/users section: SUPERADMIN-gated user management.

UsersSection is a RoutingClass the ServerApplication attaches under users (endpoints at /_server/users/...). Every route is gated auth_rule="SUPERADMIN" — the section is ALWAYS declared (fixed structure, D26), and each handler answers the {"error": ...} shape (coherent with login) when the server has no user_store wired.

The credential invariant: password_hash NEVER crosses the wire. list and get strip it from the record; save never accepts it (it merges the non-credential fields of the body over the stored record, preserving the hash); a password enters the system only as plaintext through create_user and set_password, hashed server-side via UserStore.hash_password.

Route responsibilities are separated:

  • create_user — births a NEW record (error if the identity exists): the body carries password/password_confirm (the server checks they match) plus the metadata and tags;

  • save — updates an EXISTING record only (error if absent): merges the body’s metadata/tags over the stored record, password_hash untouched. The body is taken whole (body_data) so tomorrow’s metadata fields need no signature change;

  • set_password — changes the credential of an existing record only, with the same password/password_confirm server-side check;

  • delete — removes a record.

Parent (dual relationship): the ServerApplication, stored as self.application; the store is reached via self.application.server.user_store.

class genro_asgi.applications.server_sections.users_section.UsersSection(application)[source]

Bases: RoutingClass

The _server/users mount: SUPERADMIN CRUD over the server’s UserStore.

Note

Parent (dual relationship): the ServerApplication, stored as self.application. The store is self.application.server.user_store.

Parameters:

application (ServerApplication)

__init__(application)[source]

Bind the section to its ServerApplication (dual relationship).

Parameters:

application (ServerApplication)

Return type:

None

property user_store: UserStore | None

The server’s UserStore, or None when identity is unconfigured.

public_record(record)[source]

A record without its password_hash — the wire-safe projection.

Return type:

dict[str, Any]

Parameters:

record (dict[str, Any])

matched_password(body)[source]

The password when it is present and matches its confirmation, else None.

The caller distinguishes a mismatch (this returns None) from an absent password by checking the body itself: only create_user requires one.

Return type:

str | None

Parameters:

body (dict[str, Any])

list()[source]

Every user record, password_hash stripped.

Return type:

dict[str, Any]

get(identity='')[source]

One user record, password_hash stripped.

Return type:

dict[str, Any]

Parameters:

identity (str)

create_user(identity='', body_data=None)[source]

Create a NEW user from the body (metadata + tags + password/confirm).

Errors if the identity already exists. The password is validated against its confirmation and hashed server-side; password_hash never arrives pre-formed. Metadata and tags from the body land on the new record. A new record defaults to enabled: True and tags: [] (creating a user with a password means letting them log in — the body can still say enabled: false to create it disabled); verify requires enabled and the login Avatar requires tags, so a minimal body must still produce a working user.

Return type:

dict[str, Any]

Parameters:
  • identity (str)

  • body_data (dict | None)

save(identity='', body_data=None)[source]

Update an EXISTING record’s metadata/tags; password_hash untouched.

Errors if the user does not exist (creation is create_user’s job). The body is merged whole over the stored record — new metadata fields persist with no signature change — but the credential never moves here.

Return type:

dict[str, Any]

Parameters:
  • identity (str)

  • body_data (dict | None)

set_password(identity='', body_data=None)[source]

Change an existing user’s password (plaintext in, hashed server-side).

The body carries password/password_confirm; the server checks they match. Errors if the user does not exist.

Return type:

dict[str, Any]

Parameters:
  • identity (str)

  • body_data (dict | None)

delete(identity='')[source]

Remove a user; reports whether a record was actually removed.

Return type:

dict[str, Any]

Parameters:

identity (str)

metadata_of(body)[source]

The non-credential, non-key fields of a body (never password/hash/identity).

Credential and key fields are owned by their dedicated routes, so they are dropped here: whatever else the body carries is user metadata.

Return type:

dict[str, Any]

Parameters:

body (dict[str, Any])

The _server/tokens section: SUPERADMIN-gated issued credentials.

TokensSection is the ONE section for credentials the server issues: API keys (gak_) and short-lived JWTs. It is a RoutingClass the ServerApplication attaches under tokens (endpoints at /_server/tokens/...), ALWAYS declared (fixed structure, D26). Every route is auth_rule="SUPERADMIN" and answers the {"error": ...} shape when the server has no api_key_store wired.

The secret invariant mirrors the users section: an api key’s secret_hash NEVER crosses the wire (list strips it), and the full gak_ key is returned ONLY by issue, once — it is never retrievable again (the record keeps only its hash).

create_jwt mints a JWT signed with the FIRST symmetric (HS*) verifier in the auth config (AuthCore.signing_jwt_config): the token verifies against the same config, so no new key material is introduced. With no symmetric verifier configured it answers the error shape.

Parent (dual relationship): the ServerApplication, stored as self.application; the stores are reached via self.application.server.

class genro_asgi.applications.server_sections.tokens_section.TokensSection(application)[source]

Bases: RoutingClass

The _server/tokens mount: SUPERADMIN api-key registry + JWT minting.

Note

Parent (dual relationship): the ServerApplication, stored as self.application. The stores live on self.application.server.

Parameters:

application (ServerApplication)

__init__(application)[source]

Bind the section to its ServerApplication (dual relationship).

Parameters:

application (ServerApplication)

Return type:

None

property api_key_store: ApiKeyStore | None

The server’s ApiKeyStore, or None when tokens are unconfigured.

property auth_core: AuthCore | None

The server’s AuthCore (carries the JWT signing seam), or None.

public_record(record)[source]

A key record without its secret_hash — the wire-safe projection.

Return type:

dict[str, Any]

Parameters:

record (dict[str, Any])

list()[source]

Every api-key record, secret_hash stripped.

Return type:

dict[str, Any]

issue(body_data=None)[source]

Mint an api key; return the full gak_ key ONCE (never again).

Body: label (required), tags (default []), expires_at (POSIX timestamp or absent for a key that never expires).

Return type:

dict[str, Any]

Parameters:

body_data (dict | None)

revoke(key_id='')[source]

Disable a key (the record stays, listed, for audit).

Return type:

dict[str, Any]

Parameters:

key_id (str)

delete(key_id='')[source]

Remove a key record entirely.

Return type:

dict[str, Any]

Parameters:

key_id (str)

create_jwt(body_data=None)[source]

Mint a JWT signed with the first symmetric verifier of the auth config.

Body: sub (required — the token’s subject), tags (default []), expires_in (seconds; absent for no exp claim). The token verifies against the same config it was signed with (no new key material). Answers the error shape when no symmetric verifier is configured.

Return type:

dict[str, Any]

Parameters:

body_data (dict | None)

The _server/tasks section: SUPERADMIN-gated task backbone endpoints.

A RoutingClass the ServerApplication attaches (attach_section), so its routes live at /_server/tasks/.... JSON endpoints ONLY — no HTML/JS panel ships with the core (ratified). Two surfaces over the server’s TaskManager:

  • schedules (the recurring scheduler’s store): list/create/ update/enable/disable/run_now/delete/logs;

  • spool (the batch folders): spool_list (by owner or status), progress, cancel, result.

Every route is gated auth_rule="SUPERADMIN". A server composed without the task backbone — or with tasks=False — answers every endpoint with the {"error": ...} document (HTTP 200): the section is ALWAYS declared, fixed structure (D26), the payload states the availability.

Parent (dual relationship): the section holds its ServerApplication as self.application and reaches the manager via self.application.server.tasks (guarded by tasks_enabled — the property raises when disabled).

class genro_asgi.applications.server_sections.tasks_section.TasksSection(application)[source]

Bases: RoutingClass

The /_server/tasks endpoints over the server’s task backbone.

Note

Bound to its ServerApplication (dual relationship: self.application); the manager, store, scheduler and spool are reached through self.application.server.tasks.

Parameters:

application (ServerApplication)

__init__(application)[source]

Bind the section to its ServerApplication (dual relationship).

Parameters:

application (ServerApplication)

Return type:

None

property manager: TaskManager | None

The server’s TaskManager, or None when tasks are off/absent.

list()[source]

Every schedule record.

Return type:

dict[str, Any]

create(body_data=None)[source]

Create a schedule: code, kind, spec required.

task_name defaults to code; kwargs and enabled are optional. next_run_ts is computed here (an invalid spec is the {"error": ...} answer, not a record).

Return type:

dict[str, Any]

Parameters:

body_data (dict | None)

update(body_data=None)[source]

Merge the editable fields into a schedule (code names it).

A changed kind/spec recomputes next_run_ts; the run-outcome fields are the scheduler’s and never settable from the wire.

Return type:

dict[str, Any]

Parameters:

body_data (dict | None)

enable(code='')[source]

Arm a schedule.

Return type:

dict[str, Any]

Parameters:

code (str)

disable(code='')[source]

Disarm a schedule (the record stays).

Return type:

dict[str, Any]

Parameters:

code (str)

run_now(code='')[source]

Fire a schedule immediately (same no-overlap guard as the loop).

Return type:

dict[str, Any]

Parameters:

code (str)

delete(code='')[source]

Remove a schedule record. deleted reports whether it existed.

Return type:

dict[str, Any]

Parameters:

code (str)

logs(task_name='', limit='')[source]

A task’s capped JSONL run log, oldest first.

Return type:

dict[str, Any]

Parameters:
spool_list(owner='', status='')[source]

Task descriptors by owner OR by status (one filter required).

Return type:

dict[str, Any]

Parameters:
progress(task_id='')[source]

A task’s latest progress snapshot (None until the worker writes).

Return type:

dict[str, Any]

Parameters:

task_id (str)

cancel(task_id='')[source]

Drop the cancel marker (the worker honors it at its own pace).

Return type:

dict[str, Any]

Parameters:

task_id (str)

result(task_id='')[source]

A task’s result (None until written; JSON-encodable results only).

Return type:

dict[str, Any]

Parameters:

task_id (str)