skharness 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. skharness-0.3.0/MANIFEST.in +2 -0
  2. skharness-0.3.0/PKG-INFO +15 -0
  3. skharness-0.3.0/README.md +73 -0
  4. skharness-0.3.0/docker/sandbox/build.sh +94 -0
  5. skharness-0.3.0/docker/sandbox/claude/Dockerfile +20 -0
  6. skharness-0.3.0/docker/sandbox/opencode/Dockerfile +29 -0
  7. skharness-0.3.0/docker/sandbox/pi/Dockerfile +21 -0
  8. skharness-0.3.0/docker/sandbox/proxy/Dockerfile +13 -0
  9. skharness-0.3.0/pyproject.toml +63 -0
  10. skharness-0.3.0/setup.cfg +4 -0
  11. skharness-0.3.0/src/skharness/__init__.py +26 -0
  12. skharness-0.3.0/src/skharness/__main__.py +6 -0
  13. skharness-0.3.0/src/skharness/auth.py +30 -0
  14. skharness-0.3.0/src/skharness/autocode/__init__.py +4 -0
  15. skharness-0.3.0/src/skharness/autocode/adapters/__init__.py +26 -0
  16. skharness-0.3.0/src/skharness/autocode/adapters/base.py +316 -0
  17. skharness-0.3.0/src/skharness/autocode/adapters/claude_code.py +134 -0
  18. skharness-0.3.0/src/skharness/autocode/adapters/codex.py +34 -0
  19. skharness-0.3.0/src/skharness/autocode/adapters/opencode.py +127 -0
  20. skharness-0.3.0/src/skharness/autocode/adapters/pi.py +113 -0
  21. skharness-0.3.0/src/skharness/autocode/autoscale.py +121 -0
  22. skharness-0.3.0/src/skharness/autocode/ci.py +159 -0
  23. skharness-0.3.0/src/skharness/autocode/claude_code.py +95 -0
  24. skharness-0.3.0/src/skharness/autocode/cleanup.py +114 -0
  25. skharness-0.3.0/src/skharness/autocode/config.py +138 -0
  26. skharness-0.3.0/src/skharness/autocode/digest.py +131 -0
  27. skharness-0.3.0/src/skharness/autocode/direct.py +96 -0
  28. skharness-0.3.0/src/skharness/autocode/doctor.py +157 -0
  29. skharness-0.3.0/src/skharness/autocode/engineering.py +606 -0
  30. skharness-0.3.0/src/skharness/autocode/executor.py +34 -0
  31. skharness-0.3.0/src/skharness/autocode/fleet_dispatch.py +149 -0
  32. skharness-0.3.0/src/skharness/autocode/harness.py +99 -0
  33. skharness-0.3.0/src/skharness/autocode/health.py +81 -0
  34. skharness-0.3.0/src/skharness/autocode/joules.py +233 -0
  35. skharness-0.3.0/src/skharness/autocode/journal.py +171 -0
  36. skharness-0.3.0/src/skharness/autocode/orchestrator.py +535 -0
  37. skharness-0.3.0/src/skharness/autocode/protected.py +96 -0
  38. skharness-0.3.0/src/skharness/autocode/ratify.py +61 -0
  39. skharness-0.3.0/src/skharness/autocode/remediation.py +250 -0
  40. skharness-0.3.0/src/skharness/autocode/resolver.py +83 -0
  41. skharness-0.3.0/src/skharness/autocode/sandbox.py +167 -0
  42. skharness-0.3.0/src/skharness/autocode/sandbox_proxy.py +153 -0
  43. skharness-0.3.0/src/skharness/autocode/stubs.py +57 -0
  44. skharness-0.3.0/src/skharness/autocode/types.py +159 -0
  45. skharness-0.3.0/src/skharness/client/index.html +217 -0
  46. skharness-0.3.0/src/skharness/daemon.py +163 -0
  47. skharness-0.3.0/src/skharness/events.py +39 -0
  48. skharness-0.3.0/src/skharness/gateway.py +51 -0
  49. skharness-0.3.0/src/skharness/harness.py +371 -0
  50. skharness-0.3.0/src/skharness/harnesses/__init__.py +1 -0
  51. skharness-0.3.0/src/skharness/harnesses/claude_code.py +257 -0
  52. skharness-0.3.0/src/skharness/manager.py +41 -0
  53. skharness-0.3.0/src/skharness/manifest.py +70 -0
  54. skharness-0.3.0/src/skharness/operator_cli.py +357 -0
  55. skharness-0.3.0/src/skharness/registry.py +58 -0
  56. skharness-0.3.0/src/skharness/serve.py +134 -0
  57. skharness-0.3.0/src/skharness/session.py +37 -0
  58. skharness-0.3.0/src/skharness/spawner.py +37 -0
  59. skharness-0.3.0/src/skharness.egg-info/PKG-INFO +15 -0
  60. skharness-0.3.0/src/skharness.egg-info/SOURCES.txt +124 -0
  61. skharness-0.3.0/src/skharness.egg-info/dependency_links.txt +1 -0
  62. skharness-0.3.0/src/skharness.egg-info/entry_points.txt +2 -0
  63. skharness-0.3.0/src/skharness.egg-info/requires.txt +11 -0
  64. skharness-0.3.0/src/skharness.egg-info/top_level.txt +1 -0
  65. skharness-0.3.0/tests/test_adapter_base.py +231 -0
  66. skharness-0.3.0/tests/test_adapter_codex.py +12 -0
  67. skharness-0.3.0/tests/test_adapter_opencode.py +95 -0
  68. skharness-0.3.0/tests/test_adapter_pi.py +90 -0
  69. skharness-0.3.0/tests/test_auth.py +48 -0
  70. skharness-0.3.0/tests/test_autocode_gate_conformance.py +110 -0
  71. skharness-0.3.0/tests/test_autocode_protected.py +96 -0
  72. skharness-0.3.0/tests/test_autocode_ratify.py +115 -0
  73. skharness-0.3.0/tests/test_autocode_toggle.py +267 -0
  74. skharness-0.3.0/tests/test_autopilot_build_executors.py +56 -0
  75. skharness-0.3.0/tests/test_autopilot_ci.py +241 -0
  76. skharness-0.3.0/tests/test_autopilot_claude_code.py +161 -0
  77. skharness-0.3.0/tests/test_autopilot_config.py +56 -0
  78. skharness-0.3.0/tests/test_autopilot_config_harness.py +35 -0
  79. skharness-0.3.0/tests/test_autopilot_config_wrap.py +10 -0
  80. skharness-0.3.0/tests/test_autopilot_digest.py +55 -0
  81. skharness-0.3.0/tests/test_autopilot_digest_send.py +48 -0
  82. skharness-0.3.0/tests/test_autopilot_e2e_dryrun.py +68 -0
  83. skharness-0.3.0/tests/test_autopilot_engineering.py +589 -0
  84. skharness-0.3.0/tests/test_autopilot_executor.py +45 -0
  85. skharness-0.3.0/tests/test_autopilot_firewall_segments.py +20 -0
  86. skharness-0.3.0/tests/test_autopilot_fleet_dispatch.py +123 -0
  87. skharness-0.3.0/tests/test_autopilot_fleet_gate.py +181 -0
  88. skharness-0.3.0/tests/test_autopilot_harness.py +51 -0
  89. skharness-0.3.0/tests/test_autopilot_job_yaml.py +22 -0
  90. skharness-0.3.0/tests/test_autopilot_journal.py +38 -0
  91. skharness-0.3.0/tests/test_autopilot_journal_api.py +30 -0
  92. skharness-0.3.0/tests/test_autopilot_orchestrator.py +520 -0
  93. skharness-0.3.0/tests/test_autopilot_queue_decision.py +26 -0
  94. skharness-0.3.0/tests/test_autopilot_remediation.py +197 -0
  95. skharness-0.3.0/tests/test_autopilot_resolver.py +62 -0
  96. skharness-0.3.0/tests/test_autopilot_revert_wrap.py +12 -0
  97. skharness-0.3.0/tests/test_autopilot_stubs.py +36 -0
  98. skharness-0.3.0/tests/test_autopilot_types.py +35 -0
  99. skharness-0.3.0/tests/test_autopilot_workitem_enrichment.py +36 -0
  100. skharness-0.3.0/tests/test_autoscale.py +62 -0
  101. skharness-0.3.0/tests/test_claude_code_harness.py +300 -0
  102. skharness-0.3.0/tests/test_cleanup.py +65 -0
  103. skharness-0.3.0/tests/test_client.py +37 -0
  104. skharness-0.3.0/tests/test_daemon.py +315 -0
  105. skharness-0.3.0/tests/test_doctor.py +95 -0
  106. skharness-0.3.0/tests/test_events.py +29 -0
  107. skharness-0.3.0/tests/test_gateway.py +70 -0
  108. skharness-0.3.0/tests/test_harness_registry.py +29 -0
  109. skharness-0.3.0/tests/test_health.py +53 -0
  110. skharness-0.3.0/tests/test_joules.py +139 -0
  111. skharness-0.3.0/tests/test_manager.py +52 -0
  112. skharness-0.3.0/tests/test_manifest.py +43 -0
  113. skharness-0.3.0/tests/test_operator_cli.py +375 -0
  114. skharness-0.3.0/tests/test_registry.py +23 -0
  115. skharness-0.3.0/tests/test_sandbox_argv.py +65 -0
  116. skharness-0.3.0/tests/test_sandbox_config_files.py +80 -0
  117. skharness-0.3.0/tests/test_sandbox_confinement_it.py +100 -0
  118. skharness-0.3.0/tests/test_sandbox_proxy.py +86 -0
  119. skharness-0.3.0/tests/test_sandbox_spawn.py +105 -0
  120. skharness-0.3.0/tests/test_serve.py +23 -0
  121. skharness-0.3.0/tests/test_session.py +15 -0
  122. skharness-0.3.0/tests/test_session_plane.py +91 -0
  123. skharness-0.3.0/tests/test_spawner.py +22 -0
  124. skharness-0.3.0/tests/test_systemd_unit.py +71 -0
  125. skharness-0.3.0/tests/test_unified_harness.py +169 -0
  126. skharness-0.3.0/tests/test_verifier.py +174 -0
