From 18d52bdb7cfc3e35e1d81d1ee32fd52f757c6213 Mon Sep 17 00:00:00 2001 From: Vishesh 'ironeagle' Bangotra Date: Wed, 14 Oct 2026 16:18:14 +0530 Subject: [PATCH] feat(standards): integrate auth-server via pinned spec (docs://spec) + auth recipe and template slice - serve the pinned auth-server OpenAPI contract as the standards MCP resource docs://spec (no auth-server MCP bundle) - add Auth Integration recipe (14) and wire it into the registry, prompts, index, and nav - un-defer Auth Server in the library registry (spec/recipe pointers) - ship the opt-in template auth slice: interfaces/auth/ (placeholder + mergeable bearerAuth security.yaml) and AUTH_SERVER/JWT_SERVER env - rebuild bundle (13 modules, 44 components); template stays authless by default --- build.py | 2 + registry.py | 27 +- standards/lib/01_build_recipe/index.html | 22 + standards/lib/02_layering_rules/index.html | 22 + standards/lib/03_composition_root/index.html | 22 + .../lib/04_spec_first_routing/index.html | 22 + standards/lib/05_crud_template/index.html | 22 + standards/lib/06_pipeline_scaffold/index.html | 22 + .../lib/07_worker_and_progress/index.html | 22 + standards/lib/08_human_in_loop/index.html | 22 + standards/lib/09_infra_adaptors/index.html | 22 + standards/lib/12_genericity_matrix/index.html | 24 +- standards/lib/13_library_registry/index.html | 39 +- standards/lib/14_auth_integration/index.html | 971 ++++++++++++++++++ standards/lib/404.html | 22 + standards/lib/index.html | 24 + standards/lib/search/search_index.json | 2 +- standards/lib/sitemap.xml.gz | Bin 127 -> 127 bytes standards/mcp/components.json | 6 + standards/mcp/index.json | 2 +- standards/mcp/libraries.json | 11 +- standards/mcp/modules/auth_integration.json | 8 + standards/mcp/modules/index.json | 2 +- standards/mcp/modules/library_registry.json | 2 +- standards/mcp/nav.json | 5 + standards/mcp/specs/auth-server.json | 563 ++++++++++ standards/mcp/templates/app/.env.example | 4 +- .../templates/app/interfaces/auth/__init__.py | 9 + .../app/interfaces/auth/security.yaml | 14 + standards/src/lib/13_library_registry.md | 13 +- standards/src/lib/14_auth_integration.md | 140 +++ standards/src/lib/index.md | 2 + standards/src/mkdocs.lib.yml | 1 + standards/src/templates/app/.env.example | 4 +- .../templates/app/interfaces/auth/__init__.py | 9 + .../app/interfaces/auth/security.yaml | 14 + standards/wiki/sitemap.xml.gz | Bin 127 -> 127 bytes 37 files changed, 2094 insertions(+), 24 deletions(-) create mode 100644 standards/lib/14_auth_integration/index.html create mode 100644 standards/mcp/modules/auth_integration.json create mode 100644 standards/mcp/specs/auth-server.json create mode 100644 standards/mcp/templates/app/interfaces/auth/__init__.py create mode 100644 standards/mcp/templates/app/interfaces/auth/security.yaml create mode 100644 standards/src/lib/14_auth_integration.md create mode 100644 standards/src/templates/app/interfaces/auth/__init__.py create mode 100644 standards/src/templates/app/interfaces/auth/security.yaml diff --git a/build.py b/build.py index 798bf20..607077b 100644 --- a/build.py +++ b/build.py @@ -187,6 +187,8 @@ _TEMPLATE_COMPONENTS: list[dict[str, str]] = [ "description": "Thin HTTP handlers bound to operationIds."}, {"kind": "openapi-routes", "path": "interfaces/openapi.py", "placeholder": "", "description": "Route registry: flat re-export of every handler namespace."}, + {"kind": "auth", "path": "interfaces/auth/security.yaml", "placeholder": "", + "description": "Mergeable bearerAuth security scheme + global security for openapi.yaml."}, {"kind": "test-model", "path": "tests/unit/test_item_models.py", "placeholder": "resource", "description": "Unit test for the resource model."}, {"kind": "test-routes", "path": "tests/integration/test_item_routes.py", "placeholder": "resource", diff --git a/registry.py b/registry.py index 9b3a6d5..91ed39f 100644 --- a/registry.py +++ b/registry.py @@ -7,6 +7,7 @@ template code — before writing any infrastructure. Resources: docs://index, docs://nav, docs://modules/{module} (from the base server) + docs://spec (the pinned auth-server OpenAPI contract) registry://libraries (the structured catalog) registry://components (the template component index) @@ -46,7 +47,8 @@ _INTAKE_TEXT = ( "2. Axis 1 — resources: each resource, its fields, and relationships\n" " (or paste `openapi.yaml`).\n" "3. Axis 2 — pipelines: multi-step inputs? families + formats (or `none`).\n" - "4. Auth needed now? (yes/no; auth is deferred — `py-jwt` only).\n" + "4. Auth needed now? (yes/no). If yes, the auth-server service provides it —\n" + " read `docs://spec` + `docs://modules/auth_integration` (no auth code to write).\n" "5. Integrations: Mongo / ingestion formats / email / LLM provider.\n\n" "Then: `build(app_name=..., resources=..., pipelines=..., auth=...)`.\n" ) @@ -79,7 +81,10 @@ def _build_text(app_name: str, resources: str, pipelines: str, auth: str) -> str "4. Scaffold with `get_app_template()` (whole tree) or pull single " "files with `list_app_components()` and " "`get_app_component(kind, resource=..., family=...)`.\n" - "5. Author only Tier C; keep Tier B as shipped.\n" + "5. If auth is needed, read `docs://spec` (the auth-server contract) and " + "`docs://modules/auth_integration`; declare `securitySchemes.bearerAuth` " + "and `security` in your spec — do not write auth code.\n" + "6. Author only Tier C; keep Tier B as shipped.\n" ) @@ -114,6 +119,13 @@ class StandardsMCPServer(MCPServer): return {"error": "not_found", "path": str(path), "components": []} return json.loads(path.read_bytes()) + def _spec(self) -> dict[str, Any]: + """Load the pinned OpenAPI contract (auth-server).""" + path = Path(self.mcp_root) / "specs" / "auth-server.json" + if not path.exists(): + return {"error": "not_found", "path": str(path)} + return json.loads(path.read_bytes()) + def _find_component(self, kind: str) -> dict[str, Any] | None: for entry in self._components().get("components", []): if entry.get("kind") == kind: @@ -183,6 +195,15 @@ class StandardsMCPServer(MCPServer): def components() -> dict[str, Any]: return self._components() + @self.app.resource("docs://spec") + def spec() -> dict[str, Any]: + """Return the pinned auth-server OpenAPI contract. + + Use this (instead of a live fetch) to learn the auth-server client + contract when wiring authentication into an app. + """ + return self._spec() + @self.app.tool() def find_library(capability: str) -> list[dict[str, Any]]: """Find internal libraries matching a capability keyword. @@ -211,6 +232,8 @@ class StandardsMCPServer(MCPServer): str(lib.get("import", "")), str(lib.get("purpose", "")), str(lib.get("use_instead_of", "")), + str(lib.get("spec", "")), + str(lib.get("recipe", "")), " ".join(lib.get("capabilities", [])), ] ).lower() diff --git a/standards/lib/01_build_recipe/index.html b/standards/lib/01_build_recipe/index.html index 2cbd208..8cae556 100644 --- a/standards/lib/01_build_recipe/index.html +++ b/standards/lib/01_build_recipe/index.html @@ -571,6 +571,28 @@ +
  • + + + + + + Auth Integration + + + + + +
  • + + + + + + + + +
  • diff --git a/standards/lib/02_layering_rules/index.html b/standards/lib/02_layering_rules/index.html index a761543..48c4454 100644 --- a/standards/lib/02_layering_rules/index.html +++ b/standards/lib/02_layering_rules/index.html @@ -562,6 +562,28 @@ +
  • + + + + + + Auth Integration + + + + + +
  • + + + + + + + + +
  • diff --git a/standards/lib/03_composition_root/index.html b/standards/lib/03_composition_root/index.html index c4ed3af..547798e 100644 --- a/standards/lib/03_composition_root/index.html +++ b/standards/lib/03_composition_root/index.html @@ -553,6 +553,28 @@ +
  • + + + + + + Auth Integration + + + + + +
  • + + + + + + + + +
  • diff --git a/standards/lib/04_spec_first_routing/index.html b/standards/lib/04_spec_first_routing/index.html index 0e63bc7..9090fa3 100644 --- a/standards/lib/04_spec_first_routing/index.html +++ b/standards/lib/04_spec_first_routing/index.html @@ -580,6 +580,28 @@ +
  • + + + + + + Auth Integration + + + + + +
  • + + + + + + + + +
  • diff --git a/standards/lib/05_crud_template/index.html b/standards/lib/05_crud_template/index.html index b5d8f5e..5d6872a 100644 --- a/standards/lib/05_crud_template/index.html +++ b/standards/lib/05_crud_template/index.html @@ -589,6 +589,28 @@ +
  • + + + + + + Auth Integration + + + + + +
  • + + + + + + + + +
  • diff --git a/standards/lib/06_pipeline_scaffold/index.html b/standards/lib/06_pipeline_scaffold/index.html index fcb6c29..f41cd97 100644 --- a/standards/lib/06_pipeline_scaffold/index.html +++ b/standards/lib/06_pipeline_scaffold/index.html @@ -580,6 +580,28 @@ +
  • + + + + + + Auth Integration + + + + + +
  • + + + + + + + + +
  • diff --git a/standards/lib/07_worker_and_progress/index.html b/standards/lib/07_worker_and_progress/index.html index 965826c..d6a1837 100644 --- a/standards/lib/07_worker_and_progress/index.html +++ b/standards/lib/07_worker_and_progress/index.html @@ -553,6 +553,28 @@ +
  • + + + + + + Auth Integration + + + + + +
  • + + + + + + + + +
  • diff --git a/standards/lib/08_human_in_loop/index.html b/standards/lib/08_human_in_loop/index.html index 6eaedab..1a48cb6 100644 --- a/standards/lib/08_human_in_loop/index.html +++ b/standards/lib/08_human_in_loop/index.html @@ -562,6 +562,28 @@ +
  • + + + + + + Auth Integration + + + + + +
  • + + + + + + + + +
  • diff --git a/standards/lib/09_infra_adaptors/index.html b/standards/lib/09_infra_adaptors/index.html index 15ce400..4284a89 100644 --- a/standards/lib/09_infra_adaptors/index.html +++ b/standards/lib/09_infra_adaptors/index.html @@ -580,6 +580,28 @@ +
  • + + + + + + Auth Integration + + + + + +
  • + + + + + + + + +
  • diff --git a/standards/lib/12_genericity_matrix/index.html b/standards/lib/12_genericity_matrix/index.html index 929fc64..daa0897 100644 --- a/standards/lib/12_genericity_matrix/index.html +++ b/standards/lib/12_genericity_matrix/index.html @@ -14,7 +14,7 @@ - + @@ -562,6 +562,28 @@ +
  • + + + + + + Auth Integration + + + + + +
  • + + + + + + + + +
  • diff --git a/standards/lib/13_library_registry/index.html b/standards/lib/13_library_registry/index.html index 589a524..df53545 100644 --- a/standards/lib/13_library_registry/index.html +++ b/standards/lib/13_library_registry/index.html @@ -11,7 +11,7 @@ - + @@ -462,6 +462,28 @@ + + +
  • + + + + + + Auth Integration + + + + + +
  • + + + + + + + @@ -734,9 +756,9 @@ can read a library's full API by connecting to its MCP and reading Auth Server (service) -Central identity service (register/login/me/introspect) +Central identity service (register/login/me/logout/introspect) building auth endpoints yourself -(deferred) +spec: docs://spec @@ -786,15 +808,20 @@ can read a library's full API by connecting to its MCP and reading

    Discovery

    A single standards MCP exposes find_library(capability), which returns the matching library, its import name, its "use instead of" guidance, and its MCP -endpoint. Query it before writing any infrastructure.

    +endpoint (or, for the auth-server service, a spec pointer). It also serves the +pinned auth-server OpenAPI contract at docs://spec. Query it before writing +any infrastructure.

    Notes

    diff --git a/standards/lib/14_auth_integration/index.html b/standards/lib/14_auth_integration/index.html new file mode 100644 index 0000000..d84f7d4 --- /dev/null +++ b/standards/lib/14_auth_integration/index.html @@ -0,0 +1,971 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Auth Integration - Aetos Standards — Reference + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + Skip to content + + +
    +
    + +
    + + + + + + +
    + + +
    + +
    + + + + + + +
    +
    + + + +
    +
    +
    + + + + + +
    +
    +
    + + + +
    +
    + +
    +
    + + + +
    +
    + + + + + +

    Auth Integration

    +

    Summary

    +

    Authentication is a service, not a library. Every Aetos application shares a +single identity service — the auth-server — and consumes it through its HTTP +contract. An app never implements registration, login, password hashing, or +token issuance itself.

    +

    The agent does not need an MCP bundle for the auth-server. It reads the +pinned OpenAPI contract (below) and declares a bearer security scheme in its own +spec; openapi_first then generates the verifying dependency. No auth code is +written.

    +
    +

    Read the contract

    +

    The auth-server OpenAPI document is pinned in the standards bundle and served as +an MCP resource:

    +
    docs://spec
    +
    +

    It is the authoritative client contract (OpenAPI 3.1). Read it to learn the +endpoints and the request/response schemas:

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    EndpointPurpose
    POST /registerCreate a user (201, returns PublicUser)
    POST /loginAuthenticate and issue a JWT (LoginResponse)
    GET /meCurrent user's PublicUser (bearer required)
    POST /logoutStateless logout (LogoutResponse)
    POST /introspectInternal token introspection (active, user)
    +

    The pinned snapshot is versioned (currently 0.0.5). If the caller has a live +auth-server and needs the current contract, they can pass the full endpoint +({AUTH_SERVER}/openapi.json) and read it directly instead of the pin.

    +
    +

    Wire it up: declare, don't code

    +

    Authentication is added by editing the app's own openapi.yaml — nothing +else. Declare a global bearer requirement and point the scheme at the auth +server (values come from the environment):

    +
     1
    + 2
    + 3
    + 4
    + 5
    + 6
    + 7
    + 8
    + 9
    +10
    +11
    +12
    +13
    +14
    +15
    +16
    security:
    +  - bearerAuth: []
    +paths:
    +  # ...
    +components:
    +  securitySchemes:
    +    bearerAuth:
    +      type: http
    +      scheme: bearer
    +      bearerFormat: JWT
    +      x-server-url: "{AUTH_SERVER}"
    +      x-introspect-path: "/introspect"
    +      x-login-path: "/login"
    +      x-register-path: "/register"
    +      x-logout-path: "/logout"
    +      x-me-path: "/me"
    +
    +

    openapi_first reads securitySchemes + security and generates a FastAPI +dependency that extracts the bearer token, POSTs {"token": <jwt>} to +{AUTH_SERVER}/introspect, and returns the user when active == true. It +raises 401 on an invalid or inactive token and 503 when the auth server is +unreachable. The generated handlers receive the authenticated user — you author +no authentication code.

    +

    Public operations

    +

    A route that must stay open opts out by overriding security with an empty +list:

    +
    1
    +2
    +3
    +4
    +5
    +6
    +7
    +8
    paths:
    +  /health:
    +    get:
    +      operationId: health
    +      security: []        # overrides the global requirement
    +      responses:
    +        "200":
    +          description: OK
    +
    +

    Environment

    +
    1
    +2
    AUTH_SERVER=http://localhost:8000     # auth-server base URL
    +JWT_SERVER=http://localhost:8000      # jwtlib client base URL (seeding/CLI)
    +
    +
    +

    Path 1: jwtlib client (no generator)

    +

    Apps that do not use a generated dependency verify requests with the py-jwt +client:

    +
    1
    +2
    +3
    +4
    +5
    +6
    +7
    +8
    from jwtlib.introspection import authenticate_request
    +
    +authorized = await authenticate_request(
    +    should_skip_authentication,  # allow public paths
    +    method,
    +    path,
    +    authorization_token,         # "Bearer <jwt>" or None
    +)
    +
    +

    authenticate_request POSTs to {JWT_SERVER}/introspect (3 second timeout) and +treats {"active": true, "user": {...}} as valid. Use this for seeding scripts +and tests (jwtlib.register_user, jwtlib.login_user).

    +
    +

    Golden rules

    +
      +
    1. Never trust an unverified token. Decode through the generated dependency + or jwtlib before handling the request.
    2. +
    3. Treat active: false as unauthenticated, even if the JWT decodes.
    4. +
    5. Do not implement your own JWT parsing. Revocation and issuer changes must + stay centralized in the auth-server.
    6. +
    7. Do not add auth until the app's outcome is validated. Ship the app + authless first; layer the security scheme once the core behaviour works.
    8. +
    +
    +

    Notes

    +
      +
    • The default template is authless on purpose. Add the securitySchemes + block above only when auth is required. Pull the shared slice with + get_app_component("auth").
    • +
    • The auth-server is deployed as a service (no importable package); the client + surface is the spec, plus jwtlib for the low-level path.
    • +
    + + + + + + + + + + + + + +
    +
    + + + + + +
    + + + +
    + + + +
    +
    +
    +
    + + + + + + + + + + + + \ No newline at end of file diff --git a/standards/lib/404.html b/standards/lib/404.html index 4da309b..56a5808 100644 --- a/standards/lib/404.html +++ b/standards/lib/404.html @@ -457,6 +457,28 @@ +
  • + + + + + + Auth Integration + + + + + +
  • + + + + + + + + +
  • diff --git a/standards/lib/index.html b/standards/lib/index.html index 5b7ed12..4419014 100644 --- a/standards/lib/index.html +++ b/standards/lib/index.html @@ -474,6 +474,28 @@ +
  • + + + + + + Auth Integration + + + + + +
  • + + + + + + + + +
  • @@ -540,6 +562,8 @@ patterns, and the internal library catalog that agents consult while coding.

  • Human-in-the-Loop — guardrails and approvals inside automations.
  • Infra Adapters — how infrastructure connects without leaking into domain.
  • Genericity Matrix — what is library-backed, template-provided, or authored.
  • +
  • Auth Integration — consuming the shared auth-server service (spec-driven, + no auth code).
  • Library Registry — the catalog of internal libraries behind all of it.
  • Read the Wiki for the philosophy and rationale; these pages are the diff --git a/standards/lib/search/search_index.json b/standards/lib/search/search_index.json index f2dfe1a..649c713 100644 --- a/standards/lib/search/search_index.json +++ b/standards/lib/search/search_index.json @@ -1 +1 @@ -{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"Standards Reference","text":"

    The reference half of the Standards description: the enforceable rules, patterns, and the internal library catalog that agents consult while coding.

    Read the Wiki for the philosophy and rationale; these pages are the reference that mirrors the standards MCP bundle.

    "},{"location":"01_build_recipe/","title":"Build Recipe","text":""},{"location":"01_build_recipe/#summary","title":"Summary","text":"

    The ordered, deterministic checklist for generating a new application of the standard shape. Every step is fixed; only the resource model (step 2), the codegen pass (step 3), and the pipeline family (step 8) need app-specific work.

    This page is served by the standards MCP as docs://modules/build_recipe. The start_app prompt on that server collects the two axes and hands off here; pull exact files with get_app_component.

    "},{"location":"01_build_recipe/#bootstrap","title":"\ud83d\ude80 Bootstrap","text":"

    From the standards docs checkout:

    cp -r <docs>/standards/src/templates/app apps/<your-app>\ncd apps/<your-app>\n

    Install the internal libraries \u2014 editable local checkouts (preferred in development) or the private index (CI / fresh machines). Credentials are environment-only; never commit them.

    # local checkouts\npip install -e ../aetos/openapi-first -e ../aetos/mongo-ops -e ../aetos/hexa \\\n            -e ../aetos/dagpipe -e ../aetos/doc-forge\n# private index\npip install --extra-index-url \\\n  \"https://$PIP_REGISTRY_USERNAME:$PIP_REGISTRY_PASSWORD@pip.aetoskia.com/simple\" .\n
    "},{"location":"01_build_recipe/#agent-prompt-copyable","title":"\ud83d\udccb Agent prompt (copyable)","text":"
    # Build a new application from the Aetos Standards blueprint\n\nYou are coding a complete new backend application conforming to the Aetos\nstandard app blueprint. Execute every step in order. Only steps 2\u20134 use the\napp-specific details provided by the user; everything else is deterministic.\n\n## Before writing any code\n\nConnect to the `standards` MCP server and read:\n- `docs://nav` (the module list)\n- `docs://modules/genericity_matrix` (Tier A library / Tier B template / Tier C authored)\n- `docs://modules/layering_rules` (the dependency rules)\n\nThen:\n- call `find_library` with `db`, `di`, `routing`, `ingestion`, `pipeline`, `docs`\n  and note the Tier A libraries that already exist \u2014 never reimplement one;\n- call `list_app_components()` and pull each file you need with\n  `get_app_component(kind, resource=..., family=...)` instead of writing it from\n  scratch.\n\n## Steps\n\n1. **Scaffold** \u2014 copy `standards/src/templates/app/` into `apps/<your-app>/`\n   (or pull the same tree via `get_app_template`). Keep the tree: `domain/`,\n   `infra/`, `interfaces/`, `pipelines/`, `tests/`, `scripts/`, plus\n   `pyproject.toml`, `openapi.yaml`, `config.py`, `init.py`, `main.py`,\n   `worker.py`, `Dockerfile`, `docker-compose.yaml`, `.env.example`.\n\n2. **Author the spec (app-specific)** \u2014 write `openapi.yaml`: resources,\n   schemas, `operationId`s, security, and extension metadata. This is the\n   contract and drives all routing. Every operation MUST have an `operationId`.\n\n3. **Generate the scaffold (codegen)** \u2014 from the spec, generate the wire\n   models and the exact handler signatures:\n   ```bash\n   openapi-first models openapi.yaml -o _gen/models.py\n   openapi-first routes openapi.yaml -o _gen/routes \\\n     --use-models --models-module _gen.models --inject-request --package --template-style\n   ```\n   `_gen/models.py` is the **wire contract** (Pydantic, one class per\n   `components.schemas` entry). `_gen/routes/<resource>/routes.py` lists every\n   `operationId` with its exact parameter signature. `--template-style` makes\n   those stubs `async def` with a `body` parameter, matching the handlers you\n   keep in `interfaces/` (without it they are sync `def` stubs taking\n   `payload`; both bind the same way). Treat `_gen/` as generated\n   (never hand-edit it) \u2014 the canonical handlers live in `interfaces/`.\n\n4. **Write each CRUD slice (app-specific)** \u2014 per resource create the four-file\n   slice: `models.py` (domain model, **no `id`**), `ports/db.py`\n   (`class XDocument(BaseDocument, X)` \u2014 id comes from `BaseDocument`),\n   `ports/service.py`, and optional `converters.py`. Follow the CRUD template.\n\n5. **Wire the composition root** \u2014 in `init.py`, build leaf repositories, then\n   dependent repositories, then services; add infra adapters (db, cache, stores,\n   progress bus, fetchers). Expose `get_svc` overloads.\n\n6. **Add routes** \u2014 create `interfaces/<resource>/routes.py` thin handlers whose\n   names match the `operationId`s (use the signatures from step 3 as your\n   checklist) and re-export them in `interfaces/openapi.py`. Add the cache\n   decorator to read endpoints.\n\n7. **Bootstrap app and worker** \u2014 confirm `main.py` runs the OpenAPI-first app\n   with its lifespan. If the app has long-running jobs, adapt `worker.py` to poll\n   the job collection, run the pipeline, and publish progress.\n\n8. **Add pipeline families (only if data-driven)** \u2014 copy `pipelines/example/`\n   to `pipelines/<family>/` with `abc.py`, `impl.py`, `pipeline.yaml`,\n   `resources/`, and reuse `pipelines/shared/` (loader + registry). Register the\n   root. Add fixtures and a parity test.\n\n9. **Add infrastructure only where needed** \u2014 Tier A capability: use the\n   library. Tier B capability: copy the template. Never hand-write a Tier A\n   library.\n\n10. **Tests** \u2014 integration tests with fakes at the HTTP edge; unit tests for\n    domain and pipeline logic; a config-consistency test for rule-driven\n    pipelines. Include `tests/integration/test_openapi_app_builds.py` (imports\n    the app, asserts `import main` binds every operation).\n\n11. **Scripts and seeds** \u2014 operator scripts under `scripts/` using\n    `init()` / `get_svc()` / `shutdown()`; run as `python -m scripts.<name>`.\n\n12. **Docs** \u2014 author GSDFC docstrings, generate `.pyi` stubs, mark the package\n    `py.typed`, and build the docs.\n\n**Definition of done:**\n- `python -c \"import main\"` succeeds.\n- `pytest` passes.\n- The app serves every operation in the spec; the worker drains its queue.\n- No domain module imports a web framework.\n- No Tier A library was reimplemented.\n
    "},{"location":"01_build_recipe/#where-the-steps-land","title":"\ud83e\udded Where the steps land","text":"

    The steps map onto the layers as follows:

    Step Package / file Depends on 1 \u00b7 Scaffold whole templates/app/ \u2014 2 \u00b7 Spec openapi.yaml \u2014 3 \u00b7 Codegen _gen/ (generated) spec, openapi-first 4 \u00b7 CRUD slices domain/<resource>/ spec, CRUD template 5 \u00b7 Wiring init.py every slice 6 \u00b7 Routes interfaces/<resource>/ services, openapi route registry 7 \u00b7 Bootstrap main.py, worker.py composition root 8 \u00b7 Pipelines pipelines/<family>/, shared/ library pipeline engine 9 \u00b7 Infra infra/ domain ports 10 \u00b7 Tests tests/ app + fixtures 11 \u00b7 Scripts scripts/ composition root 12 \u00b7 Docs docstrings + docs build code, Doc Forge"},{"location":"01_build_recipe/#the-codegen-fast-path-in-detail","title":"\ud83d\udd27 The codegen fast path, in detail","text":"

    openapi-first is spec \u2192 code only (it never generates a spec). Use it to erase the two mechanical parts of the build:

    Generated artifact Replaces Notes _gen/models.py hand-writing Pydantic wire models one class per components.schemas entry _gen/routes/<resource>/routes.py hand-typing handler signatures names == operationIds; request: Request injected

    Rules of the road:

    "},{"location":"01_build_recipe/#definition-of-done","title":"\u26a0\ufe0f Definition of done","text":""},{"location":"01_build_recipe/#related","title":"Related","text":""},{"location":"02_layering_rules/","title":"Layering Rules","text":""},{"location":"02_layering_rules/#summary","title":"Summary","text":"

    The dependency direction is fixed. Violating it is a design defect.

    "},{"location":"02_layering_rules/#allowed-imports","title":"Allowed imports","text":"
    interfaces/*      ->  domain/*            (services, ports, models)\ninterfaces/*      ->  infra/adapters/*    (only read-only value objects)\ndomain/*          ->  domain/*            (same layer)\ndomain/*          ->  pipelines/*         (call a pipeline via a port)\ndomain/*/ports/*  ->  infra/*             (bind adapters + libraries at the seam)\ninfra/adapters/*  ->  domain/ports/*      (implement the interface)\nmain.py/init.py   ->  everything          (composition root only)\n
    "},{"location":"02_layering_rules/#forbidden-imports","title":"Forbidden imports","text":"
    domain/*          ->  interfaces/*        (never)\ndomain/*          ->  fastapi/starlette   (never)\ninfra/adapters/*  ->  interfaces/*        (never)\ninterfaces/*      ->  infra/db/*          (state lives in resources, not imports)\n
    "},{"location":"02_layering_rules/#why","title":"Why","text":""},{"location":"02_layering_rules/#the-portadapter-contract","title":"The port/adapter contract","text":"

    The ports/ package is the seam where the domain meets infrastructure. Each domain/<resource>/ports/ module declares the resource's models/document shape and exposes factory functions that bind a concrete adapter \u2014 so a port module is allowed to import from infra/ and from the persistence libraries. Everything outside ports/ in domain/ stays framework- and driver-free.

    domain/<resource>/ports/db.py       -> document model + create_<resource>_repo()\ndomain/<resource>/ports/service.py  -> service + get_<resource>_service()\ninfra/db/repos/<vendor>.py          -> repository adapter factory\ninfra/adapters/service.py           -> generic CRUD service base\n

    The composition root calls the port factories; nothing else constructs an adapter.

    "},{"location":"02_layering_rules/#enforcement","title":"Enforcement","text":""},{"location":"03_composition_root/","title":"Composition Root","text":""},{"location":"03_composition_root/#summary","title":"Summary","text":"

    All wiring happens in exactly one module. It builds a flat resource registry and exposes it to the HTTP layer and to scripts.

    "},{"location":"03_composition_root/#the-resource-registry","title":"The resource registry","text":"

    init.py returns a plain dict[str, Any]. Keys follow two conventions:

    Plus named infrastructure adaptors:

    The API stores the dict on app.state.db_resources during lifespan startup. Handlers read from it; nothing imports concrete adapters.

    "},{"location":"03_composition_root/#shape","title":"Shape","text":"
    _resources: dict = {}\n_initialized = False\n\nasync def init(settings=None) -> dict:\n    global _initialized\n    if _initialized:\n        return _resources\n\n    await setup_database(uri=settings.mongo_uri, db_name=settings.mongo_db)\n\n    # 1. leaf repositories\n    user_repo = create_user_repo()\n\n    # 2. dependent repositories (dependencies passed by keyword)\n    wallet_repo = create_wallet_repo(user_repo=user_repo)\n\n    # 3. services (build via the DI library, injecting the repo)\n    _resources[\"user_repo\"]   = user_repo\n    _resources[\"wallet_repo\"] = wallet_repo\n    _resources[\"user_svc\"]    = get_user_service(user_repo)\n    _resources[\"wallet_svc\"]  = get_wallet_service(wallet_repo)\n\n    # 4. infrastructure adaptors (optional)\n    _resources[\"http_cache_store\"] = HttpCacheStore(...)\n    _resources[\"progress_bus\"]     = ProgressEventBus()\n\n    _initialized = True\n    return _resources\n\nasync def shutdown() -> None:\n    global _initialized\n    await close_stores(_resources)\n    await shutdown_database()\n    _resources.clear()\n    _initialized = False\n
    "},{"location":"03_composition_root/#typed-accessors","title":"Typed accessors","text":"

    Expose a get_svc(name) helper with one @overload per key so scripts get typing without importing concrete classes:

    @overload\ndef get_svc(name: Literal[\"user_svc\"]) -> UserService: ...\n@overload\ndef get_svc(name: Literal[\"wallet_svc\"]) -> WalletService: ...\n\ndef get_svc(name: str):\n    return _resources[name]\n

    Also expose get_db() (raw database handle) and pure helpers such as compute_fingerprint(payload) (canonical hash used for deduplication).

    "},{"location":"03_composition_root/#rules","title":"Rules","text":""},{"location":"04_spec_first_routing/","title":"Spec-First Routing","text":""},{"location":"04_spec_first_routing/#summary","title":"Summary","text":"

    The OpenAPI document is the single source of truth for the HTTP contract. Handlers are bound to it by name; there is no decorator router.

    "},{"location":"04_spec_first_routing/#the-contract","title":"The contract","text":""},{"location":"04_spec_first_routing/#app-bootstrap","title":"App bootstrap","text":"
    # main.py\nfrom openapi_first.app import OpenAPIFirstApp\n\napp = OpenAPIFirstApp(\n    openapi_path=\"openapi.yaml\",\n    routes_module=interfaces.openapi,   # module whose namespace holds handlers\n    title=\"<App> API\",\n    version=\"0.1.0\",\n    lifespan=lifespan,\n)\n

    lifespan calls the composition root:

    @asynccontextmanager\nasync def lifespan(app):\n    app.state.db_resources = await init()\n    yield\n    await shutdown()\n
    "},{"location":"04_spec_first_routing/#handler-registry","title":"Handler registry","text":"

    interfaces/openapi.py is a flat re-export of every handler, so the framework can resolve operationId across the whole app:

    from interfaces.item.routes import *\nfrom interfaces.wallet.routes import *\nfrom interfaces.progress.sse import *\n
    "},{"location":"04_spec_first_routing/#handler-shape","title":"Handler shape","text":"

    Handlers are thin and synchronous with respect to business logic:

    async def create_item(request: Request, body: dict):\n    svc = request.app.state.db_resources[\"item_svc\"]\n    return await svc.create(Item.model_validate(body))\n

    Generated stubs. openapi-first routes emits this exact shape (async def handlers taking body) when given --template-style; without that flag the stub is def create_item(payload: ItemInput) \u2014 sync, with a payload parameter. Both shapes bind identically: the router resolves handlers by operationId and introspects the FastAPI signature, and it never inspects the request body. Pass --template-style so the generated signatures match the handlers you keep in interfaces/.

    "},{"location":"04_spec_first_routing/#conventions","title":"Conventions","text":""},{"location":"04_spec_first_routing/#authless-apps","title":"Authless apps","text":"

    Auth is deferred in the standard: the same spec-first app runs with or without it, because security lives entirely in the spec, never in handlers. For an app that needs no auth yet, do not invent per-handler auth checks \u2014 just omit the security keys:

    Profile endpoints (/users/me) and profile headers (x-profile) still exist in an authless app. Define a stand-in identity in the composition root \u2014 for example get_current_user returns or lazily creates a fixed \"Demo User\" \u2014 so handlers stay auth-agnostic and only the wiring changes when real auth arrives.

    The handlers, services, and composition root are unchanged. When auth is needed later, add the global security block and an introspecting dependency; only handlers that must opt out get security: [].

    "},{"location":"04_spec_first_routing/#codegen","title":"Codegen","text":"

    openapi_first scaffolds models and route stubs from the spec (see the Build Recipe, step 3):

    openapi-first models openapi.yaml -o _gen/models.py\nopenapi-first routes openapi.yaml -o _gen/routes \\\n  --use-models --models-module _gen.models --inject-request --package --template-style\n

    Prefer codegen over hand-writing boilerplate. The result is the wire contract and a set of operationId-named signatures \u2014 not the canonical handlers.

    "},{"location":"05_crud_template/","title":"CRUD Template","text":""},{"location":"05_crud_template/#summary","title":"Summary","text":"

    Every resource follows the same four-file slice. Repetition is intentional: it makes the codebase predictable and generatable.

    "},{"location":"05_crud_template/#the-slice","title":"The slice","text":"
    domain/<resource>/\n    models.py               # pure Pydantic model (no DB fields)\n    ports/\n        db.py               # document model + repository factory\n        service.py          # service class + factory\n        converters.py       # (optional) transforms\n
    "},{"location":"05_crud_template/#the-id-rule-model-vs-document","title":"The id rule (model vs. document)","text":"

    Two different objects, two different jobs:

    Object Lives in Has id? Purpose Domain model domain/<resource>/models.py No business shape, framework-free Document domain/<resource>/ports/db.py Yes (from BaseDocument) Mongo persistence shape

    BaseDocument (mongo-ops) supplies id: Optional[PyObjectId] (aliased to _id) plus created_at / updated_at. The domain model must not declare id; Mongo owns it.

    Trap \u2014 generated wire models. openapi-first models produces the HTTP contract (see the Build Recipe, step 3). Response schemas frequently declare a required id: str, so a generated class is not a domain model. If you write class ItemDocument(Item, BaseDocument) and Item re-exports a required id: str, that field shadows BaseDocument.id and no Mongo _id can ever be set. Fix it one of three ways:

    1. Preferred: keep the domain model id-free (hand-author it from the CRUD template), and keep the required id only on the wire model.
    2. Order the bases so the document wins: class ItemDocument(BaseDocument, Item).
    3. Keep a separate document type and translate with ports/converters.py.
    "},{"location":"05_crud_template/#modelspy","title":"models.py","text":"

    A framework-free Pydantic model describing the business shape (no id).

    class Item(BaseModel):\n    name: str\n    quantity: int = 0\n
    "},{"location":"05_crud_template/#portsdbpy","title":"ports/db.py","text":"

    The persistence model extends the business model and adds database concerns. The repository factory selects the right repository flavour.

    # Item is id-free; BaseDocument adds id/created_at/updated_at.\nclass ItemDocument(Item, BaseDocument):\n    pass\n\ndef create_item_repo(*, owner_repo=None) -> ItemRepository:\n    return repo_factory(\n        collection=\"items\",\n        document=ItemDocument,\n        populate_rules=[PopulateRule(field_name=\"owner\", collection_name=\"users\")],\n        cache=None,               # or a CachedBaseRepository config\n    )\n
    "},{"location":"05_crud_template/#nested-references","title":"Nested references","text":"

    The wire contract often declares a reference as an expanded object (expense.entity: Entity), while the domain model stays id-free. Pick one convention and apply it consistently:

    For populate-backed repositories change a reference with update, not patch: patch rejects changes to populated FK fields by design.

    "},{"location":"05_crud_template/#portsservicepy","title":"ports/service.py","text":"

    The service is built through the DI library, injecting the repository. The generic CRUD base provides create/read/update/patch/delete/search/....

    class ItemService(CrudsServiceAdaptor[Item, ItemDocument]):\n    model = Item\n    schema = ItemDocument\n\ndef get_item_service(repo) -> ItemService:\n    return build(ItemService, instances={\"repo\": repo})\n
    "},{"location":"05_crud_template/#naming-conventions","title":"Naming conventions","text":"Concept Convention collection plural snake case (items) document <Resource>Document repository factory create_<resource>_repo service <Resource>Service service factory get_<resource>_service resource key <resource>_svc operationId list_items, create_item, ..."},{"location":"05_crud_template/#optional-files","title":"Optional files","text":""},{"location":"06_pipeline_scaffold/","title":"Pipeline Scaffold","text":""},{"location":"06_pipeline_scaffold/#summary","title":"Summary","text":"

    Data-driven processing is expressed as a composition graph. The graph is built by a DI library; execution is driven by a DAG engine. Stages are pure and individually testable.

    "},{"location":"06_pipeline_scaffold/#when-to-use-a-pipeline","title":"When to use a pipeline","text":"

    Use a pipeline when a resource is produced by a multi-step transformation of an input (a document, a file, a message) rather than by a simple CRUD write.

    "},{"location":"06_pipeline_scaffold/#structure-of-a-family","title":"Structure of a family","text":"

    A family is one input shape (e.g. a file format or a message kind). Each family has the same skeleton:

    pipelines/<family>/\n    abc.py            # port contracts (stage interfaces + root interface)\n    impl.py           # concrete stages + root implementation\n    pipeline.yaml     # graph spec: nodes, edges, roots, schema\n    resources/\n        pipeline.yaml # runtime spec consumed by the loader\n        patterns/*.yaml  # rule fragments (pattern registry)\n    banks/<variant>.py   # optional variants that re-pin a few slots\n
    "},{"location":"06_pipeline_scaffold/#shared-scaffolding","title":"Shared scaffolding","text":"
    pipelines/shared/\n    ports.py     # RegistryPort, FinalizePort, etc.\n    loader.py    # build(root_cls) -> wires the graph + engine\n    registry.py  # resolve_root(...) -> root class for a (kind, variant)\n
    "},{"location":"06_pipeline_scaffold/#the-loader","title":"The loader","text":"
    def build(root_cls, *, instances=None, config=None):\n    # merge resources/patterns/*.yaml into a pattern registry\n    # bind registry/config into each graph node by id\n    # build the schema (state) class and the engine\n    root = root_cls()\n    root._engine = Engine(graph, state_cls, payload_cls)\n    return root\n
    "},{"location":"06_pipeline_scaffold/#the-registry","title":"The registry","text":"

    A single lookup replaces hand-rolled if/else selection:

    _FAMILIES = {\n    \"csv\":  {\"acme\": {\"heuristic\": AcmeCsvPipeline}},\n    \"json\": {\"globex\": {\"heuristic\": GlobexJsonPipeline}},\n}\n\ndef resolve_root(kind, variant, mode=\"heuristic\"):\n    return _FAMILIES[kind][variant][mode]\n

    Adding a new variant means adding one class and one registry entry.

    "},{"location":"06_pipeline_scaffold/#driving-a-pipeline","title":"Driving a pipeline","text":"

    Stages are driven explicitly so intermediate state can be persisted:

    pipeline = build(resolve_root(kind, variant))\ncontent = pipeline.ingest.materialize(raw_bytes)\nrecords = pipeline.reader.read(content)\nitems   = pipeline.normalize.build(records)\n
    "},{"location":"06_pipeline_scaffold/#testing","title":"Testing","text":"

    Because stages are pure, test each stage in isolation and test the assembled root against fixture inputs whose expected outputs are stored as golden files. A parity test replays fixtures headlessly.

    "},{"location":"07_worker_and_progress/","title":"Worker & Progress","text":""},{"location":"07_worker_and_progress/#summary","title":"Summary","text":"

    Long-running work runs in a background worker. Jobs are durable documents that move through an explicit status machine. Progress is streamed to clients.

    "},{"location":"07_worker_and_progress/#the-status-machine","title":"The status machine","text":"
    pending -> processing -> completed\n                     \\-> failed    (retried up to N times)\n                     \\-> paused    (waiting for human input)\n
    "},{"location":"07_worker_and_progress/#the-poll-loop","title":"The poll loop","text":"
    async def poll(resources):\n    backoff = MIN_POLL\n    while True:\n        jobs = await job_svc.search(filter={\"status\": \"pending\"}, limit=100)\n        backoff = MIN_POLL if jobs else min(backoff * 2, MAX_POLL)\n        for job in jobs:\n            if (job.retry_count or 0) >= MAX_RETRIES:\n                continue\n            await job_svc.update_status(job.id, \"processing\")\n            try:\n                await process(job, resources)\n            except PauseForResolution:\n                await job_svc.update_status(job.id, \"paused\")\n            except Exception as e:\n                await job_svc.update_status(\n                    job.id, \"failed\", error=str(e),\n                    retry_count=(job.retry_count or 0) + 1,\n                )\n        await asyncio.sleep(backoff)\n

    Tunables live in environment variables: WORKER_POLL_MIN, WORKER_POLL_MAX, WORKER_MAX_RETRIES.

    "},{"location":"07_worker_and_progress/#progress-events","title":"Progress events","text":"

    A worker publishes structured progress events to a bus (a port). The HTTP edge exposes a streaming endpoint that replays events for a given job id.

    class ProgressEvent(BaseModel):\n    job_id: str\n    step: str\n    status: str            # started | progress | completed | skipped | paused\n    message: dict | None = None\n

    The bus is an in-memory adapter by default; because it is a port, it can be swapped for a durable broker. Emit from the orchestrator at each stage:

    await self.bus.publish(ProgressEvent(job_id=jid, step=\"normalize\",\n                                     status=\"completed\", message={\"count\": n}))\n
    "},{"location":"07_worker_and_progress/#streaming-endpoint","title":"Streaming endpoint","text":"
    async def stream_job_events(request, id):\n    bus = request.app.state.db_resources.get(\"progress_bus\")\n    if bus is None:\n        return {\"events\": []}\n\n    async def gen():\n        sent = 0\n        while True:\n            events = bus.get_events(id)\n            for e in events[sent:]:\n                yield f\"data: {e.model_dump_json(exclude_none=True)}\\n\\n\"\n                sent += 1\n            await asyncio.sleep(0.5)\n\n    return StreamingResponse(gen(), media_type=\"text/event-stream\")\n

    Known limitation. The default in-memory bus is per-process. If the worker and the API run as separate processes, events published by the worker are not visible to the API. For production, back the bus with a shared broker. The port boundary makes this a one-adapter change.

    "},{"location":"08_human_in_loop/","title":"Human-in-the-Loop","text":""},{"location":"08_human_in_loop/#summary","title":"Summary","text":"

    Some inputs are ambiguous and must be resolved by a human before processing can continue. The blueprint models this as a durable pause: an exception is raised, a question is recorded, the job pauses, and later resumes.

    "},{"location":"08_human_in_loop/#the-mechanism","title":"The mechanism","text":"
    1. During processing, a stage raises a domain exception carrying the context needed to ask the question:
    raise PauseForResolution(\n    job_id=jid,\n    reference=\"<stable key for this decision>\",\n    options=[...],\n    context={...},\n)\n
    1. The orchestrator catches it, records a question document, and re-raises so the worker marks the job paused.
    2. A client lists pending questions and answers one:
    GET  /jobs/{id}/questions            -> list pending questions\nPOST /questions/{qid}/resolve  {choice}  -> record the choice\n
    1. When the last pending question is resolved, the resolve endpoint flips the parent job back to pending.
    2. The worker picks it up again, rebuilds a resolution map from previously answered questions, and seeds it into the pipeline so the same decision is not asked twice.
    "},{"location":"08_human_in_loop/#the-question-document","title":"The question document","text":"
    class Question(BaseDocument):\n    job_id: str\n    reference: str            # stable key: stage|field|value\n    status: str               # pending | resolved\n    options: list[dict]\n    context: dict\n    chosen: dict | None = None\n    resolved_at: str | None = None\n
    "},{"location":"08_human_in_loop/#resume-map","title":"Resume map","text":"

    The worker turns answered questions into a lookup keyed by reference, then injects it into the pipeline so that stage can skip re-raising:

    resolved = {q.reference: q.chosen for q in await svc.search(\n    filter={\"job_id\": jid, \"status\": \"resolved\"})}\n\npipeline = build(root, instances={\"pause\": handler}, config={\"resolved\": resolved})\n
    "},{"location":"08_human_in_loop/#resolve-endpoint","title":"Resolve endpoint","text":"
    async def resolve_question(request, id, body):\n    svc = request.app.state.db_resources[\"question_svc\"]\n    q = await svc.read(id)\n    await svc.update(id, {\"chosen\": body[\"chosen\"], \"status\": \"resolved\",\n                          \"resolved_at\": now()})\n\n    pending = await svc.search(filter={\"job_id\": q.job_id, \"status\": \"pending\"},\n                               limit=1)\n    if not pending:\n        jobs = request.app.state.db_resources[\"job_svc\"]\n        job = await jobs.read(q.job_id)\n        if job and job.status == \"paused\":\n            await jobs.update_status(q.job_id, \"pending\")\n    return q\n
    "},{"location":"08_human_in_loop/#cli-parity","title":"CLI parity","text":"

    The same flow is available through scripts/CLI: list, get, create, questions, resolve, retry.

    "},{"location":"09_infra_adaptors/","title":"Infra Adapters","text":""},{"location":"09_infra_adaptors/#summary","title":"Summary","text":"

    All external systems are reached through adapters in infra/. Prefer an existing internal library over writing a new adapter; where no library exists, copy the Tier B scaffold from templates/app. See the Genericity Matrix for the Tier A / B / C split.

    "},{"location":"09_infra_adaptors/#database-mongodb","title":"Database (MongoDB)","text":"
    repo_factory(collection, document, engine=None,\n             populate_rules=None, cache=None)\n
    PopulateRule(field_name=\"owner\", collection_name=\"users\",\n             nested_rules=[], max_depth=2, projection=None)\n

    Tier A \u2014 Library (Mongo Ops). Do not write raw Motor calls in the domain; consult the registry and the library's MCP before touching the driver.

    "},{"location":"09_infra_adaptors/#http-response-cache","title":"HTTP response cache","text":"

    Tier B \u2014 template. Copy from templates/app; not a standalone library.

    A generic GET cache sits behind a decorator.

    @cache_response(ttl=60)\nasync def list_items(request): ...\n
    "},{"location":"09_infra_adaptors/#event-progress-bus","title":"Event / progress bus","text":"

    Tier B \u2014 template. Copy from templates/app.

    A port with an in-memory default adapter (publish, get_events). Swap for a durable broker in production (see Worker & Progress).

    "},{"location":"09_infra_adaptors/#external-clients-ingestion","title":"External clients (ingestion)","text":"

    External fetchers (files, mail, HTTP) are adapters that wrap ingestion libraries. Construct them in the composition root and inject into orchestrators.

    Tier A \u2014 Libraries: Omniread (formats) and Mail Intake (email).

    "},{"location":"09_infra_adaptors/#llm-clients","title":"LLM clients","text":"

    Tier B \u2014 template. Copy the pool scaffold from templates/app.

    When a pipeline calls a model provider, route through a pool adaptor that handles provider selection, retries, and client lifecycle:

    pool = setup_async_pool(provider=settings.llm_provider)\nresult = await pool.complete(model=\"...\", messages=[...])\n

    Providers are selected by LLM_PROVIDER and configured by environment.

    "},{"location":"09_infra_adaptors/#key-value-store","title":"Key-value store","text":"

    Tier B \u2014 template. Copy from templates/app.

    For report/snapshot storage, provide a pluggable store port with a Redis adapter and an in-memory fallback.

    "},{"location":"09_infra_adaptors/#rule","title":"Rule","text":""},{"location":"12_genericity_matrix/","title":"Genericity Matrix","text":""},{"location":"12_genericity_matrix/#summary","title":"Summary","text":"

    This page states precisely what is library-backed, what is template-provided, and what is app-specific and must be authored. Use it to scope a new build and to know, for any given capability, whether to call a library, copy template code, or write it yourself.

    "},{"location":"12_genericity_matrix/#tier-a-library-backed-use-never-reimplement","title":"Tier A \u2014 Library-backed (use; never reimplement)","text":"

    These capabilities already exist as internal libraries. Reach them through the library registry MCP (find_library(capability)) and read the library's own MCP for its API. Writing a private copy of any of these is a defect.

    Capability Library MCP Persistence: repositories, cache, populate, transactions Mongo Ops (mongo_ops) :8007 Dependency injection / composition Hexa (hexa) :8008 Deterministic multi-step transformation engine DAG Pipe (dagpipe) :8006 Spec-first HTTP edge, client, codegen OpenAPI First (openapi_first) :8004 Format-agnostic fetch + parse (HTML, PDF, CSV, XLSX) Omniread (omniread) :8003 Read-only email ingestion Mail Intake (mail_intake) :8002 JWT auth primitives (register, login, introspect) Py JWT (jwtlib) :8005 Documentation build (MkDocs + MCP) Doc Forge (docforge) :8001"},{"location":"12_genericity_matrix/#tier-b-template-provided-copy-from-templatesapp-then-adapt","title":"Tier B \u2014 Template-provided (copy from templates/app, then adapt)","text":"

    These are reusable internal components shipped inside the template \u2014 they are not independent libraries. Copy them as-is, then rename/populate only what is app-specific. They give every app the same wiring shape as the reference app. Fetch them by kind with get_app_component, or the whole tree with get_app_template.

    Component Template location Action Shipped Layering & dependency rule the package layout + import rules keep \u2705 Composition root shape init.py (get_svc/get_db, idempotency) keep \u2705 Spec-first web edge interfaces/openapi.py, thin handlers bound to operationId keep \u2705 CRUD slice pattern domain/<resource>/{models.py, ports/db.py, ports/service.py, converters.py} rename + fill \u2705 Generic CRUD service infra/adapters/service.py (CrudsServiceAdaptor) keep \u2705 Repository factory infra/db/repos/mongo.py (_repo_factory) keep \u2705 Worker poll + status loop worker.py keep \u2705 Progress bus infra/adapters/sse.py (keyed in-memory bus) + an app SSE endpoint keep \u2705 HTTP response cache infra/http_cache.py (cache_response + generation invalidation) keep \u2705 Key-value store infra/adapters/kv.py (port + in-memory + Redis adaptor) keep \u2705 Pipeline scaffold pipelines/shared/{ports,loader,registry}.py + pipelines/<family>/ rename + fill \u2705 Operator scripts scripts/ (init() / get_svc() / shutdown()) rename \u2705 Testing harness integration fakes + fixture-parity pattern keep \u2705 Project scaffolding pyproject.toml, Dockerfile, docker-compose.yaml, .env.example, openapi.yaml rename + fill \u2705"},{"location":"12_genericity_matrix/#tier-b-patterns-port-from-the-reference-app","title":"Tier B (patterns) \u2014 port from the reference app","text":"

    A few Tier B capabilities are real and reusable, but are shaped by the app or the provider, so the template does not ship them verbatim \u2014 copying them blindly would carry app-specific names. Treat them as patterns: lift the shape from the reference app and rename per app.

    Component Reference location Action Shipped Human-in-the-loop durable pause + question document + resolve endpoint + resume map port + reshape \u2717 LLM client pool provider-select + retry/rotation pool scaffold port + reshape \u2717"},{"location":"12_genericity_matrix/#tier-c-app-specific-must-be-authored","title":"Tier C \u2014 App-specific (must be authored)","text":"Artifact Why Resource models the business shape Repository populate/cache rules the relationships Service business logic domain behaviour Pipeline families the transformation graph Pattern/rule resources data-driven policy Fixtures & golden outputs test data"},{"location":"12_genericity_matrix/#the-two-axes","title":"The two axes","text":"

    Everything app-specific reduces to two axes:

    1. Resource model \u2014 the set of resources, their fields, relationships, and the OpenAPI contract.
    2. Pipeline family \u2014 only if inputs require multi-step transformation.

    If you can specify those two axes, the rest of the app follows mechanically: Tier A is imported, Tier B is copied, and Tier C is the only thing you author.

    "},{"location":"13_library_registry/","title":"Library Registry","text":""},{"location":"13_library_registry/#summary","title":"Summary","text":"

    The Aetos ecosystem provides internal libraries for the shared concerns of every application. Use them. Writing a private copy of any capability below is a defect.

    Each library also exposes its own documentation as an MCP server. An AI agent can read a library's full API by connecting to its MCP and reading docs://index, docs://nav, and docs://modules/{module}.

    "},{"location":"13_library_registry/#catalog","title":"Catalog","text":"Library Import Purpose Use instead of MCP DAG Pipe dagpipe Deterministic state-transformation DAG engine hand-rolled orchestration/topological ordering :8006 Hexa hexa Declarative DI / composition-root tooling manual wiring, hand-written factories :8008 Mongo Ops mongo_ops Async MongoDB repositories, cache, populate, transactions raw Motor CRUD and connection lifecycle :8007 Omniread omniread Format-agnostic fetch + parse (HTML, PDF, CSV, XLSX) custom scrapers/parsers :8003 OpenAPI First openapi_first Spec-first FastAPI bootstrap, client, codegen decorator routers and hand-written clients :8004 Mail Intake mail_intake Provider-agnostic read-only email ingestion Gmail OAuth + MIME parsing :8002 Py JWT jwtlib JWT auth primitives (register/login/introspect) password hashing + token code :8005 Doc Forge docforge Docstring compiler \u2192 MkDocs + MCP docs custom doc generators :8001 Auth Server (service) Central identity service (register/login/me/introspect) building auth endpoints yourself (deferred)"},{"location":"13_library_registry/#how-to-choose","title":"How to choose","text":"You need to\u2026 Use persist and query documents Mongo Ops compose dependencies Hexa run a multi-step transformation DAG Pipe read/parse an arbitrary input format Omniread expose a contract-first HTTP API OpenAPI First ingest email Mail Intake authenticate users / verify tokens Py JWT (+ Auth Server) generate documentation Doc Forge"},{"location":"13_library_registry/#discovery","title":"Discovery","text":"

    A single standards MCP exposes find_library(capability), which returns the matching library, its import name, its \"use instead of\" guidance, and its MCP endpoint. Query it before writing any infrastructure.

    "},{"location":"13_library_registry/#notes","title":"Notes","text":""}]} \ No newline at end of file +{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"Standards Reference","text":"

    The reference half of the Standards description: the enforceable rules, patterns, and the internal library catalog that agents consult while coding.

    Read the Wiki for the philosophy and rationale; these pages are the reference that mirrors the standards MCP bundle.

    "},{"location":"01_build_recipe/","title":"Build Recipe","text":""},{"location":"01_build_recipe/#summary","title":"Summary","text":"

    The ordered, deterministic checklist for generating a new application of the standard shape. Every step is fixed; only the resource model (step 2), the codegen pass (step 3), and the pipeline family (step 8) need app-specific work.

    This page is served by the standards MCP as docs://modules/build_recipe. The start_app prompt on that server collects the two axes and hands off here; pull exact files with get_app_component.

    "},{"location":"01_build_recipe/#bootstrap","title":"\ud83d\ude80 Bootstrap","text":"

    From the standards docs checkout:

    cp -r <docs>/standards/src/templates/app apps/<your-app>\ncd apps/<your-app>\n

    Install the internal libraries \u2014 editable local checkouts (preferred in development) or the private index (CI / fresh machines). Credentials are environment-only; never commit them.

    # local checkouts\npip install -e ../aetos/openapi-first -e ../aetos/mongo-ops -e ../aetos/hexa \\\n            -e ../aetos/dagpipe -e ../aetos/doc-forge\n# private index\npip install --extra-index-url \\\n  \"https://$PIP_REGISTRY_USERNAME:$PIP_REGISTRY_PASSWORD@pip.aetoskia.com/simple\" .\n
    "},{"location":"01_build_recipe/#agent-prompt-copyable","title":"\ud83d\udccb Agent prompt (copyable)","text":"
    # Build a new application from the Aetos Standards blueprint\n\nYou are coding a complete new backend application conforming to the Aetos\nstandard app blueprint. Execute every step in order. Only steps 2\u20134 use the\napp-specific details provided by the user; everything else is deterministic.\n\n## Before writing any code\n\nConnect to the `standards` MCP server and read:\n- `docs://nav` (the module list)\n- `docs://modules/genericity_matrix` (Tier A library / Tier B template / Tier C authored)\n- `docs://modules/layering_rules` (the dependency rules)\n\nThen:\n- call `find_library` with `db`, `di`, `routing`, `ingestion`, `pipeline`, `docs`\n  and note the Tier A libraries that already exist \u2014 never reimplement one;\n- call `list_app_components()` and pull each file you need with\n  `get_app_component(kind, resource=..., family=...)` instead of writing it from\n  scratch.\n\n## Steps\n\n1. **Scaffold** \u2014 copy `standards/src/templates/app/` into `apps/<your-app>/`\n   (or pull the same tree via `get_app_template`). Keep the tree: `domain/`,\n   `infra/`, `interfaces/`, `pipelines/`, `tests/`, `scripts/`, plus\n   `pyproject.toml`, `openapi.yaml`, `config.py`, `init.py`, `main.py`,\n   `worker.py`, `Dockerfile`, `docker-compose.yaml`, `.env.example`.\n\n2. **Author the spec (app-specific)** \u2014 write `openapi.yaml`: resources,\n   schemas, `operationId`s, security, and extension metadata. This is the\n   contract and drives all routing. Every operation MUST have an `operationId`.\n\n3. **Generate the scaffold (codegen)** \u2014 from the spec, generate the wire\n   models and the exact handler signatures:\n   ```bash\n   openapi-first models openapi.yaml -o _gen/models.py\n   openapi-first routes openapi.yaml -o _gen/routes \\\n     --use-models --models-module _gen.models --inject-request --package --template-style\n   ```\n   `_gen/models.py` is the **wire contract** (Pydantic, one class per\n   `components.schemas` entry). `_gen/routes/<resource>/routes.py` lists every\n   `operationId` with its exact parameter signature. `--template-style` makes\n   those stubs `async def` with a `body` parameter, matching the handlers you\n   keep in `interfaces/` (without it they are sync `def` stubs taking\n   `payload`; both bind the same way). Treat `_gen/` as generated\n   (never hand-edit it) \u2014 the canonical handlers live in `interfaces/`.\n\n4. **Write each CRUD slice (app-specific)** \u2014 per resource create the four-file\n   slice: `models.py` (domain model, **no `id`**), `ports/db.py`\n   (`class XDocument(BaseDocument, X)` \u2014 id comes from `BaseDocument`),\n   `ports/service.py`, and optional `converters.py`. Follow the CRUD template.\n\n5. **Wire the composition root** \u2014 in `init.py`, build leaf repositories, then\n   dependent repositories, then services; add infra adapters (db, cache, stores,\n   progress bus, fetchers). Expose `get_svc` overloads.\n\n6. **Add routes** \u2014 create `interfaces/<resource>/routes.py` thin handlers whose\n   names match the `operationId`s (use the signatures from step 3 as your\n   checklist) and re-export them in `interfaces/openapi.py`. Add the cache\n   decorator to read endpoints.\n\n7. **Bootstrap app and worker** \u2014 confirm `main.py` runs the OpenAPI-first app\n   with its lifespan. If the app has long-running jobs, adapt `worker.py` to poll\n   the job collection, run the pipeline, and publish progress.\n\n8. **Add pipeline families (only if data-driven)** \u2014 copy `pipelines/example/`\n   to `pipelines/<family>/` with `abc.py`, `impl.py`, `pipeline.yaml`,\n   `resources/`, and reuse `pipelines/shared/` (loader + registry). Register the\n   root. Add fixtures and a parity test.\n\n9. **Add infrastructure only where needed** \u2014 Tier A capability: use the\n   library. Tier B capability: copy the template. Never hand-write a Tier A\n   library.\n\n10. **Tests** \u2014 integration tests with fakes at the HTTP edge; unit tests for\n    domain and pipeline logic; a config-consistency test for rule-driven\n    pipelines. Include `tests/integration/test_openapi_app_builds.py` (imports\n    the app, asserts `import main` binds every operation).\n\n11. **Scripts and seeds** \u2014 operator scripts under `scripts/` using\n    `init()` / `get_svc()` / `shutdown()`; run as `python -m scripts.<name>`.\n\n12. **Docs** \u2014 author GSDFC docstrings, generate `.pyi` stubs, mark the package\n    `py.typed`, and build the docs.\n\n**Definition of done:**\n- `python -c \"import main\"` succeeds.\n- `pytest` passes.\n- The app serves every operation in the spec; the worker drains its queue.\n- No domain module imports a web framework.\n- No Tier A library was reimplemented.\n
    "},{"location":"01_build_recipe/#where-the-steps-land","title":"\ud83e\udded Where the steps land","text":"

    The steps map onto the layers as follows:

    Step Package / file Depends on 1 \u00b7 Scaffold whole templates/app/ \u2014 2 \u00b7 Spec openapi.yaml \u2014 3 \u00b7 Codegen _gen/ (generated) spec, openapi-first 4 \u00b7 CRUD slices domain/<resource>/ spec, CRUD template 5 \u00b7 Wiring init.py every slice 6 \u00b7 Routes interfaces/<resource>/ services, openapi route registry 7 \u00b7 Bootstrap main.py, worker.py composition root 8 \u00b7 Pipelines pipelines/<family>/, shared/ library pipeline engine 9 \u00b7 Infra infra/ domain ports 10 \u00b7 Tests tests/ app + fixtures 11 \u00b7 Scripts scripts/ composition root 12 \u00b7 Docs docstrings + docs build code, Doc Forge"},{"location":"01_build_recipe/#the-codegen-fast-path-in-detail","title":"\ud83d\udd27 The codegen fast path, in detail","text":"

    openapi-first is spec \u2192 code only (it never generates a spec). Use it to erase the two mechanical parts of the build:

    Generated artifact Replaces Notes _gen/models.py hand-writing Pydantic wire models one class per components.schemas entry _gen/routes/<resource>/routes.py hand-typing handler signatures names == operationIds; request: Request injected

    Rules of the road:

    "},{"location":"01_build_recipe/#definition-of-done","title":"\u26a0\ufe0f Definition of done","text":""},{"location":"01_build_recipe/#related","title":"Related","text":""},{"location":"02_layering_rules/","title":"Layering Rules","text":""},{"location":"02_layering_rules/#summary","title":"Summary","text":"

    The dependency direction is fixed. Violating it is a design defect.

    "},{"location":"02_layering_rules/#allowed-imports","title":"Allowed imports","text":"
    interfaces/*      ->  domain/*            (services, ports, models)\ninterfaces/*      ->  infra/adapters/*    (only read-only value objects)\ndomain/*          ->  domain/*            (same layer)\ndomain/*          ->  pipelines/*         (call a pipeline via a port)\ndomain/*/ports/*  ->  infra/*             (bind adapters + libraries at the seam)\ninfra/adapters/*  ->  domain/ports/*      (implement the interface)\nmain.py/init.py   ->  everything          (composition root only)\n
    "},{"location":"02_layering_rules/#forbidden-imports","title":"Forbidden imports","text":"
    domain/*          ->  interfaces/*        (never)\ndomain/*          ->  fastapi/starlette   (never)\ninfra/adapters/*  ->  interfaces/*        (never)\ninterfaces/*      ->  infra/db/*          (state lives in resources, not imports)\n
    "},{"location":"02_layering_rules/#why","title":"Why","text":""},{"location":"02_layering_rules/#the-portadapter-contract","title":"The port/adapter contract","text":"

    The ports/ package is the seam where the domain meets infrastructure. Each domain/<resource>/ports/ module declares the resource's models/document shape and exposes factory functions that bind a concrete adapter \u2014 so a port module is allowed to import from infra/ and from the persistence libraries. Everything outside ports/ in domain/ stays framework- and driver-free.

    domain/<resource>/ports/db.py       -> document model + create_<resource>_repo()\ndomain/<resource>/ports/service.py  -> service + get_<resource>_service()\ninfra/db/repos/<vendor>.py          -> repository adapter factory\ninfra/adapters/service.py           -> generic CRUD service base\n

    The composition root calls the port factories; nothing else constructs an adapter.

    "},{"location":"02_layering_rules/#enforcement","title":"Enforcement","text":""},{"location":"03_composition_root/","title":"Composition Root","text":""},{"location":"03_composition_root/#summary","title":"Summary","text":"

    All wiring happens in exactly one module. It builds a flat resource registry and exposes it to the HTTP layer and to scripts.

    "},{"location":"03_composition_root/#the-resource-registry","title":"The resource registry","text":"

    init.py returns a plain dict[str, Any]. Keys follow two conventions:

    Plus named infrastructure adaptors:

    The API stores the dict on app.state.db_resources during lifespan startup. Handlers read from it; nothing imports concrete adapters.

    "},{"location":"03_composition_root/#shape","title":"Shape","text":"
    _resources: dict = {}\n_initialized = False\n\nasync def init(settings=None) -> dict:\n    global _initialized\n    if _initialized:\n        return _resources\n\n    await setup_database(uri=settings.mongo_uri, db_name=settings.mongo_db)\n\n    # 1. leaf repositories\n    user_repo = create_user_repo()\n\n    # 2. dependent repositories (dependencies passed by keyword)\n    wallet_repo = create_wallet_repo(user_repo=user_repo)\n\n    # 3. services (build via the DI library, injecting the repo)\n    _resources[\"user_repo\"]   = user_repo\n    _resources[\"wallet_repo\"] = wallet_repo\n    _resources[\"user_svc\"]    = get_user_service(user_repo)\n    _resources[\"wallet_svc\"]  = get_wallet_service(wallet_repo)\n\n    # 4. infrastructure adaptors (optional)\n    _resources[\"http_cache_store\"] = HttpCacheStore(...)\n    _resources[\"progress_bus\"]     = ProgressEventBus()\n\n    _initialized = True\n    return _resources\n\nasync def shutdown() -> None:\n    global _initialized\n    await close_stores(_resources)\n    await shutdown_database()\n    _resources.clear()\n    _initialized = False\n
    "},{"location":"03_composition_root/#typed-accessors","title":"Typed accessors","text":"

    Expose a get_svc(name) helper with one @overload per key so scripts get typing without importing concrete classes:

    @overload\ndef get_svc(name: Literal[\"user_svc\"]) -> UserService: ...\n@overload\ndef get_svc(name: Literal[\"wallet_svc\"]) -> WalletService: ...\n\ndef get_svc(name: str):\n    return _resources[name]\n

    Also expose get_db() (raw database handle) and pure helpers such as compute_fingerprint(payload) (canonical hash used for deduplication).

    "},{"location":"03_composition_root/#rules","title":"Rules","text":""},{"location":"04_spec_first_routing/","title":"Spec-First Routing","text":""},{"location":"04_spec_first_routing/#summary","title":"Summary","text":"

    The OpenAPI document is the single source of truth for the HTTP contract. Handlers are bound to it by name; there is no decorator router.

    "},{"location":"04_spec_first_routing/#the-contract","title":"The contract","text":""},{"location":"04_spec_first_routing/#app-bootstrap","title":"App bootstrap","text":"
    # main.py\nfrom openapi_first.app import OpenAPIFirstApp\n\napp = OpenAPIFirstApp(\n    openapi_path=\"openapi.yaml\",\n    routes_module=interfaces.openapi,   # module whose namespace holds handlers\n    title=\"<App> API\",\n    version=\"0.1.0\",\n    lifespan=lifespan,\n)\n

    lifespan calls the composition root:

    @asynccontextmanager\nasync def lifespan(app):\n    app.state.db_resources = await init()\n    yield\n    await shutdown()\n
    "},{"location":"04_spec_first_routing/#handler-registry","title":"Handler registry","text":"

    interfaces/openapi.py is a flat re-export of every handler, so the framework can resolve operationId across the whole app:

    from interfaces.item.routes import *\nfrom interfaces.wallet.routes import *\nfrom interfaces.progress.sse import *\n
    "},{"location":"04_spec_first_routing/#handler-shape","title":"Handler shape","text":"

    Handlers are thin and synchronous with respect to business logic:

    async def create_item(request: Request, body: dict):\n    svc = request.app.state.db_resources[\"item_svc\"]\n    return await svc.create(Item.model_validate(body))\n

    Generated stubs. openapi-first routes emits this exact shape (async def handlers taking body) when given --template-style; without that flag the stub is def create_item(payload: ItemInput) \u2014 sync, with a payload parameter. Both shapes bind identically: the router resolves handlers by operationId and introspects the FastAPI signature, and it never inspects the request body. Pass --template-style so the generated signatures match the handlers you keep in interfaces/.

    "},{"location":"04_spec_first_routing/#conventions","title":"Conventions","text":""},{"location":"04_spec_first_routing/#authless-apps","title":"Authless apps","text":"

    Auth is deferred in the standard: the same spec-first app runs with or without it, because security lives entirely in the spec, never in handlers. For an app that needs no auth yet, do not invent per-handler auth checks \u2014 just omit the security keys:

    Profile endpoints (/users/me) and profile headers (x-profile) still exist in an authless app. Define a stand-in identity in the composition root \u2014 for example get_current_user returns or lazily creates a fixed \"Demo User\" \u2014 so handlers stay auth-agnostic and only the wiring changes when real auth arrives.

    The handlers, services, and composition root are unchanged. When auth is needed later, add the global security block and an introspecting dependency; only handlers that must opt out get security: [].

    "},{"location":"04_spec_first_routing/#codegen","title":"Codegen","text":"

    openapi_first scaffolds models and route stubs from the spec (see the Build Recipe, step 3):

    openapi-first models openapi.yaml -o _gen/models.py\nopenapi-first routes openapi.yaml -o _gen/routes \\\n  --use-models --models-module _gen.models --inject-request --package --template-style\n

    Prefer codegen over hand-writing boilerplate. The result is the wire contract and a set of operationId-named signatures \u2014 not the canonical handlers.

    "},{"location":"05_crud_template/","title":"CRUD Template","text":""},{"location":"05_crud_template/#summary","title":"Summary","text":"

    Every resource follows the same four-file slice. Repetition is intentional: it makes the codebase predictable and generatable.

    "},{"location":"05_crud_template/#the-slice","title":"The slice","text":"
    domain/<resource>/\n    models.py               # pure Pydantic model (no DB fields)\n    ports/\n        db.py               # document model + repository factory\n        service.py          # service class + factory\n        converters.py       # (optional) transforms\n
    "},{"location":"05_crud_template/#the-id-rule-model-vs-document","title":"The id rule (model vs. document)","text":"

    Two different objects, two different jobs:

    Object Lives in Has id? Purpose Domain model domain/<resource>/models.py No business shape, framework-free Document domain/<resource>/ports/db.py Yes (from BaseDocument) Mongo persistence shape

    BaseDocument (mongo-ops) supplies id: Optional[PyObjectId] (aliased to _id) plus created_at / updated_at. The domain model must not declare id; Mongo owns it.

    Trap \u2014 generated wire models. openapi-first models produces the HTTP contract (see the Build Recipe, step 3). Response schemas frequently declare a required id: str, so a generated class is not a domain model. If you write class ItemDocument(Item, BaseDocument) and Item re-exports a required id: str, that field shadows BaseDocument.id and no Mongo _id can ever be set. Fix it one of three ways:

    1. Preferred: keep the domain model id-free (hand-author it from the CRUD template), and keep the required id only on the wire model.
    2. Order the bases so the document wins: class ItemDocument(BaseDocument, Item).
    3. Keep a separate document type and translate with ports/converters.py.
    "},{"location":"05_crud_template/#modelspy","title":"models.py","text":"

    A framework-free Pydantic model describing the business shape (no id).

    class Item(BaseModel):\n    name: str\n    quantity: int = 0\n
    "},{"location":"05_crud_template/#portsdbpy","title":"ports/db.py","text":"

    The persistence model extends the business model and adds database concerns. The repository factory selects the right repository flavour.

    # Item is id-free; BaseDocument adds id/created_at/updated_at.\nclass ItemDocument(Item, BaseDocument):\n    pass\n\ndef create_item_repo(*, owner_repo=None) -> ItemRepository:\n    return repo_factory(\n        collection=\"items\",\n        document=ItemDocument,\n        populate_rules=[PopulateRule(field_name=\"owner\", collection_name=\"users\")],\n        cache=None,               # or a CachedBaseRepository config\n    )\n
    "},{"location":"05_crud_template/#nested-references","title":"Nested references","text":"

    The wire contract often declares a reference as an expanded object (expense.entity: Entity), while the domain model stays id-free. Pick one convention and apply it consistently:

    For populate-backed repositories change a reference with update, not patch: patch rejects changes to populated FK fields by design.

    "},{"location":"05_crud_template/#portsservicepy","title":"ports/service.py","text":"

    The service is built through the DI library, injecting the repository. The generic CRUD base provides create/read/update/patch/delete/search/....

    class ItemService(CrudsServiceAdaptor[Item, ItemDocument]):\n    model = Item\n    schema = ItemDocument\n\ndef get_item_service(repo) -> ItemService:\n    return build(ItemService, instances={\"repo\": repo})\n
    "},{"location":"05_crud_template/#naming-conventions","title":"Naming conventions","text":"Concept Convention collection plural snake case (items) document <Resource>Document repository factory create_<resource>_repo service <Resource>Service service factory get_<resource>_service resource key <resource>_svc operationId list_items, create_item, ..."},{"location":"05_crud_template/#optional-files","title":"Optional files","text":""},{"location":"06_pipeline_scaffold/","title":"Pipeline Scaffold","text":""},{"location":"06_pipeline_scaffold/#summary","title":"Summary","text":"

    Data-driven processing is expressed as a composition graph. The graph is built by a DI library; execution is driven by a DAG engine. Stages are pure and individually testable.

    "},{"location":"06_pipeline_scaffold/#when-to-use-a-pipeline","title":"When to use a pipeline","text":"

    Use a pipeline when a resource is produced by a multi-step transformation of an input (a document, a file, a message) rather than by a simple CRUD write.

    "},{"location":"06_pipeline_scaffold/#structure-of-a-family","title":"Structure of a family","text":"

    A family is one input shape (e.g. a file format or a message kind). Each family has the same skeleton:

    pipelines/<family>/\n    abc.py            # port contracts (stage interfaces + root interface)\n    impl.py           # concrete stages + root implementation\n    pipeline.yaml     # graph spec: nodes, edges, roots, schema\n    resources/\n        pipeline.yaml # runtime spec consumed by the loader\n        patterns/*.yaml  # rule fragments (pattern registry)\n    banks/<variant>.py   # optional variants that re-pin a few slots\n
    "},{"location":"06_pipeline_scaffold/#shared-scaffolding","title":"Shared scaffolding","text":"
    pipelines/shared/\n    ports.py     # RegistryPort, FinalizePort, etc.\n    loader.py    # build(root_cls) -> wires the graph + engine\n    registry.py  # resolve_root(...) -> root class for a (kind, variant)\n
    "},{"location":"06_pipeline_scaffold/#the-loader","title":"The loader","text":"
    def build(root_cls, *, instances=None, config=None):\n    # merge resources/patterns/*.yaml into a pattern registry\n    # bind registry/config into each graph node by id\n    # build the schema (state) class and the engine\n    root = root_cls()\n    root._engine = Engine(graph, state_cls, payload_cls)\n    return root\n
    "},{"location":"06_pipeline_scaffold/#the-registry","title":"The registry","text":"

    A single lookup replaces hand-rolled if/else selection:

    _FAMILIES = {\n    \"csv\":  {\"acme\": {\"heuristic\": AcmeCsvPipeline}},\n    \"json\": {\"globex\": {\"heuristic\": GlobexJsonPipeline}},\n}\n\ndef resolve_root(kind, variant, mode=\"heuristic\"):\n    return _FAMILIES[kind][variant][mode]\n

    Adding a new variant means adding one class and one registry entry.

    "},{"location":"06_pipeline_scaffold/#driving-a-pipeline","title":"Driving a pipeline","text":"

    Stages are driven explicitly so intermediate state can be persisted:

    pipeline = build(resolve_root(kind, variant))\ncontent = pipeline.ingest.materialize(raw_bytes)\nrecords = pipeline.reader.read(content)\nitems   = pipeline.normalize.build(records)\n
    "},{"location":"06_pipeline_scaffold/#testing","title":"Testing","text":"

    Because stages are pure, test each stage in isolation and test the assembled root against fixture inputs whose expected outputs are stored as golden files. A parity test replays fixtures headlessly.

    "},{"location":"07_worker_and_progress/","title":"Worker & Progress","text":""},{"location":"07_worker_and_progress/#summary","title":"Summary","text":"

    Long-running work runs in a background worker. Jobs are durable documents that move through an explicit status machine. Progress is streamed to clients.

    "},{"location":"07_worker_and_progress/#the-status-machine","title":"The status machine","text":"
    pending -> processing -> completed\n                     \\-> failed    (retried up to N times)\n                     \\-> paused    (waiting for human input)\n
    "},{"location":"07_worker_and_progress/#the-poll-loop","title":"The poll loop","text":"
    async def poll(resources):\n    backoff = MIN_POLL\n    while True:\n        jobs = await job_svc.search(filter={\"status\": \"pending\"}, limit=100)\n        backoff = MIN_POLL if jobs else min(backoff * 2, MAX_POLL)\n        for job in jobs:\n            if (job.retry_count or 0) >= MAX_RETRIES:\n                continue\n            await job_svc.update_status(job.id, \"processing\")\n            try:\n                await process(job, resources)\n            except PauseForResolution:\n                await job_svc.update_status(job.id, \"paused\")\n            except Exception as e:\n                await job_svc.update_status(\n                    job.id, \"failed\", error=str(e),\n                    retry_count=(job.retry_count or 0) + 1,\n                )\n        await asyncio.sleep(backoff)\n

    Tunables live in environment variables: WORKER_POLL_MIN, WORKER_POLL_MAX, WORKER_MAX_RETRIES.

    "},{"location":"07_worker_and_progress/#progress-events","title":"Progress events","text":"

    A worker publishes structured progress events to a bus (a port). The HTTP edge exposes a streaming endpoint that replays events for a given job id.

    class ProgressEvent(BaseModel):\n    job_id: str\n    step: str\n    status: str            # started | progress | completed | skipped | paused\n    message: dict | None = None\n

    The bus is an in-memory adapter by default; because it is a port, it can be swapped for a durable broker. Emit from the orchestrator at each stage:

    await self.bus.publish(ProgressEvent(job_id=jid, step=\"normalize\",\n                                     status=\"completed\", message={\"count\": n}))\n
    "},{"location":"07_worker_and_progress/#streaming-endpoint","title":"Streaming endpoint","text":"
    async def stream_job_events(request, id):\n    bus = request.app.state.db_resources.get(\"progress_bus\")\n    if bus is None:\n        return {\"events\": []}\n\n    async def gen():\n        sent = 0\n        while True:\n            events = bus.get_events(id)\n            for e in events[sent:]:\n                yield f\"data: {e.model_dump_json(exclude_none=True)}\\n\\n\"\n                sent += 1\n            await asyncio.sleep(0.5)\n\n    return StreamingResponse(gen(), media_type=\"text/event-stream\")\n

    Known limitation. The default in-memory bus is per-process. If the worker and the API run as separate processes, events published by the worker are not visible to the API. For production, back the bus with a shared broker. The port boundary makes this a one-adapter change.

    "},{"location":"08_human_in_loop/","title":"Human-in-the-Loop","text":""},{"location":"08_human_in_loop/#summary","title":"Summary","text":"

    Some inputs are ambiguous and must be resolved by a human before processing can continue. The blueprint models this as a durable pause: an exception is raised, a question is recorded, the job pauses, and later resumes.

    "},{"location":"08_human_in_loop/#the-mechanism","title":"The mechanism","text":"
    1. During processing, a stage raises a domain exception carrying the context needed to ask the question:
    raise PauseForResolution(\n    job_id=jid,\n    reference=\"<stable key for this decision>\",\n    options=[...],\n    context={...},\n)\n
    1. The orchestrator catches it, records a question document, and re-raises so the worker marks the job paused.
    2. A client lists pending questions and answers one:
    GET  /jobs/{id}/questions            -> list pending questions\nPOST /questions/{qid}/resolve  {choice}  -> record the choice\n
    1. When the last pending question is resolved, the resolve endpoint flips the parent job back to pending.
    2. The worker picks it up again, rebuilds a resolution map from previously answered questions, and seeds it into the pipeline so the same decision is not asked twice.
    "},{"location":"08_human_in_loop/#the-question-document","title":"The question document","text":"
    class Question(BaseDocument):\n    job_id: str\n    reference: str            # stable key: stage|field|value\n    status: str               # pending | resolved\n    options: list[dict]\n    context: dict\n    chosen: dict | None = None\n    resolved_at: str | None = None\n
    "},{"location":"08_human_in_loop/#resume-map","title":"Resume map","text":"

    The worker turns answered questions into a lookup keyed by reference, then injects it into the pipeline so that stage can skip re-raising:

    resolved = {q.reference: q.chosen for q in await svc.search(\n    filter={\"job_id\": jid, \"status\": \"resolved\"})}\n\npipeline = build(root, instances={\"pause\": handler}, config={\"resolved\": resolved})\n
    "},{"location":"08_human_in_loop/#resolve-endpoint","title":"Resolve endpoint","text":"
    async def resolve_question(request, id, body):\n    svc = request.app.state.db_resources[\"question_svc\"]\n    q = await svc.read(id)\n    await svc.update(id, {\"chosen\": body[\"chosen\"], \"status\": \"resolved\",\n                          \"resolved_at\": now()})\n\n    pending = await svc.search(filter={\"job_id\": q.job_id, \"status\": \"pending\"},\n                               limit=1)\n    if not pending:\n        jobs = request.app.state.db_resources[\"job_svc\"]\n        job = await jobs.read(q.job_id)\n        if job and job.status == \"paused\":\n            await jobs.update_status(q.job_id, \"pending\")\n    return q\n
    "},{"location":"08_human_in_loop/#cli-parity","title":"CLI parity","text":"

    The same flow is available through scripts/CLI: list, get, create, questions, resolve, retry.

    "},{"location":"09_infra_adaptors/","title":"Infra Adapters","text":""},{"location":"09_infra_adaptors/#summary","title":"Summary","text":"

    All external systems are reached through adapters in infra/. Prefer an existing internal library over writing a new adapter; where no library exists, copy the Tier B scaffold from templates/app. See the Genericity Matrix for the Tier A / B / C split.

    "},{"location":"09_infra_adaptors/#database-mongodb","title":"Database (MongoDB)","text":"
    repo_factory(collection, document, engine=None,\n             populate_rules=None, cache=None)\n
    PopulateRule(field_name=\"owner\", collection_name=\"users\",\n             nested_rules=[], max_depth=2, projection=None)\n

    Tier A \u2014 Library (Mongo Ops). Do not write raw Motor calls in the domain; consult the registry and the library's MCP before touching the driver.

    "},{"location":"09_infra_adaptors/#http-response-cache","title":"HTTP response cache","text":"

    Tier B \u2014 template. Copy from templates/app; not a standalone library.

    A generic GET cache sits behind a decorator.

    @cache_response(ttl=60)\nasync def list_items(request): ...\n
    "},{"location":"09_infra_adaptors/#event-progress-bus","title":"Event / progress bus","text":"

    Tier B \u2014 template. Copy from templates/app.

    A port with an in-memory default adapter (publish, get_events). Swap for a durable broker in production (see Worker & Progress).

    "},{"location":"09_infra_adaptors/#external-clients-ingestion","title":"External clients (ingestion)","text":"

    External fetchers (files, mail, HTTP) are adapters that wrap ingestion libraries. Construct them in the composition root and inject into orchestrators.

    Tier A \u2014 Libraries: Omniread (formats) and Mail Intake (email).

    "},{"location":"09_infra_adaptors/#llm-clients","title":"LLM clients","text":"

    Tier B \u2014 template. Copy the pool scaffold from templates/app.

    When a pipeline calls a model provider, route through a pool adaptor that handles provider selection, retries, and client lifecycle:

    pool = setup_async_pool(provider=settings.llm_provider)\nresult = await pool.complete(model=\"...\", messages=[...])\n

    Providers are selected by LLM_PROVIDER and configured by environment.

    "},{"location":"09_infra_adaptors/#key-value-store","title":"Key-value store","text":"

    Tier B \u2014 template. Copy from templates/app.

    For report/snapshot storage, provide a pluggable store port with a Redis adapter and an in-memory fallback.

    "},{"location":"09_infra_adaptors/#rule","title":"Rule","text":""},{"location":"12_genericity_matrix/","title":"Genericity Matrix","text":""},{"location":"12_genericity_matrix/#summary","title":"Summary","text":"

    This page states precisely what is library-backed, what is template-provided, and what is app-specific and must be authored. Use it to scope a new build and to know, for any given capability, whether to call a library, copy template code, or write it yourself.

    "},{"location":"12_genericity_matrix/#tier-a-library-backed-use-never-reimplement","title":"Tier A \u2014 Library-backed (use; never reimplement)","text":"

    These capabilities already exist as internal libraries. Reach them through the library registry MCP (find_library(capability)) and read the library's own MCP for its API. Writing a private copy of any of these is a defect.

    Capability Library MCP Persistence: repositories, cache, populate, transactions Mongo Ops (mongo_ops) :8007 Dependency injection / composition Hexa (hexa) :8008 Deterministic multi-step transformation engine DAG Pipe (dagpipe) :8006 Spec-first HTTP edge, client, codegen OpenAPI First (openapi_first) :8004 Format-agnostic fetch + parse (HTML, PDF, CSV, XLSX) Omniread (omniread) :8003 Read-only email ingestion Mail Intake (mail_intake) :8002 JWT auth primitives (register, login, introspect) Py JWT (jwtlib) :8005 Documentation build (MkDocs + MCP) Doc Forge (docforge) :8001"},{"location":"12_genericity_matrix/#tier-b-template-provided-copy-from-templatesapp-then-adapt","title":"Tier B \u2014 Template-provided (copy from templates/app, then adapt)","text":"

    These are reusable internal components shipped inside the template \u2014 they are not independent libraries. Copy them as-is, then rename/populate only what is app-specific. They give every app the same wiring shape as the reference app. Fetch them by kind with get_app_component, or the whole tree with get_app_template.

    Component Template location Action Shipped Layering & dependency rule the package layout + import rules keep \u2705 Composition root shape init.py (get_svc/get_db, idempotency) keep \u2705 Spec-first web edge interfaces/openapi.py, thin handlers bound to operationId keep \u2705 CRUD slice pattern domain/<resource>/{models.py, ports/db.py, ports/service.py, converters.py} rename + fill \u2705 Generic CRUD service infra/adapters/service.py (CrudsServiceAdaptor) keep \u2705 Repository factory infra/db/repos/mongo.py (_repo_factory) keep \u2705 Worker poll + status loop worker.py keep \u2705 Progress bus infra/adapters/sse.py (keyed in-memory bus) + an app SSE endpoint keep \u2705 HTTP response cache infra/http_cache.py (cache_response + generation invalidation) keep \u2705 Key-value store infra/adapters/kv.py (port + in-memory + Redis adaptor) keep \u2705 Pipeline scaffold pipelines/shared/{ports,loader,registry}.py + pipelines/<family>/ rename + fill \u2705 Operator scripts scripts/ (init() / get_svc() / shutdown()) rename \u2705 Testing harness integration fakes + fixture-parity pattern keep \u2705 Project scaffolding pyproject.toml, Dockerfile, docker-compose.yaml, .env.example, openapi.yaml rename + fill \u2705"},{"location":"12_genericity_matrix/#tier-b-patterns-port-from-the-reference-app","title":"Tier B (patterns) \u2014 port from the reference app","text":"

    A few Tier B capabilities are real and reusable, but are shaped by the app or the provider, so the template does not ship them verbatim \u2014 copying them blindly would carry app-specific names. Treat them as patterns: lift the shape from the reference app and rename per app.

    Component Reference location Action Shipped Human-in-the-loop durable pause + question document + resolve endpoint + resume map port + reshape \u2717 LLM client pool provider-select + retry/rotation pool scaffold port + reshape \u2717"},{"location":"12_genericity_matrix/#tier-c-app-specific-must-be-authored","title":"Tier C \u2014 App-specific (must be authored)","text":"Artifact Why Resource models the business shape Repository populate/cache rules the relationships Service business logic domain behaviour Pipeline families the transformation graph Pattern/rule resources data-driven policy Fixtures & golden outputs test data"},{"location":"12_genericity_matrix/#the-two-axes","title":"The two axes","text":"

    Everything app-specific reduces to two axes:

    1. Resource model \u2014 the set of resources, their fields, relationships, and the OpenAPI contract.
    2. Pipeline family \u2014 only if inputs require multi-step transformation.

    If you can specify those two axes, the rest of the app follows mechanically: Tier A is imported, Tier B is copied, and Tier C is the only thing you author.

    "},{"location":"13_library_registry/","title":"Library Registry","text":""},{"location":"13_library_registry/#summary","title":"Summary","text":"

    The Aetos ecosystem provides internal libraries for the shared concerns of every application. Use them. Writing a private copy of any capability below is a defect.

    Each library also exposes its own documentation as an MCP server. An AI agent can read a library's full API by connecting to its MCP and reading docs://index, docs://nav, and docs://modules/{module}.

    "},{"location":"13_library_registry/#catalog","title":"Catalog","text":"Library Import Purpose Use instead of MCP DAG Pipe dagpipe Deterministic state-transformation DAG engine hand-rolled orchestration/topological ordering :8006 Hexa hexa Declarative DI / composition-root tooling manual wiring, hand-written factories :8008 Mongo Ops mongo_ops Async MongoDB repositories, cache, populate, transactions raw Motor CRUD and connection lifecycle :8007 Omniread omniread Format-agnostic fetch + parse (HTML, PDF, CSV, XLSX) custom scrapers/parsers :8003 OpenAPI First openapi_first Spec-first FastAPI bootstrap, client, codegen decorator routers and hand-written clients :8004 Mail Intake mail_intake Provider-agnostic read-only email ingestion Gmail OAuth + MIME parsing :8002 Py JWT jwtlib JWT auth primitives (register/login/introspect) password hashing + token code :8005 Doc Forge docforge Docstring compiler \u2192 MkDocs + MCP docs custom doc generators :8001 Auth Server (service) Central identity service (register/login/me/logout/introspect) building auth endpoints yourself spec: docs://spec"},{"location":"13_library_registry/#how-to-choose","title":"How to choose","text":"You need to\u2026 Use persist and query documents Mongo Ops compose dependencies Hexa run a multi-step transformation DAG Pipe read/parse an arbitrary input format Omniread expose a contract-first HTTP API OpenAPI First ingest email Mail Intake authenticate users / verify tokens Py JWT (+ Auth Server) generate documentation Doc Forge"},{"location":"13_library_registry/#discovery","title":"Discovery","text":"

    A single standards MCP exposes find_library(capability), which returns the matching library, its import name, its \"use instead of\" guidance, and its MCP endpoint (or, for the auth-server service, a spec pointer). It also serves the pinned auth-server OpenAPI contract at docs://spec. Query it before writing any infrastructure.

    "},{"location":"13_library_registry/#notes","title":"Notes","text":""},{"location":"14_auth_integration/","title":"Auth Integration","text":""},{"location":"14_auth_integration/#summary","title":"Summary","text":"

    Authentication is a service, not a library. Every Aetos application shares a single identity service \u2014 the auth-server \u2014 and consumes it through its HTTP contract. An app never implements registration, login, password hashing, or token issuance itself.

    The agent does not need an MCP bundle for the auth-server. It reads the pinned OpenAPI contract (below) and declares a bearer security scheme in its own spec; openapi_first then generates the verifying dependency. No auth code is written.

    "},{"location":"14_auth_integration/#read-the-contract","title":"Read the contract","text":"

    The auth-server OpenAPI document is pinned in the standards bundle and served as an MCP resource:

    docs://spec\n

    It is the authoritative client contract (OpenAPI 3.1). Read it to learn the endpoints and the request/response schemas:

    Endpoint Purpose POST /register Create a user (201, returns PublicUser) POST /login Authenticate and issue a JWT (LoginResponse) GET /me Current user's PublicUser (bearer required) POST /logout Stateless logout (LogoutResponse) POST /introspect Internal token introspection (active, user)

    The pinned snapshot is versioned (currently 0.0.5). If the caller has a live auth-server and needs the current contract, they can pass the full endpoint ({AUTH_SERVER}/openapi.json) and read it directly instead of the pin.

    "},{"location":"14_auth_integration/#wire-it-up-declare-dont-code","title":"Wire it up: declare, don't code","text":"

    Authentication is added by editing the app's own openapi.yaml \u2014 nothing else. Declare a global bearer requirement and point the scheme at the auth server (values come from the environment):

    security:\n  - bearerAuth: []\npaths:\n  # ...\ncomponents:\n  securitySchemes:\n    bearerAuth:\n      type: http\n      scheme: bearer\n      bearerFormat: JWT\n      x-server-url: \"{AUTH_SERVER}\"\n      x-introspect-path: \"/introspect\"\n      x-login-path: \"/login\"\n      x-register-path: \"/register\"\n      x-logout-path: \"/logout\"\n      x-me-path: \"/me\"\n

    openapi_first reads securitySchemes + security and generates a FastAPI dependency that extracts the bearer token, POSTs {\"token\": <jwt>} to {AUTH_SERVER}/introspect, and returns the user when active == true. It raises 401 on an invalid or inactive token and 503 when the auth server is unreachable. The generated handlers receive the authenticated user \u2014 you author no authentication code.

    "},{"location":"14_auth_integration/#public-operations","title":"Public operations","text":"

    A route that must stay open opts out by overriding security with an empty list:

    paths:\n  /health:\n    get:\n      operationId: health\n      security: []        # overrides the global requirement\n      responses:\n        \"200\":\n          description: OK\n
    "},{"location":"14_auth_integration/#environment","title":"Environment","text":"
    AUTH_SERVER=http://localhost:8000     # auth-server base URL\nJWT_SERVER=http://localhost:8000      # jwtlib client base URL (seeding/CLI)\n
    "},{"location":"14_auth_integration/#path-1-jwtlib-client-no-generator","title":"Path 1: jwtlib client (no generator)","text":"

    Apps that do not use a generated dependency verify requests with the py-jwt client:

    from jwtlib.introspection import authenticate_request\n\nauthorized = await authenticate_request(\n    should_skip_authentication,  # allow public paths\n    method,\n    path,\n    authorization_token,         # \"Bearer <jwt>\" or None\n)\n

    authenticate_request POSTs to {JWT_SERVER}/introspect (3 second timeout) and treats {\"active\": true, \"user\": {...}} as valid. Use this for seeding scripts and tests (jwtlib.register_user, jwtlib.login_user).

    "},{"location":"14_auth_integration/#golden-rules","title":"Golden rules","text":"
    1. Never trust an unverified token. Decode through the generated dependency or jwtlib before handling the request.
    2. Treat active: false as unauthenticated, even if the JWT decodes.
    3. Do not implement your own JWT parsing. Revocation and issuer changes must stay centralized in the auth-server.
    4. Do not add auth until the app's outcome is validated. Ship the app authless first; layer the security scheme once the core behaviour works.
    "},{"location":"14_auth_integration/#notes","title":"Notes","text":""}]} \ No newline at end of file diff --git a/standards/lib/sitemap.xml.gz b/standards/lib/sitemap.xml.gz index f0957ead3fa71accf94fc2db32d8580ce204bea1..0d62163b11b5b3333d1b610c44f453310b4c022e 100644 GIT binary patch delta 13 Ucmb=gXP58h;9xj*ZX$aH034wNWB>pF delta 13 Ucmb=gXP58h;Akj0JCVHt03HJbkpKVy diff --git a/standards/mcp/components.json b/standards/mcp/components.json index 9adb280..39ae3da 100644 --- a/standards/mcp/components.json +++ b/standards/mcp/components.json @@ -42,6 +42,12 @@ "placeholder": "", "description": "Route registry: flat re-export of every handler namespace." }, + { + "kind": "auth", + "path": "interfaces/auth/security.yaml", + "placeholder": "", + "description": "Mergeable bearerAuth security scheme + global security for openapi.yaml." + }, { "kind": "test-model", "path": "tests/unit/test_item_models.py", diff --git a/standards/mcp/index.json b/standards/mcp/index.json index 406d7a2..e2413a7 100644 --- a/standards/mcp/index.json +++ b/standards/mcp/index.json @@ -1,6 +1,6 @@ { "project": "standards", "type": "standards", - "modules_count": 12, + "modules_count": 13, "source": "hand-authored" } \ No newline at end of file diff --git a/standards/mcp/libraries.json b/standards/mcp/libraries.json index f020ea4..b7edd22 100644 --- a/standards/mcp/libraries.json +++ b/standards/mcp/libraries.json @@ -68,12 +68,13 @@ }, { "name": "Auth Server", - "import": "auth-server", - "purpose": "Central identity service (register/login/me/introspect).", + "import": null, + "purpose": "Central identity service (register/login/me/logout/introspect).", "use_instead_of": "building authentication endpoints yourself", - "capabilities": ["auth", "identity", "service", "register", "login", "introspect"], - "status": "deferred", - "mcp": { "server": null, "port": null, "note": "service; no MCP bundle; deferred" } + "capabilities": ["auth", "identity", "service", "register", "login", "logout", "introspect", "bearer"], + "spec": "docs://spec", + "recipe": "docs://modules/auth_integration", + "mcp": { "server": null, "port": null, "note": "service; read contract via docs://spec; no MCP bundle" } } ] } diff --git a/standards/mcp/modules/auth_integration.json b/standards/mcp/modules/auth_integration.json new file mode 100644 index 0000000..2fb2a0f --- /dev/null +++ b/standards/mcp/modules/auth_integration.json @@ -0,0 +1,8 @@ +{ + "module": "auth_integration", + "content": { + "path": "auth_integration", + "docstring": "# Auth Integration\n\n## Summary\n\nAuthentication is a **service**, not a library. Every Aetos application shares a\nsingle identity service — the **auth-server** — and consumes it through its HTTP\ncontract. An app never implements registration, login, password hashing, or\ntoken issuance itself.\n\nThe agent does **not** need an MCP bundle for the auth-server. It reads the\npinned OpenAPI contract (below) and declares a bearer security scheme in its own\nspec; `openapi_first` then generates the verifying dependency. **No auth code is\nwritten.**\n\n---\n\n## Read the contract\n\nThe auth-server OpenAPI document is pinned in the standards bundle and served as\nan MCP resource:\n\n```\ndocs://spec\n```\n\nIt is the authoritative client contract (OpenAPI 3.1). Read it to learn the\nendpoints and the request/response schemas:\n\n| Endpoint | Purpose |\n|---|---|\n| `POST /register` | Create a user (201, returns `PublicUser`) |\n| `POST /login` | Authenticate and issue a JWT (`LoginResponse`) |\n| `GET /me` | Current user's `PublicUser` (bearer required) |\n| `POST /logout` | Stateless logout (`LogoutResponse`) |\n| `POST /introspect` | Internal token introspection (`active`, `user`) |\n\nThe pinned snapshot is versioned (currently `0.0.5`). If the caller has a live\nauth-server and needs the *current* contract, they can pass the full endpoint\n(`{AUTH_SERVER}/openapi.json`) and read it directly instead of the pin.\n\n---\n\n## Wire it up: declare, don't code\n\nAuthentication is added by editing the app's **own** `openapi.yaml` — nothing\nelse. Declare a global bearer requirement and point the scheme at the auth\nserver (values come from the environment):\n\n```yaml\nsecurity:\n - bearerAuth: []\npaths:\n # ...\ncomponents:\n securitySchemes:\n bearerAuth:\n type: http\n scheme: bearer\n bearerFormat: JWT\n x-server-url: \"{AUTH_SERVER}\"\n x-introspect-path: \"/introspect\"\n x-login-path: \"/login\"\n x-register-path: \"/register\"\n x-logout-path: \"/logout\"\n x-me-path: \"/me\"\n```\n\n`openapi_first` reads `securitySchemes` + `security` and generates a FastAPI\ndependency that extracts the bearer token, POSTs `{\"token\": }` to\n`{AUTH_SERVER}/introspect`, and returns the user when `active == true`. It\nraises `401` on an invalid or inactive token and `503` when the auth server is\nunreachable. The generated handlers receive the authenticated user — you author\n**no** authentication code.\n\n### Public operations\n\nA route that must stay open opts out by overriding `security` with an empty\nlist:\n\n```yaml\npaths:\n /health:\n get:\n operationId: health\n security: [] # overrides the global requirement\n responses:\n \"200\":\n description: OK\n```\n\n### Environment\n\n```bash\nAUTH_SERVER=http://localhost:8000 # auth-server base URL\nJWT_SERVER=http://localhost:8000 # jwtlib client base URL (seeding/CLI)\n```\n\n---\n\n## Path 1: `jwtlib` client (no generator)\n\nApps that do not use a generated dependency verify requests with the `py-jwt`\nclient:\n\n```python\nfrom jwtlib.introspection import authenticate_request\n\nauthorized = await authenticate_request(\n should_skip_authentication, # allow public paths\n method,\n path,\n authorization_token, # \"Bearer \" or None\n)\n```\n\n`authenticate_request` POSTs to `{JWT_SERVER}/introspect` (3 second timeout) and\ntreats `{\"active\": true, \"user\": {...}}` as valid. Use this for seeding scripts\nand tests (`jwtlib.register_user`, `jwtlib.login_user`).\n\n---\n\n## Golden rules\n\n1. **Never trust an unverified token.** Decode through the generated dependency\n or `jwtlib` before handling the request.\n2. **Treat `active: false` as unauthenticated**, even if the JWT decodes.\n3. **Do not implement your own JWT parsing.** Revocation and issuer changes must\n stay centralized in the auth-server.\n4. **Do not add auth until the app's outcome is validated.** Ship the app\n authless first; layer the security scheme once the core behaviour works.\n\n---\n\n## Notes\n\n- The default template is **authless** on purpose. Add the `securitySchemes`\n block above only when auth is required. Pull the shared slice with\n `get_app_component(\"auth\")`.\n- The auth-server is deployed as a service (no importable package); the client\n surface is the spec, plus `jwtlib` for the low-level path.\n", + "objects": {} + } +} \ No newline at end of file diff --git a/standards/mcp/modules/index.json b/standards/mcp/modules/index.json index 0f880f9..7b4a61e 100644 --- a/standards/mcp/modules/index.json +++ b/standards/mcp/modules/index.json @@ -2,7 +2,7 @@ "module": "index", "content": { "path": "index", - "docstring": "# Standards Reference\n\nThe reference half of the Standards description: the enforceable rules,\npatterns, and the internal library catalog that agents consult while coding.\n\n- **Build Recipe** — the ordered, agentic checklist for a new app.\n- **Layering Rules** — the strict request → domain → repository layering and\n the rules that enforce it.\n- **Composition Root** — a single composition point wiring the whole app.\n- **Spec-First Routing** — the OpenAPI contract as the routing source of truth.\n- **CRUD Template** — the standardized generic resource endpoint scaffold.\n- **Pipeline Scaffold** — the deterministic pipeline skeleton for multi-stage\n jobs.\n- **Worker & Progress** — queued work, progress reporting, and visibility.\n- **Human-in-the-Loop** — guardrails and approvals inside automations.\n- **Infra Adapters** — how infrastructure connects without leaking into domain.\n- **Genericity Matrix** — what is library-backed, template-provided, or authored.\n- **Library Registry** — the catalog of internal libraries behind all of it.\n\nRead the **Wiki** for the philosophy and rationale; these pages are the\nreference that mirrors the `standards` MCP bundle.\n", + "docstring": "# Standards Reference\n\nThe reference half of the Standards description: the enforceable rules,\npatterns, and the internal library catalog that agents consult while coding.\n\n- **Build Recipe** — the ordered, agentic checklist for a new app.\n- **Layering Rules** — the strict request → domain → repository layering and\n the rules that enforce it.\n- **Composition Root** — a single composition point wiring the whole app.\n- **Spec-First Routing** — the OpenAPI contract as the routing source of truth.\n- **CRUD Template** — the standardized generic resource endpoint scaffold.\n- **Pipeline Scaffold** — the deterministic pipeline skeleton for multi-stage\n jobs.\n- **Worker & Progress** — queued work, progress reporting, and visibility.\n- **Human-in-the-Loop** — guardrails and approvals inside automations.\n- **Infra Adapters** — how infrastructure connects without leaking into domain.\n- **Genericity Matrix** — what is library-backed, template-provided, or authored.\n- **Auth Integration** — consuming the shared auth-server service (spec-driven,\n no auth code).\n- **Library Registry** — the catalog of internal libraries behind all of it.\n\nRead the **Wiki** for the philosophy and rationale; these pages are the\nreference that mirrors the `standards` MCP bundle.\n", "objects": {} } } \ No newline at end of file diff --git a/standards/mcp/modules/library_registry.json b/standards/mcp/modules/library_registry.json index 738af31..3b30133 100644 --- a/standards/mcp/modules/library_registry.json +++ b/standards/mcp/modules/library_registry.json @@ -2,7 +2,7 @@ "module": "library_registry", "content": { "path": "library_registry", - "docstring": "# Library Registry\n\n## Summary\n\nThe Aetos ecosystem provides internal libraries for the shared concerns of\nevery application. **Use them.** Writing a private copy of any capability below\nis a defect.\n\nEach library also exposes its own documentation as an MCP server. An AI agent\ncan read a library's full API by connecting to its MCP and reading\n`docs://index`, `docs://nav`, and `docs://modules/{module}`.\n\n---\n\n## Catalog\n\n| Library | Import | Purpose | Use instead of | MCP |\n|---|---|---|---|---|\n| DAG Pipe | `dagpipe` | Deterministic state-transformation DAG engine | hand-rolled orchestration/topological ordering | `:8006` |\n| Hexa | `hexa` | Declarative DI / composition-root tooling | manual wiring, hand-written factories | `:8008` |\n| Mongo Ops | `mongo_ops` | Async MongoDB repositories, cache, populate, transactions | raw Motor CRUD and connection lifecycle | `:8007` |\n| Omniread | `omniread` | Format-agnostic fetch + parse (HTML, PDF, CSV, XLSX) | custom scrapers/parsers | `:8003` |\n| OpenAPI First | `openapi_first` | Spec-first FastAPI bootstrap, client, codegen | decorator routers and hand-written clients | `:8004` |\n| Mail Intake | `mail_intake` | Provider-agnostic read-only email ingestion | Gmail OAuth + MIME parsing | `:8002` |\n| Py JWT | `jwtlib` | JWT auth primitives (register/login/introspect) | password hashing + token code | `:8005` |\n| Doc Forge | `docforge` | Docstring compiler → MkDocs + MCP docs | custom doc generators | `:8001` |\n| Auth Server | *(service)* | Central identity service (register/login/me/introspect) | building auth endpoints yourself | *(deferred)* |\n\n## How to choose\n\n| You need to… | Use |\n|---|---|\n| persist and query documents | Mongo Ops |\n| compose dependencies | Hexa |\n| run a multi-step transformation | DAG Pipe |\n| read/parse an arbitrary input format | Omniread |\n| expose a contract-first HTTP API | OpenAPI First |\n| ingest email | Mail Intake |\n| authenticate users / verify tokens | Py JWT (+ Auth Server) |\n| generate documentation | Doc Forge |\n\n## Discovery\n\nA single **standards MCP** exposes `find_library(capability)`, which returns the\nmatching library, its import name, its \"use instead of\" guidance, and its MCP\nendpoint. Query it before writing any infrastructure.\n\n## Notes\n\n- **Hexa** is a full library with its own MCP server on `:8008`: read\n `docs://index`, `docs://nav`, and `docs://modules/{module}` (e.g.\n `docs://modules/hexa.container`) to discover the DI surface — `build(cls, *,\n instances, config)` and the `PortNode` model.\n- **Auth Server** is deferred: it has no `pyproject.toml`/importable package and\n no MCP bundle. Do not reach for it yet; `py-jwt` covers token primitives.\n- Library MCP servers expose **documentation resources** (not executable tools):\n they tell an agent the available API surface so it does not invent code.\n", + "docstring": "# Library Registry\n\n## Summary\n\nThe Aetos ecosystem provides internal libraries for the shared concerns of\nevery application. **Use them.** Writing a private copy of any capability below\nis a defect.\n\nEach library also exposes its own documentation as an MCP server. An AI agent\ncan read a library's full API by connecting to its MCP and reading\n`docs://index`, `docs://nav`, and `docs://modules/{module}`.\n\n---\n\n## Catalog\n\n| Library | Import | Purpose | Use instead of | MCP |\n|---|---|---|---|---|\n| DAG Pipe | `dagpipe` | Deterministic state-transformation DAG engine | hand-rolled orchestration/topological ordering | `:8006` |\n| Hexa | `hexa` | Declarative DI / composition-root tooling | manual wiring, hand-written factories | `:8008` |\n| Mongo Ops | `mongo_ops` | Async MongoDB repositories, cache, populate, transactions | raw Motor CRUD and connection lifecycle | `:8007` |\n| Omniread | `omniread` | Format-agnostic fetch + parse (HTML, PDF, CSV, XLSX) | custom scrapers/parsers | `:8003` |\n| OpenAPI First | `openapi_first` | Spec-first FastAPI bootstrap, client, codegen | decorator routers and hand-written clients | `:8004` |\n| Mail Intake | `mail_intake` | Provider-agnostic read-only email ingestion | Gmail OAuth + MIME parsing | `:8002` |\n| Py JWT | `jwtlib` | JWT auth primitives (register/login/introspect) | password hashing + token code | `:8005` |\n| Doc Forge | `docforge` | Docstring compiler → MkDocs + MCP docs | custom doc generators | `:8001` |\n| Auth Server | *(service)* | Central identity service (register/login/me/logout/introspect) | building auth endpoints yourself | spec: `docs://spec` |\n\n## How to choose\n\n| You need to… | Use |\n|---|---|\n| persist and query documents | Mongo Ops |\n| compose dependencies | Hexa |\n| run a multi-step transformation | DAG Pipe |\n| read/parse an arbitrary input format | Omniread |\n| expose a contract-first HTTP API | OpenAPI First |\n| ingest email | Mail Intake |\n| authenticate users / verify tokens | Py JWT (+ Auth Server) |\n| generate documentation | Doc Forge |\n\n## Discovery\n\nA single **standards MCP** exposes `find_library(capability)`, which returns the\nmatching library, its import name, its \"use instead of\" guidance, and its MCP\nendpoint (or, for the auth-server service, a spec pointer). It also serves the\npinned auth-server OpenAPI contract at `docs://spec`. Query it before writing\nany infrastructure.\n\n## Notes\n\n- **Hexa** is a full library with its own MCP server on `:8008`: read\n `docs://index`, `docs://nav`, and `docs://modules/{module}` (e.g.\n `docs://modules/hexa.container`) to discover the DI surface — `build(cls, *,\n instances, config)` and the `PortNode` model.\n- **Auth Server** is a **service**, not an importable library: it has no MCP\n bundle by design. Read its client contract from the pinned OpenAPI document\n via `docs://spec` and wire it by declaring a `bearerAuth` security scheme in\n the app's own spec (see **Auth Integration**); `py-jwt` covers the\n low-level token path.\n- Library MCP servers expose **documentation resources** (not executable tools):\n they tell an agent the available API surface so it does not invent code.\n", "objects": {} } } \ No newline at end of file diff --git a/standards/mcp/nav.json b/standards/mcp/nav.json index 124f7bf..6d0325b 100644 --- a/standards/mcp/nav.json +++ b/standards/mcp/nav.json @@ -54,6 +54,11 @@ "resource": "docs://modules/library_registry", "title": "Library Registry" }, + { + "module": "auth_integration", + "resource": "docs://modules/auth_integration", + "title": "Auth Integration" + }, { "module": "index", "resource": "docs://modules/index", diff --git a/standards/mcp/specs/auth-server.json b/standards/mcp/specs/auth-server.json new file mode 100644 index 0000000..6e4bed4 --- /dev/null +++ b/standards/mcp/specs/auth-server.json @@ -0,0 +1,563 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Aetoskia Auth Server", + "description": "\n The Aetoskia Authentication Service.\n\n Provides identity, access control, and JWT token issuance for the Aetoskia\n ecosystem. Fully async, built with FastAPI and MongoDB.\n ", + "termsOfService": "https://aetoskia.com/terms", + "contact": { + "name": "Aetoskia Dev Team", + "url": "https://dev.aetoskia.com/contact", + "email": "dev@aetoskia.com" + }, + "license": { + "name": "MIT License", + "url": "https://opensource.org/licenses/MIT" + }, + "version": "0.0.5" + }, + "servers": [ + { + "url": "https://auth.aetoskia.com", + "description": "Production Authentication Service" + }, + { + "url": "http://server-pi:9002", + "description": "Internal staging environment" + }, + { + "url": "http://localhost:8000", + "description": "Local development server" + } + ], + "paths": { + "/register": { + "post": { + "tags": [ + "Auth" + ], + "summary": "Register a new user", + "description": "Creates a new user account and returns the public user profile.", + "operationId": "create_user_register_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RegisterRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicUser" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/login": { + "post": { + "tags": [ + "Auth" + ], + "summary": "Login user", + "description": "Authenticates a user and issues a JWT access token.", + "operationId": "login_login_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LoginRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LoginResponse" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/me": { + "get": { + "tags": [ + "Auth" + ], + "summary": "Get current user", + "description": "Returns the currently authenticated user's public profile.", + "operationId": "read_users_me_me_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicUser" + } + } + } + } + }, + "security": [ + { + "HTTPBearer": [] + } + ] + } + }, + "/logout": { + "post": { + "tags": [ + "Auth" + ], + "summary": "Logout user", + "description": "Stateless logout endpoint. The client must discard the JWT access token.", + "operationId": "logout_logout_post", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LogoutResponse" + } + } + } + } + }, + "security": [ + { + "HTTPBearer": [] + } + ] + } + }, + "/introspect": { + "post": { + "tags": [ + "Auth", + "Internal" + ], + "summary": "Token introspection (internal)", + "description": "Internal service-to-service endpoint for verifying JWTs. Not intended for direct client use.", + "operationId": "introspect_introspect_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IntrospectRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IntrospectResponse" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/health": { + "get": { + "tags": [ + "Health" + ], + "summary": "Health Check", + "description": "Report service health.\n\nReturns:\n dict:\n A simple ``{\"status\": \"ok\"}`` payload used by load balancers and\n the container health check.", + "operationId": "health_check_health_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + } + } + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "type": "array", + "title": "Detail" + } + }, + "type": "object", + "title": "HTTPValidationError" + }, + "IntrospectRequest": { + "properties": { + "token": { + "type": "string", + "title": "Token", + "description": "JWT access token to introspect", + "examples": [ + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + ] + } + }, + "type": "object", + "required": [ + "token" + ], + "title": "IntrospectRequest", + "description": "Payload for requesting token introspection.\n\nUsed by internal services to verify the validity of a JWT and retrieve\nthe associated public user information.\n\nFields:\n token: JWT access token to introspect.\n\nNotes:\n - Intended for service-to-service communication.\n - Not meant for direct end-user consumption.", + "example": { + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + } + }, + "IntrospectResponse": { + "properties": { + "active": { + "type": "boolean", + "title": "Active", + "description": "Indicates whether the provided token is valid and active", + "examples": [ + true, + false + ] + }, + "user": { + "anyOf": [ + { + "$ref": "#/components/schemas/PublicUser" + }, + { + "type": "null" + } + ], + "description": "Public user details if the token is valid; null otherwise" + } + }, + "type": "object", + "required": [ + "active" + ], + "title": "IntrospectResponse", + "description": "Result of a token introspection operation.\n\nThis model communicates whether a JWT is valid and, if so, provides\nthe associated public user information.\n\nFields:\n active: Indicates whether the token is valid and active.\n user: Public user details if the token is valid; otherwise null.\n\nNotes:\n - This model is designed to avoid raising exceptions.\n - All introspection outcomes are represented as data.", + "example": { + "active": true, + "user": { + "email": "tester@example.com", + "is_active": true, + "username": "tester" + } + } + }, + "LoginRequest": { + "properties": { + "password": { + "type": "string", + "minLength": 6, + "title": "Password", + "description": "User password (minimum 6 characters). Stored only as a hash.", + "examples": [ + "SuperSecure123" + ] + }, + "username": { + "type": "string", + "maxLength": 50, + "minLength": 3, + "title": "Username", + "description": "Unique username used for authentication and display", + "examples": [ + "tester" + ] + }, + "email": { + "anyOf": [ + { + "type": "string", + "format": "email" + }, + { + "type": "null" + } + ], + "title": "Email", + "description": "Primary email address associated with the account", + "examples": [ + "tester@example.com" + ] + } + }, + "type": "object", + "required": [ + "password", + "username" + ], + "title": "LoginRequest", + "description": "Payload for authenticating a user and issuing a JWT.\n\nThis model is used to verify user credentials and request an access token.\n\nFields:\n username: Username identifier.\n password: Plain-text password to be verified.\n\nNotes:\n - Successful authentication results in a LoginResponse.\n - Failed authentication raises an AuthError.", + "example": { + "password": "SuperSecure123", + "username": "tester" + } + }, + "LoginResponse": { + "properties": { + "access_token": { + "type": "string", + "title": "Access Token", + "description": "JWT access token to be used for authenticated requests", + "examples": [ + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + ] + }, + "user": { + "$ref": "#/components/schemas/PublicUser", + "description": "Authenticated user's public profile" + } + }, + "type": "object", + "required": [ + "access_token", + "user" + ], + "title": "LoginResponse", + "description": "Response returned after successful authentication.\n\nContains the issued JWT access token and the authenticated user's\npublic profile.\n\nFields:\n access_token: JWT access token for authenticated requests.\n user: Public profile of the authenticated user.\n\nNotes:\n - The token is stateless and must be stored client-side.\n - Token expiration and validation are handled elsewhere.", + "example": { + "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "user": { + "email": "tester@example.com", + "is_active": true, + "username": "tester" + } + } + }, + "LogoutResponse": { + "properties": { + "message": { + "type": "string", + "title": "Message", + "description": "Human-readable logout confirmation message", + "examples": [ + "Successfully logged out" + ] + } + }, + "type": "object", + "required": [ + "message" + ], + "title": "LogoutResponse", + "description": "Response returned after a logout operation.\n\nSince logout is stateless, this response serves only as a confirmation\nmessage instructing the client to discard its token.\n\nFields:\n message: Human-readable logout confirmation.", + "example": { + "message": "Successfully logged out" + } + }, + "PublicUser": { + "properties": { + "is_active": { + "type": "boolean", + "title": "Is Active", + "description": "Indicates whether the account is active and allowed to authenticate", + "default": true, + "examples": [ + true + ] + }, + "username": { + "type": "string", + "maxLength": 50, + "minLength": 3, + "title": "Username", + "description": "Unique username used for authentication and display", + "examples": [ + "tester" + ] + }, + "email": { + "anyOf": [ + { + "type": "string", + "format": "email" + }, + { + "type": "null" + } + ], + "title": "Email", + "description": "Primary email address associated with the account", + "examples": [ + "tester@example.com" + ] + } + }, + "type": "object", + "required": [ + "username" + ], + "title": "PublicUser", + "description": "Public-facing user representation returned by authentication APIs.\n\nThis model represents a user profile that is safe to expose outside\nthe authentication system.\n\nFields:\n username: Unique username identifier.\n email: User's email address.\n is_active: Whether the user account is active.\n\nNotes:\n - Contains no sensitive data.\n - Can be constructed from persistence models via `from_attributes`.", + "example": { + "email": "tester@example.com", + "is_active": true, + "username": "tester" + } + }, + "RegisterRequest": { + "properties": { + "password": { + "type": "string", + "minLength": 6, + "title": "Password", + "description": "User password (minimum 6 characters). Stored only as a hash.", + "examples": [ + "SuperSecure123" + ] + }, + "username": { + "type": "string", + "maxLength": 50, + "minLength": 3, + "title": "Username", + "description": "Unique username used for authentication and display", + "examples": [ + "tester" + ] + }, + "email": { + "anyOf": [ + { + "type": "string", + "format": "email" + }, + { + "type": "null" + } + ], + "title": "Email", + "description": "Primary email address associated with the account", + "examples": [ + "tester@example.com" + ] + } + }, + "type": "object", + "required": [ + "password", + "username" + ], + "title": "RegisterRequest", + "description": "Payload for registering a new user account.\n\nThis model contains the minimum required identity and credential\ninformation to create a new user.\n\nFields:\n username: Unique username identifier.\n email: User's email address.\n password: Plain-text password (to be hashed by the repository layer).\n\nNotes:\n - Validation and normalization handled by mixins.\n - This model is never returned in responses.", + "example": { + "email": "tester@example.com", + "password": "SuperSecure123", + "username": "tester" + } + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer" + } + ] + }, + "type": "array", + "title": "Location" + }, + "msg": { + "type": "string", + "title": "Message" + }, + "type": { + "type": "string", + "title": "Error Type" + } + }, + "type": "object", + "required": [ + "loc", + "msg", + "type" + ], + "title": "ValidationError" + } + }, + "securitySchemes": { + "HTTPBearer": { + "type": "http", + "scheme": "bearer" + } + } + }, + "tags": [ + { + "name": "Auth", + "description": "Login, registration, token issuance, refresh, and identity operations." + }, + { + "name": "Health", + "description": "Service health and diagnostics." + } + ] +} \ No newline at end of file diff --git a/standards/mcp/templates/app/.env.example b/standards/mcp/templates/app/.env.example index 3c7d11b..a4f8c32 100644 --- a/standards/mcp/templates/app/.env.example +++ b/standards/mcp/templates/app/.env.example @@ -2,4 +2,6 @@ DEBUG=false MONGO_URI=mongodb://localhost:27017 MONGO_DB=app_template WORKER_POLL_SECONDS=30 -WORKER_MAX_RETRIES=3 \ No newline at end of file +WORKER_MAX_RETRIES=3 +AUTH_SERVER=http://localhost:8000 +JWT_SERVER=http://localhost:8000 \ No newline at end of file diff --git a/standards/mcp/templates/app/interfaces/auth/__init__.py b/standards/mcp/templates/app/interfaces/auth/__init__.py new file mode 100644 index 0000000..5761c3f --- /dev/null +++ b/standards/mcp/templates/app/interfaces/auth/__init__.py @@ -0,0 +1,9 @@ +"""# Summary + +Auth interface: thin FastAPI entry points related to authentication, wired to +openapi.yaml operationIds by the OpenAPI-first router. + +Authentication itself is provided by the shared auth-server service. Wire it by +declaring a ``bearerAuth`` security scheme in ``openapi.yaml`` (see +``security.yaml``) and reading the contract at ``docs://spec``. +""" diff --git a/standards/mcp/templates/app/interfaces/auth/security.yaml b/standards/mcp/templates/app/interfaces/auth/security.yaml new file mode 100644 index 0000000..dbb5846 --- /dev/null +++ b/standards/mcp/templates/app/interfaces/auth/security.yaml @@ -0,0 +1,14 @@ +security: + - bearerAuth: [] +components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + x-server-url: "{AUTH_SERVER}" + x-introspect-path: "/introspect" + x-login-path: "/login" + x-register-path: "/register" + x-logout-path: "/logout" + x-me-path: "/me" diff --git a/standards/src/lib/13_library_registry.md b/standards/src/lib/13_library_registry.md index 97ea183..9e67141 100644 --- a/standards/src/lib/13_library_registry.md +++ b/standards/src/lib/13_library_registry.md @@ -24,7 +24,7 @@ can read a library's full API by connecting to its MCP and reading | Mail Intake | `mail_intake` | Provider-agnostic read-only email ingestion | Gmail OAuth + MIME parsing | `:8002` | | Py JWT | `jwtlib` | JWT auth primitives (register/login/introspect) | password hashing + token code | `:8005` | | Doc Forge | `docforge` | Docstring compiler → MkDocs + MCP docs | custom doc generators | `:8001` | -| Auth Server | *(service)* | Central identity service (register/login/me/introspect) | building auth endpoints yourself | *(deferred)* | +| Auth Server | *(service)* | Central identity service (register/login/me/logout/introspect) | building auth endpoints yourself | spec: `docs://spec` | ## How to choose @@ -43,7 +43,9 @@ can read a library's full API by connecting to its MCP and reading A single **standards MCP** exposes `find_library(capability)`, which returns the matching library, its import name, its "use instead of" guidance, and its MCP -endpoint. Query it before writing any infrastructure. +endpoint (or, for the auth-server service, a spec pointer). It also serves the +pinned auth-server OpenAPI contract at `docs://spec`. Query it before writing +any infrastructure. ## Notes @@ -51,7 +53,10 @@ endpoint. Query it before writing any infrastructure. `docs://index`, `docs://nav`, and `docs://modules/{module}` (e.g. `docs://modules/hexa.container`) to discover the DI surface — `build(cls, *, instances, config)` and the `PortNode` model. -- **Auth Server** is deferred: it has no `pyproject.toml`/importable package and - no MCP bundle. Do not reach for it yet; `py-jwt` covers token primitives. +- **Auth Server** is a **service**, not an importable library: it has no MCP + bundle by design. Read its client contract from the pinned OpenAPI document + via `docs://spec` and wire it by declaring a `bearerAuth` security scheme in + the app's own spec (see **Auth Integration**); `py-jwt` covers the + low-level token path. - Library MCP servers expose **documentation resources** (not executable tools): they tell an agent the available API surface so it does not invent code. diff --git a/standards/src/lib/14_auth_integration.md b/standards/src/lib/14_auth_integration.md new file mode 100644 index 0000000..3513551 --- /dev/null +++ b/standards/src/lib/14_auth_integration.md @@ -0,0 +1,140 @@ +# Auth Integration + +## Summary + +Authentication is a **service**, not a library. Every Aetos application shares a +single identity service — the **auth-server** — and consumes it through its HTTP +contract. An app never implements registration, login, password hashing, or +token issuance itself. + +The agent does **not** need an MCP bundle for the auth-server. It reads the +pinned OpenAPI contract (below) and declares a bearer security scheme in its own +spec; `openapi_first` then generates the verifying dependency. **No auth code is +written.** + +--- + +## Read the contract + +The auth-server OpenAPI document is pinned in the standards bundle and served as +an MCP resource: + +``` +docs://spec +``` + +It is the authoritative client contract (OpenAPI 3.1). Read it to learn the +endpoints and the request/response schemas: + +| Endpoint | Purpose | +|---|---| +| `POST /register` | Create a user (201, returns `PublicUser`) | +| `POST /login` | Authenticate and issue a JWT (`LoginResponse`) | +| `GET /me` | Current user's `PublicUser` (bearer required) | +| `POST /logout` | Stateless logout (`LogoutResponse`) | +| `POST /introspect` | Internal token introspection (`active`, `user`) | + +The pinned snapshot is versioned (currently `0.0.5`). If the caller has a live +auth-server and needs the *current* contract, they can pass the full endpoint +(`{AUTH_SERVER}/openapi.json`) and read it directly instead of the pin. + +--- + +## Wire it up: declare, don't code + +Authentication is added by editing the app's **own** `openapi.yaml` — nothing +else. Declare a global bearer requirement and point the scheme at the auth +server (values come from the environment): + +```yaml +security: + - bearerAuth: [] +paths: + # ... +components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + x-server-url: "{AUTH_SERVER}" + x-introspect-path: "/introspect" + x-login-path: "/login" + x-register-path: "/register" + x-logout-path: "/logout" + x-me-path: "/me" +``` + +`openapi_first` reads `securitySchemes` + `security` and generates a FastAPI +dependency that extracts the bearer token, POSTs `{"token": }` to +`{AUTH_SERVER}/introspect`, and returns the user when `active == true`. It +raises `401` on an invalid or inactive token and `503` when the auth server is +unreachable. The generated handlers receive the authenticated user — you author +**no** authentication code. + +### Public operations + +A route that must stay open opts out by overriding `security` with an empty +list: + +```yaml +paths: + /health: + get: + operationId: health + security: [] # overrides the global requirement + responses: + "200": + description: OK +``` + +### Environment + +```bash +AUTH_SERVER=http://localhost:8000 # auth-server base URL +JWT_SERVER=http://localhost:8000 # jwtlib client base URL (seeding/CLI) +``` + +--- + +## Path 1: `jwtlib` client (no generator) + +Apps that do not use a generated dependency verify requests with the `py-jwt` +client: + +```python +from jwtlib.introspection import authenticate_request + +authorized = await authenticate_request( + should_skip_authentication, # allow public paths + method, + path, + authorization_token, # "Bearer " or None +) +``` + +`authenticate_request` POSTs to `{JWT_SERVER}/introspect` (3 second timeout) and +treats `{"active": true, "user": {...}}` as valid. Use this for seeding scripts +and tests (`jwtlib.register_user`, `jwtlib.login_user`). + +--- + +## Golden rules + +1. **Never trust an unverified token.** Decode through the generated dependency + or `jwtlib` before handling the request. +2. **Treat `active: false` as unauthenticated**, even if the JWT decodes. +3. **Do not implement your own JWT parsing.** Revocation and issuer changes must + stay centralized in the auth-server. +4. **Do not add auth until the app's outcome is validated.** Ship the app + authless first; layer the security scheme once the core behaviour works. + +--- + +## Notes + +- The default template is **authless** on purpose. Add the `securitySchemes` + block above only when auth is required. Pull the shared slice with + `get_app_component("auth")`. +- The auth-server is deployed as a service (no importable package); the client + surface is the spec, plus `jwtlib` for the low-level path. diff --git a/standards/src/lib/index.md b/standards/src/lib/index.md index 220e88e..9f36f6e 100644 --- a/standards/src/lib/index.md +++ b/standards/src/lib/index.md @@ -15,6 +15,8 @@ patterns, and the internal library catalog that agents consult while coding. - **Human-in-the-Loop** — guardrails and approvals inside automations. - **Infra Adapters** — how infrastructure connects without leaking into domain. - **Genericity Matrix** — what is library-backed, template-provided, or authored. +- **Auth Integration** — consuming the shared auth-server service (spec-driven, + no auth code). - **Library Registry** — the catalog of internal libraries behind all of it. Read the **Wiki** for the philosophy and rationale; these pages are the diff --git a/standards/src/mkdocs.lib.yml b/standards/src/mkdocs.lib.yml index 62bc019..db147e4 100644 --- a/standards/src/mkdocs.lib.yml +++ b/standards/src/mkdocs.lib.yml @@ -64,4 +64,5 @@ nav: - Human-in-the-Loop: 08_human_in_loop.md - Infra Adapters: 09_infra_adaptors.md - Genericity Matrix: 12_genericity_matrix.md + - Auth Integration: 14_auth_integration.md - Library Registry: 13_library_registry.md \ No newline at end of file diff --git a/standards/src/templates/app/.env.example b/standards/src/templates/app/.env.example index 3c7d11b..a4f8c32 100644 --- a/standards/src/templates/app/.env.example +++ b/standards/src/templates/app/.env.example @@ -2,4 +2,6 @@ DEBUG=false MONGO_URI=mongodb://localhost:27017 MONGO_DB=app_template WORKER_POLL_SECONDS=30 -WORKER_MAX_RETRIES=3 \ No newline at end of file +WORKER_MAX_RETRIES=3 +AUTH_SERVER=http://localhost:8000 +JWT_SERVER=http://localhost:8000 \ No newline at end of file diff --git a/standards/src/templates/app/interfaces/auth/__init__.py b/standards/src/templates/app/interfaces/auth/__init__.py new file mode 100644 index 0000000..5761c3f --- /dev/null +++ b/standards/src/templates/app/interfaces/auth/__init__.py @@ -0,0 +1,9 @@ +"""# Summary + +Auth interface: thin FastAPI entry points related to authentication, wired to +openapi.yaml operationIds by the OpenAPI-first router. + +Authentication itself is provided by the shared auth-server service. Wire it by +declaring a ``bearerAuth`` security scheme in ``openapi.yaml`` (see +``security.yaml``) and reading the contract at ``docs://spec``. +""" diff --git a/standards/src/templates/app/interfaces/auth/security.yaml b/standards/src/templates/app/interfaces/auth/security.yaml new file mode 100644 index 0000000..dbb5846 --- /dev/null +++ b/standards/src/templates/app/interfaces/auth/security.yaml @@ -0,0 +1,14 @@ +security: + - bearerAuth: [] +components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + x-server-url: "{AUTH_SERVER}" + x-introspect-path: "/introspect" + x-login-path: "/login" + x-register-path: "/register" + x-logout-path: "/logout" + x-me-path: "/me" diff --git a/standards/wiki/sitemap.xml.gz b/standards/wiki/sitemap.xml.gz index f0957ead3fa71accf94fc2db32d8580ce204bea1..0d62163b11b5b3333d1b610c44f453310b4c022e 100644 GIT binary patch delta 13 Ucmb=gXP58h;9xj*ZX$aH034wNWB>pF delta 13 Ucmb=gXP58h;Akj0JCVHt03HJbkpKVy