Executors — Decisions
Mirrored from
docs/design/executors/4_decisions.md. Edit the source document in the repository, not this generated page.
This is the append-only ADR log for the executors feature. Entries are ordered
by ascending global ADR number. Each entry is the long-form narrative keyed on a
global ID allocated through orbit.adr.add; the ADR store is the source of truth
for status, owner, related_features, and related_tasks. Resolve any global ID
with orbit tool run orbit.adr.show --input '{"id":"ADR-0196"}'.
ADR-0196 — External Executor Protocol for dynamic out-of-process executor registration
Section titled “ADR-0196 — External Executor Protocol for dynamic out-of-process executor registration”Status: Accepted · 2026-06 · [ORB-00384] (Tier 1: defined the protocol, added the external executor type, shipped a conformance test)
Context. Orbit’s ExecutorType is a sealed enum and load_from_defs is a closed match, so a homegrown executor can only be added by forking orbit-engine — an internal-tier crate with no downstream guarantees. Yet DirectAgentExecutor already implements an out-of-process transport (spawn command, write a request envelope to stdin, map the process exit code / stderr to an outcome): the capability exists but is undocumented and coupled to the agent-family direct_agent path.
Decision. Promote that transport into a documented, versioned External Executor Protocol v1 and expose it through a new ExecutorType::External (wire value external). A homegrown executor is registered by dropping a YAML executor def that points at a binary/script speaking the protocol — no recompile, no linking, language-agnostic. In-process Rust extension (an ExecutorFactory registry plus a runtime injection seam) is explicitly deferred to a separate Tier 2 decision.
Consequences.
- Most homegrown executors become config-only: a YAML def plus a conforming binary, with zero changes to Orbit.
- The stdin request envelope (
schemaVersion: 1) and the exit-code result semantics become a stability commitment — once v1 ships, the request/result shape is a contract that must be versioned, not changed in place. stdout is captured as audit data but is not parsed into workflow state in v1 (a structured stdout result envelope is a reserved, additive extension point). - Sandbox finding (during execution). The task premise that
direct_agentroutesFsProfile→sandbox was inaccurate: the registry-path transportDirectAgentExecutoruses (and whichexternalnow shares) runsNoSandbox, and the registryExecutionContextcarries noFsProfile. RealFsProfile→OS-sandbox enforcement lives only in the separate V2activity_jobpath. Tier 1 therefore ships exact parity:externalanddirect_agentproduce a byte-identicalExecRequestand both run unsandboxed; the def’ssandbox/allow_fallbackfields are inert forexternal. This does not widen the sandbox-bypass surface relative todirect_agent, but it adds no OS sandboxing either — registering an external executor is arbitrary code execution with the runner’s privileges. RealFsProfile→OS sandbox forexternalis deferred to Tier 2 (needs the V2 context). See [ORB-00384] comments. - Executors needing a non-subprocess transport (in-process SDK, gRPC, internal queue) are NOT served by Tier 1 and must wait for Tier 2.
- Cost: a documented wire protocol is a long-lived backward-compatibility obligation — every future executor capability must be expressible as an additive, versioned envelope field, and a conformance harness must be maintained so adopters do not silently depend on undocumented behavior.
Task References
Section titled “Task References”- [ORB-00384] — External Executor Protocol v1: define the contract, add
ExecutorType::External, register a generic external-process executor, document the spec, ship a conformance test.
Resolve any task above with
orbit task show <ID>orgit log --grep=<ID>.