uh-harness 0.6.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.
- uh_harness-0.6.0/LICENSE +21 -0
- uh_harness-0.6.0/PKG-INFO +251 -0
- uh_harness-0.6.0/README.md +209 -0
- uh_harness-0.6.0/pyproject.toml +58 -0
- uh_harness-0.6.0/setup.cfg +4 -0
- uh_harness-0.6.0/tests/test_agent_run_turn.py +143 -0
- uh_harness-0.6.0/tests/test_banner.py +131 -0
- uh_harness-0.6.0/tests/test_chat_persistence.py +142 -0
- uh_harness-0.6.0/tests/test_cli_chat.py +96 -0
- uh_harness-0.6.0/tests/test_commands.py +145 -0
- uh_harness-0.6.0/tests/test_config.py +138 -0
- uh_harness-0.6.0/tests/test_console.py +138 -0
- uh_harness-0.6.0/tests/test_console_answers.py +160 -0
- uh_harness-0.6.0/tests/test_encoding.py +72 -0
- uh_harness-0.6.0/tests/test_file_toolset.py +103 -0
- uh_harness-0.6.0/tests/test_memory_migration.py +101 -0
- uh_harness-0.6.0/tests/test_memory_toolset.py +50 -0
- uh_harness-0.6.0/tests/test_model_pull.py +127 -0
- uh_harness-0.6.0/tests/test_onboarding.py +172 -0
- uh_harness-0.6.0/tests/test_provider_keys.py +138 -0
- uh_harness-0.6.0/tests/test_providers.py +155 -0
- uh_harness-0.6.0/tests/test_sessions_cli.py +147 -0
- uh_harness-0.6.0/tests/test_smoke.py +45 -0
- uh_harness-0.6.0/tests/test_store.py +157 -0
- uh_harness-0.6.0/tests/test_streaming.py +212 -0
- uh_harness-0.6.0/tests/test_terminal_toolset.py +76 -0
- uh_harness-0.6.0/tests/test_web.py +426 -0
- uh_harness-0.6.0/uh/__init__.py +9 -0
- uh_harness-0.6.0/uh/__main__.py +6 -0
- uh_harness-0.6.0/uh/_compat.py +16 -0
- uh_harness-0.6.0/uh/agent.py +243 -0
- uh_harness-0.6.0/uh/banner.py +94 -0
- uh_harness-0.6.0/uh/cli.py +1396 -0
- uh_harness-0.6.0/uh/commands.py +305 -0
- uh_harness-0.6.0/uh/config.py +249 -0
- uh_harness-0.6.0/uh/console.py +505 -0
- uh_harness-0.6.0/uh/keys.py +183 -0
- uh_harness-0.6.0/uh/onboarding.py +236 -0
- uh_harness-0.6.0/uh/providers/__init__.py +102 -0
- uh_harness-0.6.0/uh/providers/ollama.py +234 -0
- uh_harness-0.6.0/uh/providers/omnirouter.py +318 -0
- uh_harness-0.6.0/uh/providers/openai_compat.py +38 -0
- uh_harness-0.6.0/uh/store.py +357 -0
- uh_harness-0.6.0/uh/toolsets/__init__.py +55 -0
- uh_harness-0.6.0/uh/toolsets/file.py +313 -0
- uh_harness-0.6.0/uh/toolsets/memory.py +190 -0
- uh_harness-0.6.0/uh/toolsets/terminal.py +151 -0
- uh_harness-0.6.0/uh/web/__init__.py +5 -0
- uh_harness-0.6.0/uh/web/app.py +477 -0
- uh_harness-0.6.0/uh/web/sse.py +33 -0
- uh_harness-0.6.0/uh/web/static/app.js +437 -0
- uh_harness-0.6.0/uh/web/static/index.html +57 -0
- uh_harness-0.6.0/uh/web/static/style.css +194 -0
- uh_harness-0.6.0/uh_harness.egg-info/PKG-INFO +251 -0
- uh_harness-0.6.0/uh_harness.egg-info/SOURCES.txt +57 -0
- uh_harness-0.6.0/uh_harness.egg-info/dependency_links.txt +1 -0
- uh_harness-0.6.0/uh_harness.egg-info/entry_points.txt +2 -0
- uh_harness-0.6.0/uh_harness.egg-info/requires.txt +17 -0
- uh_harness-0.6.0/uh_harness.egg-info/top_level.txt +1 -0
uh_harness-0.6.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Thomas Leroyer
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: uh-harness
|
|
3
|
+
Version: 0.6.0
|
|
4
|
+
Summary: Universal Harness: a provider-agnostic agent harness with a local-first CLI, tool use and an optional web UI
|
|
5
|
+
Author: Thomas Leroyer
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Antoine-Thomas/UH-Universal-Harness
|
|
8
|
+
Project-URL: Source, https://github.com/Antoine-Thomas/UH-Universal-Harness
|
|
9
|
+
Project-URL: Issues, https://github.com/Antoine-Thomas/UH-Universal-Harness/issues
|
|
10
|
+
Project-URL: Documentation, https://github.com/Antoine-Thomas/UH-Universal-Harness/tree/main/docs
|
|
11
|
+
Project-URL: Changelog, https://github.com/Antoine-Thomas/UH-Universal-Harness/releases
|
|
12
|
+
Keywords: agent,harness,llm,ollama,cli,local-first
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Environment :: Console
|
|
15
|
+
Classifier: Environment :: Web Environment
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
25
|
+
Classifier: Topic :: Terminals
|
|
26
|
+
Requires-Python: >=3.10
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
License-File: LICENSE
|
|
29
|
+
Requires-Dist: httpx>=0.27
|
|
30
|
+
Requires-Dist: tomli>=2; python_version < "3.11"
|
|
31
|
+
Provides-Extra: dev
|
|
32
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
33
|
+
Requires-Dist: build>=1; extra == "dev"
|
|
34
|
+
Requires-Dist: fastapi>=0.110; extra == "dev"
|
|
35
|
+
Requires-Dist: uvicorn>=0.29; extra == "dev"
|
|
36
|
+
Provides-Extra: ui
|
|
37
|
+
Requires-Dist: rich>=13; extra == "ui"
|
|
38
|
+
Provides-Extra: web
|
|
39
|
+
Requires-Dist: fastapi>=0.110; extra == "web"
|
|
40
|
+
Requires-Dist: uvicorn>=0.29; extra == "web"
|
|
41
|
+
Dynamic: license-file
|
|
42
|
+
|
|
43
|
+
# UH — Universal Harness
|
|
44
|
+
|
|
45
|
+
A provider-agnostic agent harness in Python. UH merges the ideas that worked in
|
|
46
|
+
four prior projects:
|
|
47
|
+
|
|
48
|
+
| Source | What UH takes from it |
|
|
49
|
+
| --- | --- |
|
|
50
|
+
| Harness Maxxing | web UX, organised sessions |
|
|
51
|
+
| OpenClaw | the harness contract (toolsets, fail-closed modes) |
|
|
52
|
+
| Hermes Agent | the synchronous agent loop (`run_turn`) |
|
|
53
|
+
| UHB | deterministic routing, fail-closed modes |
|
|
54
|
+
|
|
55
|
+
**The CLI is universal.** UH makes no assumption about a specific vendor: it
|
|
56
|
+
must drive any local or free provider (Ollama on `127.0.0.1:11434`, a local
|
|
57
|
+
free router such as OmniRouter/Nemotron) and must never require an API key on
|
|
58
|
+
first launch. A paid provider is an opt-in, never a prerequisite.
|
|
59
|
+
|
|
60
|
+
The web UI will be **copied and adapted from Harness Maxxing**, not written from
|
|
61
|
+
scratch and not vendored as a dependency.
|
|
62
|
+
|
|
63
|
+
- Requires Python ≥ 3.10 (`tomli` is installed automatically on 3.10)
|
|
64
|
+
- Single runtime dependency: `httpx`
|
|
65
|
+
- Installable on its own: no dependency on `hermes-home-vision` or `uhb`
|
|
66
|
+
|
|
67
|
+
## Install (development)
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
python -m venv .venv
|
|
71
|
+
source .venv/bin/activate # Windows: .venv\Scripts\activate
|
|
72
|
+
pip install -e ".[dev]"
|
|
73
|
+
uh --version
|
|
74
|
+
uh doctor
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Roadmap — five milestones
|
|
78
|
+
|
|
79
|
+
- [x] **Jalon 0 — Skeleton.** PyPI-installable package (`uh-harness`), `uh` CLI
|
|
80
|
+
surface, `uh --version`, `uh doctor` (python / httpx / git / ollama probe),
|
|
81
|
+
README, MIT license. No agent loop, no storage, no UI.
|
|
82
|
+
- [x] **Jalon 1 — Agent core.** Synchronous `run_turn` loop, toolsets
|
|
83
|
+
(`file`, `terminal`, `memory`), in-memory message history, `uh chat`.
|
|
84
|
+
No persistence yet: a session lives only as long as the process.
|
|
85
|
+
- [x] **Jalon 2 — Local model layer.** Streamed chat (`--no-stream` to disable),
|
|
86
|
+
real `uh model pull` with progress, provider registry (`ollama`,
|
|
87
|
+
`omnirouter`), traced fallback chain, `uh providers list`.
|
|
88
|
+
- [x] **Jalon 3 — Sessions and configuration.** Persisted sessions backed by a
|
|
89
|
+
SQLite FTS5 store (`uh sessions list|show|search|delete|resume`), layered
|
|
90
|
+
configuration with `uh config show|get|set|path`, `uh memory migrate|show`,
|
|
91
|
+
extended `uh doctor`.
|
|
92
|
+
- [x] **Jalon 4 — Console experience.** `uh` with no argument opens a chat
|
|
93
|
+
console: banner, status bar, slash commands (`docs/SLASH_COMMANDS.md`),
|
|
94
|
+
zero-configuration onboarding (local model, offer to pull one sized to the
|
|
95
|
+
RAM, local router, or a guided free key), `uh providers add|remove` with
|
|
96
|
+
keys in `$UH_HOME/keys.toml` (0600), UTF-8 stdio on Windows, CI matrix.
|
|
97
|
+
- [x] **Jalon 5 — Web UI.** `uh web` serves a local interface (sessions column,
|
|
98
|
+
streaming chat, collapsible tool cards, context gauge, per-session model,
|
|
99
|
+
full-text search, command palette, stop button, light/dark themes) from
|
|
100
|
+
three static files with no build step and no third-party JavaScript.
|
|
101
|
+
|
|
102
|
+
Jalons 0 to 5 are done. The interface is an **original design**: it borrows the
|
|
103
|
+
feature list of Harness Maxxing (public README) and none of its code, which is
|
|
104
|
+
under a Fair Source Licence. UH stays MIT.
|
|
105
|
+
|
|
106
|
+
## CLI surface (Jalon 4)
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
uh # interactive console (piped: help, unchanged)
|
|
110
|
+
uh --version # print the version
|
|
111
|
+
uh doctor # environment report (python, httpx, git, ollama, store, disk, config, keys)
|
|
112
|
+
uh model list # list models served by Ollama, exit 1 if Ollama is down
|
|
113
|
+
uh model pull <name> # download a model with progress on stderr
|
|
114
|
+
uh chat [prompt] # agent loop with file/terminal/memory tools (streamed, saved)
|
|
115
|
+
uh sessions list # recent sessions (id, date, provider, messages, title)
|
|
116
|
+
uh sessions show <id> # one session and its messages
|
|
117
|
+
uh sessions search <q> # full-text search across every message
|
|
118
|
+
uh sessions resume <id> # continue a session (--fork copies it into a new one)
|
|
119
|
+
uh config show|get|set # layered configuration
|
|
120
|
+
uh memory migrate|show # one-shot import of the flat memory file
|
|
121
|
+
uh providers list # registered providers and their state
|
|
122
|
+
uh providers add <name> # store an OpenAI-compatible provider (key never printed)
|
|
123
|
+
uh providers remove <name>
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
`uh chat` reads stdin or takes a prompt argument; `--once` (or any non-TTY
|
|
127
|
+
stdin) answers one line and exits. Answers stream by default, `--no-stream`
|
|
128
|
+
waits for the whole message. Conversations are persisted unless `--no-save`;
|
|
129
|
+
`--session <id>` resumes (full id, unique prefix, or the short form shown by
|
|
130
|
+
`uh sessions list`). Tool activity goes to stderr, the answer to stdout.
|
|
131
|
+
|
|
132
|
+
## Web interface (optional)
|
|
133
|
+
|
|
134
|
+
```
|
|
135
|
+
pip install "uh-harness[web]"
|
|
136
|
+
uh web # http://127.0.0.1:8765/?token=...
|
|
137
|
+
uh web --port 9000 --no-open
|
|
138
|
+
uh web --allow-terminal # enable the shell toolset in the browser (off by default)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
`uh web` without the extra prints the exact install command and exits 1. The
|
|
142
|
+
core package stays on `httpx`; `fastapi`/`uvicorn` only exist in `[web]`.
|
|
143
|
+
|
|
144
|
+
Security, because a local server is still a server:
|
|
145
|
+
|
|
146
|
+
- binds `127.0.0.1`; any other `--host` prints a warning before starting;
|
|
147
|
+
- a 43-character token is generated at startup, accepted once as `?token=` and
|
|
148
|
+
then kept in an `HttpOnly; SameSite=Strict` cookie; every `/api` route answers
|
|
149
|
+
401 without it (curl: `Authorization: Bearer <token>`);
|
|
150
|
+
- `Host` and `Origin` are checked (DNS-rebinding / CSRF), no CORS is enabled;
|
|
151
|
+
- strict CSP without `unsafe-inline`, so no inline script or style exists in the
|
|
152
|
+
HTML; `X-Frame-Options: DENY`;
|
|
153
|
+
- the **terminal toolset is disabled** unless `--allow-terminal`; file tools stay
|
|
154
|
+
inside `--workspace`;
|
|
155
|
+
- no provider key is ever returned by the API.
|
|
156
|
+
|
|
157
|
+
Design notes and the feature-by-feature rationale live in
|
|
158
|
+
[docs/WEB_DESIGN.md](https://github.com/Antoine-Thomas/UH-Universal-Harness/blob/main/docs/WEB_DESIGN.md).
|
|
159
|
+
|
|
160
|
+
## Console and slash commands
|
|
161
|
+
|
|
162
|
+
`uh` on a terminal starts the console; piped, it still prints help (scriptable
|
|
163
|
+
unchanged). The status bar shows `provider · model · session · messages ·
|
|
164
|
+
context estimate`. `/help` lists everything; the full inventory and the reason
|
|
165
|
+
each Hermes command was kept or dropped lives in
|
|
166
|
+
[docs/SLASH_COMMANDS.md](https://github.com/Antoine-Thomas/UH-Universal-Harness/blob/main/docs/SLASH_COMMANDS.md).
|
|
167
|
+
|
|
168
|
+
`/help`, `/model`, `/provider`, `/tools`, `/clear`, `/new`, `/sessions`,
|
|
169
|
+
`/resume`, `/history`, `/retry`, `/title`, `/status`, `/config`, `/doctor`,
|
|
170
|
+
`/memory`, `/version`, `/exit` (aliases `/h`, `/?`, `/reset`, `/quit`, `/q`,
|
|
171
|
+
`/v`). An unknown command is never sent to the model: it is refused with the
|
|
172
|
+
closest match suggested.
|
|
173
|
+
|
|
174
|
+
First run with nothing configured, in order: an installed Ollama model, else an
|
|
175
|
+
offer to pull a small model sized to the detected RAM (size shown, never
|
|
176
|
+
downloaded without a yes), else a local OmniRouter gateway, else a provider you
|
|
177
|
+
already added, else a guided setup that explains where to get a free key and
|
|
178
|
+
stores it in `$UH_HOME/keys.toml` (0600). UH ships **no key**: a published key
|
|
179
|
+
is a stolen key.
|
|
180
|
+
|
|
181
|
+
The console can use [rich](https://rich.readthedocs.io/) when installed
|
|
182
|
+
(`pip install "uh-harness[ui]"`); without it, or off a TTY, everything falls
|
|
183
|
+
back to plain text with the same output contract.
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
echo "list the python files here" | uh chat --once --model qwen3:8b --workspace .
|
|
187
|
+
uh chat --session 1829-9474 "et ensuite ?" # resume
|
|
188
|
+
uh sessions search clavier # FTS5 across all messages
|
|
189
|
+
uh memory migrate # MEMORY.md -> one session, once
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## Sessions, store and configuration
|
|
193
|
+
|
|
194
|
+
- Store: `$UH_HOME/uh.db` (`~/.uh/uh.db`), schema v1, WAL, foreign keys ON.
|
|
195
|
+
Deleting a session cascades to its messages and to the FTS index.
|
|
196
|
+
- Two tables (`sessions`, `messages`) plus an external-content FTS5 index kept
|
|
197
|
+
in sync by triggers; `search` returns a snippet and a `bm25` rank.
|
|
198
|
+
- A database written by a newer build is refused (`StoreError`), never
|
|
199
|
+
downgraded silently. No FTS5 in the SQLite build means no store at all.
|
|
200
|
+
- Configuration, lowest to highest: defaults, `~/.uh/config.toml`, `./uh.toml`,
|
|
201
|
+
`UH_*` environment, CLI flags. `uh config show` prints the source of every
|
|
202
|
+
value. Unknown keys warn and are ignored; the writer handles flat keys only
|
|
203
|
+
(booleans, integers, strings) and refuses anything else.
|
|
204
|
+
- `uh memory migrate` copies `MEMORY.md` into a `kind=memory` session after
|
|
205
|
+
writing a timestamped `.bak`; it is idempotent and leaves the flat file in
|
|
206
|
+
place (the memory toolset still reads and writes it).
|
|
207
|
+
|
|
208
|
+
## Providers and fallback
|
|
209
|
+
|
|
210
|
+
| Provider | Transport | Endpoint | Streaming |
|
|
211
|
+
| --- | --- | --- | --- |
|
|
212
|
+
| `ollama` | httpx, NDJSON | `POST /api/chat`, `GET /api/tags`, `POST /api/pull` | yes |
|
|
213
|
+
| `omnirouter` | httpx, SSE | `POST /v1/chat/completions` (OpenAI-compatible) | yes |
|
|
214
|
+
|
|
215
|
+
Without `--provider`, `uh chat` builds the chain `ollama` then `omnirouter`
|
|
216
|
+
(when configured). Every switch is printed on stderr, never silent:
|
|
217
|
+
|
|
218
|
+
```
|
|
219
|
+
fallback: ollama -> omnirouter (reason: cannot reach Ollama at ...)
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
`--provider <kind>` disables the fallback: one provider, its errors surface as
|
|
223
|
+
they are. Model namespaces are not shared, so the backup uses
|
|
224
|
+
`UH_OMNIROUTER_MODEL` (default `auto/best-free`) rather than the Ollama tag.
|
|
225
|
+
|
|
226
|
+
## OmniRouter discovery (measured, not guessed)
|
|
227
|
+
|
|
228
|
+
`uh doctor` and `uh providers list` find a local OmniRoute gateway from these
|
|
229
|
+
sources, first hit wins:
|
|
230
|
+
|
|
231
|
+
1. `UH_OMNIROUTER_URL` (explicit)
|
|
232
|
+
2. `OMNIROUTE_BASE_URL` (the OmniRoute CLI's own variable)
|
|
233
|
+
3. `BASE_URL=` in `~/.omniroute/.env`
|
|
234
|
+
4. `http://{HOST}:{PORT}` from that dotenv (`PORT` defaults to 20128)
|
|
235
|
+
5. otherwise: "not configured" - nothing is invented
|
|
236
|
+
|
|
237
|
+
API key: `UH_OMNIROUTER_API_KEY`, then `OMNIROUTE_API_KEY`, then the same
|
|
238
|
+
dotenv. The key is only ever sent as an `Authorization` header, never printed.
|
|
239
|
+
|
|
240
|
+
What the installed gateway actually looks like (OmniRoute 3.8.52):
|
|
241
|
+
|
|
242
|
+
- data dir `~/.omniroute`, configuration is a dotenv file plus a SQLite
|
|
243
|
+
database - **no YAML/TOML**, so UH needs no parser dependency;
|
|
244
|
+
- server on `http://127.0.0.1:20128`, `GET /v1/models` and
|
|
245
|
+
`POST /v1/chat/completions` answer with a `Bearer` key (1632 model ids on the
|
|
246
|
+
machine this was measured on, including `auto/*` combos);
|
|
247
|
+
- streaming is SSE (`data: {...}` chunks, `data: [DONE]` terminator).
|
|
248
|
+
|
|
249
|
+
## License
|
|
250
|
+
|
|
251
|
+
MIT — see [LICENSE](https://github.com/Antoine-Thomas/UH-Universal-Harness/blob/main/LICENSE).
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# UH — Universal Harness
|
|
2
|
+
|
|
3
|
+
A provider-agnostic agent harness in Python. UH merges the ideas that worked in
|
|
4
|
+
four prior projects:
|
|
5
|
+
|
|
6
|
+
| Source | What UH takes from it |
|
|
7
|
+
| --- | --- |
|
|
8
|
+
| Harness Maxxing | web UX, organised sessions |
|
|
9
|
+
| OpenClaw | the harness contract (toolsets, fail-closed modes) |
|
|
10
|
+
| Hermes Agent | the synchronous agent loop (`run_turn`) |
|
|
11
|
+
| UHB | deterministic routing, fail-closed modes |
|
|
12
|
+
|
|
13
|
+
**The CLI is universal.** UH makes no assumption about a specific vendor: it
|
|
14
|
+
must drive any local or free provider (Ollama on `127.0.0.1:11434`, a local
|
|
15
|
+
free router such as OmniRouter/Nemotron) and must never require an API key on
|
|
16
|
+
first launch. A paid provider is an opt-in, never a prerequisite.
|
|
17
|
+
|
|
18
|
+
The web UI will be **copied and adapted from Harness Maxxing**, not written from
|
|
19
|
+
scratch and not vendored as a dependency.
|
|
20
|
+
|
|
21
|
+
- Requires Python ≥ 3.10 (`tomli` is installed automatically on 3.10)
|
|
22
|
+
- Single runtime dependency: `httpx`
|
|
23
|
+
- Installable on its own: no dependency on `hermes-home-vision` or `uhb`
|
|
24
|
+
|
|
25
|
+
## Install (development)
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
python -m venv .venv
|
|
29
|
+
source .venv/bin/activate # Windows: .venv\Scripts\activate
|
|
30
|
+
pip install -e ".[dev]"
|
|
31
|
+
uh --version
|
|
32
|
+
uh doctor
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Roadmap — five milestones
|
|
36
|
+
|
|
37
|
+
- [x] **Jalon 0 — Skeleton.** PyPI-installable package (`uh-harness`), `uh` CLI
|
|
38
|
+
surface, `uh --version`, `uh doctor` (python / httpx / git / ollama probe),
|
|
39
|
+
README, MIT license. No agent loop, no storage, no UI.
|
|
40
|
+
- [x] **Jalon 1 — Agent core.** Synchronous `run_turn` loop, toolsets
|
|
41
|
+
(`file`, `terminal`, `memory`), in-memory message history, `uh chat`.
|
|
42
|
+
No persistence yet: a session lives only as long as the process.
|
|
43
|
+
- [x] **Jalon 2 — Local model layer.** Streamed chat (`--no-stream` to disable),
|
|
44
|
+
real `uh model pull` with progress, provider registry (`ollama`,
|
|
45
|
+
`omnirouter`), traced fallback chain, `uh providers list`.
|
|
46
|
+
- [x] **Jalon 3 — Sessions and configuration.** Persisted sessions backed by a
|
|
47
|
+
SQLite FTS5 store (`uh sessions list|show|search|delete|resume`), layered
|
|
48
|
+
configuration with `uh config show|get|set|path`, `uh memory migrate|show`,
|
|
49
|
+
extended `uh doctor`.
|
|
50
|
+
- [x] **Jalon 4 — Console experience.** `uh` with no argument opens a chat
|
|
51
|
+
console: banner, status bar, slash commands (`docs/SLASH_COMMANDS.md`),
|
|
52
|
+
zero-configuration onboarding (local model, offer to pull one sized to the
|
|
53
|
+
RAM, local router, or a guided free key), `uh providers add|remove` with
|
|
54
|
+
keys in `$UH_HOME/keys.toml` (0600), UTF-8 stdio on Windows, CI matrix.
|
|
55
|
+
- [x] **Jalon 5 — Web UI.** `uh web` serves a local interface (sessions column,
|
|
56
|
+
streaming chat, collapsible tool cards, context gauge, per-session model,
|
|
57
|
+
full-text search, command palette, stop button, light/dark themes) from
|
|
58
|
+
three static files with no build step and no third-party JavaScript.
|
|
59
|
+
|
|
60
|
+
Jalons 0 to 5 are done. The interface is an **original design**: it borrows the
|
|
61
|
+
feature list of Harness Maxxing (public README) and none of its code, which is
|
|
62
|
+
under a Fair Source Licence. UH stays MIT.
|
|
63
|
+
|
|
64
|
+
## CLI surface (Jalon 4)
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
uh # interactive console (piped: help, unchanged)
|
|
68
|
+
uh --version # print the version
|
|
69
|
+
uh doctor # environment report (python, httpx, git, ollama, store, disk, config, keys)
|
|
70
|
+
uh model list # list models served by Ollama, exit 1 if Ollama is down
|
|
71
|
+
uh model pull <name> # download a model with progress on stderr
|
|
72
|
+
uh chat [prompt] # agent loop with file/terminal/memory tools (streamed, saved)
|
|
73
|
+
uh sessions list # recent sessions (id, date, provider, messages, title)
|
|
74
|
+
uh sessions show <id> # one session and its messages
|
|
75
|
+
uh sessions search <q> # full-text search across every message
|
|
76
|
+
uh sessions resume <id> # continue a session (--fork copies it into a new one)
|
|
77
|
+
uh config show|get|set # layered configuration
|
|
78
|
+
uh memory migrate|show # one-shot import of the flat memory file
|
|
79
|
+
uh providers list # registered providers and their state
|
|
80
|
+
uh providers add <name> # store an OpenAI-compatible provider (key never printed)
|
|
81
|
+
uh providers remove <name>
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`uh chat` reads stdin or takes a prompt argument; `--once` (or any non-TTY
|
|
85
|
+
stdin) answers one line and exits. Answers stream by default, `--no-stream`
|
|
86
|
+
waits for the whole message. Conversations are persisted unless `--no-save`;
|
|
87
|
+
`--session <id>` resumes (full id, unique prefix, or the short form shown by
|
|
88
|
+
`uh sessions list`). Tool activity goes to stderr, the answer to stdout.
|
|
89
|
+
|
|
90
|
+
## Web interface (optional)
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
pip install "uh-harness[web]"
|
|
94
|
+
uh web # http://127.0.0.1:8765/?token=...
|
|
95
|
+
uh web --port 9000 --no-open
|
|
96
|
+
uh web --allow-terminal # enable the shell toolset in the browser (off by default)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
`uh web` without the extra prints the exact install command and exits 1. The
|
|
100
|
+
core package stays on `httpx`; `fastapi`/`uvicorn` only exist in `[web]`.
|
|
101
|
+
|
|
102
|
+
Security, because a local server is still a server:
|
|
103
|
+
|
|
104
|
+
- binds `127.0.0.1`; any other `--host` prints a warning before starting;
|
|
105
|
+
- a 43-character token is generated at startup, accepted once as `?token=` and
|
|
106
|
+
then kept in an `HttpOnly; SameSite=Strict` cookie; every `/api` route answers
|
|
107
|
+
401 without it (curl: `Authorization: Bearer <token>`);
|
|
108
|
+
- `Host` and `Origin` are checked (DNS-rebinding / CSRF), no CORS is enabled;
|
|
109
|
+
- strict CSP without `unsafe-inline`, so no inline script or style exists in the
|
|
110
|
+
HTML; `X-Frame-Options: DENY`;
|
|
111
|
+
- the **terminal toolset is disabled** unless `--allow-terminal`; file tools stay
|
|
112
|
+
inside `--workspace`;
|
|
113
|
+
- no provider key is ever returned by the API.
|
|
114
|
+
|
|
115
|
+
Design notes and the feature-by-feature rationale live in
|
|
116
|
+
[docs/WEB_DESIGN.md](https://github.com/Antoine-Thomas/UH-Universal-Harness/blob/main/docs/WEB_DESIGN.md).
|
|
117
|
+
|
|
118
|
+
## Console and slash commands
|
|
119
|
+
|
|
120
|
+
`uh` on a terminal starts the console; piped, it still prints help (scriptable
|
|
121
|
+
unchanged). The status bar shows `provider · model · session · messages ·
|
|
122
|
+
context estimate`. `/help` lists everything; the full inventory and the reason
|
|
123
|
+
each Hermes command was kept or dropped lives in
|
|
124
|
+
[docs/SLASH_COMMANDS.md](https://github.com/Antoine-Thomas/UH-Universal-Harness/blob/main/docs/SLASH_COMMANDS.md).
|
|
125
|
+
|
|
126
|
+
`/help`, `/model`, `/provider`, `/tools`, `/clear`, `/new`, `/sessions`,
|
|
127
|
+
`/resume`, `/history`, `/retry`, `/title`, `/status`, `/config`, `/doctor`,
|
|
128
|
+
`/memory`, `/version`, `/exit` (aliases `/h`, `/?`, `/reset`, `/quit`, `/q`,
|
|
129
|
+
`/v`). An unknown command is never sent to the model: it is refused with the
|
|
130
|
+
closest match suggested.
|
|
131
|
+
|
|
132
|
+
First run with nothing configured, in order: an installed Ollama model, else an
|
|
133
|
+
offer to pull a small model sized to the detected RAM (size shown, never
|
|
134
|
+
downloaded without a yes), else a local OmniRouter gateway, else a provider you
|
|
135
|
+
already added, else a guided setup that explains where to get a free key and
|
|
136
|
+
stores it in `$UH_HOME/keys.toml` (0600). UH ships **no key**: a published key
|
|
137
|
+
is a stolen key.
|
|
138
|
+
|
|
139
|
+
The console can use [rich](https://rich.readthedocs.io/) when installed
|
|
140
|
+
(`pip install "uh-harness[ui]"`); without it, or off a TTY, everything falls
|
|
141
|
+
back to plain text with the same output contract.
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
echo "list the python files here" | uh chat --once --model qwen3:8b --workspace .
|
|
145
|
+
uh chat --session 1829-9474 "et ensuite ?" # resume
|
|
146
|
+
uh sessions search clavier # FTS5 across all messages
|
|
147
|
+
uh memory migrate # MEMORY.md -> one session, once
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Sessions, store and configuration
|
|
151
|
+
|
|
152
|
+
- Store: `$UH_HOME/uh.db` (`~/.uh/uh.db`), schema v1, WAL, foreign keys ON.
|
|
153
|
+
Deleting a session cascades to its messages and to the FTS index.
|
|
154
|
+
- Two tables (`sessions`, `messages`) plus an external-content FTS5 index kept
|
|
155
|
+
in sync by triggers; `search` returns a snippet and a `bm25` rank.
|
|
156
|
+
- A database written by a newer build is refused (`StoreError`), never
|
|
157
|
+
downgraded silently. No FTS5 in the SQLite build means no store at all.
|
|
158
|
+
- Configuration, lowest to highest: defaults, `~/.uh/config.toml`, `./uh.toml`,
|
|
159
|
+
`UH_*` environment, CLI flags. `uh config show` prints the source of every
|
|
160
|
+
value. Unknown keys warn and are ignored; the writer handles flat keys only
|
|
161
|
+
(booleans, integers, strings) and refuses anything else.
|
|
162
|
+
- `uh memory migrate` copies `MEMORY.md` into a `kind=memory` session after
|
|
163
|
+
writing a timestamped `.bak`; it is idempotent and leaves the flat file in
|
|
164
|
+
place (the memory toolset still reads and writes it).
|
|
165
|
+
|
|
166
|
+
## Providers and fallback
|
|
167
|
+
|
|
168
|
+
| Provider | Transport | Endpoint | Streaming |
|
|
169
|
+
| --- | --- | --- | --- |
|
|
170
|
+
| `ollama` | httpx, NDJSON | `POST /api/chat`, `GET /api/tags`, `POST /api/pull` | yes |
|
|
171
|
+
| `omnirouter` | httpx, SSE | `POST /v1/chat/completions` (OpenAI-compatible) | yes |
|
|
172
|
+
|
|
173
|
+
Without `--provider`, `uh chat` builds the chain `ollama` then `omnirouter`
|
|
174
|
+
(when configured). Every switch is printed on stderr, never silent:
|
|
175
|
+
|
|
176
|
+
```
|
|
177
|
+
fallback: ollama -> omnirouter (reason: cannot reach Ollama at ...)
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
`--provider <kind>` disables the fallback: one provider, its errors surface as
|
|
181
|
+
they are. Model namespaces are not shared, so the backup uses
|
|
182
|
+
`UH_OMNIROUTER_MODEL` (default `auto/best-free`) rather than the Ollama tag.
|
|
183
|
+
|
|
184
|
+
## OmniRouter discovery (measured, not guessed)
|
|
185
|
+
|
|
186
|
+
`uh doctor` and `uh providers list` find a local OmniRoute gateway from these
|
|
187
|
+
sources, first hit wins:
|
|
188
|
+
|
|
189
|
+
1. `UH_OMNIROUTER_URL` (explicit)
|
|
190
|
+
2. `OMNIROUTE_BASE_URL` (the OmniRoute CLI's own variable)
|
|
191
|
+
3. `BASE_URL=` in `~/.omniroute/.env`
|
|
192
|
+
4. `http://{HOST}:{PORT}` from that dotenv (`PORT` defaults to 20128)
|
|
193
|
+
5. otherwise: "not configured" - nothing is invented
|
|
194
|
+
|
|
195
|
+
API key: `UH_OMNIROUTER_API_KEY`, then `OMNIROUTE_API_KEY`, then the same
|
|
196
|
+
dotenv. The key is only ever sent as an `Authorization` header, never printed.
|
|
197
|
+
|
|
198
|
+
What the installed gateway actually looks like (OmniRoute 3.8.52):
|
|
199
|
+
|
|
200
|
+
- data dir `~/.omniroute`, configuration is a dotenv file plus a SQLite
|
|
201
|
+
database - **no YAML/TOML**, so UH needs no parser dependency;
|
|
202
|
+
- server on `http://127.0.0.1:20128`, `GET /v1/models` and
|
|
203
|
+
`POST /v1/chat/completions` answer with a `Bearer` key (1632 model ids on the
|
|
204
|
+
machine this was measured on, including `auto/*` combos);
|
|
205
|
+
- streaming is SSE (`data: {...}` chunks, `data: [DONE]` terminator).
|
|
206
|
+
|
|
207
|
+
## License
|
|
208
|
+
|
|
209
|
+
MIT — see [LICENSE](https://github.com/Antoine-Thomas/UH-Universal-Harness/blob/main/LICENSE).
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "uh-harness"
|
|
7
|
+
version = "0.6.0"
|
|
8
|
+
description = "Universal Harness: a provider-agnostic agent harness with a local-first CLI, tool use and an optional web UI"
|
|
9
|
+
readme = { file = "README.md", content-type = "text/markdown" }
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "Thomas Leroyer" }]
|
|
14
|
+
keywords = ["agent", "harness", "llm", "ollama", "cli", "local-first"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Environment :: Console",
|
|
18
|
+
"Environment :: Web Environment",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"Operating System :: OS Independent",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
23
|
+
"Programming Language :: Python :: 3.10",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Programming Language :: Python :: 3.13",
|
|
27
|
+
"Topic :: Software Development :: Libraries :: Application Frameworks",
|
|
28
|
+
"Topic :: Terminals",
|
|
29
|
+
]
|
|
30
|
+
dependencies = [
|
|
31
|
+
"httpx>=0.27",
|
|
32
|
+
# tomllib is stdlib from 3.11; on 3.10 the backport is pulled automatically.
|
|
33
|
+
'tomli>=2; python_version < "3.11"',
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
[project.optional-dependencies]
|
|
37
|
+
dev = ["pytest>=8", "build>=1", "fastapi>=0.110", "uvicorn>=0.29"]
|
|
38
|
+
ui = ["rich>=13"]
|
|
39
|
+
web = ["fastapi>=0.110", "uvicorn>=0.29"]
|
|
40
|
+
|
|
41
|
+
[project.scripts]
|
|
42
|
+
uh = "uh.cli:main"
|
|
43
|
+
|
|
44
|
+
[project.urls]
|
|
45
|
+
Homepage = "https://github.com/Antoine-Thomas/UH-Universal-Harness"
|
|
46
|
+
Source = "https://github.com/Antoine-Thomas/UH-Universal-Harness"
|
|
47
|
+
Issues = "https://github.com/Antoine-Thomas/UH-Universal-Harness/issues"
|
|
48
|
+
Documentation = "https://github.com/Antoine-Thomas/UH-Universal-Harness/tree/main/docs"
|
|
49
|
+
Changelog = "https://github.com/Antoine-Thomas/UH-Universal-Harness/releases"
|
|
50
|
+
|
|
51
|
+
[tool.setuptools.packages.find]
|
|
52
|
+
include = ["uh*"]
|
|
53
|
+
|
|
54
|
+
[tool.setuptools.package-data]
|
|
55
|
+
"uh.web" = ["static/*"]
|
|
56
|
+
|
|
57
|
+
[tool.pytest.ini_options]
|
|
58
|
+
testpaths = ["tests"]
|