From 510c4cfe524640edd03f472ded01bdea8065ee64 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 24 Apr 2026 17:26:07 -0400 Subject: [PATCH] docs: clarify httpapi migration plan --- packages/opencode/specs/effect/http-api.md | 540 +++++---------------- 1 file changed, 126 insertions(+), 414 deletions(-) diff --git a/packages/opencode/specs/effect/http-api.md b/packages/opencode/specs/effect/http-api.md index f83b7c0555dd..6d4791af8f02 100644 --- a/packages/opencode/specs/effect/http-api.md +++ b/packages/opencode/specs/effect/http-api.md @@ -1,462 +1,174 @@ # HttpApi migration -Practical notes for an eventual migration of `packages/opencode` server routes from the current Hono handlers to Effect `HttpApi`, either as a full replacement or as a parallel surface. +Plan for replacing instance Hono route implementations with Effect `HttpApi` while preserving behavior, OpenAPI, and SDK output during the transition. -## Goal +## End State -Use Effect `HttpApi` where it gives us a better typed contract for: +- JSON route contracts and handlers live in `src/server/routes/instance/httpapi/*`. +- Route modules own their `HttpApiGroup`, schemas, handlers, and route-level middleware. +- `httpapi/server.ts` only composes groups, instance lookup, observability, and the web handler bridge. +- Hono route implementations are deleted once their `HttpApi` replacements are default, tested, and represented in the SDK/OpenAPI pipeline. +- Streaming, SSE, and websocket routes move later through Effect HTTP primitives or another explicit replacement plan; they do not need to fit `HttpApi` if `HttpApi` is the wrong abstraction. -- route definition -- request decoding and validation -- typed success and error responses -- OpenAPI generation -- handler composition inside Effect +## Current State -This should be treated as a later-stage HTTP boundary migration, not a prerequisite for ongoing service, route-handler, or schema work. +- `OPENCODE_EXPERIMENTAL_HTTPAPI` gates the bridge. Default behavior still uses Hono. +- The bridge mounts selected paths in `server/routes/instance/index.ts` before legacy Hono routes. +- Legacy Hono routes remain for default behavior and for `hono-openapi` SDK generation. +- `HttpApi` auth is independent of Hono auth. +- `Authorization` is attached in each route module, not centrally wrapped in `server.ts`. +- Auth supports Basic auth and the legacy `auth_token` query parameter through `HttpApiSecurity.apiKey`. +- Instance context is provided by `httpapi/server.ts` using `directory`, `workspace`, and `x-opencode-directory`. +- `Observability.layer` is provided in the Effect route layer and deduplicated through the shared `memoMap`. -## Core model +## Migration Rules -`HttpApi` is definition-first. +- Preserve runtime behavior first. Semantic changes, new error behavior, or route shape changes need separate PRs. +- Migrate one route group, or one coherent subset of a route group, at a time. +- Reuse existing services. Do not re-architect service logic during HTTP boundary migration. +- Effect Schema owns route DTOs. Keep `.zod` only as compatibility for remaining Hono/OpenAPI surfaces. +- Regenerate the SDK after schema or OpenAPI-affecting changes and verify the diff is expected. +- Do not delete a Hono route until the SDK/OpenAPI pipeline no longer depends on its Hono `describeRoute` entry. -- `HttpApi` is the root API -- `HttpApiGroup` groups related endpoints -- `HttpApiEndpoint` defines a single route and its request / response schemas -- handlers are implemented separately from the contract +## Schema Notes -This is a better fit once route inputs and outputs are already moving toward Effect Schema-first models. +- Use `Schema.Struct(...).annotate({ identifier })` for named OpenAPI refs when handlers return plain objects. +- Use `Schema.Class` only when the handler returns real class instances or the constructor requirement is intentional. +- Keep nested anonymous shapes as `Schema.Struct` unless a named SDK type is useful. +- Avoid parallel hand-written Zod and Effect definitions for the same route boundary. -## Why it is relevant here +## Phases -The current route-effectification work is already pushing handlers toward: +### 1. Stabilize The Bridge -- one `AppRuntime.runPromise(Effect.gen(...))` body -- yielding services from context -- using typed Effect errors instead of Promise wrappers +Before porting more routes, cover the bridge behavior that every route depends on. -That work is a good prerequisite for `HttpApi`. Once the handler body is already a composed Effect, the remaining migration is mostly about replacing the Hono route declaration and validator layer. +- Add tests that hit the Hono-mounted `HttpApi` bridge, not just `HttpApiBuilder.layer` directly. +- Cover auth disabled, Basic auth success, `auth_token` success, missing credentials, and bad credentials. +- Cover `directory` and `x-opencode-directory` instance selection. +- Verify generated SDK output remains unchanged for non-SDK work. +- Fix or remove any implemented-but-unmounted `HttpApi` groups. -## What HttpApi gives us +### 2. Complete The Inventory -### Contracts +Create a route inventory from the actual Hono registrations and classify each route. -Request params, query, payload, success payloads, and typed error payloads are declared in one place using Effect Schema. +Statuses: -### Validation and decoding +- `bridged`: served through the `HttpApi` bridge when the flag is on. +- `implemented`: `HttpApi` group exists but is not mounted through Hono. +- `next`: good JSON candidate for near-term porting. +- `later`: portable, but needs schema/service cleanup first. +- `special`: SSE, websocket, streaming, or UI bridge behavior that likely needs raw Effect HTTP rather than `HttpApi`. -Incoming data is decoded through Effect Schema instead of hand-maintained Zod validators per route. +### 3. Finish JSON Route Parity -### OpenAPI +Port remaining JSON routes in small batches. -`HttpApi` can derive OpenAPI from the API definition, which overlaps with the current `describeRoute(...)` and `resolver(...)` pattern. +Good near-term candidates: -### Typed errors +- top-level reads: `GET /path`, `GET /vcs`, `GET /vcs/diff`, `GET /command`, `GET /agent`, `GET /skill`, `GET /lsp`, `GET /formatter` +- simple mutations: `POST /instance/dispose` +- experimental JSON reads: console, tool, worktree list, resource list +- deferred JSON mutations: `PATCH /config`, project git init, workspace/worktree create/remove/reset, file search, MCP auth flows -`Schema.TaggedErrorClass` maps naturally to endpoint error contracts. +Keep large or stateful groups for later: -## Likely fit for opencode +- `session` +- `sync` +- process-level experimental routes -Best fit first: +### 4. Move OpenAPI And SDK Generation -- JSON request / response endpoints -- route groups that already mostly delegate into services -- endpoints whose request and response models can be defined with Effect Schema +Hono routes cannot be deleted while `hono-openapi` is the source of SDK generation. -Harder / later fit: +Required before route deletion: -- SSE endpoints -- websocket endpoints -- streaming handlers -- routes with heavy Hono-specific middleware assumptions +- Generate the public OpenAPI surface from Effect `HttpApi` for ported routes. +- Keep operation IDs, schemas, status codes, and SDK type names stable unless the change is intentional. +- Compare generated SDK output against `dev` for every route group deletion. +- Remove Hono OpenAPI stubs only after Effect OpenAPI is the SDK source for those paths. -## Current blockers and gaps +### 5. Make HttpApi Default For JSON Routes -### Schema split +After JSON parity and SDK generation are covered: -Many route boundaries still use Zod-first validators. That does not block all experimentation, but full `HttpApi` adoption is easier after the domain and boundary types are more consistently Schema-first with `.zod` compatibility only where needed. +- Flip the bridge default for ported JSON routes. +- Keep a short-lived fallback flag for the old Hono implementation. +- Run the same tests against both the default and fallback path during rollout. +- Stop adding new Hono handlers for JSON routes once the default flips. -### Mixed handler styles +### 6. Delete Hono Route Implementations -Many current `server/routes/instance/*.ts` handlers still mix composed Effect code with smaller Promise- or ALS-backed seams. Migrating those to consistent `Effect.gen(...)` handlers is the low-risk step to do first. +Delete Hono routes group-by-group after each group meets the deletion criteria. -### Non-JSON routes +Deletion criteria: -The server currently includes SSE, websocket, and streaming-style endpoints. Those should not be the first `HttpApi` targets. +- `HttpApi` route is mounted by default. +- Behavior is covered by bridge-level tests. +- OpenAPI/SDK generation comes from Effect for that path. +- SDK diff is zero or explicitly accepted. +- Legacy Hono route is no longer needed as a fallback. -### Existing Hono integration +After deleting a group: -The current server composition, middleware, and docs flow are Hono-centered today. That suggests a parallel or incremental adoption plan is safer than a flag day rewrite. +- Remove its Hono route file or dead endpoints. +- Remove its `.route(...)` registration from `instance/index.ts`. +- Remove duplicate Zod-only route DTOs if Effect Schema now owns the type. +- Regenerate SDK and verify output. -## Recommended strategy +### 7. Replace Special Routes -### 1. Finish the prerequisites first +Special routes need explicit designs before Hono can disappear completely. -- continue route-handler effectification in `server/routes/instance/*.ts` -- continue schema migration toward Effect Schema-first DTOs and errors -- keep removing service facades +- `event`: SSE +- `pty`: websocket +- `tui`: UI/control bridge behavior +- streaming `session` endpoints -### 2. Start with one parallel group +Use raw Effect HTTP routes where `HttpApi` does not fit. The goal is deleting Hono implementations, not forcing every transport shape through `HttpApi`. -Introduce one small `HttpApi` group for plain JSON endpoints only. Good initial candidates are the least stateful endpoints in: +## Current Route Status -- `server/routes/instance/question.ts` -- `server/routes/instance/provider.ts` -- `server/routes/instance/permission.ts` +| Area | Status | Notes | +| --- | --- | --- | +| `question` | `bridged` | `GET /question`, reply, reject | +| `permission` | `bridged` | list and reply | +| `provider` | `bridged` | list, auth, OAuth authorize/callback | +| `config` | `bridged` partial | reads only; mutation remains Hono | +| `project` | `bridged` partial | reads only; git-init remains Hono | +| `file` | `bridged` partial | list/content/status only | +| `mcp` | `bridged` partial | status only | +| `workspace` | `implemented` | `HttpApi` group exists, but bridge mounting needs verification | +| top-level instance reads | `next` | path, vcs, command, agent, skill, lsp, formatter | +| experimental JSON routes | `next/later` | console, tool, worktree, resource, global session list | +| `session` | `later/special` | large stateful surface plus streaming | +| `sync` | `later` | process/control side effects | +| `event` | `special` | SSE | +| `pty` | `special` | websocket | +| `tui` | `special` | UI bridge | -Avoid `session.ts`, SSE, websocket, and TUI-facing routes first. +## Next PRs -Recommended first slice: - -- start with `question` -- start with `GET /question` -- start with `POST /question/:requestID/reply` - -Why `question` first: - -- already JSON-only -- already delegates into an Effect service -- proves list + mutation + params + payload + OpenAPI in one small slice -- avoids the harder streaming and middleware cases - -### 3. Reuse existing services - -Do not re-architect business logic during the HTTP migration. `HttpApi` handlers should call the same Effect services already used by the Hono handlers. - -### 4. Bridge into Hono behind a feature flag - -The `HttpApi` routes are bridged into the Hono server via `HttpRouter.toWebHandler` with a shared `memoMap`. This means: - -- one process, one port — no separate server -- the Effect handler shares layer instances with `AppRuntime` (same `Question.Service`, etc.) -- Effect middleware handles auth and instance lookup independently from Hono middleware -- Hono's `.all()` catch-all intercepts matching paths before the Hono route handlers - -The bridge is gated behind `OPENCODE_EXPERIMENTAL_HTTPAPI` (or `OPENCODE_EXPERIMENTAL`). When the flag is off (default), all requests go through the original Hono handlers unchanged. - -```ts -// in instance/index.ts -if (Flag.OPENCODE_EXPERIMENTAL_HTTPAPI) { - const handler = ExperimentalHttpApiServer.webHandler().handler - app.all("/question", (c) => handler(c.req.raw)).all("/question/*", (c) => handler(c.req.raw)) -} -``` - -The Hono route handlers are always registered (after the bridge) so `hono-openapi` generates the OpenAPI spec entries that feed SDK codegen. When the flag is on, these handlers are dead code — the `.all()` bridge matches first. - -### 5. Observability - -The `webHandler` provides `Observability.layer` via `Layer.provideMerge`. Since the `memoMap` is shared with `AppRuntime`, the tracing provider is deduplicated — no extra initialization cost. - -This gives: - -- **spans**: `Effect.fn("QuestionHttpApi.list")` etc. appear in traces alongside service-layer spans -- **HTTP logs**: `HttpMiddleware.logger` emits structured `Effect.log` entries with `http.method`, `http.url`, `http.status` annotations, flowing to motel via `OtlpLogger` - -### 6. Migrate JSON route groups gradually - -As each route group is ported to `HttpApi`: - -1. add `.get(...)` / `.post(...)` bridge entries to the flag block in `server/routes/instance/index.ts` -2. for partial ports (e.g. only `GET /provider/auth`), bridge only the specific path -3. keep the legacy Hono route registered behind it for OpenAPI / SDK generation until the spec pipeline changes -4. verify SDK output is unchanged - -Leave streaming-style endpoints on Hono until there is a clear reason to move them. - -## Schema rule for HttpApi work - -Every `HttpApi` slice should follow `specs/effect/schema.md` and the Schema -> Zod interop rule in `specs/effect/migration.md`. - -Default rule: - -- Effect Schema owns the type -- `.zod` exists only as a compatibility surface -- do not introduce a new hand-written Zod schema for a type that is already migrating to Effect Schema - -Practical implication for `HttpApi` migration: - -- if a route boundary already depends on a shared DTO, ID, input, output, or tagged error, migrate that model to Effect Schema first or in the same change -- if an existing Hono route or tool still needs Zod, derive it with `@/util/effect-zod` -- avoid maintaining parallel Zod and Effect definitions for the same request or response type - -Ordering for a route-group migration: - -1. move implicated shared `schema.ts` leaf types to Effect Schema first -2. move exported `Info` / `Input` / `Output` route DTOs to Effect Schema -3. move tagged route-facing errors to `Schema.TaggedErrorClass` where needed -4. switch existing Zod boundary validators to derived `.zod` -5. define the `HttpApi` contract from the canonical Effect schemas -6. regenerate the SDK (`./packages/sdk/js/script/build.ts`) and verify zero diff against `dev` - -SDK shape rule: - -- every schema migration must preserve the generated SDK output byte-for-byte **unless the new ref is intentional** (see Schema.Class vs Schema.Struct below) -- if an unintended diff appears in `packages/sdk/js/src/v2/gen/types.gen.ts`, the migration introduced an unintended API surface change — fix it before merging - -### Schema.Class vs Schema.Struct - -The pattern choice determines whether a schema becomes a **named** export in the SDK or stays **anonymous inline**. - -**Schema.Class** emits a named `$ref` in OpenAPI via its identifier → produces a named `export type Foo = ...` in `types.gen.ts`: - -```ts -export class Info extends Schema.Class("FooConfig")({ ... }) { - static readonly zod = zod(this) -} -``` - -**Schema.Struct** stays anonymous and is inlined everywhere it is referenced: - -```ts -export const Info = Schema.Struct({ ... }).pipe( - withStatics((s) => ({ zod: zod(s) })), -) -export type Info = Schema.Schema.Type -``` - -When to use each: - -- Use **Schema.Class** when: - - the original Zod had `.meta({ ref: ... })` (preserve the existing named SDK type byte-for-byte) - - the schema is a top-level endpoint request or response (SDK consumers benefit from a stable importable name) -- Use **Schema.Struct** when: - - the type is only used as a nested field inside another named schema - - the original Zod was anonymous and promoting it would bloat SDK types with no import value - -Promoting a previously-anonymous schema to Schema.Class is acceptable when it is top-level or endpoint-facing, but call it out in the PR — it is an additive SDK change (`export type Foo = ...` newly appears) even if it preserves the JSON shape. - -Schemas that are **not** pure objects (enums, unions, records, tuples) cannot use Schema.Class. For those — and for pure-object schemas where handlers populate plain objects rather than class instances — add `.annotate({ identifier: "FooName" })` to get the same named-ref behavior without the `instanceof` requirement: - -```ts -export const Action = Schema.Literals(["ask", "allow", "deny"]).annotate({ identifier: "PermissionActionConfig" }) -``` - -Temporary exception: - -- it is acceptable to keep a route-local Zod schema for the first spike only when the type is boundary-local and migrating it would create unrelated churn -- if that happens, leave a short note so the type does not become a permanent second source of truth - -## First vertical slice - -The first `HttpApi` spike should be intentionally small and repeatable. - -Chosen slice: - -- group: `question` -- endpoints: `GET /question` and `POST /question/:requestID/reply` - -Non-goals: - -- no `session` routes -- no SSE or websocket routes -- no auth redesign -- no broad service refactor - -Behavior rule: - -- preserve current runtime behavior first -- treat semantic changes such as introducing new `404` behavior as a separate follow-up unless they are required to make the contract honest - -Add `POST /question/:requestID/reject` only after the first two endpoints work cleanly. - -## Repeatable slice template - -Use the same sequence for each route group. - -1. Pick one JSON-only route group that already mostly delegates into services. -2. Identify the shared DTOs, IDs, and errors implicated by that slice. -3. Apply the schema migration ordering above so those types are Effect Schema-first. -4. Define the `HttpApi` contract separately from the handlers. -5. Implement handlers by yielding the existing service from context. -6. Mount the new surface in parallel behind the `OPENCODE_EXPERIMENTAL_HTTPAPI` bridge. -7. Regenerate the SDK and verify zero diff against `dev` (see SDK shape rule above). -8. Add one end-to-end test and one OpenAPI-focused test. -9. Compare ergonomics before migrating the next endpoint. - -Rule of thumb: - -- migrate one route group at a time -- migrate one or two endpoints first, not the whole file -- keep business logic in the existing service -- keep the first spike easy to delete if the experiment is not worth continuing - -## Example structure - -Placement rule: - -- keep `HttpApi` code under `src/server`, not `src/effect` -- `src/effect` should stay focused on runtimes, layers, instance state, and shared Effect plumbing -- place each `HttpApi` slice next to the HTTP boundary it serves -- for instance-scoped routes, prefer `src/server/routes/instance/httpapi/*` -- if control-plane routes ever migrate, prefer `src/server/routes/control/httpapi/*` - -Suggested file layout for a repeatable spike: - -- `src/server/routes/instance/httpapi/question.ts` — contract and handler layer for one route group -- `src/server/routes/instance/httpapi/server.ts` — bridged Effect HTTP layer that composes all groups -- route or OpenAPI verification should live alongside the existing server tests; there is no dedicated `question-httpapi` test file on this branch - -Suggested responsibilities: - -- `question.ts` defines the `HttpApi` contract and `HttpApiBuilder.group(...)` handlers -- `server.ts` composes all route groups into one `HttpRouter.toWebHandler(...)` bridge with shared middleware (auth, instance lookup) -- tests should verify the bridged routes through the normal server surface - -## Example migration shape - -Each route-group spike should follow the same shape. - -### 1. Contract - -- define an experimental `HttpApi` -- define one `HttpApiGroup` -- define endpoint params, payload, success, and error schemas from canonical Effect schemas -- annotate summary, description, and operation ids explicitly so generated docs are stable - -### 2. Handler layer - -- implement with `HttpApiBuilder.group(api, groupName, ...)` -- yield the existing Effect service from context -- keep handler bodies thin -- keep transport mapping at the HTTP boundary only - -### 3. Bridged server - -- the Effect HTTP layer is composed in `httpapi/server.ts` -- it is mounted into the Hono app via `HttpRouter.toWebHandler(...)` -- routes keep their normal instance paths and are gated by the `OPENCODE_EXPERIMENTAL_HTTPAPI` flag -- the legacy Hono handlers stay registered after the bridge so current OpenAPI / SDK generation still works - -### 4. Verification - -- seed real state through the existing service -- call the bridged endpoints with the flag enabled -- assert that the service behavior is unchanged -- assert that the generated OpenAPI contains the migrated paths and schemas - -## Boundary composition - -The Effect `HttpApi` layer owns its own auth and instance middleware, but it is currently mounted inside the existing Hono server. - -### Auth - -- the bridged `HttpApi` layer implements auth as an `HttpApiMiddleware.Service` using `HttpApiSecurity.basic` -- each route group's `HttpApi` is wrapped with `.middleware(Authorization)` before being served -- this is independent of the Hono auth layer; the current bridge keeps the responsibility local to the `HttpApi` slice - -### Instance and workspace lookup - -- the bridged `HttpApi` layer resolves instance context via an `HttpRouter.middleware` that reads `x-opencode-directory` headers and `directory` query params -- this is the Effect equivalent of the Hono `WorkspaceRouterMiddleware` -- `HttpApi` handlers yield services from context and assume the correct instance has already been provided - -### Error mapping - -- keep domain and service errors typed in the service layer -- declare typed transport errors on the endpoint only when the route can actually return them intentionally -- request decoding failures are transport-level `400`s handled by Effect `HttpApi` automatically -- storage or lookup failures that are part of the route contract should be declared as typed endpoint errors - -## Exit criteria for the spike - -The first slice is successful if: - -- the bridged endpoints serve correctly through the existing Hono host when the flag is enabled -- the handlers reuse the existing Effect service -- request decoding and response shapes are schema-defined from canonical Effect schemas -- any remaining Zod boundary usage is derived from `.zod` or clearly temporary -- OpenAPI is generated from the `HttpApi` contract -- the tests are straightforward enough that the next slice feels mechanical - -## Learnings - -### Schema - -- `Schema.Class` works well for route DTOs such as `Question.Request`, `Question.Info`, and `Question.Reply`. -- scalar or collection schemas such as `Question.Answer` should stay as schemas and use helpers like `withStatics(...)` instead of being forced into classes. -- if an `HttpApi` success schema uses `Schema.Class`, the handler or underlying service needs to return real schema instances rather than plain objects. `Schema.Class`'s Declaration AST enforces `input instanceof self || input.[ClassTypeId]` during encode (see effect-smol `Schema.ts:10479-10484`). Plain objects from zod parse fail with `Expected Foo, got {...}`. This surfaced on `GET /config` where the service returns zod-parsed plain objects and `Config.InfoSchema` referenced `ConfigProvider.Info` (class). The fix was to convert pure-object classes to `Schema.Struct(...).annotate({ identifier: "..." })` — same named SDK `$ref`, no instance requirement. Verified byte-identical `types.gen.ts` vs `dev`. -- internal event payloads can stay anonymous when we want to avoid adding extra named OpenAPI component churn for non-route shapes. -- `Schema.Class` emits named `$ref` in OpenAPI — only use it for types that already had `.meta({ ref })` in the old Zod schema **and** when the handler/service returns real instances. For schemas that need a named `$ref` but are populated from plain objects, use `Schema.Struct(...).annotate({ identifier: "..." })` instead. Inner/nested types should stay as `Schema.Struct` to avoid SDK shape changes. - -### Integration - -- `HttpRouter.toWebHandler` with the shared `memoMap` from `run-service.ts` cleanly bridges Effect routes into Hono — one process, one port, shared layer instances. -- `Observability.layer` must be explicitly provided via `Layer.provideMerge` in the routes layer for OTEL spans and HTTP logs to flow. The `memoMap` deduplicates it with `AppRuntime` — no extra cost. -- `HttpMiddleware.logger` (enabled by default when `disableLogger` is not set) emits structured `Effect.log` entries with `http.method`, `http.url`, `http.status` — these flow through `OtlpLogger` to motel. -- Hono OpenAPI stubs must remain registered for SDK codegen until the SDK pipeline reads from the Effect OpenAPI spec instead. -- the `OPENCODE_EXPERIMENTAL_HTTPAPI` flag gates the bridge at the Hono router level — default off, no behavior change unless opted in. - -## Route inventory - -Status legend: - -- `bridged` - Effect HttpApi slice exists and is bridged into Hono behind the flag -- `done` - Effect HttpApi slice exists but not yet bridged -- `next` - good near-term candidate -- `later` - possible, but not first wave -- `defer` - not a good early `HttpApi` target - -Current instance route inventory: - -- `question` - `bridged` - endpoints: `GET /question`, `POST /question/:requestID/reply`, `POST /question/:requestID/reject` -- `permission` - `bridged` - endpoints: `GET /permission`, `POST /permission/:requestID/reply` -- `provider` - `bridged` - endpoints: `GET /provider`, `GET /provider/auth`, `POST /provider/:providerID/oauth/authorize`, `POST /provider/:providerID/oauth/callback` -- `config` - `bridged` (partial) - bridged endpoints: `GET /config`, `GET /config/providers` - defer `PATCH /config` for now -- `project` - `bridged` (partial) - bridged endpoints: `GET /project`, `GET /project/current` - defer git-init mutation first -- `workspace` - `bridged` - best small reads: `GET /experimental/workspace/adaptor`, `GET /experimental/workspace`, `GET /experimental/workspace/status` - defer create/remove mutations first -- `file` - `bridged` (partial) - bridged endpoints: `GET /file`, `GET /file/content`, `GET /file/status` - defer search endpoints first -- `mcp` - `bridged` (partial) - bridged endpoints: `GET /mcp` - defer interactive OAuth/auth flows first -- `session` - `defer` - large, stateful, mixes CRUD with prompt/shell/command/share/revert flows and a streaming route -- `event` - `defer` - SSE only -- `global` - `defer` - mixed bag with SSE and process-level side effects -- `pty` - `defer` - websocket-heavy route surface -- `tui` - `defer` - queue-style UI bridge, weak early `HttpApi` fit - -Recommended near-term sequence: - -1. `workspace` read endpoints (`GET /experimental/workspace/adaptor`, `GET /experimental/workspace`, `GET /experimental/workspace/status`) -2. `file` JSON read endpoints -3. `mcp` JSON read endpoints +1. Add bridge-level auth and instance-context tests for the current `HttpApi` bridge. +2. Produce a generated route inventory from Hono registrations and update `Current Route Status` with exact paths. +3. Fix the `workspace` status: mount it if it should be reachable, or remove it from the composed `HttpApi` layer. +4. Port the top-level JSON reads. +5. Start the Effect OpenAPI/SDK generation path for already-bridged routes. ## Checklist -- [x] add one small spike that defines an `HttpApi` group for a simple JSON route set -- [x] use Effect Schema request / response types for that slice -- [x] keep the underlying service calls identical to the current handlers -- [x] compare generated OpenAPI against the current Hono/OpenAPI setup -- [x] document how auth, instance lookup, and error mapping would compose in the new stack -- [x] bridge Effect routes into Hono via `toWebHandler` with shared `memoMap` -- [x] gate behind `OPENCODE_EXPERIMENTAL_HTTPAPI` flag -- [x] verify OTEL spans and HTTP logs flow to motel -- [x] bridge question, permission, and provider auth routes -- [x] port remaining provider endpoints (`GET /provider`, OAuth mutations) -- [x] port `config` providers read endpoint -- [x] port `project` read endpoints (`GET /project`, `GET /project/current`) -- [x] port `GET /config` full read endpoint -- [x] port `workspace` read endpoints -- [x] port `file` JSON read endpoints -- [x] port `mcp` status read endpoint -- [ ] decide when to remove the flag and make Effect routes the default - -## Rule of thumb - -Do not start with the hardest route file. - -If `HttpApi` is adopted here, it should arrive after the handler body is already Effect-native and after the relevant request / response models have moved to Effect Schema. +- [x] Add first `HttpApi` JSON route slices. +- [x] Bridge selected `HttpApi` routes into Hono behind `OPENCODE_EXPERIMENTAL_HTTPAPI`. +- [x] Reuse existing Effect services in handlers. +- [x] Provide auth, instance lookup, and observability in the Effect route layer. +- [x] Attach auth middleware in route modules. +- [x] Support `auth_token` as a query security scheme. +- [ ] Add bridge-level auth and instance tests. +- [ ] Complete exact Hono route inventory. +- [ ] Resolve implemented-but-unmounted route groups. +- [ ] Port remaining JSON routes. +- [ ] Generate SDK/OpenAPI from Effect routes. +- [ ] Flip ported JSON routes to default-on with fallback. +- [ ] Delete replaced Hono route implementations. +- [ ] Replace SSE/websocket/streaming Hono routes with non-Hono implementations.