Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,16 @@ contained implementation panic is not a normal API error, and a stateful
runtime resource whose operation panics must not be reused unless its owner can
prove that reuse is safe.

Connection-scoped resource stores own lookup and quota accounting. Plans and
sessions own descendant creation and release; Connection remains the logical
lifetime boundary. Keep admission and parent links under the store mutex, and
never hold a resource-state lock while acquiring it. Hosted calls and cleanup
waits run outside the store lock. See the architecture document for details.

The server accepts `api.Runtime` directly and owns its optional `RuntimeIdentity`
configuration type. Do not reintroduce runtime aliases, operation managers,
controller facades, or dependency-carrying context wrappers.

## Generated code

Protobuf definitions and Buf configuration are source. Files under
Expand All @@ -72,7 +82,7 @@ changes are suspicious and require explanation.
The `server` package, the `client` package, the shared `pkg/execution`,
`pkg/debugger`, and `pkg/failure` packages, and the versioned protobuf service
are API-sensitive. The module root intentionally has no Go compatibility
package. Follow [Client Handles](docs/client.md) for the facade ownership and
package. Follow [Client Handles](docs/client.md) for the API adapter ownership and
lifecycle contract.

Export only externally required symbols, keep logical connection and resource
Expand Down
14 changes: 9 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ See [Wire Protocol](docs/protocol.md) for every RPC/message/enum, lifecycle and
The host chooses and configures both the runtime implementation and endpoint. This function accepts caller-owned values and does not close either one:

```go
func serveRuntime(ctx context.Context, hostRuntime server.Runtime, listener net.Listener) error {
wireServer, err := server.NewServer(hostRuntime, server.WithRuntimeIdentity(execution.Identity{
func serveRuntime(ctx context.Context, hostRuntime api.Runtime, listener net.Listener) error {
wireServer, err := server.NewServer(hostRuntime, server.WithRuntimeIdentity(server.RuntimeIdentity{
Name: "my-app", Version: "1.0.0", InstanceID: "worker-1",
}))
if err != nil {
Expand All @@ -65,9 +65,13 @@ func serveRuntime(ctx context.Context, hostRuntime server.Runtime, listener net.
}
```

`server.Runtime` aliases the canonical `api.Runtime` interface. The alias lets
host-facing function signatures use the server package without changing runtime
ownership or requiring an adapter.
`NewServer` accepts the canonical `api.Runtime` directly. `server.RuntimeIdentity`
is optional host-supplied handshake metadata.

For existing hosts, replace `server.Runtime` with `api.Runtime` and
`execution.Identity` with `server.RuntimeIdentity`. The old alias and identity
type were removed without compatibility shims; protocol and ownership behavior
are unchanged.

For an application-private Unix socket, the caller creates `net.Listen("unix", socket)`, applies appropriate directory and socket permissions, and closes both the listener and runtime after the Wire server has shut down.

Expand Down
189 changes: 85 additions & 104 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ host → server → server/internal ──┘→ Unified API → runtime impleme
| FQL, runtime, output encoding, and debugger semantics | Unified API and runtime implementation |
| Runtime construction, configuration, policies, and application state | Host application |
| Versioned RPC contract | Protobuf definitions |
| Shared execution, debugger, identity, and failure semantics | `pkg/execution`, `pkg/debugger`, and `pkg/failure` |
| Shared execution, debugger, and failure semantics | `pkg/execution`, `pkg/debugger`, and `pkg/failure` |
| RPC adaptation | `server/internal/grpcserver` |
| Logical connections and resources | `server/internal/core` |
| Public server lifecycle | `server` package |
Expand All @@ -41,29 +41,35 @@ private within the owning client package.

The caller supplies and owns the physical transport. Runtime and resource
`Close` methods release logical resources with bounded detached cleanup.
`server.Runtime` continues to alias `api.Runtime`; host ownership is unchanged.
`server.NewServer` accepts `api.Runtime` directly. Optional host identity is
`server.RuntimeIdentity`, supplied through `WithRuntimeIdentity`.

## gRPC service composition

`grpcserver.Server` constructs and registers five dedicated implementations.
It contains only those service instances and owns no RPC handlers. Each service
embeds its corresponding generated service base and adapts one protocol domain.

| Service | Core dependencies beyond request-context preparation |
| Service | Invocation and ownership |
| --- | --- |
| RuntimeService | RuntimeInfo, ConnectionRegistry, Executor, Lifecycle |
| PlanService | Compiler, Lifecycle |
| SessionService | Executor, Lifecycle |
| ExecutionService | Executor, Lifecycle |
| DebugService | Debugger, Lifecycle |

