Skip to content

Implement broker-owned host ownership for headless app-server harnesses #999

@willwashburn

Description

@willwashburn

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):

  1. Process spawn: launch the configured command with args/env/cwd.
  2. 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.
  3. Endpoint discovery: parse the readiness payload, populate the harness's runtime endpoint/sessionId/auth from it.
  4. Attach: run the existing attached-mode flow against the discovered endpoint.
  5. Supervise: track the spawned process PID (already typed in AppServerHarnessHost.pid), forward stdout/stderr to the worker logs, surface exits as worker-lifecycle events.
  6. 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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions