* refactor(daemon): introduce HTTP Request Adapter + typed Deps (proof on active-context-routes)
Adds a typed HTTP boundary Adapter under apps/daemon/src/http/ that replaces
the untyped ServerContext service-locator pattern (30+ fields, mostly any)
for route handlers. Routes become pure (input, deps) -> Result<output>
functions, unit-testable without Express or supertest.
Six new modules under apps/daemon/src/http/:
- types.ts Result<T,E>, ok(), err(), JsonRouteSpec, Handler,
RouteInputContext, HttpMethod, InputParser
- parse.ts rawInput(req), validationError(message, issues?)
- response.ts sendJson(), sendApiError(), statusForError() +
ERROR_STATUS_BY_CODE map
- origin-guard.ts guardSameOrigin(req, origin) wrapping isLocalSameOrigin
as a Result
- adapter.ts defineJsonRoute(), mountJsonRoute() (only place that
knows about req/res)
- index.ts barrel
active-context-routes.ts migrated as proof of pattern. parsePostActive(),
handlePostActive(), handleGetActive() are now pure functions; postActiveRoute
and getActiveRoute are exported route specs. The wire signature
registerActiveContextRoutes(app, ctx) is preserved so server.ts is untouched.
Spec at specs/current/daemon-http-adapter.md captures the strangler migration
order for the remaining route files (mcp-routes, chat-routes, artifact
routes, etc.) and a StreamRoute follow-up where the Run Orchestrator lands.
Wire-format note: cross-origin response moves from the legacy
{ error: 'cross-origin request rejected' } shape to the structured
{ error: { code: 'FORBIDDEN', message: ... } } shape. Backwards-compatible
via the existing CompatibleErrorResponse = ApiErrorResponse | LegacyErrorResponse
union in @open-design/contracts.
Validation:
- pnpm install (post-rebase, exit 0)
- pnpm --filter @open-design/daemon typecheck (both tsconfig.json and
tsconfig.tests.json silent => pass)
- pnpm --filter @open-design/daemon test: 15 new tests pass
(tests/http/adapter.test.ts + tests/active-context-routes.test.ts).
84 pre-existing failures across 23 files are unchanged and unrelated
to this PR (Windows symlink / short-name / colon-in-filename, upstream
behavior drift, missing plugin marketplace fixtures, and a freshly-
added tools-connectors-cli suite of 38 failures that landed during
the rebase).
Sharpens W4/W5 of specs/current/maintainability-roadmap.md and unlocks
W6 (Run Orchestrator).
* chore: add core-js, electron-winstaller, protobufjs, sharp to pnpm.onlyBuiltDependencies
4.9 KiB
Daemon HTTP Adapter
Purpose
Replace the untyped ServerContext service-locator pattern at the daemon HTTP
boundary with a typed Adapter Seam plus a typed Deps record. Make
request parsing, response shaping, and error mapping the only HTTP-aware code;
let routes become pure (input, deps) -> Result<output> functions whose
interface is the test surface.
This work sharpens W4 (validation at boundary), W5 (modularize server.ts),
and unlocks W6 (run lifecycle) in maintainability-roadmap.md. It does not
re-litigate those workstreams — it provides the seam they will land against.
Module shape
Two Adapters share one request parser:
JsonRoute<Input, Output, Deps>— one-shot JSON request/response. Most of/api/*. Shipped in this PR.StreamRoute<Input, Event, Deps>— SSE streams. Deferred to a follow-up PR that introduces the Run Orchestrator (candidate #3 in the architectural sweep).
Both consume a typed Deps slice. The full Deps record is materialized by a
future composeDeps() function; routes today still receive existing
ctx.<domain> slices via the unchanged RouteDeps<K extends keyof ServerContext> shape so per-route migration does not touch server.ts.
Glossary additions
The following terms are pinned by this spec. They are not present in
docs/architecture.md or root AGENTS.md today:
- Request Adapter — the module that owns request parsing, response
shaping, and error mapping at the HTTP boundary. Lives at
apps/daemon/src/http/. - Json Route / Stream Route — the two route-definition Adapters behind the shared parser.
- Deps — the typed record of domain interfaces injected into routes.
Replaces
ServerContextover the course of the migration.
Files added
apps/daemon/src/http/
types.ts # Result<T, E>, JsonRouteSpec, InputParser, Handler
parse.ts # rawInput(req), validationError(...)
response.ts # sendJson, sendApiError, statusForError
origin-guard.ts # guardSameOrigin(req, origin) -> Result<void>
adapter.ts # defineJsonRoute, mountJsonRoute
index.ts # barrel
Route shape
export const postActiveRoute = defineJsonRoute<Input, Output, Deps>({
method: 'post',
path: '/api/active',
requireSameOrigin: true,
parse: parsePostActive, // RouteInputContext -> Result<Input>
handle: handlePostActive, // (Input, Deps) -> Result<Output>
});
registerActiveContextRoutes(app, ctx) is unchanged at the call site in
server.ts. Internally it wires the route specs through mountJsonRoute.
Migration order (strangler)
- Done — this PR. Scaffold the Adapter and migrate
active-context-routes.tsas the proof of pattern. - Next. Migrate
mcp-routes.ts(smallest remaining same-origin set; already usesisLocalSameOrigin+sendApiErrorconsistently). - Migrate
chat-routes.ts— introduceStreamRoutehere. This composes with the Run Orchestrator (sweep candidate #3). - Migrate artifact routes — composes with the Unified Artifact Validator (sweep candidate #7).
- Migrate remaining route files.
- Materialize a typed
composeDeps()and deleteserver-context.ts'sServerContextinterface once all route registrars have been migrated.
Validation strategy
This PR validates input via small per-route parse functions. The schema-
library decision (Zod / Valibot / hand-rolled) is deferred. When it lands,
the only change is that parse becomes parse: schema for chosen-library
routes; the Adapter's interface is unaffected because schema invocation is
internal to the route's parse function.
Tests
apps/daemon/tests/http/adapter.test.ts— Adapter behavior: success, parse-fail, handle-fail, same-origin block, thrown-error coverage, Deps pass-through.apps/daemon/tests/active-context-routes.test.ts— Domain handlers tested through the exportedpostActiveRoute/getActiveRoutespecs; no Express, no supertest, no mocking ofreq/res.
The existing e2e supertest suites continue to cover the wire surface and serve as the regression guard for the migrated route.
Wire-format note
The cross-origin response for the migrated route changes from the legacy
{ error: 'cross-origin request rejected' } to the structured
{ error: { code: 'FORBIDDEN', message: 'cross-origin request rejected' } }
shape defined by packages/contracts/src/errors.ts. The contracts package
already exports CompatibleErrorResponse (= ApiErrorResponse | LegacyErrorResponse), so clients that parse either shape continue to work.
Out of scope (deliberately)
- StreamRoute / SSE-aware Adapter. Lands with the Run Orchestrator follow-up.
- A typed
composeDeps()that replacesServerContextwholesale. Lands when enough routes have migrated that theany-bag is mostly empty. - A new validation library. Deferred until the migration pattern is proven across at least three route files.
- Express → Fastify swap (roadmap W12). Unaffected by this work.