Skip to content

feat(automations): native TypeScript engine — remove the OpenCode binary from v1 - #3477

Closed
reachjalil wants to merge 1 commit into
agent/automations-den-hostedfrom
agent/automations-native-engine
Closed

feat(automations): native TypeScript engine — remove the OpenCode binary from v1#3477
reachjalil wants to merge 1 commit into
agent/automations-den-hostedfrom
agent/automations-native-engine

Conversation

@reachjalil

@reachjalil reachjalil commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

Removes the OpenCode binary from Automations v1 and ships execution as a native TypeScript engine inside den-api, behind the same provider-neutral AutomationEngineAdapter contract that #3466 introduces. Stacked on agent/automations-den-hosted; review this diff alone to see exactly what the engine swap changes.

Why remove the binary

The first engine spawned a disposable opencode serve compiled binary bundled into the Den image. It worked, but it put a third-party executable in the server image, added child-process/port lifecycle management, and tied security patch cadence to opencode releases — for an execution surface that no longer needs any of it. An Automation run has no filesystem, shell, browser, or workspace tools by design. What remains is exactly:

  1. a chat-completions tool loop against the resolved model provider, and
  2. two MCP tools (search_capabilities / execute_capability) served by Den's own run-scoped Connect endpoint.

Every model path Den can resolve speaks one wire protocol — the free zen model, the OpenWork inference proxy (/api/v1/chat/completions), and custom providers, which the provider API already restricts to @ai-sdk/openai-compatible / @ai-sdk/openai at creation time. The MCP client (@modelcontextprotocol/sdk) was already a den-api dependency.

What this PR does

  • New native-runtime.ts — in-process engine attempt: authority + Connect-token expiry checks, provider endpoint resolution, bounded (32-step) non-streaming chat loop, Connect tool calls via the MCP SDK client with the run bearer token, usage + costMicros accounting, observation emission.
  • Fail-closed on both edges: custom providers outside the OpenAI-compatible allowlist are rejected before any request; the run-scoped MCP endpoint's tools/list must be exactly the reviewed pair or admission fails.
  • Credential hygiene: keys become an Authorization header only — never in prompts, observations, receipts, or persisted engine state (asserted in tests).
  • execution-adapter.ts — engine-neutral renames only (openwork-den-native-v1); the durable receipt/event/reattachment/timeout/cancellation shell is untouched.
  • Removed: opencode-runtime.ts, @opencode-ai/sdk from den-api, and the entire binary-download block from Dockerfile.den (AUTOMATIONS_OPENCODE_BIN gone; state env renamed to AUTOMATIONS_STATE_DIR, persistent /data/openwork-automations volume unchanged — receipts still live there).

What deliberately does not change

