Content-pack admin contributions¶
Content packs can add useful administration surfaces without shipping browser code. MAID renders every contribution through the fixed, audited admin bundle. Packs are trusted Python on the server, but every manifest value, schema, API response, label, and component value is treated as untrusted in the browser.
Admin contributions are optional. Existing structural ContentPack
implementations remain valid without an admin method.
First-party package dispositions¶
The admin extension boundary follows the package layering established by
ADR-003. A package contributes
only when its runtime role requires administration inside GameEngine; being
published in the same workspace is not enough.
| Package | Disposition | Reason and change gate |
|---|---|---|
maid-engine |
System/core owner | Defines validation, assembly, the fixed admin bundle, and engine-owned surfaces. It is the host, not a contributing ContentPack. This changes only if those responsibilities move to a separately loaded runtime package. |
maid-stdlib |
Contributes | Its trusted content pack supplies reusable live administration APIs and fixed-bundle descriptors. |
maid-classic-rpg |
Contributes | Its trusted content pack supplies capability-gated RPG descriptors. |
maid-tutorial-world |
Content pack; intentional none |
The tutorial README describes a teaching world, not an admin workflow. It would contribute only after gaining a real operator use case and the corresponding server-side contract; no surface is claimed today. |
maid-registry |
Not applicable | It is a separately deployed FastAPI registry service and is never loaded into GameEngine. Changing this requires adopting the engine dependency, trust, and content-pack lifecycle instead of remaining standalone. |
teltest |
Not applicable | It is zero-MAID-dependency Telnet test tooling, not server runtime content. Changing this requires deliberately becoming an engine-loaded content pack and accepting a MAID dependency. An HTTP/admin fixture alone is not a reason to cross that boundary. |
CI runs scripts/validate_admin_manifests.py. It discovers every shipped
packages/*/src/**/manifest.toml under publishable first-party packages without
importing those files, discovers trusted Python providers from first-party
maid.content_packs entry points, and reports every package's disposition and
targets. Packages classified contributes must retain at least one nonempty,
valid TOML declaration or nonempty, valid trusted Python provider. Unclassified
packages, missing contributor coverage, and targets that conflict with
system/core or not-applicable fail the policy check, so a new manifest or
provider cannot silently bypass validation.
Contract sources¶
Contract version 1 accepts two sources:
manifest.tomlunder[pack.admin_ui](preferred for portable surfaces).- The optional Python
AdminSurfaceProvider.get_admin_contributions()hook.
MAID combines both sources, validates each descriptor, and records the owner
from the trusted pack registration context. A pack cannot claim another owner.
An invalid admin descriptor is rejected independently: valid descriptors and
the rest of the pack still load, while every rejection appears in Pack
Inventory and maid pack validate exits nonzero. Invalid fundamental pack
metadata (for example, a missing name or version) still prevents discovery.
Identifiers must begin with the exact manifest name plus a dot, for example
demo-pack.status. The core., design., engine., and generic.
namespaces are reserved.
TOML example¶
[pack]
name = "demo-pack"
version = "1.0.0"
display_name = "Demo Pack"
[pack.capabilities]
provides = ["demo-status", "mood-system"]
requires = []
[[pack.admin_ui.routes]]
id = "demo-pack.status"
path_pattern = "/demo-pack/status"
view_id = "generic.rest-panel@1"
backing = "live"
min_view_role = "VIEWER"
required_http_routes = [
{ method = "GET", path_template = "/admin/demo-pack/status" },
]
actions = [
{ id = "generic.refresh", min_role = "VIEWER" },
]
[pack.admin_ui.routes.view_params]
method = "GET"
path = "/admin/demo-pack/status"
response_shape = "key_value"
fields = ["name", "status", "version"]
max_response_bytes = 65536
max_depth = 4
max_rows = 50
[[pack.admin_ui.navigation]]
id = "demo-pack.status-nav"
route_id = "demo-pack.status"
label = "Demo status"
section = "content"
icon = "activity"
order = 100
[[pack.admin_ui.inspector_tabs]]
id = "demo-pack.mood"
label = "Mood"
view_id = "generic.component-inspector@1"
target_types = ["MoodComponent"]
min_role = "VIEWER"
required_capabilities = ["mood-system"]
actions = [
{ id = "generic.component.update", min_role = "BUILDER" },
]
[pack.admin_ui.inspector_tabs.view_params]
component_type = "MoodComponent"
editable = true
The equivalent combined form is
[[pack.admin_ui.contributions]] with an explicit kind of route,
navigation, inspector_tab, toolbar_item, or external_link.
Optional Python provider¶
Use Python when the declaration depends on trusted pack configuration. Do not add this method to a structural pack unless it has contributions.
from maid_engine.admin_ui.models import (
AdminBackingKind,
AdminClientRoute,
AdminContribution,
RequiredHttpRoute,
)
def get_admin_contributions(self) -> list[AdminContribution]:
return [
AdminClientRoute(
id="demo-pack.status",
path_pattern="/demo-pack/status",
view_id="generic.rest-panel@1",
backing=AdminBackingKind.LIVE,
required_http_routes=(
RequiredHttpRoute(
method="GET",
path_template="/admin/demo-pack/status",
),
),
view_params={
"method": "GET",
"path": "/admin/demo-pack/status",
"response_shape": "key_value",
"fields": ["name", "status"],
},
)
]
The hook runs only under MAID's existing trusted-pack execution model. A failure degrades that pack's admin UI; it does not remove engine surfaces.
Views and parameters¶
Use an explicit major version when publishing a reusable contract. Additive minor versions are best-effort compatible; an unknown or incompatible major fails closed.
| View | Parameters | Behavior |
|---|---|---|
generic.component-table@1 |
component_type, columns, optional owner_filter, tag_filter, editable |
Paginated entity/component discovery through the stdlib entity API. |
generic.component-inspector@1 |
component_type, editable |
Allowlisted scalar schema form. Unsupported fields are read-only. |
generic.schema-browser@1 |
optional owner_filter |
Read-only Pydantic document-schema inventory. |
generic.rest-panel@1 |
method = "GET", exact path, response_shape, nonempty fields, optional exact path_params/query_params, bounded size/depth/rows |
Same-origin, credentialed, read-only JSON rendering. |
Component forms support only bounded string, number, integer, boolean,
and primitive enums with basic length/range constraints. References,
combinators, arbitrary formats/patterns, nested objects, prototype keys, deep
payloads, and unknown constraints are never guessed; the UI shows a read-only
fallback.
REST panels accept only the exact declared GET /admin/... route and exact
scalar parameters. The route must have passed startup ownership validation and
be owned by the contributing pack. There is no method, body, header, URL, HTML,
CSS, script, template, or callback field. Unknown response shapes fail closed
with a diagnostic.
Roles, actions, and backing¶
Roles are exactly VIEWER, MODERATOR, BUILDER, ADMIN, or SUPERADMIN.
SYSOP accounts map to SUPERADMIN. min_view_role controls display only;
FastAPI authorization remains authoritative.
Declare every mutation as a cataloged action with its minimum role.
generic.component.update requires BUILDER and uses the existing protected
component endpoint. A viewer can inspect engine surfaces and readable pack
metadata but receives no stronger action entitlement.
Every route declares backing = "live", "design", or "hybrid". Never
describe browser-local drafts or examples as live server state.
Inspector tabs and toolbar items can declare target_types and
required_capabilities. They render only while their owner is active, its
admin status is enabled, all capabilities exist, the caller role is sufficient,
the target matches, and the action/view is in the audited catalog.
HTTP route requirements¶
Pack routes are relative to /admin in register_api_routes(). Custom packs
use their canonical pack name as the first path segment:
Required route templates use the mounted form:
/admin/demo-pack/status. Generic REST is GET-only. Routes receive the engine
VIEWER baseline and retain stronger endpoint dependencies. Route declarations
are diagnostics, never authorization grants.
Startup route mounting is atomic. MAID also mounts one engine-owned dynamic
route host at /admin/ext. The manifest maps each canonical route to its
current endpoint:
For example, /admin/demo-pack/status becomes
/admin/ext/demo-pack/demo-pack/status; stdlib's /admin/entities/ becomes
/admin/ext/stdlib/entities/. Keep declaring and calling canonical
/admin/... paths. The fixed admin bundle applies the authenticated manifest
mapping; pack code must not hard-code /admin/ext or a host generation.
Late load and code reload rebuild a complete isolated child application. A
route-backed surface becomes enabled only after its new handler, dependencies,
response models, schema, lifecycle generation, endpoint mapping, and manifest
revision commit. Unload removes its mapping and new admissions atomically.
Existing startup /admin/... routes remain as compatibility paths, but their
lifecycle guard rejects stale generations.
Operators can disable runtime route activation with
MAID_ADMIN_DYNAMIC_ROUTE_HOST_ENABLED=false. This emergency switch takes
effect at process startup. With it disabled, frozen startup routes continue to
work, but late-loaded or code-reloaded route-backed surfaces are
restart_required until restart. UI-only contributions can still activate
immediately.
MAID does not load pack React, JavaScript, CSS, remote modules, or HTML.
The dynamic host is engine infrastructure, not a pack extension point.
WebSocket routes are rejected. Every HTTP route has the engine VIEWER
baseline and retains stronger pack-declared dependencies. The mounted child
has no public docs, Redoc, or OpenAPI route; authorized admins and tools can
read the generation-scoped schema at GET /admin/ext/openapi.json.
Validation¶
Validate TOML without importing code:
Validate an installed trusted Python provider and startup route context:
Invalid contracts exit nonzero. JSON output contains valid, targets, and
stable diagnostics such as:
| Code | Meaning |
|---|---|
MAID-ADMIN-001 |
TOML/manifest contract is invalid. |
MAID-ADMIN-003 / 004 |
Identifier namespace is wrong/reserved. |
MAID-ADMIN-005 / 006 |
Identifier or SPA path collision. |
MAID-ADMIN-008 / 009 |
REST ownership or required route failure. |
MAID-ADMIN-010 |
Startup HTTP route collision. |
MAID-ADMIN-011 / 013 |
Trusted route/provider hook failed. |
MAID-ADMIN-014 |
Requested installed pack was not discovered. |
MAID-ADMIN-015 |
A first-party package lacks a disposition. |
MAID-ADMIN-016 |
A first-party target conflicts with its disposition. |
MAID-ADMIN-017 |
First-party content-pack entry-point metadata is invalid. |
MAID-ADMIN-018 |
A contributing package has no valid contribution source. |
Troubleshooting¶
- Surface absent: check Pack Inventory for owner, load state, admin status, capabilities, route generation, and the fixed diagnostic code.
- Restart required: the dynamic host is disabled or degraded. Inspect its status and the pack's dynamic route generation in Pack Inventory, then restart or re-enable the host after resolving the incident.
- Read-only form: the schema uses an unsupported feature, the descriptor
omitted
editable, or the manifest did not grantgeneric.component.update. - REST panel rejected: ensure the path exactly matches a declared required route, uses the pack namespace, supplies every template parameter once, and returns the declared bounded JSON shape.
- Namespace error: use
{pack.manifest.name}.…, not a distribution name such asmaid-demo-pack.