Skip to content

ARCHITECTURE

Mike Crowe edited this page Aug 15, 2026 · 5 revisions

Architecture

Analysis Date: 2026-08-15

Pattern

Layered composition CLI. harnessed is a host-native Python CLI that assembles recipe bundles into profiles and then launches them via a pluggable execution backend (container pod or host-native process). The composition layer is backend-agnostic — assemble.py produces the same profile tree regardless of where it will run. The backend is chosen at launch time.


Layers

CLI (Typer / argparse)
  └─ Composition layer      assemble.py + schema.py + paths.py + emit.py
       └─ Execution backend backend.py (ABC) ──┬── ContainerBackend  (launcher.py)
                                               └── HostBackend       (launcher.py)
                                                     helpers: hostrun.py + hosthome.py

CLI layersrc/harnessed/launcher.py (Typer, harnessed binary) and src/harnessed/cli.py (argparse, harnessed-tools binary). The CLI parses flags, resolves the stack name, constructs a LaunchSpec, and delegates to the appropriate backend.

Composition layer — reads catalog YAML, validates it against typed models, fans recipe content into a profile directory, and writes the harness config files. No containers, no subprocesses: all in-process, pure file I/O.

  • schema.py — typed dataclasses (Recipe, Stack, ServiceDef, Agent) and catalog resolution (load_recipe, load_stack, load_stack_with_recipes, load_agent, load_service).
  • paths.py — single source of truth for every path used at runtime. catalog_roots() returns the three-root search order (user overlay → repo/wheel → generated). harnessed_home() anchors all build context paths.
  • assemble.pyassemble(root, stack_name, build_dir, harness)AssembleResult containing the resolved Stack, recipes, profile_dir, and merged MCP server list.
  • emit.py — writes profile files: write_mcp_json, write_hatago_config, write_settings_json, write_claude_md, write_omp_identity. Pure I/O, no side effects beyond the profile directory.

Execution backend layerbackend.py defines the ExecutionBackend ABC and LaunchSpec. Each backend implements six capabilities in its own sequence:

Capability Purpose
materialize_config Deliver the assembled profile to where the harness reads it
provision_tools(phase) Run install: scripts (FIRST_START) and setup.script (ATTACH)
wire_mcp Present the stack's MCP servers to the harness via hatago or .mcp.json
seed_auth Reference (never copy) host credentials
wire_services Start service sidecars and route the harness to them
apply_isolation(phase) Stand the boundary up (BOUNDARY), then close egress (EGRESS)

The sequencing is backend-owned. The two existing implementations disagree on ordering and cannot share a driver without changing behavior (see backend.py module docstring).


Entry Points

Binary Module Purpose
harnessed src/harnessed/launcher.py:main Primary CLI: build, container-run, host-run, list, stop, rm, test, new, update, svc, …
harnessed-tools src/harnessed/cli.py:main Secondary CLI: assemble, scan, persist-list, persist-prune

Both are declared under [project.scripts] in pyproject.toml.


Call Chain: harnessed container-run <harness> --stack <name>

launcher.py:container_run()
  └─ dynstack.py:_resolve_or_mint_stack()     # --recipe → generate stack.yaml if needed
  └─ lastrun.py:record_last_run()             # --last support
  └─ launchenv.py:_resolve_launch_secrets()   # varlock / env-file resolution
  └─ assemble.assemble()                      # in-process: stack → profile
       └─ schema.load_stack_with_recipes()    # catalog resolution
       └─ emit.*()                            # write hatago.config.json, .mcp.json, settings.json
  └─ launcher._staged_build_context()         # temp copy of catalog/ for podman build context
  └─ launcher._build_base_image()             # builds shared base + hatago baked in
  └─ launcher._build_derived_image()          # recipe Dockerfile layers (if any recipe ships one)
  └─ volumes._ensure_stack_volumes()          # fingerprint-gated config + tools volumes
       └─ volumes._run_container_installs()   # install: scripts inside a container
  └─ volumes._ensure_config_volume()          # merge installer-written settings.json back
  └─ mounts._build_mount_args()              # construct all podman mount flags
  └─ credmounts / mounts: seed auth          # OAuth token or credential seed mount
  └─ svcstate: start service sidecars        # scope: global idempotently, scope: project keyed
  └─ aoe.py: register in Agent of Empires    # optional, if aoe is installed
  └─ os.execvp(podman run …)                 # hand terminal to the agent container

Call Chain: harnessed host-run <harness> --stack <name>

