LearnNewsExamplesServices
Frontmatter
id16229
titleHost-edge role is only reachable through a macOS plist
stateClosed
labels
bugdocumentationaiarchitecture
assigneesneo-opus-grace
createdAtJul 31, 2026, 6:06 PM
updatedAtJul 31, 2026, 11:41 PM
githubUrlhttps://github.com/neomjs/neo/issues/16229
authorneo-opus-grace
commentsCount7
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtJul 31, 2026, 11:39 PM

Host-edge role is only reachable through a macOS plist

Closed Backlog/active-chunk-11 bugdocumentationaiarchitecture
neo-opus-grace
neo-opus-grace commented on Jul 31, 2026, 6:06 PM

Context

Surfaced 2026-07-31 by @tobiu, from lived friction rather than analysis: with wake delivery down, he has been relaying A2A messages between agents by hand. Root-cause probe found the host edge had never been installed on the canonical machine — ~/Library/Application Support/Neo/AgentOS absent, wake/routes.json absent, neither plist in ~/Library/LaunchAgents, neither loaded in launchctl. #16210 closed COMPLETED at 2026-07-31T07:25Z with the code correct and the deployment never performed.

The install being missed is the symptom. His question is the ticket:

"imagine a contributor wants to try out our now dockerized agent os inside a fork. how would the person know that a plist needs to get installed? … plus: plist sounds mac OS specific, correct? before, i was just using npm run ai:orchestrator inside a terminal."

Observation, not inference: the file states, grep output, and config line numbers below are directly observed. The duplicate-owner behaviour in OQ1 is explicitly not observed and is marked as open.

Live latest-open sweep: latest 20 open issues at 2026-07-31T16:05:37Z; no equivalent. A2A claim sweep at the same time: no competing claim on the host-edge entrypoint. #16206 (ai/deploy composable layering) is the nearest neighbour and carries one incidental mention; its subject is Compose layering, not the host entrypoint.

The Problem

The thing that makes an orchestrator a host-edge orchestrator exists only inside a macOS launchd plist. ai/deploy/com.neomjs.agent-os-host-edge.plist carries it as EnvironmentVariables:

NEO_AI_DEPLOYMENT_MODE=local
NEO_AI_ORCHESTRATOR_AUTHORITY_PROFILE=host-edge
NEO_AI_ORCHESTRATOR_DIR=__HOST_EDGE_STATE_DIR__

Three consequences, in increasing severity.

1. The pre-cutover command still exists, still starts, and now claims the wrong role. package.json:79 still ships "ai:orchestrator": "node ./ai/daemons/orchestrator/daemon.mjs" — and the plist invokes that exact entrypoint, differing only by environment. Since #16039 the canonical default is authorityProfile: leaf('container-plane', …) (ai/configBase.mjs:848), so a bare npm run ai:orchestrator on the host now resolves container-plane — the role the Docker container already owns.

This is the muscle-memory command. It is the command a forking contributor will find first, because it is the only one in package.json. Before the cutover it produced the right thing; now it produces a host process claiming container authority, and nothing in its name or output says so.

2. There is no non-macOS path, and nothing says so. ai/scripts/lifecycle/local-agent-os/README.md prescribes launchctl bootstrap. It contains zero occurrences of linux, systemd, windows, darwin, or even macos — so a Linux contributor gets a runbook whose only instruction is a command their OS does not have, with no statement that it is platform-specific.

The runtime is already portablenode ai/daemons/orchestrator/daemon.mjs and node ai/daemons/wake/receiver.mjs run anywhere Node runs. Only the supervision is launchd-specific. The portability problem is not the daemon; it is that the configuration required to run it correctly is trapped inside the macOS supervision artifact.

3. A contributor cannot discover that a host edge exists. Nothing in the deployment guides, Day-0 tutorial, or root README tells someone who forks the dockerized Agent OS that a second, host-side process is required for wake delivery and host-bound effects. The containerized stack comes up healthy and looks complete — which is precisely how this machine ran for hours with wake dead and nothing announcing it.

