diff --git a/README.md b/README.md
index 7253a10..59ab104 100644
--- a/README.md
+++ b/README.md
@@ -1,273 +1,73 @@
-# 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, 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.
-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.
-
-> **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 construction | Every 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 only | DeepSeek 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 responses | og-veil checks the enclave's signature on every response and refuses to emit a token it can't verify. |
+| Offline mode | Opt in with GHOST_LOCAL=1 and switch with ghost --local -- a local abliterated model, zero egress, nothing leaves your machine. |
+| Relentless agent | Reads 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 telemetry | Catalog served locally, web search via local ddgs, no third-party search account, isolated state in ~/.ghost. |
+
---
-## Quickstart
+## Install & update
-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):
+**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
-unzip ghost.zip -d ~/ghost && cd ~/ghost && ./install.sh
+curl -fsSL https://raw.githubusercontent.com/OpenGradient/ghost/main/install.sh | bash
```
-Then connect your account once and run **`ghost`**:
-
-```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)
-```
+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.
-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:
+Then connect once and go:
```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-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)
```
-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**.
-
---
-## Two modes
-
-ghost runs one of two kinds of model, a deliberate privacy/capability trade:
-
-| | **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` |
-
-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.
-
----
+## Models
-## What you get
+Switch with `/model` -- all open-weight, nothing closed or refusing:
-| 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. |
+| `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
-
-- **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`.
-
----
-
-## 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`).
+## How it stays private
-**Config modes (optional env vars):**
+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:
-```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
+ โโ 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
```
-### 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:
-
-- **`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.
+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
-- **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.
+- **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`, and `ghost update` (not `hermes update`) is what refreshes the fork.
---
@@ -277,5 +77,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/bin/ghost b/bin/ghost
index c36520c..a50e8ef 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)
@@ -18,18 +17,23 @@ 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=""
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 +
@@ -77,24 +81,18 @@ 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
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/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 a3bddcf..96ddb7d 100644
--- a/install.sh
+++ b/install.sh
@@ -13,16 +13,15 @@
# 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)
+# 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
@@ -33,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"
@@ -42,9 +54,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
@@ -52,6 +61,18 @@ USE_PROXY="${GHOST_PROXY:-}"
# 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; }
@@ -115,14 +136,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 +153,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
@@ -153,24 +166,29 @@ 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 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 +197,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"
@@ -208,12 +212,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)"
@@ -221,7 +226,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 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
@@ -238,6 +243,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."
-[ -n "$USE_PROXY" ] || echo " Direct mode (default). For IP-masking from the relay, reinstall with GHOST_PROXY=1."
+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/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..314ab30 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"),
]
@@ -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):
@@ -309,10 +313,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.
diff --git a/profile/config.yaml b/profile/config.yaml
index 55b0bb3..7af5982 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
@@ -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 bd7958a..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):
@@ -269,7 +274,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"