Skip to content
Mike Crowe edited this page Jul 6, 2026 · 5 revisions

Stack

Technology stack, runtime, and configuration for the harnessed Python CLI.


Language

Python 3.12+ (requires-python = ">=3.12" in pyproject.toml). Type hints are used throughout all source modules. from __future__ import annotations deferred evaluation is present in every module under src/harnessed/.


Toolchain Management: mise

mise.toml is the single toolchain configuration file. It sets the Python virtual environment location outside the repo so that a bind-mounted clone (running inside a podman container) cannot corrupt the host venv:

# mise.toml
[env]
_BRANCH = "{{exec(command=\"git branch --show-current\")}}"
UV_PROJECT_ENVIRONMENT = "{{env.HOME}}/.local/share/harnessed/venvs/{{env._BRANCH}}/.venv"
_.source = "{{env.UV_PROJECT_ENVIRONMENT}}/bin/activate"

The venv path is ~/.local/share/harnessed/venvs/<branch>/.venv — one per git branch, preventing cross-branch dependency clobbering.

Inside the container images, mise is also the runtime manager that installs and shims Node 22, Python 3.12, pnpm 11, Bun 1.2, Rust 1.87, Go 1.24, fd, ripgrep, uv, and osv-scanner (see catalog/base/Dockerfile.harnessed-base).


Package Manager: uv

uv is used for all Python dependency resolution and venv management on the host. It is not on the ambient PATH — every invocation must be prefixed with mise exec -- uv ….

The package manifest is pyproject.toml using setuptools as the build backend:

[build-system]
requires = ["setuptools>=68"]
build-backend = "setuptools.build_meta"

Lock file: uv.lock. Run mise exec -- uv sync to install.


Runtime Dependencies

Defined in pyproject.toml [project.dependencies]:

Package Pin Role
ruamel.yaml >=0.18,<0.19 YAML parsing for recipe/stack/agent/service manifests
rich >=14,<15 Terminal output: panels, tables, markdown rendering, console
typer >=0.12,<1.0 harnessed launcher CLI (Typer app in launcher.py)
pip-audit ==2.10.1 (exact) Supply-chain scan of Python requirements in recipe dirs

ruamel.yaml

schema.py constructs the parser with YAML(typ="safe", pure=True) — safe loader, pure Python (no C extension dependency). All recipe/stack/agent/service YAML is parsed through this instance.

# src/harnessed/schema.py
from ruamel.yaml import YAML
_yaml = YAML(typ="safe", pure=True)

rich

Used in both CLIs for all terminal output:

  • rich.console.Console — standard and stderr consoles in launcher.py and cli.py
  • rich.markdown.Markdown — capability report rendering in report.py
# src/harnessed/launcher.py
from rich.console import Console
_out = Console()
_err = Console(stderr=True)

typer

launcher.py builds a Typer application for the harnessed CLI:

# src/harnessed/launcher.py
import typer
app = typer.Typer(
    name="harnessed",
    help="Launch composable harness stacks (claude/omp/opencode/gemini/antigravity/codex + hatago MCP hub).",
    add_completion=False,
)

Only claude and omp are launchable today (only catalog/agents/claude and catalog/agents/omp ship an agent.yaml). opencode/gemini/antigravity/codex have base Dockerfiles but no agent definition — planned/unmerged, not shipped (tracked as bd main-9sv).

cli.py (harnessed-tools) uses the standard library argparse instead of Typer.


Dev Dependencies

Defined in pyproject.toml [project.optional-dependencies] under dev:

Package Role
pytest>=8 Test runner
pytest-cov Coverage reporting
jsonschema>=4 JSON Schema validation (used in tests/test_catalog_json_schemas.py)

Test configuration:

[tool.pytest.ini_options]
testpaths = ["tests"]

Tests are in tests/. Fixtures live under tests/fixtures/. Podman-gated integration tests are guarded by the HARNESSED_PODMAN=1 environment variable.


CLI Entrypoints

Two separate CLI entry points, both defined in pyproject.toml:

[project.scripts]
harnessed-tools = "harnessed.cli:main"
harnessed       = "harnessed.launcher:main"
  • harnessed (launcher.py) — the user-facing launcher; drives podman, builds images, starts pods, manages persist directories, seeds credentials. Built with Typer.
  • harnessed-tools (cli.py) — the emit-only build-time assembler (runs inside the tools image, never invokes podman). Built with argparse. Subcommands: assemble, test, scan, scan-image, scan-image-online, scan-snyk-container, persist-list, persist-prune.

Source Module Layout

All application code lives under src/harnessed/:

