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.
- skharness-0.3.0/MANIFEST.in +2 -0
- skharness-0.3.0/PKG-INFO +15 -0
- skharness-0.3.0/README.md +73 -0
- skharness-0.3.0/docker/sandbox/build.sh +94 -0
- skharness-0.3.0/docker/sandbox/claude/Dockerfile +20 -0
- skharness-0.3.0/docker/sandbox/opencode/Dockerfile +29 -0
- skharness-0.3.0/docker/sandbox/pi/Dockerfile +21 -0
- skharness-0.3.0/docker/sandbox/proxy/Dockerfile +13 -0
- skharness-0.3.0/pyproject.toml +63 -0
- skharness-0.3.0/setup.cfg +4 -0
- skharness-0.3.0/src/skharness/__init__.py +26 -0
- skharness-0.3.0/src/skharness/__main__.py +6 -0
- skharness-0.3.0/src/skharness/auth.py +30 -0
- skharness-0.3.0/src/skharness/autocode/__init__.py +4 -0
- skharness-0.3.0/src/skharness/autocode/adapters/__init__.py +26 -0
- skharness-0.3.0/src/skharness/autocode/adapters/base.py +316 -0
- skharness-0.3.0/src/skharness/autocode/adapters/claude_code.py +134 -0
- skharness-0.3.0/src/skharness/autocode/adapters/codex.py +34 -0
- skharness-0.3.0/src/skharness/autocode/adapters/opencode.py +127 -0
- skharness-0.3.0/src/skharness/autocode/adapters/pi.py +113 -0
- skharness-0.3.0/src/skharness/autocode/autoscale.py +121 -0
- skharness-0.3.0/src/skharness/autocode/ci.py +159 -0
- skharness-0.3.0/src/skharness/autocode/claude_code.py +95 -0
- skharness-0.3.0/src/skharness/autocode/cleanup.py +114 -0
- skharness-0.3.0/src/skharness/autocode/config.py +138 -0
- skharness-0.3.0/src/skharness/autocode/digest.py +131 -0
- skharness-0.3.0/src/skharness/autocode/direct.py +96 -0
- skharness-0.3.0/src/skharness/autocode/doctor.py +157 -0
- skharness-0.3.0/src/skharness/autocode/engineering.py +606 -0
- skharness-0.3.0/src/skharness/autocode/executor.py +34 -0
- skharness-0.3.0/src/skharness/autocode/fleet_dispatch.py +149 -0
- skharness-0.3.0/src/skharness/autocode/harness.py +99 -0
- skharness-0.3.0/src/skharness/autocode/health.py +81 -0
- skharness-0.3.0/src/skharness/autocode/joules.py +233 -0
- skharness-0.3.0/src/skharness/autocode/journal.py +171 -0
- skharness-0.3.0/src/skharness/autocode/orchestrator.py +535 -0
- skharness-0.3.0/src/skharness/autocode/protected.py +96 -0
- skharness-0.3.0/src/skharness/autocode/ratify.py +61 -0
- skharness-0.3.0/src/skharness/autocode/remediation.py +250 -0
- skharness-0.3.0/src/skharness/autocode/resolver.py +83 -0
- skharness-0.3.0/src/skharness/autocode/sandbox.py +167 -0
- skharness-0.3.0/src/skharness/autocode/sandbox_proxy.py +153 -0
- skharness-0.3.0/src/skharness/autocode/stubs.py +57 -0
- skharness-0.3.0/src/skharness/autocode/types.py +159 -0
- skharness-0.3.0/src/skharness/client/index.html +217 -0
- skharness-0.3.0/src/skharness/daemon.py +163 -0
- skharness-0.3.0/src/skharness/events.py +39 -0
- skharness-0.3.0/src/skharness/gateway.py +51 -0
- skharness-0.3.0/src/skharness/harness.py +371 -0
- skharness-0.3.0/src/skharness/harnesses/__init__.py +1 -0
- skharness-0.3.0/src/skharness/harnesses/claude_code.py +257 -0
- skharness-0.3.0/src/skharness/manager.py +41 -0
- skharness-0.3.0/src/skharness/manifest.py +70 -0
- skharness-0.3.0/src/skharness/operator_cli.py +357 -0
- skharness-0.3.0/src/skharness/registry.py +58 -0
- skharness-0.3.0/src/skharness/serve.py +134 -0
- skharness-0.3.0/src/skharness/session.py +37 -0
- skharness-0.3.0/src/skharness/spawner.py +37 -0
- skharness-0.3.0/src/skharness.egg-info/PKG-INFO +15 -0
- skharness-0.3.0/src/skharness.egg-info/SOURCES.txt +124 -0
- skharness-0.3.0/src/skharness.egg-info/dependency_links.txt +1 -0
- skharness-0.3.0/src/skharness.egg-info/entry_points.txt +2 -0
- skharness-0.3.0/src/skharness.egg-info/requires.txt +11 -0
- skharness-0.3.0/src/skharness.egg-info/top_level.txt +1 -0
- skharness-0.3.0/tests/test_adapter_base.py +231 -0
- skharness-0.3.0/tests/test_adapter_codex.py +12 -0
- skharness-0.3.0/tests/test_adapter_opencode.py +95 -0
- skharness-0.3.0/tests/test_adapter_pi.py +90 -0
- skharness-0.3.0/tests/test_auth.py +48 -0
- skharness-0.3.0/tests/test_autocode_gate_conformance.py +110 -0
- skharness-0.3.0/tests/test_autocode_protected.py +96 -0
- skharness-0.3.0/tests/test_autocode_ratify.py +115 -0
- skharness-0.3.0/tests/test_autocode_toggle.py +267 -0
- skharness-0.3.0/tests/test_autopilot_build_executors.py +56 -0
- skharness-0.3.0/tests/test_autopilot_ci.py +241 -0
- skharness-0.3.0/tests/test_autopilot_claude_code.py +161 -0
- skharness-0.3.0/tests/test_autopilot_config.py +56 -0
- skharness-0.3.0/tests/test_autopilot_config_harness.py +35 -0
- skharness-0.3.0/tests/test_autopilot_config_wrap.py +10 -0
- skharness-0.3.0/tests/test_autopilot_digest.py +55 -0
- skharness-0.3.0/tests/test_autopilot_digest_send.py +48 -0
- skharness-0.3.0/tests/test_autopilot_e2e_dryrun.py +68 -0
- skharness-0.3.0/tests/test_autopilot_engineering.py +589 -0
- skharness-0.3.0/tests/test_autopilot_executor.py +45 -0
- skharness-0.3.0/tests/test_autopilot_firewall_segments.py +20 -0
- skharness-0.3.0/tests/test_autopilot_fleet_dispatch.py +123 -0
- skharness-0.3.0/tests/test_autopilot_fleet_gate.py +181 -0
- skharness-0.3.0/tests/test_autopilot_harness.py +51 -0
- skharness-0.3.0/tests/test_autopilot_job_yaml.py +22 -0
- skharness-0.3.0/tests/test_autopilot_journal.py +38 -0
- skharness-0.3.0/tests/test_autopilot_journal_api.py +30 -0
- skharness-0.3.0/tests/test_autopilot_orchestrator.py +520 -0
- skharness-0.3.0/tests/test_autopilot_queue_decision.py +26 -0
- skharness-0.3.0/tests/test_autopilot_remediation.py +197 -0
- skharness-0.3.0/tests/test_autopilot_resolver.py +62 -0
- skharness-0.3.0/tests/test_autopilot_revert_wrap.py +12 -0
- skharness-0.3.0/tests/test_autopilot_stubs.py +36 -0
- skharness-0.3.0/tests/test_autopilot_types.py +35 -0
- skharness-0.3.0/tests/test_autopilot_workitem_enrichment.py +36 -0
- skharness-0.3.0/tests/test_autoscale.py +62 -0
- skharness-0.3.0/tests/test_claude_code_harness.py +300 -0
- skharness-0.3.0/tests/test_cleanup.py +65 -0
- skharness-0.3.0/tests/test_client.py +37 -0
- skharness-0.3.0/tests/test_daemon.py +315 -0
- skharness-0.3.0/tests/test_doctor.py +95 -0
- skharness-0.3.0/tests/test_events.py +29 -0
- skharness-0.3.0/tests/test_gateway.py +70 -0
- skharness-0.3.0/tests/test_harness_registry.py +29 -0
- skharness-0.3.0/tests/test_health.py +53 -0
- skharness-0.3.0/tests/test_joules.py +139 -0
- skharness-0.3.0/tests/test_manager.py +52 -0
- skharness-0.3.0/tests/test_manifest.py +43 -0
- skharness-0.3.0/tests/test_operator_cli.py +375 -0
- skharness-0.3.0/tests/test_registry.py +23 -0
- skharness-0.3.0/tests/test_sandbox_argv.py +65 -0
- skharness-0.3.0/tests/test_sandbox_config_files.py +80 -0
- skharness-0.3.0/tests/test_sandbox_confinement_it.py +100 -0
- skharness-0.3.0/tests/test_sandbox_proxy.py +86 -0
- skharness-0.3.0/tests/test_sandbox_spawn.py +105 -0
- skharness-0.3.0/tests/test_serve.py +23 -0
- skharness-0.3.0/tests/test_session.py +15 -0
- skharness-0.3.0/tests/test_session_plane.py +91 -0
- skharness-0.3.0/tests/test_spawner.py +22 -0
- skharness-0.3.0/tests/test_systemd_unit.py +71 -0
- skharness-0.3.0/tests/test_unified_harness.py +169 -0
- skharness-0.3.0/tests/test_verifier.py +174 -0
skharness-0.3.0/PKG-INFO
ADDED
|
@@ -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,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,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,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())
|