Skip to content

Agent harnesses move to a config registry, with Muse and Grok on board - #79

Merged
webdevcody merged 3 commits into
AgentSystemLabs:mainfrom
kaminskypavel:ft/muse-harness
Sep 16, 2026
Merged

webdevcody merged 3 commits into
AgentSystemLabs:mainfrom
kaminskypavel:ft/muse-harness

Conversation

@kaminskypavel

Copy link
Copy Markdown
Contributor

Follow-up to #66: every agent CLI used to be a Rust edit across the daemon and the TUI, and turning one off meant code-adjacent settings. This PR puts all of it in one config table, so adding or tuning a harness no longer needs a rebuild, and it keeps working for everyone because the built-ins compile in.

What you get

  • One harnesses map in config drives the n picker, the e presets, spawn, resume, hooks and session titles. User entries merge over the built-ins, legacy keys fold in as fallback, and a broken file falls back to defaults with a warning instead of refusing to start.
  • The Agents settings tab renders one section per entry, so a new harness shows up there with zero UI code. ? (or i) and s jump from the New session dialog straight to its settings.
  • nebula config harnesses prints the merged table, so you can see what the daemon sees.

Proof it is easy now: Muse and Grok

Muse ships as one built-in descriptor row. Grok needed only a config block (binary path, model flag, session flag; it has no hooks dialect, so it uses process-based status and fresh boot). No Rust changed for either, and the Grok session below is live.

New session picker showing the Grok row and the s/? settings hint

Agents settings tab with one section per built-in harness

A live Grok session running in a terminal

How it works

Descriptors live in nebula-core/harness.rs (flags, resume, system prompt, hooks dialect, effort presets). The daemon merges config over the built-ins at load; the TUI reads the same table instead of its old static specs. Users without a harnesses key in config.json see all five built-ins automatically.

Risk

Low. Reads are lenient and failures isolate per harness, existing configs are untouched, and model behavior is unchanged (suite counts below). Rollback: git revert restores the static table, and a harnesses key left in config.json is ignored by older builds since config parsing does not deny unknown fields.

Gate

cargo test green: core 30, daemon 265, TUI 831, plus new registry/merge coverage. e2e_tui has 2 failures that reproduce identically on clean HEAD (pre-existing). make ci (fmt/clippy) still needs a run where those tools exist.

…sets, hooks and titles (Muse and Grok included)

Why: every new agent CLI meant touching Rust across the daemon and the
TUI, and turning one off meant code-adjacent settings. Follow-up to AgentSystemLabs#66.

What: a harnesses map in config (built-ins compiled in, user entries
merged over them, legacy keys folded in as fallback) now drives n, e,
spawn, resume, hooks and session titles. The New session dialog and the
Agents settings tab render one section per entry, with ? and s jumping
to its settings. Muse ships as a built-in row; Grok needed only a
config block. Broken entries isolate per harness with a warning.
@kaminskypavel

Copy link
Copy Markdown
Contributor Author

this PR basically makes #67 possible very easily. @webdevcody let me know if you want me to add agy to this PR

@webdevcody

Copy link
Copy Markdown
Contributor

let me spend some time thinking about if this is the approach I want to take moving forward, but so far the idea seems good. I honestly only use claude code and codex for most of my development, so allowing a user to configure other harnesses for their own needs is nice, but I do think this opens another attack surface for the config.json might now have commands nebula executes on bootup and those config files are pushed to remote servers on nebula ssh, so I want to see if maybe putting the harness config in a separate configuration file might reduce that attack surface a bit. I'll decide over the next day or so. thanks fro the pr!

nebula ssh forwards config.json to the remote by default, and a
harnesses entry can repoint program at any binary the remote daemon
then executes on session spawn. Remote-scope exports now strip
harnesses and custom_harnesses (backups keep them), with a stderr
note, so each machine only ever runs programs its own files name.
@kaminskypavel

Copy link
Copy Markdown
Contributor Author

You were right, and my first answer understated it: a harnesses entry can repoint program, and nebula ssh forwards the whole config.json to the remote by default, where the daemon execs it on session spawn. Not partially correct, correct. I verified the path (bundle::export with Scope::Remote carries the full config object; the daemon loads the same file the import merges into).

Just pushed a fix for it: Remote-scope exports now strip harnesses and custom_harnesses before the bundle is built, with a stderr note when something is left behind. Backups are untouched. Why this closes the vector rather than narrowing it:

  • The trust boundary is the transport. After import merges forwarded values into the remote's config.json, nothing downstream can tell "the admin wrote this" from "this arrived over ssh", so no daemon-side shape check could ever be sufficient. The stripped keys never cross the machine boundary at all.
  • Each machine now only ever executes programs named by files only its local writers can touch. config.local.json pinning still works as a second layer since it was never forwarded to begin with.

What still syncs: everything else in config.json, plus presets. What no longer roams: custom harness definitions (binaries differ per machine anyway). One follow-up I did not fold in: presets are forwarded and reference harness kinds, so a preset pointing at a kind the remote lacks should be checked to degrade gracefully.

…ext, not shell syntax

`probe_cli` asked the login shell `command -v '<program>'` with the name
pasted in bare. Before the registry that name came from a fixed table;
now it comes from config.json, where a single quote closes the word and
the rest of the string runs as a command — at daemon boot, since
`warm_cli_probes` asks for every entry, so no session has to be created.

The word now goes through `shell_quote`, the escaping the launch path
already applies to every argument, so it is looked up verbatim. Built-in
names produce the identical line they always did (`command -v 'claude'`).
The test pins both: the unchanged built-in lines, and that a hostile name
fails the lookup quietly under a real /bin/sh instead of running.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@webdevcody
webdevcody merged commit 2d6edc9 into AgentSystemLabs:main Sep 16, 2026
1 check failed
@webdevcody

Copy link
Copy Markdown
Contributor

if you get time, update readme to explain how a user can setup a custom harness with hooks

@kaminskypavel

Copy link
Copy Markdown
Contributor Author

if you get time, update readme to explain how a user can setup a custom harness with hooks

https://github.com/kaminskypavel/nebula/tree/ft/muse-harness#teach-nebula-a-new-agent-cli

also added a nice table
https://github.com/kaminskypavel/nebula/tree/ft/muse-harness#supported-harnesses

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants