Problem
Headless app-server harnesses today only support attached hosts — the app-server (e.g., codex app-server, opencode server) must already be running somewhere, and the broker is given an endpoint + auth to connect to.
From packages/sdk/src/harness.ts:33-41:
export interface AppServerHarnessHost {
/**
* `broker-owned` is reserved for a future broker-supervised app-server mode.
* Current broker releases accept attached hosts only.
*/
ownership?: 'broker-owned' | 'attached';
/** Local app-server host PID to report as the harness PID when known. */
pid?: number;
}
The broker-owned variant is typed but not implemented. Implementing it unlocks the unification where a "persona" (or any recipe) is just a CLI command for both PTY and headless transports — the CLI either exec's into a stdio binary (PTY) or boots an app-server and waits (headless, broker-owned). The broker handles the lifecycle either way.
Goal
The broker can spawn and supervise an app-server process from a command, wait for it to be ready, discover its endpoint, attach, and clean up on exit.
Scope
SDK / harness definition
Extend StaticHeadlessAppServerHarnessDefinition in packages/sdk/src/harness.ts (line 69) to accept a command (+ args, env, cwd) for broker-owned hosts. When host.ownership === 'broker-owned', endpoint may be omitted from the static definition because the broker discovers it at runtime.
Broker (Rust)
In crates/broker/src/ (likely runtime/spawn_spec.rs, worker.rs, and headless-specific modules):
- Process spawn: launch the configured
command with args/env/cwd.
- Readiness signal: define a contract — likely the CLI prints a JSON line on stdout like
{"endpoint":"http://127.0.0.1:PORT","sessionId":"...","auth":{...}} and the broker reads until it sees that line. Could alternatively poll a known port or use a Unix socket. Pick one and document it.
- Endpoint discovery: parse the readiness payload, populate the harness's runtime
endpoint/sessionId/auth from it.
- Attach: run the existing attached-mode flow against the discovered endpoint.
- Supervise: track the spawned process PID (already typed in
AppServerHarnessHost.pid), forward stdout/stderr to the worker logs, surface exits as worker-lifecycle events.
- Release: honor the existing
release: 'abort' | 'detach' | 'delete' policy — abort/delete should kill the spawned process, detach should leave it running.
Tests
Integration test that boots a fake app-server CLI (a small Node or Rust binary that prints the readiness payload and accepts a single session), spawns it via broker-owned mode, runs a smoke session, releases it, confirms the process exits.
Non-goals
- Replacing attached mode — both should coexist.
- Cross-host supervision (broker-owned implies same host as broker).
Related
- Surface
spawnHeadless on AgentRelay
- Remove personas from
@agent-relay/sdk (depends on this so headless personas can work)
Problem
Headless app-server harnesses today only support attached hosts — the app-server (e.g.,
codex app-server, opencode server) must already be running somewhere, and the broker is given anendpoint+authto connect to.From
packages/sdk/src/harness.ts:33-41:The
broker-ownedvariant is typed but not implemented. Implementing it unlocks the unification where a "persona" (or any recipe) is just a CLI command for both PTY and headless transports — the CLI either exec's into a stdio binary (PTY) or boots an app-server and waits (headless, broker-owned). The broker handles the lifecycle either way.Goal
The broker can spawn and supervise an app-server process from a command, wait for it to be ready, discover its endpoint, attach, and clean up on exit.
Scope
SDK / harness definition
Extend
StaticHeadlessAppServerHarnessDefinitioninpackages/sdk/src/harness.ts(line 69) to accept acommand(+args,env,cwd) for broker-owned hosts. Whenhost.ownership === 'broker-owned',endpointmay be omitted from the static definition because the broker discovers it at runtime.Broker (Rust)
In
crates/broker/src/(likelyruntime/spawn_spec.rs,worker.rs, and headless-specific modules):commandwithargs/env/cwd.{"endpoint":"http://127.0.0.1:PORT","sessionId":"...","auth":{...}}and the broker reads until it sees that line. Could alternatively poll a known port or use a Unix socket. Pick one and document it.endpoint/sessionId/authfrom it.AppServerHarnessHost.pid), forward stdout/stderr to the worker logs, surface exits as worker-lifecycle events.release: 'abort' | 'detach' | 'delete'policy —abort/deleteshould kill the spawned process,detachshould leave it running.Tests
Integration test that boots a fake app-server CLI (a small Node or Rust binary that prints the readiness payload and accepts a single session), spawns it via broker-owned mode, runs a smoke session, releases it, confirms the process exits.
Non-goals
Related
spawnHeadlessonAgentRelay@agent-relay/sdk(depends on this so headless personas can work)