fix(host-service): provision agent hooks and shell env on headless CLI hosts
Links
- Fixes #6254 (both defects; supersedes the
spawn.ts/terminal.tshalf of #6256 — happy to rebase whichever lands second) - Related: #3931, #5113 (same user-visible symptom)
Summary
- Move
apps/desktop/src/main/lib/agent-setup/into a new sharedpackages/agent-setupand run it at standalone host-service startup, so CLI/systemd-launched hosts provision agent lifecycle hooks, PATH wrappers, shell bootstrap, and managed skills — previously Electron-only, leaving remote workspaces with permanently blank agent status. - Give host-service a
~/.supersetfallback forSUPERSET_HOME_DIR(terminal launch + shell-ready evidence) and make the CLI forward the variable to the host it spawns — without this, provisioned hooks self-disable on their own guard. - Add a machine-shared per-agent hooks-disable state: the desktop mirrors its
disabledAgentHookssetting to~/.superset/agent-hooks.json(boot + every toggle) and headless provisioners honor it plus aSUPERSET_DISABLED_AGENT_HOOKSenv override — so a machine running both converges instead of flip-flopping, and headless-only hosts get a disable mechanism at all. The login-shell merge also gained a denylist (NODE_ENV/NODE_OPTIONS/ELECTRON_RUN_AS_NODE): a dotfile exportingNODE_ENV=developmentwould otherwise flip the host into dev-mode shutdown, killing the pty-daemon on restart. - Merge the login-shell env/PATH into the standalone host process at startup, mirroring what the desktop's
getProcessEnvWithShellPath()does for hosts it spawns, sogit/gh/credential helpers resolve under systemd/cron/non-login launches.
Why / Context
Issue #6254: a CLI-only host had 18 live Claude sessions and 0 rows in terminal_agent_bindings. Two independent defects: (1) SUPERSET_HOME_DIR never reached the PTYs, and (2) nothing outside Electron ever wrote notify.sh, the managed hook entries, ~/.superset/bin, or the zsh/bash bootstrap that host-service's shell-launch.ts expects (it's an explicit read-only mirror of the desktop writers). PR #6256 addressed only (1). Fixing either alone changes nothing user-visible.
How It Works
packages/agent-setupis the old desktop module nearly verbatim (git tracks the renames). Two things had to become lazy for a shared package: the Superset home dir (resolveSupersetHomeDir()— resolved at call time because the desktop rewritesprocess.env.SUPERSET_HOME_DIRduring boot for dev-workspace builds) and the templates dir (setAgentSetupTemplatesDir()— bundles lose__dirname-relative assets). Templates live at the package root; each distribution copies them next to its bundle: desktop →dist/main/templates(existing vite copy plugin, new source path), CLI tarball →lib/agent-templates(+plugin/= repo Claude plugin), TS source → package-relative fallback.- Host-service calls
provisionAgentIntegrations()fromserve.tsonly — the standalone entry. The desktop uses its own host-service entry and keeps provisioning in Electron boot (it owns the per-agentdisabledAgentHookssetting, which has no host-side equivalent; headless hosts provision all agents). - Env:
packages/clispawn.tsnow setsSUPERSET_HOME_DIRon the child (matchinghost-service-coordinator.ts), and the two host-service readers that resolved\"\"when unset now use the shared resolver.applyLoginShellEnvToProcess()reuses the existing terminal base-env snapshot (one login-shell probe per boot); launcher-set vars and PATH order always win, login-shell entries append. - Dropped
agent-setup/utils.ts(dead code, flagged as such byno-main-process-blocking.test.ts; its biome + ratchet allowances removed).
Manual QA Checklist
Validated end-to-end in Docker (linux-arm64, the build-dist-linux-docker.sh CI-mirror flow) simulating a systemd host: env -i stripped PATH, fresh $HOME, no SUPERSET_HOME_DIR, running the shipped superset-host:
- Repo smoke test passes on the built tarball (native addons, pty spawn, host boot)
- First boot provisions
notify.sh(v8), 12 bin wrappers, zsh/bash bootstrap with OSC 133 marker, all 8 Claude hook groups, Codex/Gemini configs, managed skills — resolved fromlib/agent-templates - Full #6254 chain: provisioned
notify.sh→ POSTnotifications.hook(200) → real row interminal_agent_bindings(seeded terminal session) - Unknown terminal id still 200s and records nothing (hook is deliberately unauthenticated)
- Restart is idempotent:
notify.shmtime unchanged, no duplicate hook entries - Login-shell-only PATH dir (via
.profile) merged into host process (log asserted) - Desktop:
electron-vite buildcopies templates+plugin todist/main/templates; bundles carry the provisioning +setAgentSetupTemplatesDir(__dirname/templates)wiring - Desktop dev boot from a worktree provisions into
superset-dev-data(dev-workspaceSUPERSET_HOME_DIRcase) and logs[agent-setup] Agent integrations provisioned - macOS TS-source simulation (sandboxed
$HOME): host-service provisioning writes all artifacts, plugin falls back to repoplugins/superset
Testing
packages/agent-setup:bun test(109 pass; moved suites) +tscpackages/host-service:bun test(1089 pass, incl.no-electron-coupling) +tsc; newlogin-shell-env.test.ts(5 tests, verified they fail against a mutated implementation)packages/cli:spawn.test.ts+tscapps/desktop:bun run typecheck; suites around every touched import site (terminal, terminal-host, settings/workspaces routers, bundled-cli,no-main-process-blocking) — 460+ passbunx sherif,biome checkon all touched files- The headless E2E above is checked in as
packages/cli/scripts/headless-e2e.shand runs automatically inbuild-dist-linux-docker.shafter the smoke test (guarded behindSUPERSET_HEADLESS_E2E=1— it wipes$HOMEagent configs, so container-only). Verified end-to-end via the checked-in flow.
Design Decisions
- Shared package vs. moving into host-service: the desktop still needs the same writers (settings toggles call
setupSingleAgent/teardownSingleAgent; terminal spawn uses the shell-wrapper args), so a package both depend on avoids a desktop→host-service source dependency. Host-service'sno-electron-couplingtest stays honest — the package has zero Electron imports. - Provision in
serve.ts, notapp.ts:app.tsis shared with the desktop's host-service child, which would double-provision against the desktop's own boot call (and ignoredisabledAgentHooks). noUncheckedIndexedAccess: falsein the package tsconfig matches the desktop config the code was written under, but the five sites that leaked into stricter consumers were fixed properly — host-service compiles the package source under its own strict options.
Known Limitations
- Cross-version churn: a desktop and CLI at different versions rewrite each other's managed files at each boot (content markers differ). Both versions stay functional; writes are atomic (temp+rename), so this is churn, not corruption. Accepted.
- The full remote loop (headless host registered to a cloud org, viewed from a desktop through the relay) was verified as two halves — host side (Docker E2E: hooks → bindings) and UI side (CDP: bindings → sidebar indicator) — not as one wire; that seam is pre-existing plumbing this PR doesn't touch.
Follow-ups (surveyed, intentionally not in this PR)
Other Electron-only functionality CLI hosts miss, per the #6254-class audit: chat-v3 migrations/Claude bin absent from the CLI tarball, HOST_MANIFEST_DIR (attachments land in host/standalone), per-terminal resource metrics, serve.ts binding all interfaces without a hostname, .superset/ worktree copy.
Risks / Rollout
- Risk: provisioning now runs on hosts it never ran on; every writer is individually try/caught (one bad
$HOMEcan't block boot) and content-gated (marker/signature checks — never touches user-owned files), the same code the desktop has shipped for months. - Rollout: no ordering constraints; desktop and CLI halves are independently safe (env fallbacks cover version skew).
- Rollback: revert the two commits; provisioned files are inert without a Superset terminal (
notify.shexits unlessSUPERSET_TERMINAL_IDis set).
Summary by cubic
Provision agent hooks, PATH wrappers, and shell bootstrap on standalone host-service startup, and merge the login-shell env. Previously only Electron-launched hosts provisioned these; headless hosts inherited a minimal env, leaving agent status blank and tools unresolved.
- Shared
@superset/agent-setup: desktop setssetAgentSetupTemplatesDir(path.join(__dirname, "templates")), mirrors disabled agents viawriteSharedDisabledAgentIds(), and callssetupAgentIntegrations(). Standalone host-service resolves templates fromlib/agent-templatesorSUPERSET_AGENT_TEMPLATES_DIRand runsprovisionAgentIntegrations(); missing templates now emit one loud error. - Shared disable state: desktop mirrors its disable list to
~/.superset/agent-hooks.json; headless hosts honor the file andSUPERSET_DISABLED_AGENT_HOOKS, so all provisioners converge on one set. - Env: CLI forwards
SUPERSET_HOME_DIR; host-service falls back to~/.supersetviaresolveSupersetHomeDir()in terminal launch and shell-ready evidence. Host-service merges login-shell env/PATH at boot (launcher vars and PATH order win; missing PATH entries append), never importsNODE_ENV,NODE_OPTIONS, orELECTRON_RUN_AS_NODE, and validatesDESKTOP_NOTIFICATIONS_PORTwith a 51741 default. - Hardening: provisioning writes are idempotent and atomic via
writeFileIfChanged, avoiding torn user configs under concurrent provisioners. - Distribution/CI: CLI tarball ships
lib/agent-templates/plus the bundled Claude plugin. Linux CI installs zsh and runs a headless-host E2E that verifies first-boot provisioning, notify.sh →notifications.hookDB writes, login-shell PATH merge under a stripped env, disable/enable paths, concurrent provisioners, and restart idempotency. A per‑agent matrix dispatches through each of the 13 agents’ own artifacts and asserts host DB bindings.
Rollout
- No migration. Custom launchers must co‑locate templates next to the host bundle or set
SUPERSET_AGENT_TEMPLATES_DIR. Headless hosts provision all agents; setSUPERSET_DISABLED_AGENT_HOOKSto disable specific agents.
Summary by CodeRabbit
-
New Features
- Added lifecycle integrations for Droid, Kimi, Mastra, and Vibe.
- Expanded coding-agent support with managed hooks, wrappers, notifications, skills, and support for 13 agents.
- Added automatic integration provisioning for desktop, standalone, and headless environments.
- Added persistent controls for disabling selected agent hooks.
- Applied login-shell environment settings more consistently to terminal sessions.
- Added configurable template and installation locations.
-
Bug Fixes
- Improved preservation of existing agent configuration during updates.
- Increased reliability when recovering from incomplete or invalid configuration files.
- Improved setup consistency across desktop, CLI, and host-service environments.