Skip to content

feat(host): startAgentDevice() for embedder-owned daemons #3757

Open

Description

@thymikee

Part of #3747. Prerequisites have merged: #3806 (recorded ownership safety) and #3797 (authenticated, instance-fenced hosted shutdown). Implementation: #3843.

Purpose

Let a driver process own an agent-device runtime in-process and pass plugin objects it imports as ordinary dependencies. Other processes may attach to that exact instance.

import { startAgentDevice } from 'agent-device';
import testmu from '@agent-device/testmu';

const host = await startAgentDevice({ stateDir, host: optionalHostEnvironment, plugins: [testmu({ credentials })] });
host.client;       // AgentDeviceClient
host.clientConfig; // serializable AgentDeviceClientConfig: attach-only, pinned to this instance
const report = await host.close(); // the runtime's ShutdownReport

Contract

  • Entry: startAgentDevice and its two public types (StartAgentDeviceOptions, AgentDeviceHost) come from the main agent-device entry. There is no agent-device/host subpath. The function loads the daemon runtime lazily, so a plain import 'agent-device' costs nothing extra; ADR 0027 keeps the eager closure unchanged.
  • Runtime: it reuses the daemon runtime and serves the existing authenticated protocol on loopback. Built-in providers come first, then the supplied plugins, through the existing registry. The registry's validator reports malformed or incompatible plugins in the published inventory. The plugin store is not read. Automatic idle reap is off. Registration records ownership: 'embedded'. A refused registration propagates daemon_registration_refused.
  • Host environment: host defaults to one captured copy of the process env and cwd. The runtime never changes the process env or cwd, installs process handlers, or exits the process.
  • Attach: a client created from clientConfig goes through the one normal attach flow: it observes the daemon, requires a reuse compatibility decision, then checks the pin (token, PID, start time from the runtime's published identity) as a predicate. A missing, unreadable or replaced instance fails with daemon-binding-mismatch. A version or code-origin mismatch fails with daemon-incompatible. It never starts, replaces or redirects a daemon.
  • Close: close() is idempotent and returns the runtime's existing shutdown report. Runtime startup owns rollback. The CLI never signals or replaces an embedded runtime (fix(daemon): preserve recorded ownership through stop and cleanup #3806); an external daemon stop uses the authenticated hosted shutdown (fix(daemon): finalize every host-owned close through registration #3797).

Dropped from the earlier contract

These were dropped in #3843 to keep the design minimal. The previous body is in the issue edit history.

  • The agent-device/host subpath: one entry, agent-device, and a lazy load.
  • The separate bound-attach path and the token override: one attach flow with the pin as a predicate.
  • The log option and LogEntry: runtime console output is silent; artifact diagnostics remain.
  • Symbol.asyncDispose: use close().
  • host.plugins and the inventory copy: daemon.json / daemon status own the inventory.
  • Extra root re-exports (HostClientConfig, HostEnvironment, ShutdownReport): AgentDeviceClientConfig already describes the config.
  • The host-specific reasons state-dir-in-use, state-dir-ownership-unproven, state_dir_required and invalid_embedder_plugin, plus the separate embedder plugin check: they duplicated existing registration and registry reasons.

Done when

Activity

  1. thymikee commented on Oct 19, 2026

    @thymikee
    MemberAuthor

    Decision: publication layers: The maintainer approved three functional PRs for this work. This supersedes the body’s “At most two PRs” note.

    1. Publish ownership safety and detached-stop cleanup correctness as the prerequisite. Its tested diff is 2,036 gross lines.
    2. Keep authenticated shutdown, registration-owned completion, and bounded stop waiting together in fix(daemon): finalize every host-owned close through registration #3797 above that prerequisite. Its tested parent-relative diff is 2,480 gross lines.
    3. Keep the public agent-device/host entry and binding in the later feat(host): startAgentDevice() for embedder-owned daemons #3757 PR.

    The combined first two layers contain 4,186 gross lines. Each layer fits the repository’s 2,500-line limit. Required ownership and lifecycle tests remain. One registration owner and one shutdown file replace the request ledger and release facade. The split does not change the host API contract or the 0.21 compatibility rule.

  2. thymikee commented on Oct 19, 2026

    @thymikee
    MemberAuthor

    Current state: 2026-10-19.

    Both approved prerequisite layers have merged: #3806 at 4645ddd75f and #3797 at b5f47e7542. They add recorded ownership safety and authenticated/direct close completion. The dead-owner managed-startup regression is fixed. Final independent code and sentence reviews passed; the exact local receipts and review dispositions remain in the PRs.

    No PR remains open from this coordinator. The later public host entry and binding work remains in this issue. The accepted three-PR plan still applies. This coordinator thread is closing at the maintainer's request.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions