-
Notifications
You must be signed in to change notification settings - Fork 0
ARCHITECTURE
Analysis Date: 2026-06-27
┌─────────────────────────────────────────────────────────────────┐
│ HOST: harnessed CLI │
│ `harnessed` (Typer) `harnessed-tools` (argparse) │
│ src/harnessed/launcher.py src/harnessed/cli.py │
└────────────────────────────────────┬────────────────────────────┘
│
┌────────────────▼──────────────┐
│ Assembly Pipeline │
│ assemble.py → emit.py │
│ synclinks.py │
└────────────────┬──────────────┘
│
┌─────────────────────────────▼────────────────────────────┐
│ Schema / Model Layer │
│ src/harnessed/schema.py src/harnessed/paths.py │
└─────────────────────────────┬────────────────────────────┘
│
┌─────────────────────────────▼────────────────────────────┐
│ Catalog (source of truth) │
│ catalog/agents/ catalog/recipes/ catalog/stacks/ │
│ catalog/services/ catalog/base/ │
│ ~/.config/harnessed/catalog/ (user overlay, wins) │
└─────────────────────────────┬────────────────────────────┘
│ podman build / pod create
┌─────────────────────────────▼────────────────────────────┐
│ PODMAN POD (runtime) │
│ harness container + hatago MCP hub + service sidecars │
└──────────────────────────────────────────────────────────┘
| Component | Responsibility | File |
|---|---|---|
| Launcher | Host-facing Typer CLI: build, launch, stop, rm, prune, test, new, svc | src/harnessed/launcher.py |
| CLI / Tools | argparse CLI for harnessed-tools: assemble, scan, scan-image, test |
src/harnessed/cli.py |
| Assembler | Orchestrates catalog→profile pipeline; delegates to emit + synclinks | src/harnessed/assemble.py |
| Emitter | Writes profile artifacts: .mcp.json, settings.json, hatago.config.json, Dockerfile | src/harnessed/emit.py |
| LinkSyncer | Fans recipe skills/commands into profile .claude/ tree; detects name collisions | src/harnessed/synclinks.py |
| Schema | Typed dataclasses + ruamel.yaml parsing for agent/recipe/service/stack YAMLs | src/harnessed/schema.py |
| Paths | Single source of truth for all host/container path resolution and catalog roots | src/harnessed/paths.py |
| Scanner | Supply-chain scans (osv-scanner + pip-audit + snyk); CVSS gating logic | src/harnessed/scan.py |
| Capability | Manifest oracle vs live-pod introspection; structured CapabilityReport | src/harnessed/capability.py |
| Report | Rich terminal render of CapabilityReport; also emits JSON for CI | src/harnessed/report.py |
Overall: Emit-only assembly pipeline feeding a host-native podman orchestrator.
Key Characteristics:
- Assembly is strictly emit-only —
assemble.py,emit.py,schema.py, andsynclinks.pynever invoke podman. Onlylauncher.pydrives the container runtime. - Profiles are a pure function of the catalog: every
harnessed buildregenerates from scratch (emit.reset_profilewipes and recreates). -
Two CLI binaries:
harnessed(user-facing, Typer) andharnessed-tools(build-time assembler, argparse). The launcher callsharnessed-tools assemblein-process viaassemble.assemble(). -
Catalog precedence: user overlay
~/.config/harnessed/catalogwins over repocatalog/on any name clash (seepaths.catalog_roots()). -
One structured result drives two audiences:
CapabilityReportserves both the rich terminal report and the CI exit code (design §18 / D-11). Same pattern inScanResult.
CLI Layer:
- Purpose: Entry points and user interaction
- Location:
src/harnessed/launcher.py,src/harnessed/cli.py - Contains: Typer commands (
launch,build,stop,rm,prune,test,new,svc), argparse subcommands (assemble,test,scan,scan-image,scan-image-online,scan-snyk-container) - Depends on: All other layers
- Used by: End user (host shell),
harnessed buildpipeline
Assembly Layer:
- Purpose: Catalog → profile transformation (emit-only, no podman)
- Location:
src/harnessed/assemble.py,src/harnessed/emit.py,src/harnessed/synclinks.py - Contains:
assemble()orchestrator, file writers, skill/command fan-out with collision detection - Depends on: Schema layer, Paths layer
- Used by: CLI layer (
launcher.build,cli._run_assemble)
Schema / Model Layer:
- Purpose: Typed in-memory representation of all catalog manifests
- Location:
src/harnessed/schema.py - Contains: Dataclasses (
McpServer,Recipe,Stack,FileExt,Expect,Service), YAML loaders, lint validators (validate_no_raw_npm,validate_pin) - Depends on: Paths layer (catalog resolution via
paths.find_in_catalog) - Used by: Assembly layer, CLI layer, Capability layer
Paths Layer:
- Purpose: Single source of truth for all path computations
- Location:
src/harnessed/paths.py - Contains: XDG path helpers, catalog roots, profile dir, instance naming (
instance_name), container path mirrors - Depends on: Nothing (pure stdlib)
- Used by: All other layers
Quality / Testing Layer:
- Purpose: Supply-chain scanning and live capability verification
- Location:
src/harnessed/scan.py,src/harnessed/capability.py,src/harnessed/report.py - Contains: CVSS scoring, osv-scanner/pip-audit/snyk wrappers, headless pod introspection, rich report rendering
- Depends on: Schema layer, Paths layer
- Used by: CLI layer (
cli._run_scan,cli._run_test,launcher.test_stack)
-
launcher.build()→ callsassemble.assemble(root, stack_name, build_dir)(src/harnessed/launcher.py:759) -
assemble()loads stack + recipes viaschema.load_stack_with_recipes()(src/harnessed/assemble.py) - Validators run:
validate_no_raw_npm,validate_pinon each recipe Dockerfile -
_merge_servers()collects all MCP servers, detects name collisions -
_resolve_service_servers()mapsservice:references →host.containers.internal:<port>URLs -
emit.*writes profile artifacts toprofiles/<stack>/(src/harnessed/emit.py) -
LinkSyncer.fan()copies recipe skill/command dirs into profile.claude/tree (src/harnessed/synclinks.py) - Launcher runs
podman buildfor the hatago image, then the derived harness image -
_merge_baked_extensions()extracts baked skills/commands from image back into profile
-
launcher.launch()validates stack built, loadsstack.yaml(src/harnessed/launcher.py:576) -
_ensure_harness_image()lazy-builds agent image if missing -
_ensure_services()idempotently starts any service sidecars (host-published) -
podman pod createwith shared network namespace -
podman runhatago container (withhatago.config.jsonmount) -
podman runharness container (with profile mounts, credential mounts, project mount) -
_wait_hatago()polls port 3535 for readiness -
_attach()exec-attaches the harness binary (e.g.,claude --mcp-config ... --strict-mcp-config)
-
capability.run_capability_test()derives expected capabilities from manifest oracle (schema.expected_capabilities) - Launches stack
--freshheadless viaHARNESSED_HEADLESS=true - Introspects live pod:
hatago://serversresource for MCP, filesystem for skills/commands -
build_report()diffs actual vs expected intoCapabilityReport -
report.emit()renders rich table AND drives CI exit code (same result, two consumers)
State Management:
- Profiles are stateless artifacts regenerated from catalog on every build
- Pod instances are named
harnessed-<stack>-<sha1[:8] of project_path>for stable re-attach - Per-stack state lives in
$XDG_DATA_HOME/harnessed/profiles/<stack>/ - omp is a deliberate exception: shares host
~/.omp/agentread-write (not isolated)
Recipe (schema.Recipe):
- Purpose: A composable capability bundle — MCP servers + skills + commands + optional Dockerfile
- Examples:
catalog/recipes/time/,catalog/recipes/ping/,catalog/recipes/gstack/ - Pattern: Harness-independent; branches on
${HARNESS}build arg inside recipe Dockerfile
Stack (schema.Stack):
- Purpose: One agent + a set of recipes, named
<agent>_<recipe>[_<recipe>...] - Examples:
catalog/stacks/claude_time/stack.yaml,catalog/stacks/claude_gstack_ping_time_greet/stack.yaml - Pattern: Read-only manifest; the assembled profile is what the pod consumes
Profile:
- Purpose: Assembled output — the committed artifact the pod mounts
- Location:
$XDG_DATA_HOME/harnessed/profiles/<stack>/(not repo; generated) - Pattern: Pure function of catalog; wiped and regenerated each build
Catalog (paths.catalog_roots()):
- Purpose: Two-tier catalog lookup — user overlay wins over repo catalog
- Examples: User:
~/.config/harnessed/catalog/recipes/, Repo:catalog/recipes/ - Pattern: First-existing-wins across roots;
paths.find_in_catalog(kind, name)
harnessed (user-facing):
- Location:
src/harnessed/launcher.py(Typerapp) - Triggers: User shell; registered as
[project.scripts]inpyproject.toml - Responsibilities: All interactive commands; drives podman subprocess
harnessed-tools (build-time assembler):
- Location:
src/harnessed/cli.py(argparsemain()) - Triggers:
launcher.build()in-process OR standalone in CI - Responsibilities:
assemble,test,scan,scan-image,scan-snyk-container
-
No container in container:
harnessedruns on the host and drives rootless podman directly — no tool container, no daemon socket DooD -
Emit-only boundary:
assemble.py,emit.py,schema.py,synclinks.pymust never invoke subprocess/podman; onlylauncher.pyandcapability.pymay -
Single harness per stack: Exactly one of
claude|omp|opencode|gemini|antigravity|codexper stack; one pod per(stack, project_path)pair -
pnpm everywhere: No npm/npx; recipe lint (
validate_no_raw_npm) enforces this at build time -
SSE deprecated: MCP transport must be Streamable-HTTP (
type: http); hatago wraps stdio servers - Credentials never baked: Claude credentials mounted read-only; never in image layers or repo files
-
Global state:
_outand_errconsole instances inlauncher.pyare module-level singletons; HATAGO_PORT = 3535 constant shared acrossemit.py,capability.py,paths.py -
omp auth exception: omp bind-mounts
~/.omp/agentread-write (shared host state); all other harnesses are isolated
What happens: A recipe Dockerfile contains its own FROM <base> or bare ARG HARNESS line.
Why it's wrong: The assembler concatenates recipe Dockerfile bodies; a stray FROM resets the build stage, discarding all prior layers.
Do this instead: Recipe Dockerfiles contain only body instructions. The assembler emits FROM harnessed-${HARNESS}:latest and re-declares ARG HARNESS automatically. See emit.write_derived_dockerfile() in src/harnessed/emit.py.
What happens: A path computed inside the tool container is used as a -v source.
Why it's wrong: DooD bind sources resolve on the host daemon; the container's internal path points at nothing.
Do this instead: Pass host HOME/PWD as env; every -v uses host-absolute paths via paths.* helpers. See launcher._build_mount_args() in src/harnessed/launcher.py.
What happens: Code treats osv-scanner exit 1 as "HIGH vulnerability found."
Why it's wrong: osv-scanner exits 1 on ANY finding with no severity flag; low/medium findings would abort the build.
Do this instead: Parse --format json output and apply _cvss3_base() gating at CVSS >= 7.0. See src/harnessed/scan.py.
Strategy: Structured exceptions propagate through layers; CLI handlers catch, render with rich, return integer exit codes.
Patterns:
-
SchemaError— malformed YAML or missing required fields (schema.py) -
RecipeLintError— raw npm/npx usage (schema.py) -
PinValidationError— floating Dockerfile refs like:latest(schema.py) -
CollisionError— duplicate skill/command/MCP-server name across recipes (synclinks.py) -
ScanError— CVSS >= HIGH finding in supply-chain scan (scan.py) -
CapabilityError— capability test cannot run (launch failed) (capability.py) - All are caught at the CLI dispatch layer and rendered via
rich.Console(stderr=True)
Logging: rich.Console() for stdout; rich.Console(stderr=True) for errors. No structured logging framework — [blue][INFO][/blue] prefix convention in launcher.
Validation: Two-phase: schema validation at parse time (ruamel.yaml + dataclasses) and lint validation at assemble time (validate_no_raw_npm, validate_pin).
Authentication: Per-harness. Claude: ~/.claude/.credentials.json mounted read-only + token-free stub. omp: ~/.omp/agent mounted read-write. Other harnesses: harness-specific credential files mounted read-only.
Architecture analysis: 2026-06-27
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)