-
Notifications
You must be signed in to change notification settings - Fork 0
ARCHITECTURE
Analysis Date: 2026-08-15
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.
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 layer — src/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.py—assemble(root, stack_name, build_dir, harness)→AssembleResultcontaining the resolvedStack,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 layer — backend.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).
| 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.
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
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
-
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@registerdecorator. Backends self-register into_REGISTRY;get_backend(name)returns the class. BothContainerBackend(launcher.py:2783) andHostBackend(launcher.py:2167) live inlauncher.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.pyandhosthome.pyare 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_recipesresolves the full recipe closure includingextends:inheritance. -
catalog_roots()(src/harnessed/paths.py) — returns threePaths 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 containingcatalog/; either the repo root (dev) or the installed package directory (installed wheel). Overrideable withHARNESSED_DIR. Every build-context path anchors here.
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 | 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.
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.
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.
Start Here
Guides
- Recipe authoring
- Service authoring
- Stacks
- Extending stacks (proposed)
- Recipe catalog
- System prompt & rules (proposed)
- Secrets
- AWS SSO
- Pulumi (host login forwarding)
- Egress & exposing services
- Container filesystem
- Git hooks
- Troubleshooting
- Pin management (harnessed update)
Codebase Map
Planning & Roadmap
- open work: GitHub Issues
Research & Prompts
- research/ (home-folder requirements per harness, browse in-repo)
- prompts/ (reusable prompt templates, browse in-repo)