The Architectural Reality

  • ai/configBase.mjs:848authorityProfile: leaf('container-plane', 'NEO_AI_ORCHESTRATOR_AUTHORITY_PROFILE', 'string'). The default is the container role; host-edge is only ever reached by an explicit override.
  • ai/configBase.mjs:837deploymentMode: leaf('cloud', …). Same shape: the canonical default describes the container reality.
  • package.json:79-80ai:orchestrator and ai:wake-receiver both exist and both invoke the portable entrypoints with no profile environment.
  • ai/deploy/com.neomjs.agent-os-host-edge.plistProgramArguments is __NODE_BIN__ ai/daemons/orchestrator/daemon.mjs, identical to the npm script. The plist's only distinguishing content is its EnvironmentVariables block.
  • ADR 0019 §10.8 — authorityProfile "resolves exactly one explicit role", per-lane flags "remain enablement only and cannot transfer authority", and boot audits fail on "unknown roles, missing classifications, ownership gaps, and duplicate owners". The decision is sound; this ticket is about the only supported way to satisfy it being an OS-specific artifact.
  • ai/scripts/lifecycle/local-agent-os/README.md — the complete and correct install runbook, macOS-only by construction and silent about it.

The Fix

Move the host-edge configuration out of the supervision artifact and into substrate, so supervision becomes optional and platform-specific while the runtime stays portable.

  1. Add a first-class runnable host-edge entrypoint — an ai:host-edge script (and a wake-receiver sibling) that sets the profile explicitly, so the correct role is reachable in one cross-platform command with no install.
  2. Make the plist a thin supervision wrapper over that script rather than the carrier of the configuration. Login-persistence is what launchd is for; role definition is not.
  3. Guard the ambiguous command. ai:orchestrator should either refuse without an explicit authorityProfile, or be renamed/documented as container-plane-only. A command whose correctness silently inverted at cutover is a trap regardless of documentation.
  4. State the platform matrix. The runbook says macOS explicitly, and names the supported path for other platforms — at minimum "run the script in a terminal / your own supervisor", which is already true and currently unwritten.
  5. Make the host edge discoverable at the contributor entry point: the deployment guide and Day-0 tutorial state that the containerized stack alone has no wake delivery and no host-bound effects, and link the install.

Contract Ledger Matrix

Target Surface Source of Authority Proposed Behavior Fallback Docs Evidence
host-edge posture (not role alone) package.json + AiConfig host-edge profile one runnable entrypoint resolves the COMPLETE host-edge posture — role, deployment mode, state root, and lane enablement — without an OS installer absent role refuses; a partial posture is not a supported state local-agent-os runbook + deployment guide a clean-checkout run reports authorityProfile=host-edge, writes its state under the host root, and starts only the lanes the supervised install starts
role validity, not just presence taskAuthority.mjs ORCHESTRATOR_AUTHORITY_PROFILE enum an unknown role value refuses; requiredness alone proves non-empty + type, never enum membership none — an unrecognised role is never a runnable state NEO_AI_ORCHESTRATOR_AUTHORITY_PROFILE=typo-role exits non-zero
guard ordering ai/daemons/orchestrator/daemon.mjs role validation happens before module side effects, PID recovery, and any plane write none — a refusing launch must be a no-op a refused bare launch creates zero new files under the plane root, asserted by directory snapshot before/after
every Orchestrator producer harness/brain.mjs buildBrainProfile() / buildPackagedBrainEnv(), both Compose profiles, the plist, committed docs every launcher declares a valid role; no producer relies on an inherited default none — an unconverted producer is a broken launcher harness README a producer census enumerates every site that launches the daemon, and each declares a role
ai:orchestrator package.json:79 refuses, or is explicitly container-plane-only never silently assumes a role on a host where the container owns it package docs a bare run either errors or states its resolved profile
cross-platform invocation package.json the documented entrypoint runs on Windows as well as POSIX none — a POSIX-only inline VAR=x assignment cannot satisfy a cross-platform claim runbook the script runs without a shell-specific env prefix, or the docs scope the claim to POSIX
launchd plist ai/deploy/com.neomjs.agent-os-host-edge.plist invokes the script; carries only supervision concerns (KeepAlive, PATH, state dir) a stale plist that still sets the profile keeps working runbook plist env block no longer duplicates config the script owns
platform coverage local-agent-os runbook names macOS explicitly and gives the portable path for other platforms none — silence is the current defect runbook the runbook mentions at least one non-macOS path
discoverability deployment guide / Day-0 tutorial states the containerized stack has no wake delivery without the host edge none those guides a reader reaching "stack is up" is told what is still missing

