/run
# Goal
Stand up an HTTP endpoint behind a cloudflared tunnel that accepts
POST requests containing prompts, and forwards them to the agent
runner the same way Telegram messages are. This lets external
clients (an external AI like Claude in a web chat) post prompts
without copy-paste.
# Hard constraints
- Repo: C:\Projects\ds-coach
- Local + dev only. Production READ-ONLY.
- No git push, no tags, no deploys.
- The endpoint MUST require authentication (a secret header).
No anonymous access — that would expose the runner to the
internet.
- Cap diff at ~120 lines across 3-4 new files.
- Do NOT modify the existing cloudflared config for dev-coach.
Add a new tunnel hostname instead.
# PART A — Local HTTP receiver
Build tools/agent_runner/http_receiver.py:
- FastAPI or aiohttp (whichever's already in venv; FastAPI is)
- Listens on 127.0.0.1:8765
- One endpoint: POST /run
- Required header: X-DS-Coach-Secret matching env var
AGENT_RUNNER_HTTP_SECRET (32+ char random string)
- Required body: JSON with single key {"prompt": "<text>"}
- On valid POST: enqueue the prompt to the same dispatch path
runner.py uses for /run from Telegram (extract that path
into a shared async function if needed)
- Returns immediately with: {"status": "queued",
"run_id": "<ts>"}
- The runner sends results to Don's Saved Messages as today
- Returns 401 on missing/wrong secret, 400 on malformed body
# PART B — Run alongside the agent runner
Modify tools/agent_runner/runner.py:
- On startup, also start the http_receiver in the same asyncio
loop (uvicorn.Server programmatic start, not subprocess)
- Logs both Telegram + HTTP events through the same dispatch
- Idle pause + handoff prepend apply equally to HTTP-sourced
prompts
- /forcerun via HTTP: a separate header X-DS-Coach-Force=true
bypasses idle pause
# PART C — Cloudflared tunnel for the new endpoint
Add to %USERPROFILE%\.cloudflared\config.yml a new ingress rule
mapping
https://run.kinkforlife.com (or whatever subdomain Don
prefers — pick run.kinkforlife.com unless it conflicts) to
http://127.0.0.1:8765.
If Don's existing cloudflared tunnel doesn't have DNS configured
for run.kinkforlife.com:
- Don runs: cloudflared tunnel route dns <tunnel-name>
run.kinkforlife.com
- This is a one-time DNS step, document it in README
Restart cloudflared (or reload it via SIGHUP equivalent).
# PART D — Secret generation
Generate a 48-char random hex string. Add to .env.local as:
AGENT_RUNNER_HTTP_SECRET=<48-char-hex>
Print only the secret's length to console at startup, never
the value.
# PART E — README + smoke test
README:
- Document the new endpoint, the secret header, the
cloudflared step.
- Include a curl example using PowerShell-style Invoke-
WebRequest:
Invoke-WebRequest -Uri "
https://run.kinkforlife.com/run" `
-Method POST `
-Headers @{"X-DS-Coach-Secret"="<secret>"; "Content-Type"="application/json"} `
-Body '{"prompt":"/run echo hello"}'
Smoke test:
- Hit
http://127.0.0.1:8765/run from PowerShell with the secret.
Should return {"status":"queued",...}, runner should DM Don
"Got it. Starting run at..." within 2s.
- Hit it WITHOUT the secret. Should return 401.
- Don't smoke-test the public URL until cloudflared DNS is
set up — that's Don's manual step.
# Final report
1. Files created/modified (count + list).
2. Smoke test result: local POST with secret → DM received? Y/N
3. Smoke test result: local POST without secret → 401? Y/N
4. Cloudflared config diff (the new ingress rule added).
5. The verbatim 3 manual steps Don has to do:
a. Run cloudflared tunnel route dns command
b. Restart cloudflared (or however we tell it to reload)
c. Tell Claude (web chat) two things: the URL
(
https://run.kinkforlife.com/run) and the secret value
6. Cost in API credits this run.