A shared private `operationContextFactory` owns only the connection registry.
It resolves the logical connection, maps lookup errors, and constructs the core
operation context; each handler cancels that context when it finishes.
Services retain their own resource lookup, validation, and domain adaptation.
Stateless conversion, error mapping, recovery, and subscription functions remain
shared transport infrastructure. DebugService groups its cohesive lifecycle,
commands, inspection, and events across focused files.
| RuntimeService | Opens/closes logical connections; calls `core.Run` with the borrowed runtime and connection store |
| PlanService | Calls `core.CompilePlan` with the borrowed runtime and connection store; releases plans through that store |
| SessionService | Resolves a Plan in the connection store and calls its `NewSession` |
| ExecutionService | Resolves a Plan or Session for execution creation; resolves Execution for watches, cancellation, and release |
| DebugService | Resolves a Plan for debugger creation; resolves DebugSession for commands, inspection, watches, and release |

`prepareOperation` resolves the connection and returns its resource store plus
an ordinary `context.Context`. The context preserves request values and deadlines
and joins connection cancellation; each handler cancels it to detach the lifetime
callback. There is no dependency-carrying operation context.

Services convert protobuf sources and options to canonical API types before
calling core. Source/diagnostic, option/value, output, execution, debugger, and
failure conversions are grouped at the transport boundary. Handshake metadata
belongs to transport configuration, not resource management. Domain errors
supply shared Wire categories; gRPC owns status mapping and uses the same
category serialization as terminal failures. Canonical diagnostic extraction
is shared within server error handling.

## Execution and host boundaries

Expand Down Expand Up @@ -201,22 +207,25 @@ Wire connection
└── debug sessions
```

Internally, `Connection` owns only its opaque ID, cancellation context, open or
closing state, and admission of in-flight operations. Server-scoped
`ConnectionRegistry`, `PlanRegistry`, `SessionRegistry`, `ExecutionRegistry`,
and `DebugSessionRegistry` instances own storage, indexes, and capacity
accounting. Every resource records its owning connection ID. Plan children also
record the Plan ID, and normal-session executions record their Session ID. An
ID lookup always includes the requesting connection, so knowledge of another
connection's ID never grants access.

The server-scoped `Compiler`, `Executor`, and `Debugger` components own resource
creation. `Compiler` uses `api.Runtime` for compilation and `Executor` uses it
only for the explicit direct-runtime path; Plan execution and debugging use the
`api.Plan` obtained from `PlanRegistry`. `Lifecycle` owns cleanup spanning
resource types. Individual resources retain their own state machines, runtime
handles, watches, and local close invariants. A per-operation Wire `Context`
combines the unary or stream context with the resolved logical connection.
`ConnectionRegistry` is the only server-wide resource index. It owns connection
capacity, active/closing membership, and shutdown admission. `Connection` owns
its ID, cancellation context, retained close result, and one `ResourceStore`.

The store contains typed maps for plans, normal sessions, executions, and debug
sessions. IDs resolve only in the requesting connection's store; there are no
global resource maps or owner-ID indexes. Plans hold their child collections,
normal sessions hold their active execution, and children retain direct parent
references. Resources remain in the store while closing and are removed only
when cleanup settles.

`CompilePlan` and `Run` take the borrowed `api.Runtime` and store explicitly.
They own root allocation; neither introduces another runtime wrapper. A Plan
owns its hosted `api.Plan`, parameter metadata, child creation, and descendant
cleanup. A normal Session owns its hosted `api.Session`, poisoning state, and
execution admission. Execution owns asynchronous work, snapshots, cancellation,
and watches. DebugSession directly owns its hosted `debugger.Session`, command
state, breakpoint bookkeeping, watches, and close. No operation managers,
debugger controller, or cross-resource lifecycle manager intervene.

The client adapter uses the same ownership tree to reclaim allocations whose
responses are lost. Unknown Session IDs invalidate their Plan; unknown
Expand All @@ -233,71 +242,43 @@ narrow cleanup preserves siblings outside its subtree and never closes the
borrowed physical transport. See [Client Handles](client.md)
for the cancellation contract.

```text
Compiler ──► api.Runtime
Compiler ──► PlanRegistry ◄── Executor ──► api.Runtime.Run
◄── Debugger
Executor ──► SessionRegistry
Executor ──► ExecutionRegistry
Debugger ──► DebugSessionRegistry
Lifecycle ──► all five resource registries