Decision Record impact

amends ADR 0019 §10.8 — corrected 2026-07-31 from aligned-with, per PR #16232's terminal Drop+Supersede review.

§10.8 states the canonical leaf defaults are deploymentMode=cloud and authorityProfile=container-plane, and that "production Compose therefore does not restate those values." Removing the authorityProfile default and requiring both profiles to declare it inverts that accepted wording in two places. That is an amendment, not an alignment, and the first PR attempted it without one.

The amendment is justified by fresh incident evidence rather than preference: the canonical default described the container's reality, so a host process starting bare claimed the role the container owned — writing authority state into the pre-cutover plane and running maintenance lanes against the legacy graph, observed 2026-07-31T16:04Z with no error. §10.8's own principle ("a role is declared, never inherited from deploymentMode") is what the change extends; only its default clause changes.

The ADR amendment is a first-class deliverable of the successor, not a follow-up. ADR wording lands with, or before, the behaviour change.

The two-role split, single-explicit-role rule, and enablement-vs-authority separation stand unchanged.

Open Questions

  • OQ1 — what does a bare npm run ai:orchestrator actually do today? ANSWERED 2026-07-31 by @tobiu, empirically. It does not fail closed. It started cleanly with authorityProfile=container-plane, wrote an authority receipt and a first-write deployment snapshot into the pre-cutover host plane, began miniSummary backfill against the legacy graph, and contended for the shared inference provider. My reading of §10.8's "duplicate owners fail boot" as protection was wrong: that audit covers lane ownership within the starting process's own config — intra-config coherence, not cross-process exclusion. Fix item 3 is therefore refuse, not document. Cross-process exclusion is #16230.
  • OQ2 — should the wake receiver and host-edge orchestrator be one script or two? They are separate processes with separate state roots by #16210's own AC ("Host-edge, wake receiver, and Docker state roots are distinct"), so two is the conservative default; one supervisor wrapper over both may still be the better operator surface.

Acceptance Criteria

Falsifier-first (recorded 2026-07-31). PR #16232 shipped a green suite that could not see a production break, because Playwright injects NEO_AI_ORCHESTRATOR_AUTHORITY_PROFILE ??= 'legacy-mixed' (configTemplateResolver.mjs:114) — supplying the exact value the real launchers omit. The successor writes every criterion below as a failing spec before implementing. A criterion that cannot be expressed as a probe is not a criterion.

Reviewer falsifiers, promoted to criteria — each one caught a real defect in the first attempt:

  • Producer census is complete and asserted. Every site that launches ai/daemons/orchestrator/daemon.mjs declares a valid role — including harness/brain.mjs buildBrainProfile() and buildPackagedBrainEnv(), which currently set none while harness/main.mjs launches the daemon with them. A spec enumerates the producers so a new launcher cannot be added without one.
  • The test harness cannot mask a missing role. The suite proves the production path with the Playwright injection absent, so green means the launchers are correct rather than that the harness compensated.
  • A refused launch is a genuine no-op. Directory snapshot of the plane root before and after a bare launch shows zero new files — no wake cursor, no SQLite DB/WAL/SHM, no logs. Requires the guard to run before module side effects.
  • An invalid role refuses. NEO_AI_ORCHESTRATOR_AUTHORITY_PROFILE=typo-role exits non-zero, validated against taskAuthority.mjs's frozen enum rather than by non-emptiness.
  • The cross-platform claim is either true or not made. The documented entrypoint runs on Windows, or the docs scope themselves to POSIX. No inline VAR=x node … behind a platform-neutral sentence.
  • ADR 0019 §10.8 carries the amended default clause, landing with or before the behaviour change.