Schema, routes, scheduler, run lifecycle, thread contract, desktop UI, feature flag. engineKind on new cloud threads records the new adapter id; the UI derives cloud identity from executionLocation only (pinned by tests in #3466). The adapter contract keeps engines replaceable — OpenCode or anything else can return later as an alternative implementation without touching the domain.

Honest v1 limits

  • OpenAI-compatible providers only (matches what orgs can configure today); enforced fail-closed, surfaced as provider_unavailable.
  • Non-streaming provider calls; event delivery stays poll-based (unchanged from the previous engine).
  • If den-api dies mid-run, reattachment starts a fresh attempt under the declared ordered_at_least_once contract — practically identical to before, since the opencode child process died with den-api anyway.

What the binary already supported, and where each capability went

For reviewers weighing the swap — what the OpenCode binary brought to the table, and what happened to each piece:

Binary capability Reachable through the product? Native engine Path to parity
Broad provider catalog — could dynamically load AI-SDK provider packages (Anthropic-native, Google, …) No. Den's provider API only lets orgs register @ai-sdk/openai-compatible / @ai-sdk/openai; free zen + inference proxy also speak that protocol. The breadth was latent, not usable Supports exactly the product-reachable set, enforced fail-closed Add a second protocol client keyed off npm inside native-runtime.ts — no contract change
Internal provider streaming — no per-turn HTTP ceiling on our side Partially. Our adapter still polled its messages endpoint (250ms) and emitted step-level events, so UI-visible granularity was the same Non-streaming turns, 120s per-request timeout, same step-level events stream: true + SSE parsing; the ordered-events contract already accommodates it
Session-transcript persistence in the state dir Not used for resumption — after a den-api restart the adapter started a fresh attempt from step one, same as this engine. Both adapters declare ordered_at_least_once Same semantics; transcript lives in memory per attempt Checkpoint the transcript into the durable execution record at step boundaries — easier natively, since we own the loop and the record
~30 workspace tools (file, shell, browser, …) No — anti-reachable. Automations must not have them; we ran deny-all + a fail-closed startup inventory check to keep them off Never constructs them; the fail-closed check became structural (tools/list must equal the reviewed Connect pair) Not wanted
Separate OS process — crash isolation, scrubbed env, empty 0700 cwd Yes, real isolation value In-process; compensated by the tool surface being two HTTPS calls, the same secret hygiene (asserted in tests), and no child-process/port/orphan lifecycle If ever needed, an out-of-process engine can return behind the same adapter contract

Net: the two capabilities with user-visible value that this engine gives up (provider breadth, streaming) were either unreachable through the product or invisible at the event surface — and both have contained, contract-compatible recovery paths. The durability semantics reviewers might worry about (at_least_once re-execution after a mid-run crash) are unchanged from the binary; neither engine resumed a half-finished attempt.

Checks run on this branch

  • ee/apps/den-api automations tsc (tsconfig.automations-test.json) ✅
  • automations-native-adapter.test.ts ✅ 9/9 — includes the real in-process loop against local mock provider + mock Streamable-HTTP MCP servers: happy path with usage/cost accounting, credential-rejection → provider_unavailable, fail-closed unreviewed-tool inventory (provider never called), mid-flight abort, receipt reattachment/replay, cancellation, and no-secret-leak assertions
  • automations-model-authority (4/4), automations-run-boundary (3/3) ✅
  • packages/automations conformance suite ✅ 9/9
  • den-api build ✅

Deferred to the hardening pass (unchanged from #3466): MySQL-live integration suite, restart-recovery soak, closed-desktop acceptance run.

🤖 Generated with Claude Code

…pt engine

Remove the engine binary from v1. The Den image no longer downloads
opencode, and @opencode-ai/sdk leaves den-api; execution now runs fully
in-process behind the same AutomationEngineAdapter contract.

The native runtime is small because the isolation contract already
stripped everything else: a bounded chat-completions tool loop against
the resolved provider, plus the two run-scoped OpenWork Connect tools
called through @modelcontextprotocol/sdk (already a dependency). Every
model path Den can resolve speaks that one wire protocol - the free zen
model, the OpenWork inference proxy, and custom providers, which the
provider API restricts to OpenAI-compatible packages at creation time;
anything else fails closed before a request is made. The Connect
endpoint's tool inventory is verified at admission and the run refuses
to start on any mismatch. Credentials become an Authorization header
only and never enter prompts, observations, or persisted engine state.

The durable adapter shell (receipts, ordered events, reattachment,
timeout, cancellation) is unchanged apart from engine-neutral naming and
the openwork-den-native-v1 adapter id; the conformance suite still
passes, and the new adapter tests exercise the real loop against local
mock provider and MCP servers, including credential-rejection mapping,
fail-closed inventory, and mid-flight abort.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
openwork-app Ready Ready Preview Aug 3, 2026 9:29pm
openwork-den Ready Ready Preview Aug 3, 2026 9:29pm
openwork-den-worker-proxy Ready Ready Preview Aug 3, 2026 9:29pm
openwork-diagnostics Ready Ready Preview Aug 3, 2026 9:29pm
openwork-landing Ready Ready Preview, v0 Aug 3, 2026 9:29pm

@reachjalil

Copy link
Copy Markdown
Collaborator Author

Closing: superseded by the desktop-runner design now on #3466. That PR no longer ships any Den-side execution engine (the desktop app executes Automations through its local OpenCode runtime), so the engine swap this PR proposed has no target anymore. #3466 stands alone and is ready for review.

@reachjalil reachjalil closed this Aug 4, 2026
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