Module Responsibility
launcher.py Typer CLI: build, launch, test, new, svc, init, persist-* commands; all podman invocations
cli.py argparse CLI: emit-only assembler + scan + capability-test entrypoints for the tools image
assemble.py Orchestrates the emit-only assembly: resolves stack + recipes, merges MCP servers, fans skills/commands, calls emit functions
emit.py Writes assembled artifacts (.mcp.json, hatago.config.json, derived Dockerfile, profile tree) — pure file I/O, never touches podman
schema.py Typed dataclasses and YAML loading for Recipe, Stack, Agent, Service, McpServer, PersistEntry, InitSpec — also holds recipe lint validators (validate_no_raw_npm, validate_pin)
paths.py Single source of truth for host-side and container-side path resolution: XDG dirs, catalog roots, instance_name, profile_dir, persist_root, hatago_endpoint
capability.py Per-stack capability test oracle: derives expected capabilities from the manifest, launches the stack headless, introspects the live pod, compares expected vs. actual
report.py Renders the structured CapabilityReport as a rich markdown table or JSON for CI
scan.py Supply-chain scan gate: osv-scanner (offline source + image), pip-audit, snyk; CVSS v3 score parsing in pure Python
synclinks.py Fans recipe skills/commands/rules into the harness profile tree; fail-fast on name collisions
persist.py Global persist allowlist + ownership guard: default-deny for global: recipe persist entries; hard-deny for ~/.ssh, ~/.aws, ~/.gnupg, ~/.config/harnessed
persist_gc.py Persist directory lifecycle: list and prune entries under persist_root()

Configuration Approach

Catalog (authorable content)

All authorable content lives under catalog/ in the repo and in the user overlay at ~/.config/harnessed/catalog. The overlay wins on a name clash. Paths are resolved by paths.find_in_catalog(kind, name) in paths.py.

Catalog sub-trees:

catalog/
├── agents/<name>/agent.yaml      # AI harness definition (image, dockerfile)
├── base/                          # shared base Dockerfiles, pnpm policy, egress script
├── recipes/<name>/               # recipe.yaml [+ skills/ commands/ rules/ Dockerfile]
├── services/<name>/              # service.yaml + Dockerfile + server
└── stacks/<agent>_<recipe>…/stack.yaml

Recipe schema

Recipes are validated against schemas/recipe.schema.json (JSON Schema draft 2020-12). The schema is referenced via YAML front-matter in every recipe.yaml:

# yaml-language-server: $schema=../../../schemas/recipe.schema.json

Recipe fields include: name, description, conflicts, mcp.servers, skills, commands, rules, expect, persist, init.

Generated profiles (not in repo)

Assembled profiles are emitted to $XDG_DATA_HOME/harnessed/profiles/<stack>/ — never under the repo clone. The clone stays an immutable source. Paths follow XDG Base Directory Specification throughout paths.py:

# src/harnessed/paths.py
def xdg_data_home() -> Path:
    xdg = os.environ.get("XDG_DATA_HOME", "")
    return Path(xdg) if xdg else Path.home() / ".local" / "share"

Environment variable overrides

Variable Default Effect
HATAGO_PORT 3535 Override the hatago hub port
HARNESSED_DIR auto-detected Override the repo root (for launcher resolution)
HARNESSED_HEADLESS false Suppress interactive attach (used by capability test)
HARNESSED_NET "" Override the podman network name
HARNESSED_NO_SCANS Set to true to skip supply-chain scans during build
NO_FIREWALL false Skip egress firewall application
SNYK_TOKEN Enable Snyk supply-chain scanning
OP_SERVICE_ACCOUNT_TOKEN 1Password service account for CI (headless, no desktop app)
XDG_DATA_HOME ~/.local/share XDG data root
XDG_CONFIG_HOME ~/.config XDG config root
XDG_STATE_HOME ~/.local/state XDG state root

Container Image Toolchain (inside the base image)

The catalog/base/Dockerfile.harnessed-base image establishes the in-container toolchain via mise use -g:

node@22   pnpm@11   python@3.12   bun@1.2   rust@1.87   go@1.24
fd   ripgrep   uv   osv-scanner

pnpm is pinned at v11 and governed by a managed supply-chain policy baked at ~/.config/pnpm/config.yaml (catalog/base/pnpm/config.yaml). Key policy settings:

minimumReleaseAge: 1440          # 1 day minimum before a release can be installed
minimumReleaseAgeStrict: true
blockExoticSubdeps: true
verifyStoreIntegrity: true
strictDepBuilds: true            # default-deny lifecycle scripts

npm itself is self-upgraded inside the base image to npm@11.6.4 (pinned) to clear CVEs in the node-bundled npm that osv-scanner would otherwise flag.

User-extensible tool installs are driven by extra-tools.txt (not committed; defaults in extra-tools.default.txt) and installed via mise use -g at image build time.

Clone this wiki locally