feat(server): pluggable public API component under /api/v1 - #356
Open
Vito-168 wants to merge 1 commit into
Open
Conversation
Adds a versioned, engine-agnostic HTTP surface so third parties can drive iPolloWork without the desktop UI, organized as independently toggleable modules rather than another routes file. The design finishes two things the project had already started: - `server-route` is a declared contribution type (packages/types/src/plugins.ts) with no runtime consumer. The module registry is that concept made real: an `ApiModule` declares its operations once, and `registerApiModules` turns that single declaration into both the live route table and the OpenAPI document, so the two cannot drift. - `ConversationEngineAdapter` already unifies OpenCode and DeepSeek Harness in the browser (apps/app/.../engine/conversation-engine.ts) but had no server-side counterpart, which is why the server owned reads for both engines and writes for neither. `api/engine/` mirrors that contract, reusing its event vocabulary. Modules (all enabled by default; IPOLLOWORK_API_MODULES/_DISABLED select): sessions 11 ops create/prompt/interrupt, resumable SSE, permissions, questions tasks 5 ops submit a goal, follow it to a terminal state (in-memory) webhooks 5 ops HMAC-signed delivery, bounded retries, SSRF-checked targets policy 3 ops per-token workspace binding, approval policy, expiry openapi 3 ops OpenAPI 3.1 document, dependency-free docs page, catalogue compat 135 ops /api/v1 aliases of the legacy routes, which stay unchanged Where the engines genuinely differ, the API reports it instead of hiding it: capabilities are returned with every session, and an option an engine cannot apply is rejected with 501 rather than accepted and silently dropped. Changes to existing files are small and additive, except serve-node.ts, which had to be fixed for streaming to work at all: `request.signal` was never wired to the socket and the response body was released rather than cancelled, so an SSE producer kept running — timers, engine subscription and all — for the life of the process after a client hung up. server.ts mount registerApiV1 at the end of createRoutes tokens.ts findByHash, so policy can resolve a token without its secret serve-node.ts propagate client disconnect; cancel the response stream Verified: tsc --noEmit clean; 618 component tests; the existing suite is unaffected. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Companion to #353. Adds a versioned
/api/v1surface so callers can drive iPolloWorkwithout the desktop UI.
The gap this fills
routes/sessions.tsunifies session reads across OpenCode and DeepSeek Harness, but thereis no engine-agnostic
createorprompt. The one unified write says so itself —DELETE /workspace/:id/sessions/:sessionIdhard-codes501 session_delete_unsupportedforHarness (
sessions.ts:234). Writes today mean either the/opencode/*proxy (OpenCode'sprivate API) or the Harness JSON-RPC allowlist: two dialects, no stable contract.
Suggested reading order
Don't read this top-to-bottom. In order of what actually matters:
apps/server/src/api/engine/types.ts(~200 lines) — the whole proposal in one file.The engine abstraction: normalized event union, connection interface, capability flags.
If this interface is wrong, everything else is wrong.
apps/server/src/api/modules/types.ts+registry.ts— how a module declaresoperations once and the registry derives both the route table and the OpenAPI document
from that single declaration.
apps/server/src/api/modules/sessions/module.ts— the first real module; the patternevery other one follows.
apps/server/src/api/README.md— the module contract, written for whoever adds thenext one.
Everything else is application of those four.
engine/opencode.tsandengine/harness.tsare the two adapters;
modules/{tasks,webhooks,policy,openapi,compat}/are modules built onthe same contract.
What I'd push back on if I were reviewing
opencode-conversation-engine.ts+deepseek-harness-conversation-engine.tsand their mappers are ~1,500 lines, and this addsa server-side mirror — two copies of the same wire-format knowledge, free to drift. The
Harness internal-
<system>-block stripper is literally copied out ofdeepseek-harness-conversation-mapper.ts. Extracting the shared mapping intopackages/is a better end state than this PR, and I'd rather do that than land twocopies. It touches browser code, which is why RFC: the server unifies session reads across both engines but not writes — add an engine-agnostic write surface? #353 asks first.
apps/servercurrently contains oneOpenCode event-name string (in
toy-ui.ts); this adds about thirty, concentrated inengine/opencode.ts. That's a real move against the boundaryREADME.md:171describes,even though it's confined to one file.
compat/is the largest behaviour surface here — it aliases existing legacy routesunder
/api/v1. It deserves its own review pass and is the piece I'd most expect you towant changed or dropped.
Overlap with #355
This branch contains the
serve-node.tsfix from #355 — byte-identical, since I cut that PRfrom these files. If #355 lands first this merges cleanly with no conflict; I'll rebase and
drop the duplicate commit if you'd rather.
The streaming work here is what surfaced that bug: without a working
request.signal, everySSE endpoint added below would leak its producer on client disconnect.
How it's split if you want it in pieces
Note that "OpenAPI generation first" doesn't work — the mount builds the engine registry
unconditionally (
api/index.ts:235). The order I'd suggest:compat/last — most routes, least novel, deserves its own reviewSay the word and I'll cut them as separate PRs in that order.
State
tsc --noEmitclean across the server, not just the new codeexamples/public-api/has five runnable examples, including a CI review job incurl+jqKnown limits, stated plainly
anything durable — the module is structured so a persistent store can replace it, but I
didn't presume to pick one.
the API, unsupported operations return
501 engine_capability_unsupportedandcapabilitiesadvertises what a given engine can do. Harness has no resumable event cursor,so
seqis undefined there and reconnects can miss events.server-routeis still dormant. External plugins still cannot register routes. I addedsix descriptive
server-routeentries in an example manifest, but nothing mounts fromthem — the signed-publisher gate on executable capabilities looks deliberate and I didn't
touch it.