Skip to content

CONCERNS

Mike Crowe edited this page Jul 31, 2026 · 5 revisions

Concerns & Technical Debt

Analysis Date: 2026-07-31

High Priority

launcher.py is 7008 lines — effectively a monolith

  • Location: src/harnessed/launcher.py
  • Impact: The single largest maintenance risk in the codebase. Container lifecycle, host-native launch, image build, service management, credential forwarding, MCP emit, persist, staleness, daemon-state detection, and CLI dispatch all live in one file. Merges conflict constantly; it is hard to test any slice in isolation; the backend-abstraction epic (harnessed-0tk) cannot land cleanly until the seams are extracted.
  • Fix approach: The open epic harnessed-0tk calls for extracting a backend-interface seam (0tk.1). Start there: move container-run helpers into a _container.py, service lifecycle into _service.py, and host-native launch into _host.py. schema.py (2253 lines) is a distant second and should be reviewed for splitability once launcher is tamed.

compute_recipe_hash ignores catalog/services/ — stale images after service-only edits

  • Location: src/harnessed/assemble.py:50-65
  • Impact: Editing a service's service.yaml or Dockerfile does NOT change the stack image hash, so harnessed build considers the image current and skips a rebuild. The service sidecar image will be rebuilt (it has its own build path), but derived stack image label mismatches can cause a misleading "stale" warning at the wrong time or miss a needed rebuild of an image that embeds service config.
  • Fix approach: Filed as harnessed-p0t (P2). Include catalog/services/<name>/ files in the hash digest for every service referenced by the stack. Mirror the recipe.root.rglob("*") pattern already used for recipe dirs.

Host-native svc launch passes project_path as mount_path — skips widening

  • Location: src/harnessed/launcher.py:5134
  • Impact: In the host-native launch path, _ensure_services is called with mount_path=project_path (the raw project directory), not the widened mount path computed by _resolve_mount_path. For a bare + linked-worktree layout the widened path is the container of the bare repo, and services with location: in_repo rely on the wider mount for git remote operations (bd dolt push). With the narrower path the service container cannot see sibling worktrees, causing git operations to fail silently or error. The container launch path at line 5432 passes mount_path correctly; host-native does not.
  • Fix approach: Filed as harnessed-wnf (P3). Compute the widened mount path before calling _ensure_services in the host-native path, matching what the container launch path does.

Service secret deletion locks out a healthy sidecar (no rotation path)

  • Location: src/harnessed/launcher.py (service credential handling), catalog/services/
  • Impact: Filed as harnessed-1vx (P3 bug). Deleting a service secret while the sidecar is running leaves clients unable to authenticate against a server that is still healthy. There is no graceful rotation path — the only recovery is manual sidecar restart.
  • Fix approach: Either prohibit secret deletion while the service is running (with a clear error), or add a harnessed svc restart command that re-seeds credentials atomically.

gsd-core permission rules are non-durable across launches

  • Location: Filed as harnessed-8ej (P2 epic)
  • Impact: Permission rules are ineffective and do not survive container restarts. This is a security-surface gap — permissions that appear to be in force may not be.
  • Fix approach: Track harnessed-8ej; the fix requires durability in the per-pod permission layer.

Medium Priority

_is_daemon_state short-circuits on directory name "daemon" before content check

  • Location: src/harnessed/launcher.py:4055-4057
  • Impact: The function's docstring explicitly says identification is "by CONTENT, not by name" and that "a recipe is free to ship a directory with any name." Yet line 4055 returns True immediately if entry.name == "daemon". A recipe that ships a directory called daemon/ for its own purposes would be misidentified as Claude Code daemon state and silently preserved across rebuilds, stranding the recipe content.
  • Fix approach: Filed as harnessed-1d3 (P3). Remove the name-shortcut; rely solely on the _DAEMON_STATE_MARKERS content check, which is unambiguous.

Bare except Exception: with silent return in launcher.py

  • Location: src/harnessed/launcher.py:2550, src/harnessed/launcher.py:2569
  • Impact: Both clauses return False without logging. Line 2550 is in _is_user_catalog_stack (a path-resolution guard); line 2569 is in has_token (a credential-presence check). A programming error (wrong type, AttributeError, PermissionError) would silently suppress the check, causing wrong behavior downstream — either treating a non-user-catalog stack as one, or treating a missing token as present. The BLE001-suppressed catch at line 1070 is intentional and annotated; these two are not.
  • Fix approach: Narrow each to the specific exceptions that represent expected-failure conditions (OSError, ValueError). Add at minimum a debug-level log before returning False.

