-
Notifications
You must be signed in to change notification settings - Fork 0
STACK
Technology stack, runtime, and configuration for the harnessed Python CLI.
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/.
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).
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.
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 |
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)Used in both CLIs for all terminal output:
-
rich.console.Console— standard and stderr consoles inlauncher.pyandcli.py -
rich.markdown.Markdown— capability report rendering inreport.py
# src/harnessed/launcher.py
from rich.console import Console
_out = Console()
_err = Console(stderr=True)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,
)cli.py (harnessed-tools) uses the standard library argparse instead of Typer.
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.
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,scan,test,capability-test,persist-list,persist-prune,synclinks.
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()
|
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
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.jsonRecipe fields include: name, description, conflicts, mcp.servers, skills, commands,
rules, expect, persist, init.
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"| 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 |
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 scriptsnpm 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.
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)