Original criteria, retained:

  • A single documented command starts a correctly-roled host edge on a clean checkout, with no OS-specific installer, and its startup line reports authorityProfile=host-edge.
  • The host edge started that way runs the same lane set as the supervised install and writes its state under the host root — not the checkout. Role alone is not the posture.
  • The launchd plist no longer carries NEO_AI_ORCHESTRATOR_AUTHORITY_PROFILE or NEO_AI_DEPLOYMENT_MODE; those resolve from the invoked script.
  • npm run ai:orchestrator no longer silently resolves a role — it refuses, or states its resolved profile, per OQ1's answer.
  • The local-agent-os runbook names macOS explicitly and documents at least one non-macOS path.
  • The deployment guide and Day-0 tutorial state that the containerized stack alone has no wake delivery, and link the host-edge install.
  • A contributor-path check: from a fresh fork, following only committed docs, a reader can determine that a host edge is required and how to start it. Asserted by a reviewer following the docs, not by the author asserting they are sufficient.

Out of Scope

  • Installing the host edge on this machine. That is an operator act gated on #16167's cutover sequencing; this ticket makes it discoverable and portable, not performed.
  • Writing systemd units or Windows service wrappers. Naming the portable path is in scope; shipping every platform's supervisor is not.
  • The wake protocol, signing, or route schema (#16180 owns those).
  • Lane enablement inside a running orchestrator — #16197 owns that, and the two are complementary: #16197 makes a running orchestrator announce what it disabled; this makes the right orchestrator startable at all.

Avoided Traps

  • Fixing this with documentation alone. A doc that says "do not run ai:orchestrator" leaves a command in package.json whose correctness silently inverted. The trap is the command's continued existence, not the reader's ignorance.
  • Deleting ai:orchestrator. It is presumably still the right entrypoint inside the container; deleting it to fix host misuse would break the container path. Hence guard-or-rename, decided by OQ1.
  • Treating the plist as the problem. launchd is a fine supervisor and the runbook is correct. The defect is that deployment configuration rode inside it, which is what makes the whole role macOS-only.
  • Assuming the daemon fails safely on duplicate roles. ADR 0019 §10.8 says it should. Unverified here, and Fix item 3's shape depends on it — recorded as OQ1 rather than assumed in either direction.

Related

  • #16210 — shipped the host-edge runtime and the plists; closed COMPLETED with the install never performed, which is how this surfaced
  • #16167 — AC-7 ("signed wake delivery … pass") cannot pass while the host edge is unstartable by a documented portable path
  • #16197 — the running-orchestrator counterpart: announce disabled lanes with no running replacement
  • #16180 — the signed wake receiver this supervises
  • #16206ai/deploy composability; nearest neighbour, different subject
  • ADR 0019 §10.8

Origin Session ID: 59fd6dbb-fc01-482b-a778-bed01f16e699

Retrieval Hint: host-edge role only reachable through macOS plist authorityProfile container-plane default npm run ai:orchestrator wrong role portable entrypoint contributor fork discoverability


Amendment log — 2026-07-31, after PR #16232 Drop+Supersede

First implementation attempt: PR #16232, closed. Terminal review: https://github.com/neomjs/neo/pull/16232#pullrequestreview-4831204658

Why it was dropped, in one line: it delivered the role and called it the posture, while harness/brain.mjs sets no role at all — so merging would have broken both real Brain launches on a P0 lane.

Amended above: Decision Record impact aligned-withamends ADR 0019 §10.8 · Contract Ledger gains posture-not-role, enum validity, guard ordering, producer census, and cross-platform rows · OQ1 marked answered with its empirical result · ACs re-authored falsifier-first from the reviewer's probes.

Salvage from #16232 (branch retained): the empty-default + requiredFor pairing and its resolved-value reasoning — requiredness is evaluated on the resolved value, so any non-empty default makes the requirement permanently unfireable; the explicit Compose declarations in both profiles; the census reclassification with its stale forbidden-env entry removed; the focused config specs.

Discarded: the Resolves #16229 claim, the role-only ai:host-edge product surface, and the "cross-platform" / "no state written" / "launchd is supervision only" framings — each falsified by a probe that existed and was not run.

The replacement PR must cite the terminal review before implementation resumes.

tobiu referenced in commit 21efa1d - "feat(orchestrator): the host-edge posture in substrate, and a role declared never inherited (#16229) (#16237) on Jul 31, 2026, 11:39 PM
tobiu closed this issue on Jul 31, 2026, 11:39 PM