ConnectionRegistry ──► Connection ◄── operation Context
```

The arrows show dependencies: components depend on registries, registries do
not depend on components, and `Connection` has no dependency on either.

`Execution` and `DebugSession` retain their lifecycle and state-machine
semantics while delegating reusable subscription mechanics to a package-private
generic event stream. The stream owns sequence allocation, latest-event replay,
bounded watcher buffers, subscription accounting, fan-out, lag eviction, and
channel shutdown; it has no knowledge of execution or debugger event meaning.
`DebugSession` groups its current stop/result values in one cohesive state value
and orchestrates a session-local breakpoint set, event stream, and
`DebugController`. The controller exclusively owns and operates the Unified API
`debugger.Session`; it contains only runtime-facing commands, inspection,
breakpoint mutation, and idempotent close. The breakpoint set owns only the
Wire-side limit and successful breakpoint records. The aggregate owns command
eligibility, lifecycle and cancellation, breakpoint policy, serialization, and
semantic event construction.

```text
DebugSession
├── debugSessionState
├── breakpointSet
├── eventStream[debugger.Event]
└── DebugController
└── debugger.Session
```

Creation uses reserve, create, and commit phases. Pending capacity is reserved
before calling the Unified API, registry locks are released for runtime calls,
and publication is committed only while the connection and parent plan still
accept children. A normal Session calls `api.Plan.NewSession` once, owns that
hosted session until release, and admits one Execution at a time. Plan release
gates new children, waits for in-flight child constructors, releases direct
executions, normal sessions and their executions, and debug sessions, and only
then closes the Unified API plan.

Each registry owns its collection lock, each resource owns its state lock, and
the event stream owns the lock protecting subscriptions and publication.
`DebugSession` has a state mutex that protects only snapshots and transitions,
plus a dedicated operation mutex that serializes stopped-state commands,
inspection, breakpoint bookkeeping, pause requests, and command completion.
The breakpoint set is accessed only under that operation mutex and therefore
has no redundant lock. No debug-session state lock is held while invoking the
Unified API. The nested normal-run publication order is Plan registry, Plan,
Session registry, Session, then Execution registry. Connection shutdown first
closes operation admission
and waits for admitted creation to settle. Release paths never hold registry
locks while waiting for constructors, children, or Unified API cleanup.

When the Connect stream terminates, cleanup rejects new operations and cancels
in-flight creation, waits for creation to settle, cancels and releases
executions, closes normal and debug sessions, releases plans, and terminates
owned state and goroutines. Parent and connection traversal uses registry owner,
plan, and session indexes rather than nested resource collections.
`Execution` and `DebugSession` share a private event stream that owns sequence
allocation, latest-event replay, bounded buffers, subscription accounting, lag
eviction, and channel shutdown. It has no execution/debugger semantics.
DebugSession also retains a cohesive state value and breakpoint set; the set
owns the Wire limit and successful breakpoint records.

Creation reserves capacity before invoking the hosted API. Pending, published,
and closing resources all count toward the connection's limit. Publication
checks request cancellation, connection admission, and live ancestors under
the store mutex. Failure or abandonment closes returned hosted resources before
releasing the pending reservation. Constructors return real resource handles;
shared snapshots carry no ownership identity.

The store mutex protects maps, reservations, parent links, and allocation/release
admission. Creation gates are incremented under that mutex before release can
start waiting. Connection cancellation shares this admission lock. Resource
state locks must never be held while acquiring the store mutex. Hosted calls,
recursive release, and cleanup waits run without it.

Release belongs to each resource. Plan release gates new descendants and waits
for admitted constructors, releases executions, normal sessions, and debug
sessions, then closes its hosted plan. Session release cancels its lifetime,
waits for execution publication, releases its execution, then closes its hosted
session. The execution slot remains occupied until release finishes, even after
a terminal result. Execution/debugger release detaches storage only after local
cleanup settles. All removals also update direct parent links.

Connection teardown cancels in-flight work, closes store admission, waits for
pending creation, and settles executions, sessions, debuggers, and plans. Server
shutdown rejects new connections and closes the existing connections. Neither
path closes the borrowed runtime.

DebugSession has separate operation and state mutexes. Its operation mutex
serializes stopped-state commands, inspection, breakpoint bookkeeping, pause,
and command completion. The breakpoint set uses that mutex without adding a
redundant lock. The state mutex protects snapshots and transitions and never
spans a hosted API call.

Release is committed teardown. Concurrent callers observing the same in-flight
release wait for its retained result. After teardown finishes, the resource ID
Expand All @@ -310,10 +291,10 @@ Every stateful resource has explicit synchronization, cancellation, ownership,
and termination. Context cancellation propagates into Unified API operations.
Debug inspection cannot wait through a resume and then inspect a later stop.
An asynchronous resume releases the operation mutex while the runtime command
is active so `Pause` and close can reach the controller. Command completion
is active so `Pause` and close can reach the hosted debugger. Command completion
reacquires the operation mutex before committing state, which keeps pause
responses and event ordering deterministic. Close cancels the session and calls
the controller without waiting behind a potentially blocking stopped-state
the hosted debugger without waiting behind a potentially blocking stopped-state
operation, then serializes the final state and event commit.

Event buffers are bounded and producers are non-blocking. Each watch first
Expand All @@ -326,11 +307,11 @@ until the stream handler exits, including after lag or a terminal snapshot.
Detached cleanup has a named owner, is panic-safe, and terminates
deterministically.

`Lifecycle.settleSession` follows the existing detached-release terminal policy:
its recovery settles release waiters and registry bookkeeping if Wire
Each resource release follows the existing detached-release terminal policy:
its recovery settles release waiters and store bookkeeping if Wire
orchestration panics. This is distinct from `panicboundary`, which guards only
external implementation calls. Session-local close relies on the existing
external `api.Session.Close` boundary without adding another raw recovery site.
external implementation calls. Normal-session release invokes the hosted
`api.Session.Close` through that boundary and retains the cleanup result.

Direct Plan execution, normal Session run, and direct Runtime run construction
publish running state. Debug-session construction
Expand All @@ -344,7 +325,7 @@ completion then publishes stopped or terminal state with a monotonic sequence.
Every Wire server is a potential remote-code-execution boundary, including over
local IPC. Requests and lifecycle identifiers are untrusted.

`DefaultServerLimits` supplies the secure baseline:
`DefaultLimits` supplies the secure baseline:

| Resource | Default limit |
| --- | ---: |
Expand Down
3 changes: 3 additions & 0 deletions docs/protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,9 @@ Execution and debugger completion preserve Unified API encoded output exactly:

The handwritten Go adapters use `pkg/execution`, `pkg/debugger`, and
`pkg/failure` for shared Wire semantics, without protocol resource IDs.
Host identity is configured with `server.RuntimeIdentity`; its protobuf shape
is unchanged. Each logical connection owns its server resource store; resource
IDs are resolved only within that store.
The public client projects execution and debugging onto canonical Universal
API interfaces and events; Wire snapshots and watch streams remain private to
its implementation. The adapters copy mutable output, diagnostic, range, and
Expand Down
4 changes: 0 additions & 4 deletions pkg/execution/execution_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,8 @@ func TestExecutionSemanticTypesUseUnifiedOutputAndSharedFailure(t *testing.T) {
Failure: terminalFailure,
},
}
identity := execution.Identity{Name: "host", Version: "1.0.0", InstanceID: "instance"}

if event.Sequence != 7 || event.Snapshot.Output != output || event.Snapshot.Failure != terminalFailure {
t.Fatalf("unexpected execution event: %#v", event)
}
if identity.Name != "host" || identity.Version != "1.0.0" || identity.InstanceID != "instance" {
t.Fatalf("unexpected runtime identity: %#v", identity)
}
}
8 changes: 0 additions & 8 deletions pkg/execution/identity.go

This file was deleted.

3 changes: 1 addition & 2 deletions server/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import (
"github.com/MontFerret/api/source"
"github.com/MontFerret/wire/client"
wirev1 "github.com/MontFerret/wire/gen/ferret/wire/v1"
"github.com/MontFerret/wire/pkg/execution"
"github.com/MontFerret/wire/pkg/failure"
"github.com/MontFerret/wire/server"
"google.golang.org/grpc"
Expand Down Expand Up @@ -47,7 +46,7 @@ func TestUnifiedRuntimeCompileExecuteAndBorrowedOwnership(t *testing.T) {
runtime := &apiRuntimeSpy{compile: func(context.Context, api.Source, bool) (api.Plan, error) {
return plan, nil
}}
env := newIntegrationEnv(t, runtime, server.WithRuntimeIdentity(execution.Identity{
env := newIntegrationEnv(t, runtime, server.WithRuntimeIdentity(server.RuntimeIdentity{
Name: "test-host", Version: "1.2.3", InstanceID: "instance-1",
}))

Expand Down
Loading
Loading