diff --git a/doc/design/v2/runner/README.md b/doc/design/v2/runner/README.md
new file mode 100644
index 0000000..b643356
--- /dev/null
+++ b/doc/design/v2/runner/README.md
@@ -0,0 +1,43 @@
+# UDF Runner v2 Design
+
+This directory describes the runner-side architecture for protocol v2. It is a design document set; it does not
+define an implementation or commit to a concrete C++ class layout.
+
+The runner is organized into two source-level namespaces:
+
+1. The API namespace contains the caller-facing C++ contracts.
+2. The Internal namespace owns sockets, accepting, worker scheduling, protocol contexts, and implementation
+ dependencies.
+
+`Runner` is the composition root for those components. A runner user transfers a `WorkerFactory` to a production
+factory, which constructs `Runner` and its remaining owned components. Unit tests use the same construction seam with
+owned test doubles.
+
+The API namespace must not expose third-party symbols from the Internal namespace. A dependency may cross this
+boundary only when it is vendored into an owned project namespace or uses a well-known interoperable ABI, such as the
+Arrow C Data Interface.
+
+## Documents
+
+- [architecture.md](architecture.md) defines the namespace boundary and component responsibilities.
+- [lifecycle.md](lifecycle.md) defines connection, worker, context, and shutdown lifecycles.
+
+Mermaid `.mmd` files are the diagram sources of truth. Matching `.svg` files are rendered views linked from the
+documents.
+
+## Relationship to the protocol
+
+The runner implements the protocol described in the [v2 protocol design](../README.md). In particular:
+
+- [low_level.md](../protocol/low_level.md) defines framing, streams, transport bindings, and close behavior.
+- [call_lifecycle.md](../protocol/call_lifecycle.md) defines the generic call abstraction.
+- [high_level_calls.md](../protocol/high_level_calls.md) defines `Run`, Function operations, and callbacks.
+- [high_level_payloads.md](../protocol/high_level_payloads.md) defines JSON and named payload contracts.
+
+The runner `Context` is the implementation boundary for those protocol rules. This design does not redefine their
+wire format.
+
+## Initial transport
+
+The first concrete transport is a Unix-domain stream socket. The socket and acceptor contracts are intentionally
+transport-neutral enough to support a future TCP/TLS binding without changing the protocol context contract.
diff --git a/doc/design/v2/runner/architecture.md b/doc/design/v2/runner/architecture.md
new file mode 100644
index 0000000..d77d0a6
--- /dev/null
+++ b/doc/design/v2/runner/architecture.md
@@ -0,0 +1,106 @@
+# Runner v2 Architecture
+
+## Layering
+
+```text
+API namespace: caller-facing C++ contracts
+ |
+ v
+Internal namespace: sockets, acceptor, worker pool, factory, context manager, context
+ |
+ v
+Protocol v2
+```
+
+Related diagrams:
+
+- [layered_architecture.svg](layered_architecture.svg)
+- [component_relationships.svg](component_relationships.svg)
+
+Dependencies point downward only. The API namespace contains only project-owned caller-facing contracts. The Internal
+namespace may use C++ standard-library types, exceptions, and third-party libraries such as Arrow C++.
+
+Third-party symbols must not leak from the Internal namespace through the API namespace. A dependency is allowed at
+the API boundary only when it is vendored into an owned project namespace or communicated through a well-known ABI.
+The current well-known ABI exception is the Arrow C Data Interface: `ArrowArray` and `ArrowSchema` may cross the
+boundary under its release and ownership contract; Arrow C++ types remain internal.
+
+## Components
+
+### Runner
+
+`Runner` is the composition root. A runner user transfers ownership of a `WorkerFactory` to a production factory.
+The production factory constructs a `Runner` with that factory, transport configuration, limits, a `SocketAcceptor`,
+a worker pool, and a `ContextManager`. `Runner` validates and connects those components, starts and stops them, and
+destroys them in dependency-safe order after all work has completed.
+
+Unit tests use the same construction seam to create a `Runner` with owned fakes, stubs, or instrumented component
+implementations. The production factory is responsible only for choosing concrete production implementations; the
+runner remains responsible for their configuration and composition.
+
+### Socket
+
+`Socket` represents one connected byte stream. It owns the accepted descriptor and provides the operations required
+by the protocol transport: receive bytes, send bytes, observe closure, and close. The initial implementation uses a
+Unix-domain stream descriptor. The abstraction must not expose Unix-specific address types to the protocol context.
+
+The socket owns its descriptor after construction. Closing or destroying the socket makes the descriptor unusable;
+ownership must not be duplicated implicitly.
+
+### SocketAcceptor
+
+`SocketAcceptor` owns the listening endpoint, performs bind/listen setup, and accepts connected sockets. It submits
+each accepted descriptor to the worker scheduler. Accept failures are classified as retryable, shutdown-related, or
+fatal and must not silently become worker failures.
+
+The initial acceptor listens on a Unix-domain socket. The interface leaves endpoint configuration and accepted-socket
+creation abstract so a TCP/TLS acceptor can be added later.
+
+### WorkerFactory and worker pool
+
+The runner user initially owns `WorkerFactory` and transfers it to the production factory, which transfers ownership
+to `Runner`. The factory is used by reusable worker-pool resources to obtain a worker callable or worker object for
+an accepted descriptor. `Runner` destroys the factory only after stopping and joining all worker activity.
+
+The pool is reusable, but a worker invocation handles one accepted connection. A worker must not retain a protocol
+context or descriptor after its invocation returns. Pool sizing, queue limits, and shutdown policy are explicit runner
+configuration rather than hidden global state.
+
+### Worker
+
+A worker receives one accepted descriptor, asks the `ContextManager` to create a protocol context, runs that context
+until normal close, peer disconnect, cancellation, or error, and then releases the context and descriptor. The worker
+does not parse protocol messages outside the context boundary.
+
+### ContextManager
+
+`ContextManager` converts an owned connected descriptor into a connection-scoped `Context`. It centralizes context
+construction, protocol dependencies, limits, cancellation, and cleanup. Context creation failure must close or reclaim
+the descriptor according to the documented ownership handoff.
+
+### Context
+
+`Context` is the runner-side interface to protocol v2. It owns one connection and exposes the operations needed by the
+protocol implementation: framing, control-stream initialization, stream dispatch, call/data handling, callbacks,
+keepalive, and close/error processing.
+
+The context is not an application callback object and is not shared between worker invocations. It is the sole owner
+of protocol state for its connection and must enforce the validation, ordering, flow-control, and close rules from the
+protocol documents.
+
+## Data and dependency flow
+
+```text
+runner user --moves WorkerFactory--> production factory
+ |
+ v
+ Runner owns factory, acceptor, pool, and context manager
+ |
+SocketAcceptor -> worker pool -> Worker(fd) -> ContextManager.create(fd) -> protocol Context
+ |
+ Socket bytes + Arrow C data values
+```
+
+Arrow record batches and schemas may cross the API boundary as `ArrowArray` and `ArrowSchema` under the Arrow C Data
+Interface. Arrow C++ objects remain Internal-namespace implementation details and are released according to that
+interface's ownership rules.
diff --git a/doc/design/v2/runner/component_relationships.mmd b/doc/design/v2/runner/component_relationships.mmd
new file mode 100644
index 0000000..8df1229
--- /dev/null
+++ b/doc/design/v2/runner/component_relationships.mmd
@@ -0,0 +1,15 @@
+flowchart LR
+ User[Runner user] -->|transfers WorkerFactory| Production[Production factory]
+ Production -->|constructs| Runner[Runner\ncomposition root]
+ Runner -->|owns| Factory[WorkerFactory]
+ Runner -->|owns| Acceptor[SocketAcceptor\nlistening endpoint]
+ Runner -->|owns| Queue[Reusable worker pool]
+ Runner -->|owns| Manager[ContextManager]
+ Acceptor -->|accepted fd| Queue
+ Factory -->|creates worker| Queue
+ Queue -->|one fd per invocation| Worker[Worker callable]
+ Worker -->|create context| Manager[ContextManager]
+ Manager -->|owns connection context| Context[Protocol Context]
+ Context --> Socket[Socket abstraction]
+ Socket --> Transport[Unix stream socket\nfuture TCP/TLS binding]
+ Context --> V2[Protocol v2 interface]
diff --git a/doc/design/v2/runner/component_relationships.svg b/doc/design/v2/runner/component_relationships.svg
new file mode 100644
index 0000000..40bb7af
--- /dev/null
+++ b/doc/design/v2/runner/component_relationships.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/doc/design/v2/runner/connection_lifecycle.mmd b/doc/design/v2/runner/connection_lifecycle.mmd
new file mode 100644
index 0000000..1204597
--- /dev/null
+++ b/doc/design/v2/runner/connection_lifecycle.mmd
@@ -0,0 +1,29 @@
+sequenceDiagram
+ participant User as Runner user
+ participant Production as Production factory
+ participant Runner
+ participant Acceptor as SocketAcceptor
+ participant Pool as Worker pool
+ participant Factory as WorkerFactory
+ participant Worker
+ participant Manager as ContextManager
+ participant Context
+
+ User->>Production: transfer WorkerFactory
+ Production->>Runner: construct with owned components
+ Runner->>Acceptor: configure and listen
+ Acceptor->>Acceptor: accept connection
+ Acceptor->>Pool: enqueue owned fd
+ Pool->>Factory: create worker for fd
+ Factory-->>Pool: worker callable
+ Pool->>Worker: invoke(fd)
+ Worker->>Manager: create_context(fd)
+ Manager-->>Worker: Context(connection)
+ Worker->>Context: run protocol v2
+ Context-->>Context: close / disconnect / cancel / error
+ Context-->>Worker: return and release connection
+ Worker-->>Pool: invocation complete
+
+ alt queue, worker, or context creation failure
+ Acceptor-->>Acceptor: close fd exactly once
+ end
diff --git a/doc/design/v2/runner/connection_lifecycle.svg b/doc/design/v2/runner/connection_lifecycle.svg
new file mode 100644
index 0000000..74360f2
--- /dev/null
+++ b/doc/design/v2/runner/connection_lifecycle.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/doc/design/v2/runner/descriptor_ownership.mmd b/doc/design/v2/runner/descriptor_ownership.mmd
new file mode 100644
index 0000000..f91a975
--- /dev/null
+++ b/doc/design/v2/runner/descriptor_ownership.mmd
@@ -0,0 +1,15 @@
+stateDiagram-v2
+ [*] --> AcceptorOwned: accept()
+ AcceptorOwned --> QueueOwned: enqueue succeeds
+ AcceptorOwned --> Released: queue rejects
+ QueueOwned --> WorkerOwned: worker starts
+ QueueOwned --> Released: worker unavailable
+ WorkerOwned --> ContextOwned: context creation succeeds
+ WorkerOwned --> Released: context creation fails
+ ContextOwned --> Released: close / disconnect / cancellation / error
+ Released --> [*]
+
+ note right of AcceptorOwned
+ The descriptor has one owner
+ at every handoff.
+ end note
diff --git a/doc/design/v2/runner/descriptor_ownership.svg b/doc/design/v2/runner/descriptor_ownership.svg
new file mode 100644
index 0000000..2d20b0b
--- /dev/null
+++ b/doc/design/v2/runner/descriptor_ownership.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/doc/design/v2/runner/layered_architecture.mmd b/doc/design/v2/runner/layered_architecture.mmd
new file mode 100644
index 0000000..d754135
--- /dev/null
+++ b/doc/design/v2/runner/layered_architecture.mmd
@@ -0,0 +1,9 @@
+flowchart TB
+ Api[API namespace\ncaller-facing C++ contracts]
+ Internal[Internal namespace\nsockets, acceptor, worker pool, contexts]
+ Arrow[Arrow C Data Interface\nArrowArray and ArrowSchema]
+ Protocol[Protocol v2\nframing, streams, calls, payloads]
+
+ Api --> Internal
+ Api --- Arrow
+ Internal --> Protocol
diff --git a/doc/design/v2/runner/layered_architecture.svg b/doc/design/v2/runner/layered_architecture.svg
new file mode 100644
index 0000000..fe510e3
--- /dev/null
+++ b/doc/design/v2/runner/layered_architecture.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/doc/design/v2/runner/lifecycle.md b/doc/design/v2/runner/lifecycle.md
new file mode 100644
index 0000000..b4fcfc7
--- /dev/null
+++ b/doc/design/v2/runner/lifecycle.md
@@ -0,0 +1,92 @@
+# Runner v2 Lifecycle
+
+## Startup
+
+1. The runner user transfers ownership of a `WorkerFactory` to the production factory.
+2. The production factory creates a `Runner` with that factory, transport configuration, limits, a `SocketAcceptor`,
+ a reusable worker pool, and a `ContextManager`.
+3. The runner validates and connects its owned components, then the acceptor binds and listens on the configured
+ Unix-domain endpoint.
+4. The runner starts accepting connections and dispatching descriptors.
+
+The runner must reject invalid endpoint configuration before starting worker threads. Startup failure leaves no live
+listener or worker-owned descriptor.
+
+## Accepted connection
+
+Related diagrams:
+
+- [connection_lifecycle.svg](connection_lifecycle.svg)
+- [descriptor_ownership.svg](descriptor_ownership.svg)
+
+```text
+SocketAcceptor
+ | accept()
+ v
+owned connected fd
+ | enqueue
+ v
+reusable worker-pool resource
+ | invoke Runner-owned WorkerFactory product
+ v
+Worker(fd)
+ | ContextManager.create(fd)
+ v
+Context(connection)
+ | initialize control stream and run protocol
+ v
+normal close / peer disconnect / cancellation / error
+```
+
+The descriptor has exactly one owner at each handoff:
+
+- the acceptor owns it until successful submission;
+- the worker owns it while creating the context;
+- the context owns it after successful creation;
+- the context closes or transfers it during final teardown.
+
+Failed queue submission, worker creation, or context creation must close the descriptor exactly once.
+
+## Worker and context behavior
+
+The worker creates a fresh context for every accepted descriptor. The context then:
+
+1. performs protocol initialization and advertises or validates capabilities as required by the endpoint role;
+2. receives and dispatches framed stream messages;
+3. manages calls, data streams, callbacks, keepalive, flow control, and validation;
+4. handles normal close, error close, cancellation, and peer disconnect;
+5. releases all protocol and transport resources before returning.
+
+Worker-pool resources may be reused after the worker returns, but connection-scoped protocol state may not be reused.
+
+## Shutdown
+
+Shutdown has three phases:
+
+1. Stop accepting new descriptors and wake a blocked accept operation.
+2. Stop queueing work and request cancellation for queued and active workers.
+3. Wait for active contexts to close or reach the configured shutdown deadline, then reclaim remaining resources.
+
+The runner must close the listening socket before reporting that accepting has stopped. After all workers have
+returned, it destroys its contexts, worker pool, acceptor, context manager, and `WorkerFactory` in dependency-safe
+order.
+
+## Failure cases
+
+| Failure | Required behavior |
+| --- | --- |
+| Accept failure | Retry transient failures; terminate cleanly for shutdown; surface fatal failures. |
+| Queue full | Apply the configured admission policy; do not leak the accepted descriptor. |
+| Worker creation failure | Close the descriptor and report the failure through runner diagnostics. |
+| Context creation failure | Reclaim the descriptor and return the creation error. |
+| Protocol validation error | Let the context perform protocol error/close handling, then terminate the connection if required. |
+| Peer disconnect | Let the context release all connection state and return normally unless cleanup fails. |
+| Worker exception | Contain it within the Internal namespace and guarantee descriptor/context cleanup before reporting it through the API contract. |
+
+## Concurrency requirements
+
+- A context is single-owner unless the protocol implementation explicitly documents internal concurrency.
+- Worker-pool resources may run concurrently, but no connection-scoped stream state is shared between contexts.
+- The acceptor, queue, pool, and context manager must define their thread-safety guarantees in the API contract;
+ Internal-namespace implementation details remain encapsulated.
+- Cancellation and shutdown must be safe when they race with accept, queue submission, context creation, or peer close.