Story: Generate HTTP routes from the models
Table of Contents
This page documents a story in Sprint 26. It captures the goal, current status, acceptance criteria, and the tasks that compose it.
1. Goal
ores.http serves every component through routes generated from that component's models, with one uniform security model, and ores.assets is the first component landed through it.
2. Status
| Field | Value |
|---|---|
| State | DONE |
| Parent sprint | Sprint 26 |
| Now | Nothing. |
| Waiting on | Nothing. |
| Next | Nothing. |
| Last touched | 2026-09-27 |
3. Result
The gateway's routes generate from the models. A codegen facet,
ores.cpp.http-route, projects an entity or an operation model onto a
route unit with a header, an implementation and a test. Three components
are landed, each in its own part beside the hand-written aggregator, and
each with its hand-written route file deleted rather than kept in
parallel: ores.http.assets (image, tag, image_tag), ores.http.refdata
(currency) and ores.http.iam (account, role, permission, session,
login_info, and nineteen operations). Ninety-two routes generate in
total, and the gateway serves 99 endpoints: those 92, the 5 storage and
1 variability routes that stay hand-written, and the server's own
/api/v1/info.
The gateway is a protocol gateway. A generated route builds the canonical request, sends it over NATS and maps the reply to an HTTP response, so it depends on the target's wire protocol and not on its service library. The server stopped linking three component libraries.
Security is one declaration and one enforcement point. The route forwards the caller's bearer token by delegation, so the service's own permission check governs the real caller, and it states its authentication position from the model. A route that states neither position cannot be built or registered, and a gate fails the build if any registered route is silent. The storage routes were public by omission and are now authenticated.
Verification on the merged head af159c1084:
ctest -R 'http|storage'is 8 of 8.pytest projects/ores.codegen/testsis 787 passed.check_component_drift.py --all --dry-runreports no drift.check_route_auth_declarations.pypasses, and handler permissions, model drift, protocol twin coverage, populate references and the shell recipe inventory are all green.validate_docs.shreports all 40 components pass, andcompass lintpasses.
Live checks, run against the systemd fleet while the branch was open: a generated entity route answered 401 without a token, a generated public operation answered 200, an unexposed operation answered 404, and an unknown path answered 404.
Three failures remained and none belonged to this story. The whole tree
did not build: ores.trading.core and ores.workflow.core included
ores.dq.api/messaging/fsm_protocol.hpp, which dd131c388b split into
fsm_state_protocol.hpp and fsm_transition_protocol.hpp without
migrating the including sites. Two other components owned those files, so
the repair went there, and main has since repaired it: the whole tree
builds again. regenerate_cmake_component_files.py --all
--check reports stale lists in ores.workspace. ores.iam.core.tests
fails against the restored database because its stored
ores_iam_accounts_insert_fn() predates the tree's SQL and reads a
column the SQL no longer has; the tree's SQL is correct.
4. Acceptance
- A codegen facet generates a route unit per entity, with a header, an implementation and a test, from the entity's model.
- The generated route speaks the canonical NATS protocol and carries the caller's session, so the service's own permission check governs access.
- Every route that needs a session is behind one by construction, declared in the model rather than by hand at the call site.
- The hand-written route file for ores.assets is deleted, and its endpoints still answer.
5. Tasks
| Task | State | Start | End | Description |
|---|---|---|---|---|
| Scaffold story: Generate HTTP routes from the models | DONE | 2026-09-26 | 2026-09-27 | Story scaffolding rides this task: documents, sprint wiring, and the scaffold PR. Close it before merging that PR. |
| Build the HTTP route facet and land ores.assets through it | DONE | 2026-09-27 | Initial task for: Generate HTTP routes from the models |
6. Decisions
| # | Decision | Why |
|---|---|---|
| 1 | A generated route reaches its component over NATS, not in process. | It mirrors the shell, so one generator serves both surfaces from the same models, and the gateway stops linking every component's core library. |
| 2 | Security is declared once in the model and enforced once, in the service. | The gateway forwards the caller's bearer token by delegation, so the service's own permission check governs the real caller. Shell and HTTP answer to one check, not two, and the gateway does not re-implement authorization. |
| 3 | A route must state its authentication position, and silence is refused. | route::requires_auth defaulted to false, so a route that forgot the call was silently public. Three storage routes were public by omission. build() and add_route() now throw, naming the method and the pattern. |
| 4 | An operation states :http_route: true to become a route; absence means it is not exposed. |
Whole-model opt-in exposed the model's entire declared surface, including two public endpoints that never existed: bootstrap/provision-tenant and auth/service-login. Refusal is the right default for a gateway. |
| 5 | Generated paths are namespaced by component, and the resulting URL changes are accepted. | The models state a NATS subject, not an HTTP path. Measured before deciding: no in-repo client calls the iam or refdata REST paths, because the web UI reaches the backend over NATS. |
7. Work units
| Unit | Commit | What it decided and proved |
|---|---|---|
| Scaffold | 9f22d14dfe |
The story, its acceptance points and its two tasks. |
| The facet and assets | 2fb572bc6e |
A codegen facet projects an entity model onto a route unit. Assets' three entities generate 29 routes and compile. The verb-to-route mapping: a put becomes two routes because the claim is the whole difference and the method carries it. |
| Wiring and security | 0dd86ddc07 |
The generated routes replace the hand-written ones and the gateway reports 29 generated endpoints live. A route can no longer be public by omission, and a gate proves it in CI. |
| Refdata | b2af226191 |
risk_routes was a misnomer for the refdata currency group; it is deleted, not renamed. Second component landed. |
| Operations and iam | 721bd92d2d |
The operation archetype, and iam's 53 routes across eleven units. Recorded the two findings that followed: the URL changes, and blanket exposure. |
| Exposure and the storage client | 41981304b2 |
Per-message exposure, so the nineteen operations the models opt in are served and no other operation is. The storage client requires its caller's token, and every in-repo caller follows. |
8. Out of scope
Recorded rather than done, each with its reason:
storagekeeps its five routes hand-written. Four are file operations on{bucket}/{key}and the fifth lists a bucket, so there is no entity to generate from.variabilitykeeps its one route hand-written. It has no models, so it has nothing for the facet to read.- The whole tree build, at the time. It failed on a pre-existing break in
other components:
ores.trading.coreandores.workflow.coreincludedores.dq.api/messaging/fsm_protocol.hpp, whichdd131c388bsplit into two headers and did not migrate the including sites. Both components had active owners, so the repair belonged there, and main has since repaired it. No CI job builds the tree, which is how main broke without a check noticing. - The trade and market series export handlers carry the reporting service's token, not the end user's, because the reporting service calls them without delegation. Authorization there is service-level; propagating the caller needs a reporting-side change.
- The compute wrapper has its own HTTP client and holds no JWT, so its storage transfers still receive 401. No credential exists to give it.
- A trailing
{key}GET shadows the versions path beneath it, so/roles/{name}hides/roles/{name}/versions. Assets has the same shape with images. Pre-existing and worth its own fix. journey_sign_up.orgstill namesPOST /api/v1/auth/signup, which now answers 404. It is a journey document, not a client.