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.
Files changed (59) hide show
  1. uh_harness-0.6.0/LICENSE +21 -0
  2. uh_harness-0.6.0/PKG-INFO +251 -0
  3. uh_harness-0.6.0/README.md +209 -0
  4. uh_harness-0.6.0/pyproject.toml +58 -0
  5. uh_harness-0.6.0/setup.cfg +4 -0
  6. uh_harness-0.6.0/tests/test_agent_run_turn.py +143 -0
  7. uh_harness-0.6.0/tests/test_banner.py +131 -0
  8. uh_harness-0.6.0/tests/test_chat_persistence.py +142 -0
  9. uh_harness-0.6.0/tests/test_cli_chat.py +96 -0
  10. uh_harness-0.6.0/tests/test_commands.py +145 -0
  11. uh_harness-0.6.0/tests/test_config.py +138 -0
  12. uh_harness-0.6.0/tests/test_console.py +138 -0
  13. uh_harness-0.6.0/tests/test_console_answers.py +160 -0
  14. uh_harness-0.6.0/tests/test_encoding.py +72 -0
  15. uh_harness-0.6.0/tests/test_file_toolset.py +103 -0
  16. uh_harness-0.6.0/tests/test_memory_migration.py +101 -0
  17. uh_harness-0.6.0/tests/test_memory_toolset.py +50 -0
  18. uh_harness-0.6.0/tests/test_model_pull.py +127 -0
  19. uh_harness-0.6.0/tests/test_onboarding.py +172 -0
  20. uh_harness-0.6.0/tests/test_provider_keys.py +138 -0
  21. uh_harness-0.6.0/tests/test_providers.py +155 -0
  22. uh_harness-0.6.0/tests/test_sessions_cli.py +147 -0
  23. uh_harness-0.6.0/tests/test_smoke.py +45 -0
  24. uh_harness-0.6.0/tests/test_store.py +157 -0
  25. uh_harness-0.6.0/tests/test_streaming.py +212 -0
  26. uh_harness-0.6.0/tests/test_terminal_toolset.py +76 -0
  27. uh_harness-0.6.0/tests/test_web.py +426 -0
  28. uh_harness-0.6.0/uh/__init__.py +9 -0
  29. uh_harness-0.6.0/uh/__main__.py +6 -0
  30. uh_harness-0.6.0/uh/_compat.py +16 -0
  31. uh_harness-0.6.0/uh/agent.py +243 -0
  32. uh_harness-0.6.0/uh/banner.py +94 -0
  33. uh_harness-0.6.0/uh/cli.py +1396 -0
  34. uh_harness-0.6.0/uh/commands.py +305 -0
  35. uh_harness-0.6.0/uh/config.py +249 -0
  36. uh_harness-0.6.0/uh/console.py +505 -0
  37. uh_harness-0.6.0/uh/keys.py +183 -0
  38. uh_harness-0.6.0/uh/onboarding.py +236 -0
  39. uh_harness-0.6.0/uh/providers/__init__.py +102 -0
  40. uh_harness-0.6.0/uh/providers/ollama.py +234 -0
  41. uh_harness-0.6.0/uh/providers/omnirouter.py +318 -0
  42. uh_harness-0.6.0/uh/providers/openai_compat.py +38 -0
  43. uh_harness-0.6.0/uh/store.py +357 -0
  44. uh_harness-0.6.0/uh/toolsets/__init__.py +55 -0
  45. uh_harness-0.6.0/uh/toolsets/file.py +313 -0
  46. uh_harness-0.6.0/uh/toolsets/memory.py +190 -0
  47. uh_harness-0.6.0/uh/toolsets/terminal.py +151 -0
  48. uh_harness-0.6.0/uh/web/__init__.py +5 -0
  49. uh_harness-0.6.0/uh/web/app.py +477 -0
  50. uh_harness-0.6.0/uh/web/sse.py +33 -0
  51. uh_harness-0.6.0/uh/web/static/app.js +437 -0
  52. uh_harness-0.6.0/uh/web/static/index.html +57 -0
  53. uh_harness-0.6.0/uh/web/static/style.css +194 -0
  54. uh_harness-0.6.0/uh_harness.egg-info/PKG-INFO +251 -0
  55. uh_harness-0.6.0/uh_harness.egg-info/SOURCES.txt +57 -0
  56. uh_harness-0.6.0/uh_harness.egg-info/dependency_links.txt +1 -0
  57. uh_harness-0.6.0/uh_harness.egg-info/entry_points.txt +2 -0
  58. uh_harness-0.6.0/uh_harness.egg-info/requires.txt +17 -0
  59. uh_harness-0.6.0/uh_harness.egg-info/top_level.txt +1 -0
@@ -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"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+