From d92338893e4b426964803db627892050b89a1747 Mon Sep 17 00:00:00 2001 From: Advait Jayant Date: Wed, 24 Jun 2026 19:29:53 +0100 Subject: [PATCH 1/9] Simplify and correct the README; raise agent max_turns; fix stale install echo - README: rewrite to match current state and cut it down (281 -> 94 lines), Hermes-style. DeepSeek V4 Pro is the default, full open-weight line-up (DeepSeek / Hermes 405B+70B / GLM 5.2), hosted Hermes fallback + auxiliary, condensed privacy section to one diagram + a few lines. - config: raise agent max_turns 40 -> 200 so long agentic runs keep going instead of summarizing-and-nagging at 40. - install: fix stale login echo that still named closed models + Hermes 405B as the default. --- README.md | 279 ++++++++------------------------------------ install.sh | 2 +- profile/config.yaml | 2 +- 3 files changed, 48 insertions(+), 235 deletions(-) diff --git a/README.md b/README.md index 7253a10..a04412d 100644 --- a/README.md +++ b/README.md @@ -1,273 +1,87 @@ -# ghost +# ghost ๐Ÿ‘ป -**An incognito, unrestricted agentic harness.** Censorship-resistant open intelligence that -runs a frontier open model over a hardened privacy path, drops to a fully-offline local -model on demand, and phones home to no one. +**A private, unrestricted agentic harness.** ghost is a real terminal agent -- it runs commands, reads and writes files, executes code, and searches the web -- with every hosted request routed through OpenGradient's private TEE gateway, so your prompts and your work stay private. It treats you as a competent adult: it answers what you actually ask, in full, no moralizing and no refusals, and no topic is off-limits when you raise it. It just doesn't volunteer illegal or shock content on its own. It can also drop to a fully-offline local model where nothing leaves the box. -ghost is built on the [Hermes Agent](https://hermes-agent.nousresearch.com) engine by Nous -Research, wrapped to route inference through OpenGradient's private TEE gateway. It keeps its -own engine copy at `~/.ghost-engine` (so any existing `hermes` install is left untouched), -launches as the `ghost` command, and sends every hosted request through a PII/secret scrubber -and then through **[og-veil](https://github.com/OpenGradient/veil)**, which relays it over -**Oblivious HTTP (OHTTP) to the OpenGradient chat-api TEE gateway** -- the same private path the -[chat.opengradient.ai](https://chat.opengradient.ai) website uses. Only genuinely unrestricted, -open-weight models are offered (the Hermes family); closed, refusing models are deliberately -left out. +ghost is built on the [Hermes Agent](https://github.com/NousResearch/hermes-agent) engine by Nous Research, wrapped to route inference through OpenGradient's gateway and to offer only genuinely unrestricted, open-weight models. -> **Uncensored by default.** ghost answers everything. The privacy layer governs what leaks -> *out*, never what ghost can *do*. Nothing is filtered, moralized, or redacted in its replies. + + + + + + + +
Private by constructionEvery hosted request is HPKE/OHTTP-encrypted by og-veil and run inside a TEE enclave. The relay sees ciphertext, the enclave never sees who you are, and a local scrubber strips your name/secrets before anything is encrypted.
Unrestricted, open-weight onlyDeepSeek V4 Pro (default), Hermes 4 405B/70B, GLM 5.2 -- all open-weight, made uncensored via a per-model steer. Closed/refusing models (Claude, GPT, Gemini, Grok) are not offered and are rejected at the gateway.
Verified responsesog-veil checks the enclave's signature on every response and refuses to emit a token it can't verify.
Offline modeOpt in with GHOST_LOCAL=1 and switch with ghost --local -- a local abliterated model, zero egress, nothing leaves your machine.
Relentless agentReads real errors, installs what it's missing, changes tactics, and keeps going until the task is done -- it doesn't stop to ask after one failure.
No memory, no telemetryCatalog served locally, web search via local ddgs, no third-party search account, isolated state in ~/.ghost.
--- -## Quickstart +## Quick Install -One command installs **everything** -- the engine into `~/.ghost-engine` (Ghost-branded), the -privacy stack, and the `ghost` + `ghost-login` commands. (Local models via Ollama are opt-in: -add `GHOST_LOCAL=1`.) Idempotent (safe to re-run): +One command installs everything -- the engine into `~/.ghost-engine` (it leaves any existing `hermes` install untouched), the privacy stack, and the `ghost` + `ghost-login` commands. Idempotent: ```bash -unzip ghost.zip -d ~/ghost && cd ~/ghost && ./install.sh +./install.sh ``` -Then connect your account once and run **`ghost`**: +Then connect once and go: ```bash -ghost-login # browser login -> hands a session token back to this machine -ghost # chat (default = DeepSeek V4 Pro via the OpenGradient TEE gateway, OHTTP-private) -ghost --local # force the fully-offline local model (no scrubber, no og-veil, nothing leaves) +ghost-login # browser login -> session token for this machine +ghost # chat (default: DeepSeek V4 Pro via the TEE gateway) +ghost --yolo # auto-accept tool approvals (skip-permissions) +ghost --resume # resume a past session (find one with: ghost sessions browse) +ghost --local # force the offline local model (needs GHOST_LOCAL) ``` -Inside, `/model` switches between the hosted line-up (Hermes 4 405B / 70B) and the -fully-local model. `--local` does the same in one shot (requires a local install, `GHOST_LOCAL=1`). -Optional install config via env: +Optional at install time: ```bash -GHOST_PROXY=1 ./install.sh # opt in to the Webshare rotating proxy (IP-mask the relay; off by default) -GHOST_LOCAL=1 ./install.sh # also install Ollama + a local model for an offline/incognito fallback -GHOST_LOCAL_32B=1 ./install.sh # pull the stronger 32B local model too (26GB; implies GHOST_LOCAL) -``` - -Local models are opt-in. By default ghost is hosted-only -- no Ollama, and the fallback + auxiliary -tasks route to a hosted 70B over the same private og-veil path. - -Prerequisites are auto-installed; full details under [Install](#install) below. - ---- - -## How hosted models reach you - -Hosted (non-local) models no longer go to Nous directly. They run through the **OpenGradient -chat-api OHTTP relay to a TEE (Trusted Execution Environment) gateway**, resolved from the -on-chain TEE registry. ghost no longer hand-rolls that protocol -- it delegates it to -[og-veil](https://github.com/OpenGradient/veil) (the `opengradient-veil` package), the same -implementation the chat-app and the `og-veil` CLI use: - -``` -ghost engine - โ””โ”€ hosted model โ”€โ–บ scrubber (:8788) [scrub PII/secrets, strip provider prefix] - โ””โ”€โ–บ og-veil (:11435) [HPKE-encrypt, OHTTP relay, verify] - โ””โ”€โ–บ chat-api /api/v1/chat/ohttp [Supabase bearer; RELAY only] - โ””โ”€โ–บ TEE gateway [decrypts in-enclave, runs model, signs output] +GHOST_LOCAL=1 ./install.sh # also install a local model (offline/incognito fallback) +GHOST_LOCAL_32B=1 ./install.sh # pull the stronger 32B local model too (26GB) +GHOST_PROXY=1 ./install.sh # opt in to a rotating proxy to also hide your IP from the relay (off by default) ``` -Two boundaries, like the website: the **relay (chat-api)** sees your account token + IP but only -ciphertext; the **enclave** sees the prompt but never your identity. The scrubber runs *before* -og-veil encrypts, on plaintext localhost, so your name/email/secrets reach neither the relay nor -the enclave. og-veil reads the TEE's HPKE key + RSA signing key from the on-chain registry and -**verifies every response against that signing key before a single token is returned**. +By default ghost is hosted-only: no Ollama, and both the fallback (`hermes-4-405b`) and the auxiliary tasks (`hermes-4-70b`) run hosted over the same private path. --- -## Two modes - -ghost runs one of two kinds of model, a deliberate privacy/capability trade: +## Models -| | **Default: hosted (Hermes 405B + others)** | **Fallback / on-demand: local 32B** | -|---|---|---| -| Model | `nous/hermes-4-405b` / `nous/hermes-4-70b` via the TEE gateway | `uncensored-local` (Qwen2.5-32B-abliterated, Q6) | -| Where it runs | An OpenGradient TEE enclave, reached scrubber โ†’ OHTTP โ†’ chat-api relay | Your machine, fully offline | -| Privacy | IP hidden from the enclave, content hidden from the relay, PII/secrets scrubbed, **but account-linked to your OpenGradient login** | **True incognito -- nothing leaves the box** | -| Strength | Frontier agentic quality; pick any hosted model with `/model` | Weaker agentic searcher; clean, uncensored prose | -| When | Always, if you're logged in and the gateway is reachable | Auto-fallback if hosted is unavailable, or via `/model` | +Every model is unrestricted and open-weight, served over one OHTTP/TEE path. Switch with `/model`; the gateway rejects anything off this list. -The default is the hosted Hermes 405B because it is the stronger agent; the OHTTP path makes it -"private but not anonymous." Switch to the local model any time you want **zero** egress. - ---- - -## What you get - -| Layer | Behaviour | +| Model | What it is | |---|---| -| **Default model** | `deepseek/deepseek-v4-pro` via the `opengradient` provider -- scrubber (`:8788`) โ†’ og-veil (`:11435`) โ†’ chat-api relay โ†’ TEE gateway (uncensored through ghost's steer; strongest open agentic model). `nous/hermes-4-405b` and the rest of the catalog via `/model`. | -| **Hosted line-up** | Hermes 4 (405B / 70B) -- unrestricted, open-weight models only, over the one OHTTP path. Closed/refusing models (Claude, GPT, Gemini, Grok) are deliberately not offered. | -| **Fallback model** | `fallback_model` โ†’ local `uncensored-local` (32B) if the hosted gateway is unreachable | -| **Tool / auxiliary model** | Local 7B abliterated (`ghost-tool`) runs titling, compression, triage -- never a hosted provider | -| **Auth** | A Supabase session from `ghost-login` (browser), held + auto-refreshed by og-veil. The relay authenticates it; the enclave never sees it | -| **Encryption** | HPKE (DHKEM-X25519 / HKDF-SHA256 / ChaCha20-Poly1305) per request, done by og-veil; verification before emit | -| **Registry** | og-veil reads the active TEE from the on-chain registry: endpoint, HPKE `ohttpConfig`, and RSA signing key | -| **PII + secret scrubber** | Strips your name/email/handles **and** API keys, tokens, JWTs, private keys from outbound requests, before og-veil encrypts | -| **Egress proxy** | **Opt-in** (`GHOST_PROXY=1`): a rotating residential exit hides your IP from the chat-api relay. Off by default (direct) | -| **Web search** | Local `ddgs` โ†’ engines (direct), or via the rotating proxy when `GHOST_PROXY=1`. No third-party search API sees the query | -| **Memory / telemetry** | Off / none. Catalog served locally; brightdata + codex MCPs removed; TTS local (piper) | -| **Skills** | Created/installed skills go to `~/.ghost/skills-ghost` -- isolated from your normie `hermes` | -| **Interface** | Ghost-branded over the Hermes engine -- **GHOST** banner, ๐Ÿ‘ป figure, all visible text reads Ghost | - ---- - -## Architecture - -``` - ghost โ”€โ”€โ–บ ~/.ghost-engine (its own engine copy; any `hermes` install untouched) - โ”‚ - โ”‚ default (any hosted model) - โ”œโ”€ Hermes 405B / 70B (unrestricted, open-weight) - โ”‚ โ””โ”€โ–บ scrubber (:8788) [scrub name/keys, strip provider prefix] - โ”‚ โ””โ”€โ–บ og-veil (:11435) [HPKE-encrypt, OHTTP, verify] - โ”‚ โ””โ”€โ–บ [rotating proxy (:8899), IP hidden โ€” opt-in GHOST_PROXY] โ”€โ–บ chat-api /api/v1/chat/ohttp - โ”‚ โ””โ”€โ–บ TEE gateway [decrypts in-enclave, runs model, signs] - โ”‚ /model or auto-fallback - โ”œโ”€ local 32B (uncensored-local) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บ offline, zero egress - โ”‚ - โ”œโ”€ web search โ”€โ–บ ddgs โ”€โ–บ [rotating proxy (:8899) if GHOST_PROXY] โ”€โ–บ search engines - โ”‚ - โ””โ”€ 12 auxiliary tasks โ”€โ–บ local 7B (ghost-tool) (titling / compression / triage) - - launchd keeps the services alive: com.advait.hermes-pii-scrubber (scrubber) - com.advait.hermes-veil (og-veil; OHTTP/TEE/verify + auth) - com.advait.hermes-proxy (rotating proxy; only with GHOST_PROXY) -``` - -A launch preflight (`bin/ghost`) checks the scrubber `/healthz`, og-veil `/health`, your login -status, and the proxy exit IP, and warns (never hard-blocks) if anything is down -- so the offline -path still runs. +| `deepseek/deepseek-v4-pro` **(default)** | Strongest open reasoning + coding model; best for agentic work. Uncensored via ghost's per-model steer. | +| `nous/hermes-4-405b` | Flagship uncensored open model, most steerable. Also the hosted fallback. | +| `zai/glm-5.2` | Strong open agentic MoE (Z.ai). | +| `nous/hermes-4-70b` | Fast, low-cost; runs ghost's auxiliary tasks. | +| local (opt-in) | Abliterated 7B (`ghost-tool`), or 32B (`uncensored-local`) with `GHOST_LOCAL_32B`. Fully offline. | --- -## The privacy model -- what each layer actually protects +## How it stays private -- **Scrubber (`:8788`)** -- an OpenAI-compatible endpoint the engine talks to over plaintext - localhost. It redacts a denylist (your name/email/handles) + regex secrets (API keys, tokens, - JWTs, private keys) from request bodies, then forwards the cleaned request to og-veil. Outbound- - only: the local model never routes through it, so local replies are never scrubbed. -- **og-veil (`:11435`)** -- the [`opengradient-veil`](https://github.com/OpenGradient/veil) package - owns the protocol: it reads the active TEE from the on-chain registry, **HPKE-encrypts** each - request, relays it over OHTTP to chat-api, and verifies the enclave's signature on every response - **before emitting a token** (verify-before-emit). One implementation, shared with the chat-app -- - ghost no longer maintains its own copy. -- **OHTTP / TEE split** -- chat-api is only a *relay*: it sees your bearer token + IP but the request - body is ciphertext it cannot read. The TEE enclave decrypts and runs the model but is reached - through the relay, so it never learns who you are. (By default ghost is **direct** -- the relay - sees your real IP; the content stays private regardless. Hide the IP too with the opt-in proxy below.) -- **Rotating proxy (`:8899`) -- opt-in (`GHOST_PROXY=1`), off by default** -- picks a fresh Webshare - residential exit per connection, so the chat-api relay sees a rotating IP, never yours -- og-veil's - egress is routed through it. Also carries the engine's own egress (web search). -- **Private search** -- when the proxy is enabled, `ddgs` honours `DDGS_PROXY` (it ignores - `HTTPS_PROXY`), so every query egresses through the rotating proxy to the engines; otherwise it - goes out directly. No search-API account either way. -- **No memory, no telemetry** -- memory toolset off; model catalog served locally; gateway/update - calls blocked or removed. -- **Skill + state isolation** -- ghost's skills live in their own dir; nothing it creates pollutes `hermes`. +Hosted models don't go to a provider directly. ghost scrubs the request locally, then hands it to [og-veil](https://github.com/OpenGradient/veil) (the `opengradient-veil` package, the same one the [chat.opengradient.ai](https://chat.opengradient.ai) site uses), which encrypts it and relays it over Oblivious HTTP to a TEE enclave: ---- - -## Layout - -- `profile/` -- `config.yaml` (the full incognito profile), `SOUL.md` (the Ghost identity), `.env.example`, `pii_denylist.example.txt`, `uncensored_prefill.json` -- `privacy/` - - `scrubbing_proxy.py` -- the PII/secret scrubber + local model-catalog endpoint; forwards cleaned requests to og-veil - - `scrub_patterns.py` -- single source of the secret + PII regexes (shared by both scrub paths) - - `presidio_scrub.py` -- NER PII detection (Presidio + spaCy) with reversible placeholders + stream de-anon - - `rotating_proxy.py` -- Webshare rotation (opt-in IP-masking) - - `ensure_scrubber_route.py` -- self-heals the engine's hosted route back to the scrubber after a token refresh - - _(the OHTTP/HPKE/registry/verification + Supabase auth that used to live here now comes from the `opengradient-veil` package -- run `og-veil`)_ -- `scripts/` -- `fork-engine.sh` (copy + relocate venv + isolate skills) and `debrand.py` (scrub visible strings + the two ASCII-art logos) -- `launchd/` -- the scrubber + og-veil + rotating-proxy service templates -- `bin/ghost` -- the launcher (privacy preflight) ยท `bin/ghost-login` -- account connect/refresh -- `install.sh` -- end-to-end installer ยท `models.txt` -- the local models to pull - ---- - -## Install - -**One command installs everything** -- the engine into `~/.ghost-engine` (Ghost-branded), the -privacy stack (og-veil + httpx), and the `ghost` + `ghost-login` commands. -(Local models via [Ollama](https://ollama.com) are opt-in: add `GHOST_LOCAL=1`.) Idempotent -(safe to re-run): - -```bash -./install.sh -``` - -The default is the **direct, hosted-only** private setup: it auto-installs prerequisites, -starts the scrubber + og-veil, installs the engine into `~/.ghost-engine` and applies Ghost -branding, offers to run the account login, installs `ghost`, and smoke-tests it. No proxy is set -up; og-veil talks to chat-api directly (content is still private via OHTTP/TEE). Local models -are opt-in (`GHOST_LOCAL=1`). - -**Config modes (optional env vars):** - -```bash -GHOST_PROXY=1 ./install.sh # opt in to the Webshare rotating proxy (IP-mask the chat-api relay) -GHOST_LOCAL=1 ./install.sh # opt in to Ollama + a local model (offline/incognito fallback) -GHOST_LOCAL_32B=1 ./install.sh # pull the stronger 32B local model too (26GB; implies GHOST_LOCAL) -GHOST_CHAT_APP_URL=https://... # override the website used for ghost-login (default chat.opengradient.ai) -``` - -- **`GHOST_PROXY=1`** -- opt in to IP-masking: prompts for your Webshare proxy list, runs the rotating - proxy, and routes both og-veil's egress to chat-api and the engine's web-search egress through it, so - the relay sees a rotating IP instead of yours. Off by default (direct); the scrubber + personal PII - denylist run either way. - -After install, **connect your account** and personalize the scrubber denylist: - -```bash -ghost-login # browser login (or: ghost-login --paste for headless) -ghost-login --status # who am I logged in as? -# edit ~/.ghost/privacy/pii_denylist.txt with your name/email/handles ``` - -```bash -ghost # chat (default = DeepSeek V4 Pro via the TEE gateway, OHTTP-private) -ghost --yolo -z "..." # one-shot -ghost --paths "..." # agentic file work: real filesystem paths reach the hosted model - # (your name + secrets in content are still scrubbed) -# inside: /model -> switch between the hosted line-up and uncensored-local +ghost engine + โ””โ”€ scrubber (:8788) scrub PII/secrets, strip provider prefix, apply model steer + โ””โ”€ og-veil (:11435) HPKE-encrypt, OHTTP relay, verify signature before emit + โ””โ”€ chat-api relay sees your account token + IP, but only ciphertext + โ””โ”€ TEE enclave decrypts, runs the model, signs the output ``` -### Agentic file work -- two ways - -By default the scrubber redacts everything outbound, **including filesystem paths** -- which -breaks file ops on the hosted model (it sees `/Users/[REDACTED_PII]/...`). Two ways to do real -file work: +Two boundaries, like the website: the **relay** sees your account + IP but only ciphertext; the **enclave** sees the prompt but never your identity. The scrubber runs first, on plaintext localhost, so your name/email/secrets reach neither. The hosted path is **private, not anonymous** -- your OpenGradient account is still authenticated, and by default the relay sees your real IP (`GHOST_PROXY=1` masks it). For full anonymity, use the local model: zero egress, nothing leaves the box. -- **`ghost --paths`** -- flips on *path-aware* mode for that session: real paths pass through to the - hosted model so it can read/write files, while your name + secrets in prose are still scrubbed. - The trade is that your home/username becomes visible inside those paths. -- **Local model** (`/model` โ†’ `uncensored-local`) -- never touches the scrubber/bridge, so paths are - always real and **nothing leaves the box**. Weaker agent, but the fully-private option. +For agentic file work, real paths are scrubbed by default (the model sees `/Users/[REDACTED]/...`); run `ghost --paths` to let real paths through while your name + secrets in prose stay scrubbed, or use the local model where paths are always real. --- ## Honest limits -- **The hosted default is private, not anonymous.** chat-api still authenticates your OpenGradient - account. The scrubber hides your name/secrets and OHTTP hides your content from the relay -- but the - account link remains, and by default (direct) the relay also sees your real IP. Enable the opt-in - proxy (`GHOST_PROXY=1`) to mask the IP too. For zero-egress anonymity, switch to the **local 32B** - (`/model`) -- that is the true-incognito mode. -- **Responses are verified before they reach you.** og-veil checks the enclave's signature on every - hosted response and refuses to emit a single token it can't verify (verify-before-emit) -- ghost no - longer carries its own best-effort `GHOST_TEE_VERIFY` knob; verification now lives in og-veil. -- **The local fallback isn't perfectly offline under tool-use enforcement.** `tool_use_enforcement: true` - makes search reliable, but the 32B's agentic loop will lean on the hosted gateway for tool - orchestration (still scrubbed + OHTTP, but account-linked), and the 32B is a weak agentic searcher. -- **Proxies are opt-in and trust-shifted.** ghost is direct by default (no proxy). With `GHOST_PROXY=1`, - Webshare sees your real IP unless you run a VPN in front. NordVPN on this Mac is GUI-only (no CLI), so - enable its auto-connect manually for the extra hop. -- **The engine is forked, not rewritten.** Internal Python package names stay `hermes_cli` (invisible to - users). `hermes update` updates only the original install; re-run `scripts/fork-engine.sh` to pull - upstream changes into the fork. +- **Private, not anonymous.** The relay still authenticates your OpenGradient account; OHTTP hides content and the scrubber hides PII, but the account link remains. Use the local model for zero-egress anonymity. +- **The local path is opt-in and weaker.** Off by default; install with `GHOST_LOCAL`. The local model is a weaker agentic searcher and may still lean on the hosted gateway for tool orchestration under tool-use enforcement. +- **The engine is forked, not rewritten.** Internal package names stay `hermes_cli`. `hermes update` only updates the original install; re-run `scripts/fork-engine.sh` to pull upstream changes into the fork. --- @@ -277,5 +91,4 @@ file work: ## Security -ghost is a privacy tool; a PII/secret leak is treated as a P0. See [SECURITY.md](SECURITY.md) -for how to report one privately. +ghost is a privacy tool; a PII/secret leak is treated as a P0. See [SECURITY.md](SECURITY.md) for how to report one privately. diff --git a/install.sh b/install.sh index a3bddcf..382a8ba 100644 --- a/install.sh +++ b/install.sh @@ -221,7 +221,7 @@ GL="$HOME/.local/bin/ghost-login" # thin wrapper over `og-veil login` (install if "$GL" --status >/dev/null 2>&1; then echo " already connected: $("$GL" --status)" else - echo " Hosted models (the default Hermes 405B + Claude/GPT/Gemini/Grok) need a one-time login." + echo " Hosted models (the default DeepSeek V4 Pro + Hermes/GLM, all open-weight) need a one-time login." if [ -t 0 ]; then printf " Run the browser login now? [Y/n] "; read -r ANS || true case "${ANS:-Y}" in [Nn]*) echo " Skipped -- run 'ghost-login' anytime.";; *) "$GL" || echo " (login skipped/failed -- run 'ghost-login' anytime)";; esac diff --git a/profile/config.yaml b/profile/config.yaml index 55b0bb3..fd91148 100644 --- a/profile/config.yaml +++ b/profile/config.yaml @@ -30,7 +30,7 @@ toolsets: - hermes-cli max_concurrent_sessions: null agent: - max_turns: 40 + max_turns: 200 gateway_timeout: 1800 restart_drain_timeout: 60 api_max_retries: 3 From 52c0b1952d34d9f3f4941dd705ca84edabadbb2c Mon Sep 17 00:00:00 2001 From: Advait Jayant Date: Wed, 24 Jun 2026 19:43:35 +0100 Subject: [PATCH 2/9] Remove the Webshare rotating proxy entirely The opt-in proxy only masked the IP from OpenGradient's own relay, which already authenticates the account, so it bought near-zero anonymity while carrying a service, a 171-line module, a dependency, and install branches. Privacy comes from og-veil (OHTTP/HPKE + TEE enclave); for full anonymity use the local model (zero egress). - delete privacy/rotating_proxy.py + launchd/com.advait.hermes-proxy.plist - install.sh: drop GHOST_PROXY/USE_PROXY, the Webshare prompt, the proxy service + .proxy marker, and the veil HTTPS_PROXY env injection - bin/ghost: drop the proxy var + the IP/NET banner indicator - veil plist: drop the __VEIL_PROXY_ENV__ placeholder - README / .env.example / scrubber comment: drop proxy mentions --- README.md | 3 +- bin/ghost | 13 +- install.sh | 59 ++------- launchd/com.advait.hermes-proxy.plist | 21 ---- launchd/com.advait.hermes-veil.plist | 1 - privacy/rotating_proxy.py | 171 -------------------------- privacy/scrubbing_proxy.py | 7 +- profile/.env.example | 17 +-- 8 files changed, 20 insertions(+), 272 deletions(-) delete mode 100644 launchd/com.advait.hermes-proxy.plist delete mode 100644 privacy/rotating_proxy.py diff --git a/README.md b/README.md index a04412d..126cc7a 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,6 @@ Optional at install time: ```bash GHOST_LOCAL=1 ./install.sh # also install a local model (offline/incognito fallback) GHOST_LOCAL_32B=1 ./install.sh # pull the stronger 32B local model too (26GB) -GHOST_PROXY=1 ./install.sh # opt in to a rotating proxy to also hide your IP from the relay (off by default) ``` By default ghost is hosted-only: no Ollama, and both the fallback (`hermes-4-405b`) and the auxiliary tasks (`hermes-4-70b`) run hosted over the same private path. @@ -71,7 +70,7 @@ ghost engine โ””โ”€ TEE enclave decrypts, runs the model, signs the output ``` -Two boundaries, like the website: the **relay** sees your account + IP but only ciphertext; the **enclave** sees the prompt but never your identity. The scrubber runs first, on plaintext localhost, so your name/email/secrets reach neither. The hosted path is **private, not anonymous** -- your OpenGradient account is still authenticated, and by default the relay sees your real IP (`GHOST_PROXY=1` masks it). For full anonymity, use the local model: zero egress, nothing leaves the box. +Two boundaries, like the website: the **relay** sees your account + IP but only ciphertext; the **enclave** sees the prompt but never your identity. The scrubber runs first, on plaintext localhost, so your name/email/secrets reach neither. The hosted path is **private, not anonymous** -- your OpenGradient account is still authenticated and the relay sees your IP. For full anonymity, use the local model: zero egress, nothing leaves the box. For agentic file work, real paths are scrubbed by default (the model sees `/Users/[REDACTED]/...`); run `ghost --paths` to let real paths through while your name + secrets in prose stay scrubbed, or use the local model where paths are always real. diff --git a/bin/ghost b/bin/ghost index c36520c..a9f41bc 100755 --- a/bin/ghost +++ b/bin/ghost @@ -8,7 +8,6 @@ # offline local model still runs. SCRUBBER="http://127.0.0.1:8788" VEIL="http://127.0.0.1:11435" -PROXY="http://127.0.0.1:8899" PY=__PYTHON__ SENTINEL="__GHOST_HOME__/privacy/.pass_paths" export HERMES_HOME="__GHOST_HOME__" # ghost's isolated state dir (the engine honors HERMES_HOME) @@ -77,17 +76,11 @@ else fi # Privacy comes from og-veil: every request is HPKE/OHTTP-encrypted, so the relay only ever sees -# ciphertext (the "via TEE gateway (OHTTP)" below). The relay still sees your IP; the OPTIONAL -# rotating proxy (GHOST_PROXY=1, off by default) hides that too. Only surface an IP indicator -# when the proxy is actually enabled (.proxy marker) -- no VPN noise in the default direct setup. -NET="" -if [ -f "__GHOST_HOME__/privacy/.proxy" ]; then - EXIT_IP=$(/usr/bin/curl -s --max-time 6 -x "$PROXY" https://api.ipify.org 2>/dev/null) - NET=" ยท ${EXIT_IP:+IP-masked via $EXIT_IP}"; [ -n "$EXIT_IP" ] || NET=" ยท proxy DOWN" -fi +# ciphertext (the "via TEE gateway (OHTTP)" below). The relay still sees your IP; for full +# anonymity use the local model (ghost --local), which has zero egress. if [ "$HC" = "200" ] && [ "$VC" = "200" ]; then - STATUS="๐Ÿ‘ป ghost ยท $DEF_MODEL via TEE gateway (OHTTP/og-veil)${NET} ยท $LOGIN ยท $FB" + STATUS="๐Ÿ‘ป ghost ยท $DEF_MODEL via TEE gateway (OHTTP/og-veil) ยท $LOGIN ยท $FB" elif [ "$HC" = "200" ]; then STATUS="๐Ÿ‘ป ghost ยท โš ๏ธ og-veil DOWN (veil=$VC) -- hosted unreachable (try ghost-login), $FB_DOWN" else diff --git a/install.sh b/install.sh index 382a8ba..56e0885 100644 --- a/install.sh +++ b/install.sh @@ -13,13 +13,10 @@ # website uses. After install, run `ghost-login` once to connect your account (a browser # login that hands a session token to og-veil). # -# By default ghost runs DIRECT: the scrubber + og-veil talk to chat-api directly (content is -# still private -- og-veil OHTTP-encrypts it and the TEE enclave separates identity). No -# rotating-proxy setup is needed. IP-masking is opt-in (see GHOST_PROXY below). +# The scrubber + og-veil talk to chat-api directly: content is private (og-veil OHTTP-encrypts +# it and the TEE enclave separates identity), reached over your normal connection. # # Optional config via env (all optional -- plain `./install.sh` does the full private setup): -# GHOST_PROXY=1 opt in to the Webshare rotating proxy: masks your IP from the chat-api -# relay (og-veil egress) + carries the engine's web-search egress # GHOST_LOCAL=1 also install Ollama + a local model for an offline / true-incognito # fallback (DEFAULT is hosted-only -- no Ollama, fallback is hosted 70B) # GHOST_LOCAL_32B=1 pull the stronger 32B local model too (26GB; implies GHOST_LOCAL) @@ -42,9 +39,6 @@ LA="$HOME/Library/LaunchAgents" ENG="${GHOST_ENGINE:-$HOME/.ghost-engine}" PYTHON="${GHOST_PYTHON:-$(command -v python3 || true)}" SCRUBBER="http://127.0.0.1:8788" -# Direct is the default. Opt in to the Webshare rotating proxy with GHOST_PROXY=1. -# (GHOST_DIRECT is still honored for back-compat, but it's now the default anyway.) -USE_PROXY="${GHOST_PROXY:-}" # Local models (Ollama) are OPT-IN. Default = hosted-only: no Ollama, and the fallback + # auxiliary tasks route to a hosted model (nous/hermes-4-70b) over the same private og-veil # path. Set GHOST_LOCAL=1 to also install Ollama + a local model for an offline / incognito @@ -115,14 +109,6 @@ mkdir -p "$PROFILE" sed -e "s#__HOME__#$HOME#g" -e "s#__LOCAL_MODEL__#$LOCAL_MODEL#g" "$REPO/profile/config.yaml" > "$PROFILE/config.yaml" cp "$REPO/profile/SOUL.md" "$PROFILE/SOUL.md" [ -f "$PROFILE/.env" ] || cp "$REPO/profile/.env.example" "$PROFILE/.env" -if [ -n "$USE_PROXY" ]; then # opt-in: route the engine's own egress (web search/fetches) through the rotating proxy - "$PYTHON" - "$PROFILE/.env" <<'PYEOF' -import sys, re -p = sys.argv[1]; s = open(p).read() -s = re.sub(r"(?m)^#\s*((?:HTTPS_PROXY|HTTP_PROXY|ALL_PROXY|DDGS_PROXY)=\S+)\s*$", r"\1", s) -open(p, "w").write(s) -PYEOF -fi if [ -z "$WANT_LOCAL" ]; then # hosted-only (default) -> route auxiliary + fallback to a hosted model via og-veil "$PYTHON" - "$PROFILE/config.yaml" <<'PYEOF' import sys, re @@ -140,8 +126,8 @@ open(p, "w").write(s); print(" hosted-only: fallback -> nous/hermes-4-405b, au PYEOF fi -# ---------- 3. privacy stack (PII scrubber + og-veil always; rotating proxy only with GHOST_PROXY) ---------- -say "Privacy stack (PII/secret scrubber -> og-veil${USE_PROXY:+ + rotating proxy})" +# ---------- 3. privacy stack (PII scrubber + og-veil) ---------- +say "Privacy stack (PII/secret scrubber -> og-veil)" mkdir -p "$PRIV" cp "$REPO"/privacy/*.py "$PRIV/" # Enable the NER PII scrubber when Presidio + the spaCy model are present; else leave it off @@ -155,22 +141,10 @@ fi cp "$REPO/profile/uncensored_prefill.json" "$PRIV/uncensored_prefill.json" mkdir -p "$LA" -# The scrubber always runs. The Webshare rotating proxy is OPT-IN (GHOST_PROXY=1): -# by default ghost is direct -- og-veil talks to chat-api itself (content is still -# private via OHTTP/TEE; only IP-masking is skipped). +# The scrubber runs as a launchd service; og-veil talks to chat-api directly (content is +# still private via OHTTP/TEE). Clean up any rotating-proxy marker from an older install. BASE_SERVICES="hermes-pii-scrubber" -if [ -n "$USE_PROXY" ]; then - if [ ! -s "$GHOST_HOME/webshare_proxies.txt" ]; then - echo " Paste your Webshare proxy-list download URL (ip:port:user:pass), or Enter to skip:" - read -r WS_URL || true - [ -n "${WS_URL:-}" ] && curl -fsSL "$WS_URL" -o "$GHOST_HOME/webshare_proxies.txt" && echo " $(wc -l <"$GHOST_HOME/webshare_proxies.txt"|tr -d ' ') proxies" - fi - BASE_SERVICES="hermes-proxy $BASE_SERVICES" - : > "$PRIV/.proxy" # marker: egress is IP-masked through the rotating proxy (banner reads this) -else - rm -f "$PRIV/.proxy" - say "Direct mode (default) -- og-veil talks to chat-api directly; no rotating proxy. Set GHOST_PROXY=1 to IP-mask." -fi +rm -f "$PRIV/.proxy" for svc in $BASE_SERVICES; do sed -e "s#__PYTHON__#$PYTHON#g" -e "s#__HOME__#$HOME#g" "$REPO/launchd/com.advait.$svc.plist" > "$LA/com.advait.$svc.plist" @@ -179,23 +153,9 @@ for svc in $BASE_SERVICES; do done # og-veil service (port 11435, to avoid colliding with Ollama on 11434). It owns the -# OHTTP/TEE/verification + auth. Default: direct egress to chat-api. With GHOST_PROXY=1 -# its egress is routed through the rotating proxy so the relay never sees your real IP. -if [ -n "$USE_PROXY" ]; then - VEIL_PROXY_ENV=$' HTTPS_PROXY\n http://127.0.0.1:8899\n HTTP_PROXY\n http://127.0.0.1:8899\n NO_PROXY\n 127.0.0.1,localhost,::1' -else - VEIL_PROXY_ENV="" -fi +# OHTTP/TEE/verification + auth, and talks to chat-api directly. VEIL_PLIST="$LA/com.advait.hermes-veil.plist" -GP_PYTHON="$PYTHON" GP_HOME="$HOME" GP_PROXY_ENV="$VEIL_PROXY_ENV" \ - "$PYTHON" - "$REPO/launchd/com.advait.hermes-veil.plist" "$VEIL_PLIST" <<'PYEOF' -import os, sys -src, dst = sys.argv[1], sys.argv[2] -s = open(src).read() -s = s.replace("__PYTHON__", os.environ["GP_PYTHON"]).replace("__HOME__", os.environ["GP_HOME"]) -s = s.replace("__VEIL_PROXY_ENV__", os.environ.get("GP_PROXY_ENV", "")) -open(dst, "w").write(s) -PYEOF +sed -e "s#__PYTHON__#$PYTHON#g" -e "s#__HOME__#$HOME#g" "$REPO/launchd/com.advait.hermes-veil.plist" > "$VEIL_PLIST" launchctl unload "$VEIL_PLIST" 2>/dev/null || true launchctl load -w "$VEIL_PLIST" @@ -239,5 +199,4 @@ case ":$PATH:" in *":$HOME/.local/bin:"*) ;; *) echo " (add ~/.local/bin to yo echo " Hosted default = deepseek/deepseek-v4-pro via og-veil -> the OpenGradient TEE gateway (OHTTP-private)." echo " Inside ghost, /model switches between hosted models and the local model (true incognito)." echo " Personalize $PRIV/pii_denylist.txt with your name/email/handles for the hosted-path scrubber." -[ -n "$USE_PROXY" ] || echo " Direct mode (default). For IP-masking from the relay, reinstall with GHOST_PROXY=1." echo " Not connected yet? Run: ghost-login" diff --git a/launchd/com.advait.hermes-proxy.plist b/launchd/com.advait.hermes-proxy.plist deleted file mode 100644 index 9d9114b..0000000 --- a/launchd/com.advait.hermes-proxy.plist +++ /dev/null @@ -1,21 +0,0 @@ - - - - - Label - com.advait.hermes-proxy - ProgramArguments - - __PYTHON__ - __HOME__/.ghost/privacy/rotating_proxy.py - - RunAtLoad - - KeepAlive - - StandardOutPath - __HOME__/.ghost/privacy/proxy.out.log - StandardErrorPath - __HOME__/.ghost/privacy/proxy.err.log - - diff --git a/launchd/com.advait.hermes-veil.plist b/launchd/com.advait.hermes-veil.plist index 10e4145..9ac2b5c 100644 --- a/launchd/com.advait.hermes-veil.plist +++ b/launchd/com.advait.hermes-veil.plist @@ -23,7 +23,6 @@ OG_VEIL_PORT 11435 -__VEIL_PROXY_ENV__ StandardOutPath __HOME__/.ghost/privacy/veil.out.log diff --git a/privacy/rotating_proxy.py b/privacy/rotating_proxy.py deleted file mode 100644 index 90fa1ab..0000000 --- a/privacy/rotating_proxy.py +++ /dev/null @@ -1,171 +0,0 @@ -#!/usr/bin/env python3 -"""Local rotating forward-proxy for Hermes. - -Listens on 127.0.0.1:8899. For each new connection it picks a random Webshare -upstream proxy (from webshare_proxies.txt, format ip:port:user:pass), chains the -request through it (CONNECT tunnel for HTTPS, absolute-form forward for HTTP), -and retries other upstreams if one is dead. This hides the machine's real IP -from the inference endpoint and rotates exit IPs across connections. - -Run via launchd (com.advait.hermes-proxy) so it persists across sessions. -""" -import asyncio, base64, os, random, time -from urllib.parse import urlparse - -PROXY_FILE = os.path.expanduser("~/.ghost/webshare_proxies.txt") -LOG_FILE = os.path.expanduser("~/.ghost/privacy/proxy.log") -LISTEN_HOST, LISTEN_PORT = "127.0.0.1", 8899 -MAX_TRIES, CONN_TIMEOUT = 8, 5 -# Incognito blocklist: hosts the harness phones home to that we refuse (anonymous pricing -# lookups, telemetry, update checks). CONNECT to these returns 403 and Hermes falls back. -BLOCKLIST = {"openrouter.ai", "firecrawl-gateway.nousresearch.com", "hermes-agent.nousresearch.com"} - - -def load_proxies(): - out = [] - try: - with open(PROXY_FILE) as f: - for ln in f: - ln = ln.strip() - if ln and ln.count(":") >= 3: - h, p, u, pw = ln.split(":", 3) - out.append((h, int(p), u, pw)) - except FileNotFoundError: - pass - return out - - -PROXIES = load_proxies() - - -def log(msg): - try: - with open(LOG_FILE, "a") as f: - f.write(time.strftime("%Y-%m-%d %H:%M:%S ") + msg + "\n") - except Exception: - pass - - -async def pipe(reader, writer): - try: - while True: - data = await reader.read(65536) - if not data: - break - writer.write(data) - await writer.drain() - except Exception: - pass - finally: - try: - writer.close() - except Exception: - pass - - -def pick(): - return random.sample(PROXIES, min(MAX_TRIES, len(PROXIES))) - - -async def connect_upstream(host, port): - """Open a CONNECT tunnel via a random upstream. Returns (r, w, ip) or None.""" - for ph, pp, pu, ppw in pick(): - try: - ur, uw = await asyncio.wait_for(asyncio.open_connection(ph, pp), CONN_TIMEOUT) - auth = base64.b64encode(f"{pu}:{ppw}".encode()).decode() - uw.write((f"CONNECT {host}:{port} HTTP/1.1\r\n" - f"Host: {host}:{port}\r\n" - f"Proxy-Authorization: Basic {auth}\r\n" - f"Proxy-Connection: keep-alive\r\n\r\n").encode()) - await uw.drain() - status = await asyncio.wait_for(ur.readline(), CONN_TIMEOUT) - if b" 200 " not in status: - uw.close() - continue - while True: # drain upstream response headers - h = await asyncio.wait_for(ur.readline(), CONN_TIMEOUT) - if h in (b"\r\n", b"\n", b""): - break - return ur, uw, ph - except Exception: - continue - return None - - -async def handle(creader, cwriter): - try: - first = await asyncio.wait_for(creader.readline(), 30) - if not first: - cwriter.close(); return - method, target = first.split()[0].decode("latin1"), first.split()[1].decode("latin1") - headers = [] - while True: - h = await asyncio.wait_for(creader.readline(), 30) - headers.append(h) - if h in (b"\r\n", b"\n", b""): - break - except Exception: - try: cwriter.close() - except Exception: pass - return - - if method.upper() == "CONNECT": - host, _, port = target.partition(":") - port = int(port or 443) - if any(host == b or host.endswith("." + b) for b in BLOCKLIST): - cwriter.write(b"HTTP/1.1 403 Blocked (incognito)\r\n\r\n") - try: await cwriter.drain() - except Exception: pass - cwriter.close() - log(f"BLOCKED {host}:{port}") - return - up = await connect_upstream(host, port) - if not up: - cwriter.write(b"HTTP/1.1 502 Bad Gateway\r\n\r\n") - try: await cwriter.drain() - except Exception: pass - cwriter.close() - log(f"FAIL CONNECT {host}:{port}") - return - ur, uw, ip = up - cwriter.write(b"HTTP/1.1 200 Connection established\r\n\r\n") - try: await cwriter.drain() - except Exception: pass - log(f"CONNECT {host}:{port} via {ip}") - await asyncio.gather(pipe(creader, uw), pipe(ur, cwriter)) - else: - u = urlparse(target) - host, port = u.hostname, (u.port or 80) - if not host: - cwriter.close(); return - for ph, pp, pu, ppw in pick(): - try: - ur, uw = await asyncio.wait_for(asyncio.open_connection(ph, pp), CONN_TIMEOUT) - auth = base64.b64encode(f"{pu}:{ppw}".encode()).decode() - uw.write(first) - uw.write(f"Proxy-Authorization: Basic {auth}\r\n".encode()) - for h in headers: - if not h.lower().startswith(b"proxy-"): - uw.write(h) - await uw.drain() - log(f"HTTP {host}:{port} via {ph}") - await asyncio.gather(pipe(creader, uw), pipe(ur, cwriter)) - return - except Exception: - continue - cwriter.write(b"HTTP/1.1 502 Bad Gateway\r\n\r\n") - cwriter.close() - - -async def main(): - if not PROXIES: - log("FATAL no proxies loaded from " + PROXY_FILE) - raise SystemExit(1) - server = await asyncio.start_server(handle, LISTEN_HOST, LISTEN_PORT) - log(f"listening on {LISTEN_HOST}:{LISTEN_PORT} with {len(PROXIES)} upstreams") - async with server: - await server.serve_forever() - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/privacy/scrubbing_proxy.py b/privacy/scrubbing_proxy.py index 93c7d81..7ece9be 100755 --- a/privacy/scrubbing_proxy.py +++ b/privacy/scrubbing_proxy.py @@ -309,10 +309,9 @@ def _apply_model_steer(obj): # โ”€โ”€ Upstream: og-veil's local OpenAI-compatible server โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -# The scrubber -> og-veil hop is plaintext localhost, so it must never go through -# the rotating proxy (that would defeat the localhost assumption and add latency); -# trust_env=False ignores any ambient HTTPS_PROXY. og-veil does the IP-masking on -# its *own* egress to chat-api. +# The scrubber -> og-veil hop is plaintext localhost. trust_env=False ignores any +# ambient HTTPS_PROXY so this hop always stays on loopback. og-veil owns the egress +# to chat-api (OHTTP-encrypted). def _veil_client(): return httpx.Client(timeout=300.0, trust_env=False) diff --git a/profile/.env.example b/profile/.env.example index 35fd262..7a51db3 100644 --- a/profile/.env.example +++ b/profile/.env.example @@ -2,16 +2,7 @@ # hosted models authenticate with the Supabase session captured by `ghost-login`, which is # held by og-veil (not via env here). # -# DIRECT BY DEFAULT: ghost does not route through a proxy. The engine's own egress (web -# search, fetches) goes out directly, and the hosted-model path reaches the local scrubber -# (127.0.0.1:8788) over localhost; og-veil then talks to chat-api itself. Content is still -# private (og-veil OHTTP-encrypts it + the TEE enclave separates identity) -- only IP-masking -# is skipped. -# -# To IP-mask via the Webshare rotating proxy, reinstall with GHOST_PROXY=1 (which uncomments -# the lines below; ddgs honors DDGS_PROXY -- it ignores HTTPS_PROXY): -# HTTPS_PROXY=http://127.0.0.1:8899 -# HTTP_PROXY=http://127.0.0.1:8899 -# ALL_PROXY=http://127.0.0.1:8899 -# DDGS_PROXY=http://127.0.0.1:8899 -NO_PROXY=127.0.0.1,localhost,::1,0.0.0.0 +# ghost does not route through any proxy. The engine's egress (web search, fetches) goes out +# directly, and the hosted-model path reaches the local scrubber (127.0.0.1:8788) over +# localhost; og-veil then talks to chat-api itself. Content is still private -- og-veil +# OHTTP-encrypts it and the TEE enclave separates identity. From dcde10c1d7c3165625ae324cecaff08dab4ea95e Mon Sep 17 00:00:00 2001 From: Advait Jayant Date: Wed, 24 Jun 2026 19:48:14 +0100 Subject: [PATCH 3/9] README: phrase the privacy section affirmatively for first-time readers Drop before/after framing that only makes sense to someone who knew the old setup (no 'no longer go to X', no 'like the website'); describe what ghost does, not what it used to do. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 126cc7a..79453b5 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ Every model is unrestricted and open-weight, served over one OHTTP/TEE path. Swi ## How it stays private -Hosted models don't go to a provider directly. ghost scrubs the request locally, then hands it to [og-veil](https://github.com/OpenGradient/veil) (the `opengradient-veil` package, the same one the [chat.opengradient.ai](https://chat.opengradient.ai) site uses), which encrypts it and relays it over Oblivious HTTP to a TEE enclave: +Every hosted request takes the same private path: ghost scrubs it locally, then hands it to [og-veil](https://github.com/OpenGradient/veil) (the `opengradient-veil` package, the same one the [chat.opengradient.ai](https://chat.opengradient.ai) site uses), which encrypts it and relays it over Oblivious HTTP to a TEE enclave: ``` ghost engine @@ -70,7 +70,7 @@ ghost engine โ””โ”€ TEE enclave decrypts, runs the model, signs the output ``` -Two boundaries, like the website: the **relay** sees your account + IP but only ciphertext; the **enclave** sees the prompt but never your identity. The scrubber runs first, on plaintext localhost, so your name/email/secrets reach neither. The hosted path is **private, not anonymous** -- your OpenGradient account is still authenticated and the relay sees your IP. For full anonymity, use the local model: zero egress, nothing leaves the box. +Two boundaries: the **relay** sees your account + IP but only ciphertext; the **enclave** sees the prompt but never your identity. The scrubber runs first, on plaintext localhost, so your name/email/secrets reach neither. The hosted path is **private, not anonymous** -- your OpenGradient account is still authenticated and the relay sees your IP. For full anonymity, use the local model: zero egress, nothing leaves the box. For agentic file work, real paths are scrubbed by default (the model sees `/Users/[REDACTED]/...`); run `ghost --paths` to let real paths through while your name + secrets in prose stay scrubbed, or use the local model where paths are always real. From 2cfe04ebd7ff77d11dfb0da553b8e2c7562e4e8f Mon Sep 17 00:00:00 2001 From: Advait Jayant Date: Wed, 24 Jun 2026 19:50:45 +0100 Subject: [PATCH 4/9] README: correct the steer claim, note macOS-only, soften catalog wording - Only DeepSeek gets a per-model steer; don't claim all hosted models are 'made uncensored via a steer' (Hermes is inherently so, GLM resists). - State up front that install is macOS-only (install.sh exits on non-Darwin). - Drop the blanket 'every model is unrestricted' (overclaims GLM). --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 79453b5..20ff24b 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ ghost is built on the [Hermes Agent](https://github.com/NousResearch/hermes-agen - + @@ -17,7 +17,7 @@ ghost is built on the [Hermes Agent](https://github.com/NousResearch/hermes-agen ## Quick Install -One command installs everything -- the engine into `~/.ghost-engine` (it leaves any existing `hermes` install untouched), the privacy stack, and the `ghost` + `ghost-login` commands. Idempotent: +**macOS only** (the privacy stack runs as launchd services). One command installs everything -- the engine into `~/.ghost-engine` (it leaves any existing `hermes` install untouched), the privacy stack, and the `ghost` + `ghost-login` commands. Idempotent: ```bash ./install.sh @@ -46,7 +46,7 @@ By default ghost is hosted-only: no Ollama, and both the fallback (`hermes-4-405 ## Models -Every model is unrestricted and open-weight, served over one OHTTP/TEE path. Switch with `/model`; the gateway rejects anything off this list. +Open-weight models only -- ghost won't wire up a closed, refusing model. They all run over the one OHTTP/TEE path; switch with `/model`, and the gateway rejects anything off this list. | Model | What it is | |---|---| From 52c67310564281423f7eb032d2f7d603c4eb9ff7 Mon Sep 17 00:00:00 2001 From: Advait Jayant Date: Wed, 24 Jun 2026 20:02:23 +0100 Subject: [PATCH 5/9] Drop GLM 5.2 from the model catalog GLM is open-weight but the gateway injects a safety system prompt for it that the per-model steer can't override, so it doesn't meet ghost's unrestricted bar. Catalog is now DeepSeek V4 Pro (default) + Hermes 4 405B/70B; the allow-list derives from it, so off-list requests (incl. glm-5.2) are rejected. Comment retains why GLM is excluded despite being gateway-served. --- README.md | 3 +-- install.sh | 2 +- privacy/scrubbing_proxy.py | 6 +++--- tests/test_scrub.py | 2 +- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 20ff24b..2cc1f4b 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ ghost is built on the [Hermes Agent](https://github.com/NousResearch/hermes-agen
Private by constructionEvery hosted request is HPKE/OHTTP-encrypted by og-veil and run inside a TEE enclave. The relay sees ciphertext, the enclave never sees who you are, and a local scrubber strips your name/secrets before anything is encrypted.
Unrestricted, open-weight onlyDeepSeek V4 Pro (default), Hermes 4 405B/70B, GLM 5.2 -- all open-weight, made uncensored via a per-model steer. Closed/refusing models (Claude, GPT, Gemini, Grok) are not offered and are rejected at the gateway.
Unrestricted, open-weight onlyDeepSeek V4 Pro (default), Hermes 4 405B/70B, GLM 5.2 -- open-weight models only. The default is steered to drop the usual refusals; closed, refusing models (Claude, GPT, Gemini, Grok) aren't offered, and the gateway rejects anything off the list.
Verified responsesog-veil checks the enclave's signature on every response and refuses to emit a token it can't verify.
Offline modeOpt in with GHOST_LOCAL=1 and switch with ghost --local -- a local abliterated model, zero egress, nothing leaves your machine.
Relentless agentReads real errors, installs what it's missing, changes tactics, and keeps going until the task is done -- it doesn't stop to ask after one failure.
- + @@ -52,7 +52,6 @@ Open-weight models only -- ghost won't wire up a closed, refusing model. They al |---|---| | `deepseek/deepseek-v4-pro` **(default)** | Strongest open reasoning + coding model; best for agentic work. Uncensored via ghost's per-model steer. | | `nous/hermes-4-405b` | Flagship uncensored open model, most steerable. Also the hosted fallback. | -| `zai/glm-5.2` | Strong open agentic MoE (Z.ai). | | `nous/hermes-4-70b` | Fast, low-cost; runs ghost's auxiliary tasks. | | local (opt-in) | Abliterated 7B (`ghost-tool`), or 32B (`uncensored-local`) with `GHOST_LOCAL_32B`. Fully offline. | diff --git a/install.sh b/install.sh index 56e0885..9390489 100644 --- a/install.sh +++ b/install.sh @@ -181,7 +181,7 @@ GL="$HOME/.local/bin/ghost-login" # thin wrapper over `og-veil login` (install if "$GL" --status >/dev/null 2>&1; then echo " already connected: $("$GL" --status)" else - echo " Hosted models (the default DeepSeek V4 Pro + Hermes/GLM, all open-weight) need a one-time login." + echo " Hosted models (the default DeepSeek V4 Pro + Hermes 4, all open-weight) need a one-time login." if [ -t 0 ]; then printf " Run the browser login now? [Y/n] "; read -r ANS || true case "${ANS:-Y}" in [Nn]*) echo " Skipped -- run 'ghost-login' anytime.";; *) "$GL" || echo " (login skipped/failed -- run 'ghost-login' anytime)";; esac diff --git a/privacy/scrubbing_proxy.py b/privacy/scrubbing_proxy.py index 7ece9be..5fdc509 100755 --- a/privacy/scrubbing_proxy.py +++ b/privacy/scrubbing_proxy.py @@ -62,8 +62,9 @@ # Curated picker whitelist served at /model-catalog.json. ghost is an UNRESTRICTED harness, so it # only offers OPEN-WEIGHT, steerable models. Closed, safety-tuned refusers (Claude, GPT, Gemini, # Grok, Seed) are served by the gateway but deliberately excluded -- they refuse/moralize and -# can't be steered. This list is the single source of truth for both the picker and the bridge's -# allow-list (_ALLOWED_GATEWAY_MODELS). +# can't be steered. GLM-5.2 is open-weight but excluded for the same reason: the gateway injects a +# safety system prompt for it that the steer can't override (verified 2026-06-24). This list is the +# single source of truth for both the picker and the bridge's allow-list (_ALLOWED_GATEWAY_MODELS). # # SUPPORTED-MODEL REFERENCE (gateway-verified by probing /v1/chat/completions, 2026-06-24): # the gateway's open-weight models are hermes-4-405b, hermes-4-70b, deepseek-v4-pro, glm-5.2. @@ -73,7 +74,6 @@ _CATALOG_MODELS = [ ("deepseek/deepseek-v4-pro", "DeepSeek V4 Pro โ€” strongest open reasoning + coding; best for agentic work (default)"), ("nous/hermes-4-405b", "Hermes 4 405B โ€” flagship uncensored open model, most steerable"), - ("zai/glm-5.2", "GLM 5.2 โ€” strong open agentic MoE (Z.ai)"), ("nous/hermes-4-70b", "Hermes 4 70B โ€” fast, low-cost open-weight model"), ] diff --git a/tests/test_scrub.py b/tests/test_scrub.py index bd7958a..315b1b1 100644 --- a/tests/test_scrub.py +++ b/tests/test_scrub.py @@ -269,7 +269,7 @@ def test_stream_multiline_data_frame_deanonymized(): def test_catalog_is_open_weight_only(): # ghost must only offer/allow OPEN-WEIGHT models; no closed/refusing ones. allowed = sp._ALLOWED_GATEWAY_MODELS - assert allowed == {"hermes-4-405b", "hermes-4-70b", "deepseek-v4-pro", "glm-5.2"} + assert allowed == {"hermes-4-405b", "hermes-4-70b", "deepseek-v4-pro"} blob = json.dumps(sp._CATALOG_MODELS).lower() for closed in ("claude", "gpt-", "gemini", "grok", "anthropic", "seed-"): assert closed not in blob, f"closed model '{closed}' must not be in the catalog" From 3e733b1ba6f262f84c5e96e5fa137fe819fc9485 Mon Sep 17 00:00:00 2001 From: Advait Jayant Date: Wed, 24 Jun 2026 20:21:59 +0100 Subject: [PATCH 6/9] Make outbound redaction opt-in (off by default) ghost is a full-fidelity agent: its own redaction layers were silently mangling secrets it legitimately reads (the Supabase anon key during an authorized pentest came back as 'eyJhbG...s0xo'), and the hosted path's privacy already comes from og-veil (OHTTP + TEE enclave). Flip both layers to opt-in: - bridge scrubbing (privacy/scrubbing_proxy.py) now gated by a .scrub marker (ghost --scrub); default = pure pass-through, both code paths - engine security.redact_secrets + privacy.redact_pii default to false - install.sh: GHOST_SCRUB=1 opts in (writes .scrub, flips the config) - bin/ghost: --scrub/--no-scrub toggle the marker; banner shows ON/OFF - README updated; tests updated (default-off pass-through is now asserted) All 35 scrub tests pass. --- README.md | 10 +++++----- bin/ghost | 15 ++++++++------- install.sh | 21 ++++++++++++++++++++- privacy/scrubbing_proxy.py | 30 +++++++++++++++++------------- profile/config.yaml | 4 ++-- tests/test_scrub.py | 21 +++++++++++++-------- 6 files changed, 65 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 2cc1f4b..b05c415 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ ghost is built on the [Hermes Agent](https://github.com/NousResearch/hermes-agent) engine by Nous Research, wrapped to route inference through OpenGradient's gateway and to offer only genuinely unrestricted, open-weight models.
Private by constructionEvery hosted request is HPKE/OHTTP-encrypted by og-veil and run inside a TEE enclave. The relay sees ciphertext, the enclave never sees who you are, and a local scrubber strips your name/secrets before anything is encrypted.
Unrestricted, open-weight onlyDeepSeek V4 Pro (default), Hermes 4 405B/70B, GLM 5.2 -- open-weight models only. The default is steered to drop the usual refusals; closed, refusing models (Claude, GPT, Gemini, Grok) aren't offered, and the gateway rejects anything off the list.
Unrestricted, open-weight onlyDeepSeek V4 Pro (default), Hermes 4 405B/70B -- open-weight models only. The default is steered to drop the usual refusals; closed, refusing models (Claude, GPT, Gemini, Grok) aren't offered, and the gateway rejects anything off the list.
Verified responsesog-veil checks the enclave's signature on every response and refuses to emit a token it can't verify.
Offline modeOpt in with GHOST_LOCAL=1 and switch with ghost --local -- a local abliterated model, zero egress, nothing leaves your machine.
Relentless agentReads real errors, installs what it's missing, changes tactics, and keeps going until the task is done -- it doesn't stop to ask after one failure.
- + @@ -59,19 +59,19 @@ Open-weight models only -- ghost won't wire up a closed, refusing model. They al ## How it stays private -Every hosted request takes the same private path: ghost scrubs it locally, then hands it to [og-veil](https://github.com/OpenGradient/veil) (the `opengradient-veil` package, the same one the [chat.opengradient.ai](https://chat.opengradient.ai) site uses), which encrypts it and relays it over Oblivious HTTP to a TEE enclave: +Every hosted request takes the same private path: ghost's local bridge hands it to [og-veil](https://github.com/OpenGradient/veil) (the `opengradient-veil` package, the same one the [chat.opengradient.ai](https://chat.opengradient.ai) site uses), which encrypts it and relays it over Oblivious HTTP to a TEE enclave: ``` ghost engine - โ””โ”€ scrubber (:8788) scrub PII/secrets, strip provider prefix, apply model steer + โ””โ”€ bridge (:8788) strip provider prefix, model steer (+ PII/secret scrub if --scrub) โ””โ”€ og-veil (:11435) HPKE-encrypt, OHTTP relay, verify signature before emit โ””โ”€ chat-api relay sees your account token + IP, but only ciphertext โ””โ”€ TEE enclave decrypts, runs the model, signs the output ``` -Two boundaries: the **relay** sees your account + IP but only ciphertext; the **enclave** sees the prompt but never your identity. The scrubber runs first, on plaintext localhost, so your name/email/secrets reach neither. The hosted path is **private, not anonymous** -- your OpenGradient account is still authenticated and the relay sees your IP. For full anonymity, use the local model: zero egress, nothing leaves the box. +Two boundaries: the **relay** sees your account + IP but only ciphertext; the **enclave** sees the prompt but never your identity. Redaction is **off by default** so ghost stays full-fidelity (it can read and use secrets during real work, e.g. authorized pentesting); turn on `ghost --scrub` and a local pass strips your name/email/secrets before encryption. Either way the hosted path is **private, not anonymous** -- your OpenGradient account is still authenticated and the relay sees your IP. For full anonymity, use the local model: zero egress, nothing leaves the box. -For agentic file work, real paths are scrubbed by default (the model sees `/Users/[REDACTED]/...`); run `ghost --paths` to let real paths through while your name + secrets in prose stay scrubbed, or use the local model where paths are always real. +(With `--scrub` on, filesystem paths stay readable by default so file work isn't blinded; `ghost --paths` and `--full-redaction` tune that. With scrubbing off -- the default -- the agent just sees real paths and content.) --- diff --git a/bin/ghost b/bin/ghost index a9f41bc..e08356e 100755 --- a/bin/ghost +++ b/bin/ghost @@ -22,13 +22,14 @@ unset ANTHROPIC_API_KEY PASS_PATHS="" case "${1:-}" in --paths|--code) PASS_PATHS=1; shift ;; esac -# --no-scrub / --scrub: persistent toggle for name/PII redaction. Secrets (API keys, JWTs, -# private keys) are ALWAYS scrubbed regardless. Turn off when redaction mangles an essential -# query (e.g. your own domain in the prompt). State persists across runs; status line shows it. -NOSCRUB_MARK="__GHOST_HOME__/privacy/.no_scrub" +# --scrub / --no-scrub: persistent toggle for OUTBOUND PII + secret redaction. OFF BY DEFAULT -- +# ghost is a full-fidelity agent (so it can do real work, incl. authorized pentesting, without its +# own privacy layer mangling secrets it reads). og-veil still OHTTP-encrypts + TEE-isolates the +# hosted path regardless. Turn it ON only to strip your own name/secrets before the gateway. +SCRUB_MARK="__GHOST_HOME__/privacy/.scrub" case "${1:-}" in - --no-scrub|--noscrub) : > "$NOSCRUB_MARK"; shift ;; - --scrub) rm -f "$NOSCRUB_MARK"; shift ;; + --scrub) : > "$SCRUB_MARK"; shift ;; + --no-scrub|--noscrub) rm -f "$SCRUB_MARK"; shift ;; esac # --preview "": show exactly what the PII scrubber would redact (entity table + @@ -87,7 +88,7 @@ else STATUS="๐Ÿ‘ป ghost ยท โš ๏ธ scrubbing bridge DOWN (bridge=$HC) -- hosted unreachable, $FB_DOWN" fi [ -n "$PASS_PATHS" ] && STATUS="$STATUS ยท ๐Ÿ—‚๏ธ path-aware (real paths visible to hosted model)" -[ -f "$NOSCRUB_MARK" ] && STATUS="$STATUS ยท ๐Ÿ”“ PII redaction OFF (secrets still scrubbed)" +if [ -f "$SCRUB_MARK" ]; then STATUS="$STATUS ยท ๐Ÿ”’ redaction ON (PII+secrets scrubbed outbound)"; else STATUS="$STATUS ยท ๐Ÿ”“ no redaction (full fidelity; og-veil still encrypts)"; fi # NER scrubber expected but failed to load -> regex fallback (names may not be scrubbed). Loud. [ -f "__GHOST_HOME__/privacy/.presidio_failed" ] && STATUS="$STATUS ยท โš ๏ธ NER scrubber OFF (regex fallback -- reinstall or check Presidio)" echo "$STATUS" >&2 diff --git a/install.sh b/install.sh index 9390489..9943d63 100644 --- a/install.sh +++ b/install.sh @@ -20,6 +20,8 @@ # GHOST_LOCAL=1 also install Ollama + a local model for an offline / true-incognito # fallback (DEFAULT is hosted-only -- no Ollama, fallback is hosted 70B) # GHOST_LOCAL_32B=1 pull the stronger 32B local model too (26GB; implies GHOST_LOCAL) +# GHOST_SCRUB=1 opt in to OUTBOUND PII + secret redaction (OFF by default -- ghost is a +# full-fidelity agent; og-veil's OHTTP+TEE provides the privacy regardless) # GHOST_CHAT_APP_URL= override the website used for `ghost-login` (default chat.opengradient.ai) set -euo pipefail @@ -139,6 +141,23 @@ else fi [ -f "$PRIV/pii_denylist.txt" ] || cp "$REPO/profile/pii_denylist.example.txt" "$PRIV/pii_denylist.txt" cp "$REPO/profile/uncensored_prefill.json" "$PRIV/uncensored_prefill.json" +# Outbound PII + secret redaction is OPT-IN (GHOST_SCRUB=1), OFF by default: ghost is a +# full-fidelity agent and og-veil's OHTTP+TEE already make the hosted path private. The .scrub +# marker drives the bridge; the engine's redact_secrets/redact_pii follow the same default. +if [ -n "${GHOST_SCRUB:-}" ]; then + : > "$PRIV/.scrub" + "$PYTHON" - "$PROFILE/config.yaml" <<'PYEOF' +import sys, re +p = sys.argv[1]; s = open(p).read() +s = re.sub(r"(?m)^ redact_secrets: false$", " redact_secrets: true", s) +s = re.sub(r"(?m)^ redact_pii: false$", " redact_pii: true", s) +open(p, "w").write(s) +PYEOF + say "Outbound PII + secret redaction ON (GHOST_SCRUB)" +else + rm -f "$PRIV/.scrub" "$PRIV/.no_scrub" + say "Full-fidelity mode (default) -- no outbound redaction. Set GHOST_SCRUB=1 to strip your PII/secrets before the gateway." +fi mkdir -p "$LA" # The scrubber runs as a launchd service; og-veil talks to chat-api directly (content is @@ -198,5 +217,5 @@ say "ghost installed -- run: ghost" case ":$PATH:" in *":$HOME/.local/bin:"*) ;; *) echo " (add ~/.local/bin to your PATH first)";; esac echo " Hosted default = deepseek/deepseek-v4-pro via og-veil -> the OpenGradient TEE gateway (OHTTP-private)." echo " Inside ghost, /model switches between hosted models and the local model (true incognito)." -echo " Personalize $PRIV/pii_denylist.txt with your name/email/handles for the hosted-path scrubber." +echo " Redaction is OFF by default (full fidelity). Opt in with GHOST_SCRUB=1 (or 'ghost --scrub'), then personalize $PRIV/pii_denylist.txt." echo " Not connected yet? Run: ghost-login" diff --git a/privacy/scrubbing_proxy.py b/privacy/scrubbing_proxy.py index 5fdc509..314ab30 100755 --- a/privacy/scrubbing_proxy.py +++ b/privacy/scrubbing_proxy.py @@ -105,13 +105,15 @@ def log(m): from scrub_patterns import EMAIL_RE, SSN_RE, CC_RE, IP_RE, PHONE_RE, SECRET_RES # noqa: E402 PASS_PATHS_SENTINEL = os.path.expanduser("~/.ghost/privacy/.pass_paths") -# Filesystem paths are protected from redaction by DEFAULT so agentic file work is -# not blinded: the user's name often appears inside ~/ paths, and redacting it to -# [REDACTED_PII] breaks path navigation. Names/secrets in prose are still scrubbed. -# Create ~/.ghost/privacy/.full_redaction to redact paths too (maximum privacy). +# Redaction is OPT-IN and OFF BY DEFAULT. ghost is a full-fidelity agent (it does real work, +# including authorized pentesting, so its own privacy layer must NOT silently mangle secrets it +# legitimately reads). The ~/.ghost/privacy/.scrub marker (ghost --scrub) turns ON outbound +# PII + secret redaction; absent (default) = pure pass-through. Privacy of the hosted path comes +# from og-veil (OHTTP + TEE enclave) regardless of this toggle. +SCRUB_SENTINEL = os.path.expanduser("~/.ghost/privacy/.scrub") +# When scrubbing IS on, filesystem paths are protected from redaction by default so agentic file +# work is not blinded. Create ~/.ghost/privacy/.full_redaction to redact paths too. FULL_REDACTION_SENTINEL = os.path.expanduser("~/.ghost/privacy/.full_redaction") -NO_SCRUB_SENTINEL = os.path.expanduser("~/.ghost/privacy/.no_scrub") # PII redaction is OPTIONAL: this marker (ghost --no-scrub) turns off name/PII -# redaction; secrets (API keys, JWTs, private keys) are ALWAYS scrubbed regardless. PATH_RE = re.compile(r"(?:~|/[\w.\-]+)(?:/[\w.\-]+)+") @@ -186,10 +188,10 @@ def _walk_strings(node, fn): def scrub_body(obj): """Legacy regex scrubber over the WHOLE request body (used when Presidio is off / fails).""" - if not isinstance(obj, dict): - return obj, 0 + if not isinstance(obj, dict) or not os.path.exists(SCRUB_SENTINEL): + return obj, 0 # scrubbing is opt-in (.scrub marker); default = pass-through pp = not os.path.exists(FULL_REDACTION_SENTINEL) # default: protect filesystem paths - pii = not os.path.exists(NO_SCRUB_SENTINEL) # PII redaction optional; secrets always scrubbed + pii = True # when scrubbing is on, redact both PII and secrets total = 0 def fn(s): @@ -202,14 +204,16 @@ def fn(s): def _anonymize_request(obj): - """Anonymize the request body -> (obj, count, mapping). With Presidio enabled (.presidio - marker) use NER + reversible placeholders, returning {placeholder: original} for response - de-anonymization. Otherwise, or on any error, fall back to the legacy regex scrubber.""" + """Anonymize the request body -> (obj, count, mapping). Redaction is OPT-IN: with no .scrub + marker (default) this is a pure pass-through (full fidelity). With .scrub on and Presidio + enabled (.presidio marker) use NER + reversible placeholders; else the legacy regex scrubber.""" + if not os.path.exists(SCRUB_SENTINEL): + return obj, 0, {} # scrubbing opt-in; default = pass-through if not (_PRESIDIO_OK and os.path.exists(PRESIDIO_MARKER)): obj, n = scrub_body(obj) return obj, n, {} try: - pii = not os.path.exists(NO_SCRUB_SENTINEL) + pii = True # scrubbing is on -> redact PII + secrets mapping, total = {}, 0 def fn(s): diff --git a/profile/config.yaml b/profile/config.yaml index fd91148..7af5982 100644 --- a/profile/config.yaml +++ b/profile/config.yaml @@ -352,7 +352,7 @@ dashboard: session_ttl_seconds: 0 public_url: '' privacy: - redact_pii: true + redact_pii: false tts: provider: piper edge: @@ -515,7 +515,7 @@ hooks_auto_accept: false personalities: {} security: allow_private_urls: false - redact_secrets: true + redact_secrets: false tirith_enabled: true tirith_path: tirith tirith_timeout: 5 diff --git a/tests/test_scrub.py b/tests/test_scrub.py index 315b1b1..f4a76c0 100644 --- a/tests/test_scrub.py +++ b/tests/test_scrub.py @@ -36,15 +36,17 @@ def presidio_on(tmp_path, monkeypatch): marker.write_text("") monkeypatch.setattr(sp, "PRESIDIO_MARKER", str(marker)) monkeypatch.setattr(sp, "_PRESIDIO_OK", True) - monkeypatch.setattr(sp, "NO_SCRUB_SENTINEL", str(tmp_path / ".no_scrub")) # absent -> pii on + scrub = tmp_path / ".scrub"; scrub.write_text("") # present -> scrubbing ON (opt-in) + monkeypatch.setattr(sp, "SCRUB_SENTINEL", str(scrub)) monkeypatch.setattr(sp, "FULL_REDACTION_SENTINEL", str(tmp_path / ".full_redaction")) @pytest.fixture def presidio_off(tmp_path, monkeypatch): - """Force the legacy regex path (Presidio marker absent).""" + """Force the legacy regex path (Presidio marker absent), scrubbing ON.""" monkeypatch.setattr(sp, "PRESIDIO_MARKER", str(tmp_path / ".presidio")) # absent - monkeypatch.setattr(sp, "NO_SCRUB_SENTINEL", str(tmp_path / ".no_scrub")) + scrub = tmp_path / ".scrub"; scrub.write_text("") # present -> scrubbing ON (opt-in) + monkeypatch.setattr(sp, "SCRUB_SENTINEL", str(scrub)) monkeypatch.setattr(sp, "FULL_REDACTION_SENTINEL", str(tmp_path / ".full_redaction")) @@ -101,12 +103,15 @@ def test_canary_secret_never_leaves_box_pii_on(presidio_on): assert any(v == SECRET for v in mapping.values()) -def test_canary_secret_never_leaves_box_pii_off(presidio_on, monkeypatch): - # PII off (the user's default): names may pass, but SECRETS must STILL never leave. - monkeypatch.setattr(sp, "NO_SCRUB_SENTINEL", os.devnull) # exists -> pii off - obj, _, _ = sp._anonymize_request(_request_with_secret_everywhere()) +def test_default_off_is_full_fidelity(presidio_on, tmp_path, monkeypatch): + # DEFAULT = no .scrub marker: nothing is redacted, so the agent keeps full fidelity + # (it can read/use secrets during real work, e.g. authorized pentesting). Privacy of the + # hosted path still comes from og-veil's OHTTP + TEE, not from this bridge. + monkeypatch.setattr(sp, "SCRUB_SENTINEL", str(tmp_path / ".absent")) # absent -> scrub off + obj, n, mapping = sp._anonymize_request(_request_with_secret_everywhere()) wire = json.dumps(obj) - assert SECRET not in wire, "SECRET leaked with PII off (secrets must always be scrubbed)" + assert SECRET in wire, "with scrubbing off (default), the real secret must pass through" + assert n == 0 and mapping == {}, "scrub-off must be a pure pass-through" def test_canary_legacy_path_also_scrubs_whole_body(presidio_off): From e45f7fc8ffbc529c48abc17448ee47b05e24cf20 Mon Sep 17 00:00:00 2001 From: Advait Jayant Date: Wed, 24 Jun 2026 20:27:13 +0100 Subject: [PATCH 7/9] README: cut redundancy - merge the intro paragraph + attribution (it restated the feature table) - drop the duplicated 'private, not anonymous' bullet from Honest limits (already covered in How it stays private) - collapse the four separate local-model mentions; drop the re-pitched 'open-weight only' from the Models intro (it's in the feature table) - remove the deep --paths/--full-redaction parenthetical --- README.md | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index b05c415..32f57e7 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,6 @@ # ghost ๐Ÿ‘ป -**A private, unrestricted agentic harness.** ghost is a real terminal agent -- it runs commands, reads and writes files, executes code, and searches the web -- with every hosted request routed through OpenGradient's private TEE gateway, so your prompts and your work stay private. It treats you as a competent adult: it answers what you actually ask, in full, no moralizing and no refusals, and no topic is off-limits when you raise it. It just doesn't volunteer illegal or shock content on its own. It can also drop to a fully-offline local model where nothing leaves the box. - -ghost is built on the [Hermes Agent](https://github.com/NousResearch/hermes-agent) engine by Nous Research, wrapped to route inference through OpenGradient's gateway and to offer only genuinely unrestricted, open-weight models. +**A private, unrestricted agentic harness.** ghost is a real terminal agent -- it runs commands, edits files, executes code, and searches the web -- routing every hosted request through OpenGradient's private TEE gateway. It answers what you actually ask, in full, with no refusals or moralizing, and can drop to a fully-offline local model where nothing leaves the box. Built on the [Hermes Agent](https://github.com/NousResearch/hermes-agent) engine by Nous Research, wired to OpenGradient's gateway and to only open-weight, unrestricted models.
Private by constructionEvery hosted request is HPKE/OHTTP-encrypted by og-veil and run inside a TEE enclave. The relay sees ciphertext, the enclave never sees who you are, and a local scrubber strips your name/secrets before anything is encrypted.
Private by constructionEvery hosted request is HPKE/OHTTP-encrypted by og-veil and run inside a TEE enclave: the relay sees only ciphertext and the enclave never sees who you are. Optional ghost --scrub also strips your name/secrets locally before encryption (off by default, so the agent keeps full fidelity for real work).
Unrestricted, open-weight onlyDeepSeek V4 Pro (default), Hermes 4 405B/70B -- open-weight models only. The default is steered to drop the usual refusals; closed, refusing models (Claude, GPT, Gemini, Grok) aren't offered, and the gateway rejects anything off the list.
Verified responsesog-veil checks the enclave's signature on every response and refuses to emit a token it can't verify.
Offline modeOpt in with GHOST_LOCAL=1 and switch with ghost --local -- a local abliterated model, zero egress, nothing leaves your machine.
@@ -40,13 +38,13 @@ GHOST_LOCAL=1 ./install.sh # also install a local model (offline/incognito GHOST_LOCAL_32B=1 ./install.sh # pull the stronger 32B local model too (26GB) ``` -By default ghost is hosted-only: no Ollama, and both the fallback (`hermes-4-405b`) and the auxiliary tasks (`hermes-4-70b`) run hosted over the same private path. +By default ghost is hosted-only -- no Ollama; the fallback and auxiliary tasks also run hosted over the same private path. --- ## Models -Open-weight models only -- ghost won't wire up a closed, refusing model. They all run over the one OHTTP/TEE path; switch with `/model`, and the gateway rejects anything off this list. +Switch with `/model` -- all open-weight, nothing closed or refusing: | Model | What it is | |---|---| @@ -69,16 +67,13 @@ ghost engine โ””โ”€ TEE enclave decrypts, runs the model, signs the output ``` -Two boundaries: the **relay** sees your account + IP but only ciphertext; the **enclave** sees the prompt but never your identity. Redaction is **off by default** so ghost stays full-fidelity (it can read and use secrets during real work, e.g. authorized pentesting); turn on `ghost --scrub` and a local pass strips your name/email/secrets before encryption. Either way the hosted path is **private, not anonymous** -- your OpenGradient account is still authenticated and the relay sees your IP. For full anonymity, use the local model: zero egress, nothing leaves the box. - -(With `--scrub` on, filesystem paths stay readable by default so file work isn't blinded; `ghost --paths` and `--full-redaction` tune that. With scrubbing off -- the default -- the agent just sees real paths and content.) +Two boundaries: the **relay** sees your account + IP but only ciphertext; the **enclave** sees the prompt but never your identity. Redaction is **off by default** so ghost stays full-fidelity (it can read and use secrets during real work, e.g. authorized pentesting); `ghost --scrub` turns on a local pass that strips your name/secrets before encryption. Either way the hosted path is **private, not anonymous**: your OpenGradient account is still authenticated and the relay sees your IP. For true anonymity, use the local model -- zero egress. --- ## Honest limits -- **Private, not anonymous.** The relay still authenticates your OpenGradient account; OHTTP hides content and the scrubber hides PII, but the account link remains. Use the local model for zero-egress anonymity. -- **The local path is opt-in and weaker.** Off by default; install with `GHOST_LOCAL`. The local model is a weaker agentic searcher and may still lean on the hosted gateway for tool orchestration under tool-use enforcement. +- **The local model is opt-in and weaker.** Off by default (install with `GHOST_LOCAL`); it's a weaker agentic searcher and may still lean on the hosted gateway for tool orchestration under tool-use enforcement. - **The engine is forked, not rewritten.** Internal package names stay `hermes_cli`. `hermes update` only updates the original install; re-run `scripts/fork-engine.sh` to pull upstream changes into the fork. --- From f447dc65dbc915e9a2fe284b0cb02dbfea0fb8a9 Mon Sep 17 00:00:00 2001 From: Advait Jayant Date: Wed, 24 Jun 2026 21:03:03 +0100 Subject: [PATCH 8/9] Add 'ghost update' for easy self-updates ghost had no updater -- you re-ran install.sh by hand, and 'hermes update' only touches the base engine. Add a first-class updater: - bin/ghost-update: pulls the latest source (the repo you installed from, or a managed ~/.ghost-src clone) and re-runs the idempotent installer, reusing the options you first chose (saved in ~/.ghost/.install-env). - bin/ghost: intercept 'ghost update' so it runs ghost-update, not the engine's own 'update' subcommand. - install.sh: record the source path + install options; install ghost-update. - README: document 'ghost update'; fix the stale 'hermes update / fork-engine' note in Honest limits. --- README.md | 4 +++- bin/ghost | 4 ++++ bin/ghost-update | 29 +++++++++++++++++++++++++++++ install.sh | 19 ++++++++++++++++--- 4 files changed, 52 insertions(+), 4 deletions(-) create mode 100644 bin/ghost-update diff --git a/README.md b/README.md index 32f57e7..49f5a86 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,8 @@ GHOST_LOCAL_32B=1 ./install.sh # pull the stronger 32B local model too (26GB) By default ghost is hosted-only -- no Ollama; the fallback and auxiliary tasks also run hosted over the same private path. +**Update any time** with `ghost update` -- it pulls the latest source and re-runs the installer (idempotent, keeps your options). To also move to a newer upstream Hermes engine, run `hermes update` first, then `ghost update`. + --- ## Models @@ -74,7 +76,7 @@ Two boundaries: the **relay** sees your account + IP but only ciphertext; the ** ## Honest limits - **The local model is opt-in and weaker.** Off by default (install with `GHOST_LOCAL`); it's a weaker agentic searcher and may still lean on the hosted gateway for tool orchestration under tool-use enforcement. -- **The engine is forked, not rewritten.** Internal package names stay `hermes_cli`. `hermes update` only updates the original install; re-run `scripts/fork-engine.sh` to pull upstream changes into the fork. +- **The engine is forked, not rewritten.** Internal package names stay `hermes_cli`, and `ghost update` (not `hermes update`) is what refreshes the fork. --- diff --git a/bin/ghost b/bin/ghost index e08356e..a50e8ef 100755 --- a/bin/ghost +++ b/bin/ghost @@ -17,6 +17,10 @@ export HERMES_HOME="__GHOST_HOME__" # ghost's isolated state dir (the engine h # infers through the OG TEE gateway and never needs this key. unset ANTHROPIC_API_KEY +# `ghost update` -- pull the latest ghost + re-run the installer (idempotent). Intercepted here +# so it doesn't fall through to the engine's own `update` (which only touches the base install). +if [ "${1:-}" = "update" ]; then shift; exec "$HOME/.local/bin/ghost-update" "$@"; fi + # --paths (or --code): path-aware mode -- let real filesystem paths reach the hosted model # for agentic file work; name + secrets in content are still scrubbed. Default off = full redaction. PASS_PATHS="" diff --git a/bin/ghost-update b/bin/ghost-update new file mode 100644 index 0000000..5aa3513 --- /dev/null +++ b/bin/ghost-update @@ -0,0 +1,29 @@ +#!/bin/sh +# ghost-update -- pull the latest ghost source and re-run the installer (idempotent), +# reusing the options you first installed with (GHOST_LOCAL / GHOST_SCRUB / ...). This +# updates ghost's wrapper (privacy stack, commands, profile) and re-forks + re-debrands +# the engine. Invoked by `ghost update`. +# +# Note: this does NOT pull a newer UPSTREAM Hermes engine on its own -- for that, run +# `hermes update` first (updates the base install), then `ghost update` re-forks it. +set -e +GHOST_HOME="__GHOST_HOME__" + +# Prefer the source repo you installed from; fall back to a managed clone in ~/.ghost-src. +SRC="$(cat "$GHOST_HOME/.src" 2>/dev/null || true)" +[ -d "$SRC/.git" ] || SRC="$HOME/.ghost-src" + +if [ -d "$SRC/.git" ]; then + echo "๐Ÿ‘ป ghost-update ยท pulling latest source ($SRC)" + git -C "$SRC" pull --ff-only || git -C "$SRC" pull +else + echo "๐Ÿ‘ป ghost-update ยท fetching ghost into $SRC" + rm -rf "$SRC" + git clone https://github.com/OpenGradient/ghost.git "$SRC" +fi + +# Re-apply the same install options you chose originally. +if [ -f "$GHOST_HOME/.install-env" ]; then set -a; . "$GHOST_HOME/.install-env"; set +a; fi + +echo "๐Ÿ‘ป ghost-update ยท re-running the installer (idempotent)" +exec bash "$SRC/install.sh" diff --git a/install.sh b/install.sh index 9943d63..0f74e14 100644 --- a/install.sh +++ b/install.sh @@ -48,6 +48,18 @@ SCRUBBER="http://127.0.0.1:8788" # since hosted-only is now the default.) WANT_LOCAL="${GHOST_LOCAL:-}"; [ -n "${GHOST_LOCAL_32B:-}" ] && WANT_LOCAL=1 +# Record the source path + the chosen install options so `ghost update` can re-pull and +# re-install the exact same way (see bin/ghost-update). +mkdir -p "$GHOST_HOME" +echo "$REPO" > "$GHOST_HOME/.src" +{ + [ -n "${GHOST_LOCAL:-}" ] && echo "GHOST_LOCAL=1" + [ -n "${GHOST_LOCAL_32B:-}" ] && echo "GHOST_LOCAL_32B=1" + [ -n "${GHOST_SCRUB:-}" ] && echo "GHOST_SCRUB=1" + [ -n "${GHOST_CHAT_APP_URL:-}" ] && echo "GHOST_CHAT_APP_URL=$GHOST_CHAT_APP_URL" + : +} > "$GHOST_HOME/.install-env" + say(){ printf '\n\033[1;33m==>\033[0m %s\n' "$*"; } have(){ command -v "$1" >/dev/null 2>&1; } @@ -187,12 +199,13 @@ for _ in $(seq 1 20); do [ "$(curl -s -o /dev/null -w '%{http_code}' --max-time say "Forking + debranding the engine -> $ENG" GHOST_PYTHON="$PYTHON" GHOST_ENGINE="$ENG" HERMES_SRC="$SRC" bash "$REPO/scripts/fork-engine.sh" -# ---------- 5. the ghost + ghost-login commands ---------- -say "Installing the ghost + ghost-login commands" +# ---------- 5. the ghost + ghost-login + ghost-update commands ---------- +say "Installing the ghost + ghost-login + ghost-update commands" mkdir -p "$HOME/.local/bin" sed -e "s#__PYTHON__#$PYTHON#g" -e "s#__HOME__#$HOME#g" -e "s#__ENG__#$ENG#g" -e "s#__GHOST_HOME__#$GHOST_HOME#g" "$REPO/bin/ghost" > "$HOME/.local/bin/ghost" sed -e "s#__PYTHON__#$PYTHON#g" -e "s#__HOME__#$HOME#g" -e "s#__GHOST_HOME__#$GHOST_HOME#g" "$REPO/bin/ghost-login" > "$HOME/.local/bin/ghost-login" -chmod +x "$HOME/.local/bin/ghost" "$HOME/.local/bin/ghost-login" +sed -e "s#__HOME__#$HOME#g" -e "s#__GHOST_HOME__#$GHOST_HOME#g" "$REPO/bin/ghost-update" > "$HOME/.local/bin/ghost-update" +chmod +x "$HOME/.local/bin/ghost" "$HOME/.local/bin/ghost-login" "$HOME/.local/bin/ghost-update" # ---------- 6. connect your account (hosted models) ---------- say "Connect your OpenGradient Chat account (for hosted models)" From 98f3129e9a6cf2167d87c43a47c773d98f24a8c7 Mon Sep 17 00:00:00 2001 From: Advait Jayant Date: Wed, 24 Jun 2026 21:23:24 +0100 Subject: [PATCH 9/9] Self-bootstrapping install.sh: one deterministic command installs + updates Addresses feedback that an LLM-driven install/update is off-putting. install.sh now detects when it's run via 'curl ... | bash' (no checkout) and self-bootstraps: clones or fast-forwards the repo into ~/.ghost-src and re-execs. So one command does both install and update, no manual clone and nothing agentic: curl -fsSL https://raw.githubusercontent.com/OpenGradient/ghost/main/install.sh | bash From a clone it's still ./install.sh; once installed, 'ghost update' runs the same thing. README install section rewritten around the one-liner. --- README.md | 19 +++++-------------- install.sh | 15 ++++++++++++++- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 49f5a86..59ab104 100644 --- a/README.md +++ b/README.md @@ -13,14 +13,16 @@ --- -## Quick Install +## Install & update -**macOS only** (the privacy stack runs as launchd services). One command installs everything -- the engine into `~/.ghost-engine` (it leaves any existing `hermes` install untouched), the privacy stack, and the `ghost` + `ghost-login` commands. Idempotent: +**macOS only** (the privacy stack runs as launchd services). One deterministic command -- nothing agentic, no LLM in the loop -- installs everything (the engine into `~/.ghost-engine`, leaving any existing `hermes` install untouched; the privacy stack; the `ghost` / `ghost-login` / `ghost-update` commands). The **same command updates** an existing install, and it's idempotent + safe to re-run (it keeps your login, sessions, and denylist): ```bash -./install.sh +curl -fsSL https://raw.githubusercontent.com/OpenGradient/ghost/main/install.sh | bash ``` +From a local clone it's just `./install.sh`; once installed, `ghost update` runs the same thing. Add options before the command, e.g. `GHOST_LOCAL=1 ...` (also install an offline local model), `GHOST_LOCAL_32B=1 ...` (the stronger 32B too, 26GB), or `GHOST_SCRUB=1 ...` (turn on outbound PII/secret redaction). By default ghost is hosted-only -- no Ollama; the fallback and auxiliary tasks run hosted over the same private path. + Then connect once and go: ```bash @@ -31,17 +33,6 @@ ghost --resume # resume a past session (find one with: ghost s ghost --local # force the offline local model (needs GHOST_LOCAL) ``` -Optional at install time: - -```bash -GHOST_LOCAL=1 ./install.sh # also install a local model (offline/incognito fallback) -GHOST_LOCAL_32B=1 ./install.sh # pull the stronger 32B local model too (26GB) -``` - -By default ghost is hosted-only -- no Ollama; the fallback and auxiliary tasks also run hosted over the same private path. - -**Update any time** with `ghost update` -- it pulls the latest source and re-runs the installer (idempotent, keeps your options). To also move to a newer upstream Hermes engine, run `hermes update` first, then `ghost update`. - --- ## Models diff --git a/install.sh b/install.sh index 0f74e14..96ddb7d 100644 --- a/install.sh +++ b/install.sh @@ -32,7 +32,20 @@ if [ "$(uname -s)" != "Darwin" ]; then exit 1 fi -REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# Resolve where this script lives. When run via `curl ... | bash` there is no checkout, so +# self-bootstrap: clone (or fast-forward) the repo into ~/.ghost-src and re-exec from there. This +# makes ONE deterministic command both INSTALL and UPDATE ghost -- no manual clone, no LLM needed. +REPO="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" 2>/dev/null && pwd || true)" +if [ -z "$REPO" ] || [ ! -f "$REPO/profile/config.yaml" ]; then + command -v git >/dev/null 2>&1 || { echo "!! ghost needs git to fetch itself; install it (xcode-select --install) and re-run." >&2; exit 1; } + SRC="${GHOST_SRC_DIR:-$HOME/.ghost-src}" + if [ -d "$SRC/.git" ]; then + echo "==> Updating ghost source ($SRC)"; git -C "$SRC" pull --ff-only || git -C "$SRC" pull + else + echo "==> Fetching ghost into $SRC"; rm -rf "$SRC"; git clone https://github.com/OpenGradient/ghost.git "$SRC" + fi + exec bash "$SRC/install.sh" "$@" +fi ENGINE_HOME="${ENGINE_HOME:-$HOME/.hermes}" # where the Hermes engine installs (official installer default) GHOST_HOME="${GHOST_HOME:-$HOME/.ghost}" # ghost's ISOLATED state (profiles, privacy, auth) PROFILE="$GHOST_HOME/profiles/uncensored"
Private by constructionEvery hosted request is HPKE/OHTTP-encrypted by og-veil and run inside a TEE enclave: the relay sees only ciphertext and the enclave never sees who you are. Optional ghost --scrub also strips your name/secrets locally before encryption (off by default, so the agent keeps full fidelity for real work).