Skip to content

feat(server): pluggable public API component under /api/v1 - #356

Open
Vito-168 wants to merge 1 commit into
Devin-AXIS:mainfrom
Vito-168:feat/public-api-component
Open

feat(server): pluggable public API component under /api/v1#356
Vito-168 wants to merge 1 commit into
Devin-AXIS:mainfrom
Vito-168:feat/public-api-component

Conversation

@Vito-168

Copy link
Copy Markdown

Opened alongside #353 so the discussion has real code to point at. It's 61 files, and
#353 asks whether this is even the shape you want — so please treat the design question
there as the one that decides this PR's fate, not the diff here. If the answer is "share
the mapping in packages/" or "don't build this," I'll rework or close it, no hard
feelings. I'd rather land the right thing in four small PRs than this one as-is; see
"How it's split" below.

Companion to #353. Adds a versioned /api/v1 surface so callers can drive iPolloWork
without the desktop UI.

The gap this fills

routes/sessions.ts unifies session reads across OpenCode and DeepSeek Harness, but there
is no engine-agnostic create or prompt. The one unified write says so itself —
DELETE /workspace/:id/sessions/:sessionId hard-codes 501 session_delete_unsupported for
Harness (sessions.ts:234). Writes today mean either the /opencode/* proxy (OpenCode's
private 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:

  1. 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.
  2. apps/server/src/api/modules/types.ts + registry.ts — how a module declares
    operations once and the registry derives both the route table and the OpenAPI document
    from that single declaration.
  3. apps/server/src/api/modules/sessions/module.ts — the first real module; the pattern
    every other one follows.
  4. apps/server/src/api/README.md — the module contract, written for whoever adds the
    next one.

Everything else is application of those four. engine/opencode.ts and engine/harness.ts
are the two adapters; modules/{tasks,webhooks,policy,openapi,compat}/ are modules built on
the same contract.

What I'd push back on if I were reviewing

  • It duplicates the browser's engine layer. opencode-conversation-engine.ts +
    deepseek-harness-conversation-engine.ts and their mappers are ~1,500 lines, and this adds
    a server-side mirror — two copies of the same wire-format knowledge, free to drift. The
    Harness internal-<system>-block stripper is literally copied out of
    deepseek-harness-conversation-mapper.ts. Extracting the shared mapping into
    packages/ is a better end state than this PR
    , and I'd rather do that than land two
    copies. 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.
  • It increases OpenCode coupling in the server. apps/server currently contains one
    OpenCode event-name string (in toy-ui.ts); this adds about thirty, concentrated in
    engine/opencode.ts. That's a real move against the boundary README.md:171 describes,
    even though it's confined to one file.
  • compat/ is the largest behaviour surface here — it aliases existing legacy routes
    under /api/v1. It deserves its own review pass and is the piece I'd most expect you to
    want changed or dropped.

Overlap with #355

This branch contains the serve-node.ts fix from #355 — byte-identical, since I cut that PR
from 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, every
SSE 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:

  1. engine adapter + sessions module (the part worth arguing about)
  2. OpenAPI generation + docs
  3. tasks / webhooks / policy
  4. compat/ last — most routes, least novel, deserves its own review

Say the word and I'll cut them as separate PRs in that order.

State

  • tsc --noEmit clean across the server, not just the new code
  • 618 tests for the new code; existing suite unaffected
  • TypeScript SDK has no runtime dependencies; Python SDK is standard library only
  • examples/public-api/ has five runnable examples, including a CI review job in curl + jq

Known limits, stated plainly

  • Task state is in-memory. A server restart drops running tasks. Fine for CI, wrong for
    anything durable — the module is structured so a persistent store can replace it, but I
    didn't presume to pick one.
  • The two engines don't have the same capabilities. Rather than lowest-common-denominator
    the API, unsupported operations return 501 engine_capability_unsupported and
    capabilities advertises what a given engine can do. Harness has no resumable event cursor,
    so seq is undefined there and reconnects can miss events.
  • server-route is still dormant. External plugins still cannot register routes. I added
    six descriptive server-route entries in an example manifest, but nothing mounts from
    them — the signed-publisher gate on executable capabilities looks deliberate and I didn't
    touch it.

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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant