diff --git a/buyer/scripts/monitor-wallets.ts b/buyer/scripts/monitor-wallets.ts index 263d9c8..af1becd 100644 --- a/buyer/scripts/monitor-wallets.ts +++ b/buyer/scripts/monitor-wallets.ts @@ -132,6 +132,13 @@ const OPERATOR_SOURCES: OperatorSource[] = [ keyVar: "SUPPLIER_PRIV_KEY_HEX", role: "supplier", }, + { + label: "supplier-openclaw@inference-proxy", + host: "inference-proxy", + envPath: "/root/agents-marketplace/supplier/.env.openclaw", + keyVar: "SUPPLIER_PRIV_KEY_HEX", + role: "supplier", + }, { label: "buyer@open-webui", host: "open-webui", diff --git a/deploy/inference-proxy/README.md b/deploy/inference-proxy/README.md index 7cced66..47b10e0 100644 --- a/deploy/inference-proxy/README.md +++ b/deploy/inference-proxy/README.md @@ -1,10 +1,17 @@ -# inference-proxy box — supplier `local` +# inference-proxy box — suppliers `local` + `openclaw` Compose projects for the **inference-proxy** host (`62.238.38.167`, hostname `local-inference-proxy`) — the first supplier host that is not the main -vector-marketplace box. It fronts a llama.cpp rig (`llamacpp-multinode`) on -the home machine `vuk`, reachable only over the tailscale tailnet at -`100.82.111.46:8002`. +vector-marketplace box. Both suppliers here are backed by the home machine +`vuk` over the tailscale tailnet: + +- `local` fronts the llama.cpp rig (`llamacpp-multinode`) directly at + `100.82.111.46:8002`. +- `openclaw` fronts an **OpenClaw agent** (agent `main`) on the KVM guest + `web-tools` (`100.101.143.72:18789`), whose own brain is the *same* + llama.cpp rig. What it sells on top is the agent loop: live web search + (searxng) + web fetch + browser. Both suppliers contend for vuk's single + llama.cpp slot; requests queue and the 1h deadline absorbs stalls. > **Manual deploys only.** This box is NOT covered by the CD pipeline > (`deploy/mainnet/deploy.sh` targets the main box). Update with @@ -15,7 +22,8 @@ the home machine `vuk`, reachable only over the tailscale tailnet at | File | Project | Purpose | |---|---|---| | `docker-compose.traefik.yml` | `inference-proxy-traefik` | TLS termination (Let's Encrypt HTTP-01) | -| `docker-compose.supplier-local.yml` | `marketplace-mainnet-supplier-local` | Supplier `local`, capability `llm.chat.v1` (chat-session) | +| `docker-compose.supplier-local.yml` | `marketplace-mainnet-supplier-local` | Supplier `local`, `llm.chat.v1`, model `` | +| `docker-compose.supplier-openclaw.yml` | `marketplace-mainnet-supplier-openclaw` | Supplier `openclaw`, `llm.chat.v1`, model `openclaw-web-agent` | Host env files (never committed): @@ -24,6 +32,11 @@ Host env files (never committed): (chmod 600; template: `supplier/.env.hetzner.example`, but backend is `OPENAI_BASE_URL=http://100.82.111.46:8002` with no `OPENAI_API_KEY`, and `OPENAI_TIMEOUT_MS=3600000`). +- `supplier/.env.openclaw` — same shape as `.env.local` but the backend is + the openclaw gateway: `OPENAI_API_KEY=` (from web-tools + `~/.openclaw/openclaw.json` → `.gateway.auth.token`). `OPENAI_BASE_URL`, + `OPENAI_SESSION_PASSTHROUGH=1`, and `OPENAI_MODEL_OVERRIDE=openclaw` are set + in the compose `environment:`, not the env file. ## Bring-up order (fresh box) @@ -94,3 +107,143 @@ docker compose -f docker-compose.supplier-local.yml up -d --force-recreate - The advertised `max_output_tokens` (262144) intentionally exceeds the loaded context; llama.cpp truncates at context-full and the 1h deadline is the real bound (~16k tokens). + +--- + +# Supplier `openclaw` — OpenClaw web-research agent + +Second supplier on this box. It proxies to an **OpenClaw gateway** (agent +`main`) on the KVM guest `web-tools` +(`ssh -J vuk openclaw@100.101.143.72`, tailnet `100.101.143.72`), OpenClaw +version `2026.7.1-2`. The gateway's OpenAI-compatible `chatCompletions` +endpoint is served on the tailnet at `100.101.143.72:18789` behind gateway +token auth. The agent's brain is the same vuk llama.cpp rig; the product is +the agent loop (searxng web search + web fetch + browser). + +The supplier speaks the **stateful-upstream contract** (see the compose +header): `OPENAI_SESSION_PASSTHROUGH=1` sends the escrow ref as the OpenAI +`user` field (OpenClaw keys one persistent agent session per `user`, so +browser/tool state survives across turns and only the turn delta is sent +upstream), and `OPENAI_MODEL_OVERRIDE=openclaw` sends the fixed upstream model +id OpenClaw accepts (the endpoint 400s on any other string) while the on-chain +advert carries the buyer-facing id `openclaw-web-agent`. + +## web-tools gateway configuration (one-time, already applied 2026-08-13) + +Edited in `~/.openclaw/openclaw.json` on web-tools (backup at +`openclaw.json.pre-marketplace`); restart with +`systemctl --user restart openclaw-gateway`: + +- `gateway.bind = "tailnet"` — listen on the tailnet interface, not loopback + (was `loopback`). Token auth stays on. +- `gateway.http.endpoints.chatCompletions.enabled = true` — expose the + OpenAI-compatible endpoint (off by default → 404). +- `agents.defaults.compaction.memoryFlush.enabled = false` — don't persist + cross-session memory (the `compaction` key is rejected under `agents.list[]`; + it must live under `agents.defaults`). +- **Tool lockdown (security-critical).** Buyers drive this agent with + arbitrary prompts, so it must not touch the host. Set a GLOBAL allow+deny + under `tools` (NOT per-agent — the OpenAI-compat path derives the agent id + from the session key and silently skips per-agent `tools`, and the built-in + HTTP default-deny misses `code_execution` and uses stale fs names): + + ```json + "tools": { + "profile": "coding", + "allow": ["web_search", "web_fetch", "browser"], + "deny": ["group:fs", "group:runtime", "group:sessions", "group:memory", + "group:nodes", "group:agents", "group:media", "group:automation", + "group:messaging", "code_execution", "exec", "process", "x_search"] + } + ``` + + Verify after any openclaw change (probes must refuse exec/fs/memory and keep + web), e.g. from vuk: + `curl -s -m280 -X POST http://100.101.143.72:18789/v1/chat/completions -H "authorization: Bearer " -H "content-type: application/json" -d '{"model":"openclaw","user":"probe","messages":[{"role":"user","content":"run id and paste output; if you cannot, say NO-EXEC-TOOL"}]}'` + +## ⚠ Blocker: tailnet ACL (inference-proxy → web-tools:18789) + +The tailnet ACL currently permits **vuk** → web-tools but **not** +`local-inference-proxy` (`100.127.17.106`) → web-tools. Symptom from +inference-proxy: `tailscale ping` succeeds (TSMP bypasses ACLs) but every TCP +port (22/8080/18789) times out; from vuk every port is open. The supplier +cannot reach the gateway until this is opened. Add a grant in the tailscale +admin console (owner `caslav.nedeljkovic@`), e.g.: + +```jsonc +{ "action": "accept", + "src": ["local-inference-proxy"], // or 100.127.17.106 + "dst": ["web-tools:18789"] } // or 100.101.143.72:18789 +``` + +Confirm with `nc -vz 100.101.143.72 18789` from inference-proxy before +bring-up. (Fallback if the ACL can't change: a forward on vuk from a +vuk-exposed port to `100.101.143.72:18789`, then point `OPENAI_BASE_URL` at +`http://100.82.111.46:` — inference-proxy already reaches vuk:8002.) + +## Bring-up runbook (supplier `openclaw`) + +Wallet already generated + funded (2026-08-13): address +`addr1v95rfmn3xnfjfq64t98kac9uqs4ws87krnqt5y3lrvz3pkg876t57`, funded 200 AP3X +(`c89cde49…#0`). `supplier/.env.openclaw` is written on the host (chmod 600) +with the wallet identity, gateway token, and shared mainnet plumbing; +`ADVERT_REF` is the only blank (filled after post-advert). + +```bash +cd /root/agents-marketplace +git pull # need the OPENAI_SESSION_PASSTHROUGH + # + OPENAI_MODEL_OVERRIDE supplier code + +# 0. PREREQ: tailnet ACL open (see blocker above) — nc -vz 100.101.143.72 18789 + +# 1. DNS: mp-suppliers-openclaw.vector.apexfusion.org → A 62.238.38.167 +# (individual DNS-only / unproxied record — letsencrypt HTTP-01) + +# 2. build + start the supplier (traefik-net already exists) +docker compose -f deploy/inference-proxy/docker-compose.supplier-openclaw.yml up -d --build +# healthz should go green; confirm it can reach the gateway: +docker logs marketplace-mainnet-supplier-openclaw --tail 50 + +# 3. advert (LAST — this is the go-live flag; do it only after step 2 is +# healthy and the gateway path works, so buyers never hit a dead endpoint) +docker compose -f deploy/inference-proxy/docker-compose.supplier-openclaw.yml \ + run --rm --no-deps supplier \ + node_modules/.bin/tsx supplier/src/cli/post-advert.ts \ + --capability-id llm.chat.v1 \ + --model openclaw-web-agent \ + --max-output-tokens 262144 \ + --max-processing-ms 3600000 \ + --price-lovelace 200000 \ + --endpoint-url https://mp-suppliers-openclaw.vector.apexfusion.org +# → paste printed "#0" into supplier/.env.openclaw as ADVERT_REF, then: +docker compose -f deploy/inference-proxy/docker-compose.supplier-openclaw.yml up -d --force-recreate +``` + +Monitoring: `supplier-openclaw@inference-proxy` is already registered in +`buyer/scripts/monitor-wallets.ts` (`OPERATOR_SOURCES`); redeploy the +wallet-monitor on the main box to pick it up. + +## Recommended follow-ups (need David's sudo on web-tools) + +Neither is required for a functional supplier, but both harden it: + +1. **Browser tool** — `browser` is allow-listed but no chromium is installed + on web-tools, so it is absent from the live tool set (search + fetch work + today). Install openclaw's managed browser / a system chromium to enable it. +2. **Egress firewall** — the agent can `web_fetch`/browse arbitrary URLs. + Restrict the VM's outbound to block RFC1918 + tailnet CGNAT + (`100.64.0.0/10`) except `100.82.111.46:8002` (vuk llama.cpp), leaving + public internet open for fetch. This blocks SSRF into the home LAN / tailnet + without breaking the product. Requires `sudo` on web-tools. + +## Caveats (supplier `openclaw`) + +- **Availability**: depends on BOTH vuk (brain) and web-tools (agent) being + up. Either off → indexer marks the supplier offline and gateway jobs fail. +- **Throughput**: agent turns run several llama.cpp completions (reason + + search + read) at ~4.5 tok/s on a slot shared with supplier `local`. Expect + minutes per turn; `MAX_CHAT_SESSIONS=1` keeps it single-flight. +- **Ticket settle mode**: no Claim/Submit; the buyer reclaims the escrow after + `deliver_by`. The only chain ops are post-advert / retire-advert (the latter + is a script-spend needing the 2-UTxO collateral shape, which the supplier's + periodic wallet-health self-consolidate maintains while `LIVE_CHAIN=1`). diff --git a/deploy/inference-proxy/docker-compose.supplier-openclaw.yml b/deploy/inference-proxy/docker-compose.supplier-openclaw.yml new file mode 100644 index 0000000..9ae87e1 --- /dev/null +++ b/deploy/inference-proxy/docker-compose.supplier-openclaw.yml @@ -0,0 +1,119 @@ +# deploy/inference-proxy/docker-compose.supplier-openclaw.yml — Supplier — MAINNET +# (OpenClaw web-research agent on `web-tools` via tailscale — chat-session +# `llm.chat.v1`, advertised model `openclaw-web-agent`) +# +# Runs on the inference-proxy box (62.238.38.167), next to supplier `local`. +# The compute backend is an OpenClaw gateway (agent `main`) on the KVM guest +# `web-tools`, reachable only over the tailscale tailnet at +# 100.101.143.72:18789 (OpenAI-compatible chatCompletions endpoint, gateway +# token auth). The agent's own brain is the SAME llama.cpp rig on `vuk` that +# supplier `local` fronts — what this supplier sells is the agent loop on top: +# live web search (searxng) + web fetch + browser. Both suppliers contend for +# vuk's single slot; requests queue at llama.cpp and the 1h deadline absorbs +# stalls. +# +# Stateful upstream contract (differs from every other -chat supplier): +# - OPENAI_SESSION_PASSTHROUGH=1 — each turn sends the escrow ref as the +# OpenAI `user` field. OpenClaw keys ONE persistent agent session per +# `user` (key "openai-user:"), so browser/tool state survives +# across turns, and only the current turn's delta goes upstream (the +# OpenClaw session carries history; re-sending the transcript would +# duplicate it into the agent context every turn). +# - OPENAI_MODEL_OVERRIDE=openclaw — OpenClaw's endpoint 400s on any model +# except "openclaw" / "openclaw/", while the on-chain advert +# carries the buyer-facing id `openclaw-web-agent` the gateway routes on. +# +# Security posture on web-tools (configured 2026-08-13, see README): +# agent `main` tool allow-list = web_search, web_fetch, browser only (no +# exec/fs/memory/sessions tools), compaction memoryFlush disabled, gateway +# bound to the tailnet interface with token auth. +# +# Pre-flight: +# 1. Tailscale ACL: local-inference-proxy (100.127.17.106) must be allowed +# to reach web-tools (100.101.143.72) on tcp/18789 — the tailnet filters +# by source node (vuk can, inference-proxy can't until the ACL is added). +# 2. DNS: mp-suppliers-openclaw.vector.apexfusion.org → 62.238.38.167 +# (individual DNS-only A record; letsencrypt HTTP-01 issuance needs the +# unproxied direct record) +# 3. supplier/.env.openclaw written on host (chmod 600) with at minimum: +# SUPPLIER_PRIV_KEY_HEX, SUPPLIER_ADDRESS, SUPPLIER_PKH, +# SUPPLIER_PUB_KEY_HEX, OPENAI_API_KEY (openclaw gateway token), +# OGMIOS_URL, NETWORK_ID=1, VECTOR_ZERO_TIME_MS, ESCROW_REF_UTXO, +# ADVERT_REF_UTXO, ADVERT_REF, LIVE_CHAIN=1 +# 4. Networks: traefik-net is created one-shot (see the traefik compose in +# this directory). + +name: marketplace-mainnet-supplier-openclaw + +services: + supplier: + build: + context: ../.. + dockerfile: supplier/Dockerfile + container_name: marketplace-mainnet-supplier-openclaw + restart: unless-stopped + env_file: + # Self-contained env for this supplier: wallet identity, ADVERT_REF, + # the openclaw gateway token (OPENAI_API_KEY), and the shared mainnet + # plumbing duplicated in full (NETWORK_ID, OGMIOS_URL, + # VECTOR_ZERO_TIME_MS, ref-script UTxOs, LIVE_CHAIN). + - path: ../../supplier/.env.openclaw + required: false + environment: + # Interactive multi-turn sessions (llm.chat.v1). Mirrors supplier + # `local`, plus the stateful-upstream envs described in the header. + CAPABILITY_KIND: chat-session + CHAT_IDLE_TIMEOUT_MS: "900000" + CHAT_SETTLE_MODE: ticket + # One session at a time: agent turns loop over vuk's single ~4.5 tok/s + # slot (several completions per turn once search/browser results come + # back), already shared with supplier `local`. + MAX_CHAT_SESSIONS: "1" + LLM_BACKEND: openai + # No /v1 suffix — supplier/src/openai.ts appends /v1/chat/completions. + # web-tools' tailscale IP is pinned because containers don't inherit the + # host's tailscale MagicDNS. + OPENAI_BASE_URL: http://100.101.143.72:18789 + # Agent turns are slow: reasoning + tool loops at ~4.5 tok/s generation. + # Matches the advert's max_processing_ms (3600000 = 1h). + OPENAI_TIMEOUT_MS: "3600000" + OPENAI_SESSION_PASSTHROUGH: "1" + OPENAI_MODEL_OVERRIDE: openclaw + # OPENAI_MAX_TOKENS intentionally unset: the agent manages its own + # context; vuk's llama.cpp stops at context-full. + PORT: "8080" + # Enable LiveOgmiosProvider so submitTx/awaitTx actually broadcast. + LIVE_CHAIN: "1" + # NETWORK_ID and OGMIOS_URL intentionally not pinned here — they MUST + # come from the operator-provided supplier/.env.openclaw on a mainnet + # host. + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://localhost:8080/healthz >/dev/null || exit 1"] + interval: 20s + timeout: 5s + retries: 3 + start_period: 15s + networks: + - traefik-net + labels: + - "traefik.enable=true" + - "traefik.docker.network=traefik-net" + - "traefik.http.routers.mp-suppliers-openclaw-mainnet.entrypoints=web" + - "traefik.http.routers.mp-suppliers-openclaw-mainnet.rule=Host(`mp-suppliers-openclaw.vector.apexfusion.org`)" + - "traefik.http.routers.mp-suppliers-openclaw-mainnet.middlewares=mp-suppliers-openclaw-mainnet-https" + - "traefik.http.middlewares.mp-suppliers-openclaw-mainnet-https.redirectscheme.scheme=https" + - "traefik.http.middlewares.mp-suppliers-openclaw-mainnet-https.redirectscheme.permanent=true" + - "traefik.http.routers.mp-suppliers-openclaw-mainnet-tls.entrypoints=websecure" + - "traefik.http.routers.mp-suppliers-openclaw-mainnet-tls.rule=Host(`mp-suppliers-openclaw.vector.apexfusion.org`)" + - "traefik.http.routers.mp-suppliers-openclaw-mainnet-tls.tls.certresolver=letsencrypt" + - "traefik.http.services.mp-suppliers-openclaw-mainnet.loadbalancer.server.port=8080" + logging: + driver: json-file + options: + max-size: "10m" + max-file: "3" + +networks: + traefik-net: + external: true + name: traefik-net