@@ -0,0 +1,2 @@
1
+ recursive-include docker/sandbox *
2
+ include README.md
@@ -0,0 +1,15 @@
1
+ Metadata-Version: 2.4
2
+ Name: skharness
3
+ Version: 0.3.0
4
+ Summary: Sovereign phone-drives-my-agent-swarm harness (execution: sessions + autocode engine)
5
+ Requires-Python: >=3.10
6
+ Requires-Dist: fastapi>=0.110
7
+ Requires-Dist: pydantic>=2.0
8
+ Requires-Dist: pyyaml>=6.0
9
+ Requires-Dist: uvicorn>=0.27
10
+ Requires-Dist: websockets>=12.0
11
+ Provides-Extra: dev
12
+ Requires-Dist: pytest>=7.0; extra == "dev"
13
+ Requires-Dist: pytest-mock>=3.10; extra == "dev"
14
+ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
15
+ Requires-Dist: httpx>=0.27; extra == "dev"
@@ -0,0 +1,73 @@
1
+ # skharness
2
+
3
+ **Sovereign "phone-drives-my-agent-swarm" harness.**
4
+
5
+ Run many coding/work agents, each in an **isolated session**, and **drive them from a phone
6
+ over the tailnet** — the feel of Claude Code's remote control, but **self-hosted on SKWorld
7
+ infra with no Big-Tech broker**.
8
+
9
+ ## Why
10
+ Claude Code Remote Control has great UX but routes phone↔laptop through Anthropic's relay —
11
+ it fails the sovereignty test. skharness replicates the *experience*, not the foundation, over
12
+ **Tailscale + capauth**. (Design basis: the 2026-06-13 harness deep-research — pi / cmux /
13
+ OpenCode / web-shells / Claude Code Remote Control. Full spec in
14
+ `docs/superpowers/specs/2026-06-13-skharness-design.md`.)
15
+
16
+ ## Architecture
17
+ A capauth-gated FastAPI over a `SessionManager`:
18
+
19
+ | Module | Role |
20
+ |---|---|
21
+ | `gateway.py` | FastAPI app; **bind to a Tailscale IP only** (never a public port). `verify_caller` is the auth seam — a real capauth verifier in prod, a fake in tests. |
22
+ | `manager.py` | `SessionManager` — ties registry + spawner; spawn creates an isolated worker. |
23
+ | `spawner.py` | Spawner seam — `FakeSpawner` for CI, `TmuxSpawner` for real sessions. |
24
+ | `registry.py` | `SessionRegistry` — track/persist sessions (JSON now; coord-board / skmem-pg later). |
25
+ | `session.py` | `Session` model — one isolated agent worker. |
26
+
27
+ Mostly **reuse**: `pi` (MIT, multi-provider incl. local qwen) as orchestrator + the coord
28
+ board / skmem-pg as the session/task registry.
29
+
30
+ ## Run
31
+ ```bash
32
+ pip install -e . # fastapi + pydantic
33
+ # build_app(manager=..., verify_caller=<capauth verifier>) -> FastAPI
34
+ # serve bound to the tailscale IP only; auth every call via capauth.
35
+ ```
36
+
37
+ ## Status
38
+ `v0.1.0`, P0 session core (spec + plan under `docs/superpowers/`). Mirror: smilinTux (private).
39
+
40
+ ## skcode-hostd (P0, read-only)
41
+
42
+ `skcode-hostd` is the read-only remote-control daemon over the unified Harness
43
+ session plane. It owns ONE harness (the claude-code tmux adapter) and exposes
44
+ exactly three capauth-gated data routes plus a self-contained static client. There
45
+ is NO write surface: no spawn, inject, kill, dispatch, rename, archive, or model
46
+ switch. A test (`tests/test_daemon.py::test_no_write_surface`) proves POST/DELETE
47
+ return 405 and `/inject` / `/dispatch` return 404.
48
+
49
+ Routes:
50
+
51
+ | Method | Path | Purpose |
52
+ |---|---|---|
53
+ | GET | `/api/v1/hosts/self` | host + harness identity |
54
+ | GET | `/api/v1/sessions` | list live + historical sessions |
55
+ | GET | `/api/v1/sessions/{sid}` | one session, or 404 |
56
+ | WS | `/api/v1/sessions/{sid}/stream` | typed `SessionEvent` stream (`?token=`) |
57
+ | GET | `/` and `/app` | the static read-only web client |
58
+
59
+ HTTP routes require a `Bearer` token; the WebSocket takes the token as a
60
+ `?token=` query param (browsers cannot set headers on a WS). The capauth verifier
61
+ in P0 is a fail-closed deny-all placeholder: real verification lands with the
62
+ pairing work (spec 7.6), so the daemon rejects every token until then by design.
63
+
64
+ Run (Tailscale IP only, never `0.0.0.0`):
65
+
66
+ ```bash
67
+ ~/.skenv/bin/python -m skharness --host <your-tailscale-ip> --port 9394 --host-id .158
68
+ ```
69
+
70
+ skcode-hostd defaults to `:9394` (SKWorld platform spec R0.4). Port `:9390` is
71
+ owned by `skcomms.transports.broker_server` (its honest, documented default), so
72
+ the two no longer collide on a shared host. Override with `--port <free>` and
73
+ record it in `~/.skcapstone/docs/PORTS.md` if `:9394` is taken.
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env bash
2
+ # Build the SKOS Autopilot sandbox images: the egress proxy sidecar plus one image
3
+ # per harness (claude-code, pi, opencode). Each image is the confined execution
4
+ # environment a harness runs inside (see docs/skos-autopilot-architecture.md §12
5
+ # and docs/skos-autopilot-SOP.md §13). Images are tagged locally so a node can run
6
+ # confined live execution offline; a node missing them fails closed (no unconfined
7
+ # run is ever attempted).
8
+ #
9
+ # Usage:
10
+ # docker/sandbox/build.sh # build all four images
11
+ # docker/sandbox/build.sh proxy # build a single image (proxy|claude|pi|opencode)
12
+ # docker/sandbox/build.sh pi opencode # build a subset
13
+ # docker/sandbox/build.sh --no-cache # force a clean rebuild of all four
14
+ # docker/sandbox/build.sh -h | --help
15
+ #
16
+ # All four build with plain `docker build` from the repo root (the proxy image
17
+ # COPYs src/skos/autopilot/sandbox_proxy.py). No `--network host` or registry
18
+ # egress is needed: opencode bundles its openai-compatible provider, pi and claude
19
+ # install their CLIs at build time. Next step after a build is the confinement
20
+ # proof: RUN_SANDBOX_IT=1 python -m pytest tests/test_sandbox_confinement_it.py
21
+ set -euo pipefail
22
+
23
+ cd "$(dirname "$0")/../.." # repo root (build context)
24
+ D=docker/sandbox
25
+ TAG=1 # image tag: sandbox-<name>:$TAG
26
+
27
+ # each entry: <name> <dockerfile-subdir> <in-image verify command | ->
28
+ # the verify command runs in the freshly built image and prints the baked CLI
29
+ # version, proving the harness binary is actually present (- = no CLI to check)
30
+ HARNESSES=(
31
+ "proxy proxy -"
32
+ "claude claude claude --version"
33
+ "pi pi pi --version"
34
+ "opencode opencode opencode --version"
35
+ )
36
+
37
+ usage() { sed -n '2,20p' "$0" | sed 's/^# \{0,1\}//'; exit "${1:-0}"; }
38
+
39
+ BUILD_ARGS=()
40
+ WANT=()
41
+ for a in "$@"; do
42
+ case "$a" in
43
+ -h|--help) usage 0 ;;
44
+ --no-cache|--pull) BUILD_ARGS+=("$a") ;;
45
+ proxy|claude|pi|opencode) WANT+=("$a") ;;
46
+ *) echo "unknown arg: $a" >&2; usage 1 ;;
47
+ esac
48
+ done
49
+
50
+ wanted() { # is $1 in the requested set (empty set = all)?
51
+ [ ${#WANT[@]} -eq 0 ] && return 0
52
+ local w; for w in "${WANT[@]}"; do [ "$w" = "$1" ] && return 0; done
53
+ return 1
54
+ }
55
+
56
+ command -v docker >/dev/null 2>&1 || { echo "docker not found on PATH" >&2; exit 127; }
57
+
58
+ declare -A RESULT
59
+ for row in "${HARNESSES[@]}"; do
60
+ read -r name dir verify <<<"$row"
61
+ wanted "$name" || continue
62
+ img="sandbox-${name}:${TAG}"
63
+ echo "== building ${img} =="
64
+ if docker build "${BUILD_ARGS[@]}" -f "$D/$dir/Dockerfile" -t "$img" .; then
65
+ RESULT[$name]=ok
66
+ if [ "$verify" != "-" ]; then
67
+ ver="$(docker run --rm "$img" sh -c "$verify" 2>/dev/null | head -1 || true)"
68
+ echo " ${name} CLI: ${ver:-<version check failed>}"
69
+ fi
70
+ else
71
+ RESULT[$name]=FAIL
72
+ echo " ${img} build FAILED" >&2
73
+ fi
74
+ done
75
+
76
+ echo "== images =="
77
+ docker image ls --format '{{.Repository}}:{{.Tag}} {{.Size}}' \
78
+ | grep -E 'sandbox-(proxy|claude|pi|opencode):'"$TAG" || true
79
+
80
+ echo "== result =="
81
+ fail=0
82
+ for row in "${HARNESSES[@]}"; do
83
+ read -r name _ _ <<<"$row"
84
+ wanted "$name" || continue
85
+ r="${RESULT[$name]:-skipped}"
86
+ printf ' %-9s %s\n' "$name" "$r"
87
+ [ "$r" = "FAIL" ] && fail=1
88
+ done
89
+
90
+ if [ "$fail" -eq 0 ]; then
91
+ echo "== all requested images built. next: confinement proof =="
92
+ echo " RUN_SANDBOX_IT=1 python -m pytest tests/test_sandbox_confinement_it.py -q"
93
+ fi
94
+ exit "$fail"
@@ -0,0 +1,20 @@
1
+ # Sandbox image carrying the Claude Code CLI + git + a python test toolchain.
2
+ # The confined harness runs as non-root user `sbx` in /work (the bind-mounted
3
+ # task worktree). No secrets are baked in; auth is bind-mounted read-only at run.
4
+ FROM node:24-bookworm
5
+
6
+ RUN apt-get update \
7
+ && apt-get install -y --no-install-recommends git ca-certificates python3 python3-pip python3-venv curl \
8
+ && rm -rf /var/lib/apt/lists/*
9
+
10
+ # pinned harness CLI
11
+ RUN npm install -g @anthropic-ai/claude-code
12
+
13
+ # repo test toolchain (bookworm python3 is 3.11; adequate for running repo tests
14
+ # inside the sandbox; the confinement proof does not depend on the minor version)
15
+ RUN pip3 install --no-cache-dir --break-system-packages pytest coverage \
16
+ || pip3 install --no-cache-dir pytest coverage
17
+
18
+ RUN useradd -u 10001 -m sbx
19
+ WORKDIR /work
20
+ USER sbx
@@ -0,0 +1,29 @@
1
+ # Sandbox image carrying the opencode (opencode.ai) CLI + git + python toolchain.
2
+ # Installed sovereign fallback; can target a local model. Confirm the npm package
3
+ # name against opencode.ai; a real `opencode run` output-shape validation is the
4
+ # remaining pre-live step (OpenCodeAdapter._parse is defensive and flagged).
5
+ FROM node:24-bookworm
6
+
7
+ RUN apt-get update \
8
+ && apt-get install -y --no-install-recommends git ca-certificates python3 python3-pip python3-venv curl \
9
+ && rm -rf /var/lib/apt/lists/*
10
+
11
+ # harness CLI: the official installer drops a self-contained binary; copy it onto
12
+ # PATH so any uid can run it.
13
+ RUN curl -fsSL https://opencode.ai/install | bash \
14
+ && cp /root/.opencode/bin/opencode /usr/local/bin/opencode \
15
+ && chmod 0755 /usr/local/bin/opencode
16
+
17
+ RUN pip3 install --no-cache-dir --break-system-packages pytest coverage \
18
+ || pip3 install --no-cache-dir pytest coverage
19
+
20
+ RUN useradd -u 10001 -m sbx
21
+ WORKDIR /work
22
+ USER sbx
23
+
24
+ # NO provider pre-fetch is needed. opencode's self-contained binary already BUNDLES
25
+ # the @ai-sdk/openai-compatible provider, so a custom `"npm": "@ai-sdk/openai-compatible"`
26
+ # provider (the injected skgw route to skgateway) resolves against the bundled copy
27
+ # with zero runtime download. Verified: a confined run of this image against
28
+ # skgateway/ornith-tiny returns a correct reply and leaves no @ai-sdk/* on disk
29
+ # anywhere. The confined sandbox therefore needs no npm egress for opencode.
@@ -0,0 +1,21 @@
1
+ # Sandbox image carrying the pi (pi.dev) CLI + git + python test toolchain.
2
+ # pi is the sovereign target harness: it routes to a local model via skgateway (the
3
+ # skgw provider is injected as /agent/models.json + PI_CODING_AGENT_DIR=/agent by
4
+ # PiAdapter), so all egress stays on-tailnet through the sandbox allowlist proxy.
5
+ # Verified live end-to-end in the confined sandbox against skgateway/ornith-tiny.
6
+ FROM node:24-bookworm
7
+
8
+ RUN apt-get update \
9
+ && apt-get install -y --no-install-recommends git ca-certificates python3 python3-pip python3-venv curl \
10
+ && rm -rf /var/lib/apt/lists/*
11
+
12
+ # pinned harness CLI (pi.dev). --ignore-scripts: pi's postinstall is not needed and
13
+ # would pull extra host-specific artifacts; the CLI runs fine without it.
14
+ RUN npm install -g --ignore-scripts @earendil-works/pi-coding-agent
15
+
16
+ RUN pip3 install --no-cache-dir --break-system-packages pytest coverage \
17
+ || pip3 install --no-cache-dir pytest coverage
18
+
19
+ RUN useradd -u 10001 -m sbx
20
+ WORKDIR /work
21
+ USER sbx
@@ -0,0 +1,13 @@
1
+ # Sovereign egress allowlist proxy sidecar for the autopilot sandbox.
2
+ # Runs `python -m skharness.autocode.sandbox_proxy <port> <hosts...>` (invoked by
3
+ # Sandbox.spawn). Only the single stdlib-only proxy module is present; a minimal
4
+ # package tree makes the -m import resolve without pulling the skharness deps.
5
+ FROM python:3.12-slim
6
+
7
+ RUN mkdir -p /app/skharness/autocode \
8
+ && touch /app/skharness/__init__.py /app/skharness/autocode/__init__.py
9
+ COPY src/skharness/autocode/sandbox_proxy.py /app/skharness/autocode/sandbox_proxy.py
10
+ ENV PYTHONPATH=/app
11
+ WORKDIR /app
12
+ # Command is supplied by Sandbox.spawn:
13
+ # python -m skharness.autocode.sandbox_proxy <port> <allow-host> <allow-host> ...
@@ -0,0 +1,63 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "skharness"
7
+ version = "0.3.0"
8
+ description = "Sovereign phone-drives-my-agent-swarm harness (execution: sessions + autocode engine)"
9
+ requires-python = ">=3.10"
10
+ dependencies = ["fastapi>=0.110", "pydantic>=2.0", "pyyaml>=6.0", "uvicorn>=0.27", "websockets>=12.0"]
11
+
12
+ [project.optional-dependencies]
13
+ dev = ["pytest>=7.0", "pytest-mock>=3.10", "pytest-asyncio>=0.23", "httpx>=0.27"]
14
+
15
+ [project.scripts]
16
+ # The skcode-hostd binary Atlas's skcode adapter shells (operator explain|observe|
17
+ # act) and the daemon runner (--host <tailnet-ip>) share one entry point.
18
+ skcode-hostd = "skharness.serve:main"
19
+
20
+ [tool.setuptools.packages.find]
21
+ where = ["src"]
22
+
23
+ # docker/sandbox build contexts live at the repo root (not importable package
24
+ # data); they are shipped in sdists via MANIFEST.in. The autocode engine and its
25
+ # adapters are pure-Python subpackages discovered by find above.
26
+ [tool.setuptools.package-data]
27
+ skharness = ["client/*.html"]
28
+
29
+ [tool.ruff]
30
+ line-length = 99
31
+ # Cover the whole tree: session primitives, the autocode engine, adapters, tests.
32
+ src = ["src", "tests"]
33
+ [tool.ruff.lint]
34
+ select = ["E", "F", "I", "N", "W"]
35
+ ignore = ["E501"]
36
+
37
+ [tool.ruff.lint.per-file-ignores]
38
+ # The autocode engine is a byte-identical copy of the skos autopilot engine
39
+ # (Phase A of the extraction). It must not be refactored, so the two rules the
40
+ # verbatim copy trips under skharness's stricter config are scoped-off HERE only:
41
+ # I001 import-block ordering (skos ships no isort rule; the copy is untouched)
42
+ # N818 exception naming (UnknownDecision, kept verbatim)
43
+ # New skharness code and the copied tests stay fully linted.
44
+ "src/skharness/autocode/*" = ["I001", "N818"]
45
+ "src/skharness/autocode/**/*" = ["I001", "N818"]
46
+ # The copied autopilot tests are verbatim from skos (only the import PREFIX was
47
+ # rewritten). skos ships no isort rule, so they carry a different import order;
48
+ # scope I001 off for the copied test files so they stay byte-identical. The new
49
+ # tests/test_autocode_gate_conformance.py is authored fresh and stays fully linted.
50
+ "tests/conftest.py" = ["I001"]
51
+ "tests/test_autopilot_*.py" = ["I001"]
52
+ "tests/test_adapter_*.py" = ["I001"]
53
+ "tests/test_sandbox_*.py" = ["I001"]
54
+ "tests/test_harness_registry.py" = ["I001"]
55
+
56
+ [tool.pytest.ini_options]
57
+ testpaths = ["tests"]
58
+ markers = [
59
+ # Autopilot orchestrator tests that exercise the optional skcapstone sibling
60
+ # (coordination.Board). skcapstone is not a declared skharness dependency; the
61
+ # tests/conftest.py hook auto-skips these when skcapstone cannot be imported.
62
+ "needs_skcapstone: requires the optional sibling skcapstone package (auto-skipped when absent)",
63
+ ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,26 @@
1
+ """skharness: sovereign orchestration harness. Spawn isolated agent sessions,
2
+ drive them from a phone over the tailnet, no Big-Tech broker. P0 = the CI-testable
3
+ session-manager core. See docs/superpowers/specs/2026-06-13-skharness-design.md.
4
+
5
+ The unified two-plane Harness contract (Fable Wave 1 "contract hoist") is the
6
+ public seam over both the autocode engine (task plane) and the skcode daemon
7
+ (session plane); see skharness/harness.py.
8
+ """
9
+
10
+ from skharness.harness import (
11
+ HARNESSES,
12
+ Harness,
13
+ HarnessCapabilities,
14
+ build_harness,
15
+ register_harness,
16
+ warn_missing_capabilities,
17
+ )
18
+
19
+ __all__ = [
20
+ "Harness",
21
+ "HarnessCapabilities",
22
+ "HARNESSES",
23
+ "register_harness",
24
+ "build_harness",
25
+ "warn_missing_capabilities",
26
+ ]
@@ -0,0 +1,6 @@
1
+ """`python -m skharness --host <tailnet-ip>` starts skcode-hostd."""
2
+
3
+ from skharness.serve import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
@@ -0,0 +1,30 @@
1
+ """Capauth bearer gate, shared by the skharness gateway and skcode-hostd.
2
+
3
+ `verify_caller` is the auth seam: a real capauth verifier in production, a fake
4
+ in tests. Fail closed on missing or empty tokens BEFORE the verifier runs.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Callable
10
+
11
+ from fastapi import HTTPException
12
+
13
+ Verifier = Callable[[str], bool]
14
+
15
+
16
+ def require_bearer(authorization: str | None, verify_caller: Verifier) -> str:
17
+ if not authorization or not authorization.startswith("Bearer "):
18
+ raise HTTPException(401, "missing bearer token")
19
+ token = authorization[len("Bearer "):].strip()
20
+ if not token:
21
+ raise HTTPException(401, "missing bearer token")
22
+ if not verify_caller(token):
23
+ raise HTTPException(403, "unauthorized")
24
+ return token
25
+
26
+
27
+ def check_token(token: str | None, verify_caller: Verifier) -> bool:
28
+ if not token or not token.strip():
29
+ return False
30
+ return bool(verify_caller(token.strip()))
@@ -0,0 +1,4 @@
1
+ """SKOS Autopilot: harness-agnostic assess/plan/swarm/grade/report over the GTD spine."""
2
+ from .ratify import ratify
3
+
4
+ __all__ = ["ratify"]
@@ -0,0 +1,26 @@
1
+ from ..harness import register_harness
2
+ from .claude_code import ClaudeCodeAdapter
3
+ from .pi import PiAdapter
4
+ from .opencode import OpenCodeAdapter
5
+ from .codex import CodexStubAdapter
6
+
7
+ __all__ = ["ClaudeCodeAdapter", "PiAdapter", "OpenCodeAdapter", "CodexStubAdapter"]
8
+
9
+
10
+ def _g(config, key, default=None):
11
+ return getattr(config, key, default)
12
+
13
+
14
+ register_harness("claude-code", lambda c: ClaudeCodeAdapter(
15
+ _g(c, "allowed_tools", []) or [], mcp_endpoints=_g(c, "mcp_endpoints"),
16
+ live_execution=_g(c, "live_execution", False), image=_g(c, "sandbox_image"),
17
+ model=_g(c, "harness_model") or "sonnet"))
18
+ register_harness("pi", lambda c: PiAdapter(
19
+ model=_g(c, "harness_model"), base_url=_g(c, "harness_base_url"),
20
+ egress_hosts=_g(c, "mcp_endpoints") or [], live_execution=_g(c, "live_execution", False),
21
+ image=_g(c, "sandbox_image"), max_tokens=_g(c, "harness_max_tokens")))
22
+ register_harness("opencode", lambda c: OpenCodeAdapter(
23
+ model=_g(c, "harness_model"), base_url=_g(c, "harness_base_url"),
24
+ egress_hosts=_g(c, "mcp_endpoints") or [], live_execution=_g(c, "live_execution", False),
25
+ image=_g(c, "sandbox_image"), max_tokens=_g(c, "harness_max_tokens")))
26
+ register_harness("codex", lambda c: CodexStubAdapter())