Most subprocess.run calls in launcher.py lack timeout — hangs possible

  • Location: src/harnessed/launcher.py:161, 168, 177, 263, 389, 584, 884, 953, 1091, 1345, 1351, 1388, 1505, 1555, 1592, 1596, 1627, 1642, 1661, 1787, 1799, 2332, 2366 (and more — 62 total calls, majority without timeout)
  • Impact: subprocess.run without timeout= hangs indefinitely if podman becomes unresponsive (e.g., during a network partition while pulling, or a runc deadlock). The process is killed only by the user or the OS. scan.py:160 and capability.py correctly set timeout= per call; launcher.py is inconsistent.
  • Fix approach: Add an optional timeout kwarg to the _podman helper at line 448 and propagate it. Audit each call: podman write operations (rm, create, build) should have a timeout on the order of minutes; inspect/status calls should be seconds.

beads-server restart wedges live sessions (open P1 bug)

  • Location: Filed as harnessed-kf9 (P1 bug)
  • Impact: In published-port mode, restarting the beads-server sidecar pins live agent sessions to the old unix socket reference, causing them to lose access to the beads service without any error surfaced to the user.
  • Fix approach: Track harnessed-kf9; root cause and fix are still under investigation.

env-file token detection can silently remount credential file

  • Location: Filed as harnessed-9hp.3 (P2 bug)
  • Impact: The guard that detects token presence in an env-file may silently remount the credential file even when it should not. A missed guard means credentials are forwarded in contexts where they should not be, widening the blast radius of a compromised container.
  • Fix approach: Track harnessed-9hp.3; requires tightening the detection logic.

Low Priority / Improvement Opportunities

schema.py is the second-largest module at 2253 lines

  • Location: src/harnessed/schema.py
  • Note: Validation, defaults, coercion, and model definitions are all co-located. Not an immediate risk (it has test coverage via tests/test_schema.py and tests/test_schema_thread_safety.py), but splitting validation logic from model definitions would improve readability and isolated testing.

SSE deprecation warns but does not reject

  • Location: src/harnessed/schema.py:1031
  • Note: The schema emits a user-facing warning that SSE transport is deprecated, but does not fail validation. A recipe silently using transport: sse will warn but still assemble. As SSE is deprecated per the architecture constraint ("Streamable-HTTP MCP only"), validation should eventually reject it with a migration hint.

Network-bound hostspike test balloons suite runtime

  • Location: Filed as harnessed-5jo (P3)
  • Note: The network-bound test stretches suite runtime from ~66 s to ~626 s. It should be marked with a pytest marker (@pytest.mark.slow or @pytest.mark.network) and excluded from the default pytest -q invocation, with CI running it on a separate job.

noqa suppressions are minimal and justified

  • Location: src/harnessed/update.py:283, src/harnessed/launcher.py:272, :1070, :2663
  • Note: Four noqa annotations exist: S310 (fixed-host urlopen), BLE001 (annotated broad catch), E731/E306 (cosmetic style). None suppress real risk; no action needed.

TODOs and FIXMEs Found

None found in src/harnessed/. All tracked debt lives in beads (43 open issues as of analysis date).


Missing or Weak Areas

  • No backend abstraction layer: All container operations go through direct subprocess.run([rt, ...]) calls scattered across launcher.py. The backend-abstraction epic (harnessed-0tk) is the right fix, but until it lands there is no seam to swap podman for another runtime in tests, making integration tests depend on a real container runtime.
  • No integration tests for project-scoped service lifecycle: tests/test_launcher_scan.py and tests/test_launcher_build.py cover image build paths, but the project-scoped service lifecycle (keyed by git-common-dir, bind-mount data dir) has no dedicated test file. The harnessed-wnf and harnessed-1vx bugs live in exactly this untested area. A test that validates mount widening in the host-native path would have caught harnessed-wnf before it was filed.
  • Bare worktree / credential gutting incident (harnessed-0cw) has no root-cause prevention: The 2026-07-21 incident (rmtree over live daemon state, credential file zeroed) was mitigated by _clear_host_home_except_runtime. The root cause of why the credential file was overwritten remains open (harnessed-0cw, P2). Until that is understood, the fix is an incomplete guard.

Clone this wiki locally