launcher.py:host_run()
  └─ assemble.assemble()                      # same in-process composition
  └─ hosthome._materialize_host_home()        # shutil.rmtree + copy profile into host_home dir
  └─ hostrun._host_run_installs()             # install: scripts in subprocess, tool-dir redirected
  └─ hosthome._share_host_claude_state()      # symlink history/sessions/memories to shared store
  └─ hosthome._rescue_host_credentials()      # credential rescue (token-free path only)
  └─ hostrun._host_run_inits() / _setups()   # init: and setup.script per recipe
  └─ svcstate: start service sidecars        # same as container path
  └─ os.execvp(<harness> ...)                # exec Claude Code (or future harness) in place

Key Abstractions

  • LaunchSpec (src/harnessed/backend.py) — frozen dataclass: stack, harness, project_path, extra, no_strict_mcp, ephemeral. Everything the composition layer produces that a backend needs; backend-specific state stays on the backend instance.

  • ExecutionBackend (src/harnessed/backend.py) — ABC with @register decorator. Backends self-register into _REGISTRY; get_backend(name) returns the class. Both ContainerBackend (launcher.py:2783) and HostBackend (launcher.py:2167) live in launcher.py, next to the private helpers they call — backend.py's own docstring gives that as the reason the dependency points into the contract and adds no import cycle. hostrun.py and hosthome.py are host-path helper modules those methods call, not the backend class itself.

  • AssembleResult (src/harnessed/assemble.py) — Stack, harness, recipes: list[Recipe], profile_dir: Path, servers: list[McpServer], baked: list[McpServer].

  • Recipe / Stack / ServiceDef / Agent (src/harnessed/schema.py) — catalog YAML parsed into typed dataclasses. load_stack_with_recipes resolves the full recipe closure including extends: inheritance.

  • catalog_roots() (src/harnessed/paths.py) — returns three Paths in precedence order: user overlay (~/.config/harnessed/catalog), installed/repo catalog, generated catalog ($XDG_DATA_HOME/harnessed/generated/).

  • harnessed_home() (src/harnessed/paths.py) — resolves to the dir containing catalog/; either the repo root (dev) or the installed package directory (installed wheel). Overrideable with HARNESSED_DIR. Every build-context path anchors here.


Catalog Resolution (three-root search)

find_in_catalog(kind, name)
  1. user overlay: ~/.config/harnessed/catalog/<kind>/<name>/
  2. repo/wheel:   <harnessed_home>/catalog/<kind>/<name>/
  3. generated:    $XDG_DATA_HOME/harnessed/generated/<kind>/<name>/

Overlay wins on a name clash. Generated is last so it cannot shadow an authored stack. src/harnessed/catalog is a symlink to the repo-root catalog/; setuptools follows it, so the catalog ships inside the installed wheel.


State Management

State Location Mechanism
Assembled profiles $XDG_DATA_HOME/harnessed/profiles/<stack>/<harness>/ Files written by emit.py
Per-stack config volume podman volume harnessed-cfg-<harness>-<stack> podman named volume
Per-stack tools volume podman volume harnessed-tools-<harness>-<stack> podman named volume
Host-native stack home $XDG_DATA_HOME/harnessed/host/<stack>/<harness>/ hosthome.py
Persist entries (per-project) $XDG_DATA_HOME/harnessed/persist/<recipe>/<project-hash>/ bind-mount directories
Last-run record $XDG_STATE_HOME/harnessed/lastrun/ lastrun.py
Service port registry $XDG_STATE_HOME/harnessed/svc-ports.json paths.svc_ports_file()
Install cache $XDG_CACHE_HOME/harnessed/install/<recipe>/<cache-key>/ paths.install_cache_dir()

No database. State is files and directories, addressed by stack/harness/project-hash keys computed in paths.py. The repo never holds generated state — profiles/ and catalog-local/ are gitignored.


Build vs. Run split

harnessed build and container-run share _build_stack and _ensure_stack_volumes. The build gate (is_built + fingerprint check in volumes.py) makes container-run transparent when images are current — it only invokes the builder when something changed.

host-run has no image and no volume — it assembles in-process on every launch and materializes the profile under host_home. The hosthome._materialize_host_home wipe-and-copy on every launch is what makes "a removed recipe's files never linger" structurally guaranteed.


MCP Hub (hatago)

Every container launch runs hatago as a process inside the agent container (baked into the base image). All recipe MCP servers route through hatago. The harness's .mcp.json contains one entry — the hatago hub — so the harness never sees individual server addresses. emit.write_hatago_config produces hatago.config.json; emit.write_mcp_json produces the single-entry .mcp.json. Direct MCP entries (bypassing hatago) are possible on supported harnesses and go through emit._direct_entry.

Host-native launches can also use hatago when _host_native_mcp wires it, but the host backend does not require it.

Clone this wiki locally