feat: scaffold claude_wire — respuesta de claude interceptando el SSE de la red
Obtiene el texto del modelo interceptando el stream SSE de api.anthropic.com /v1/messages con un mitmproxy, en vez de parsear el render de la terminal. Dirige la TUI interactiva real (NUNCA claude -p) por el proxy con claude_pipe, y emite el texto exacto token a token como NDJSON. Compone tee_anthropic_sse_py_cybersecurity (addon mitmproxy). Corta por message_stop (sin idle ciego): ~9s vs ~15s de parsear la TUI, y texto exacto sin artefactos. Validado end-to-end contra claude real.
This commit is contained in:
@@ -0,0 +1,3 @@
|
|||||||
|
claude_wire
|
||||||
|
*.db-shm
|
||||||
|
*.db-wal
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
---
|
||||||
|
name: claude_wire
|
||||||
|
lang: go
|
||||||
|
domain: infra
|
||||||
|
version: 0.1.0
|
||||||
|
description: "Obtiene la respuesta de claude interceptando el stream SSE del modelo en la red (api.anthropic.com /v1/messages) en vez de parsear el render de la terminal. Dirige la TUI interactiva real (NUNCA claude -p) a traves de un mitmproxy que teea el SSE, y emite el texto exacto del modelo token a token como NDJSON. Mas rapido que parsear la TUI (corta por message_stop, sin idle ciego) y exacto (sin artefactos del render)."
|
||||||
|
tags: [cli, claude, mitmproxy, sse, streaming, wire, web-proxy]
|
||||||
|
uses_functions:
|
||||||
|
- tee_anthropic_sse_py_cybersecurity
|
||||||
|
uses_types: []
|
||||||
|
framework: ""
|
||||||
|
entry_point: "main.go"
|
||||||
|
dir_path: "apps/claude_wire"
|
||||||
|
icon:
|
||||||
|
phosphor: "wifi-high"
|
||||||
|
accent: "#10b981"
|
||||||
|
e2e_checks:
|
||||||
|
- id: build
|
||||||
|
cmd: "go build -o claude_wire ."
|
||||||
|
timeout_s: 60
|
||||||
|
- id: needs_prompt
|
||||||
|
cmd: "./claude_wire 2>&1 || true"
|
||||||
|
expect_stdout_contains: "no prompt"
|
||||||
|
timeout_s: 10
|
||||||
|
---
|
||||||
|
|
||||||
|
# claude_wire
|
||||||
|
|
||||||
|
## Que hace
|
||||||
|
|
||||||
|
Devuelve la respuesta de `claude` **interceptando el stream del modelo en la red**, no parseando
|
||||||
|
el render de la terminal. Dirige la TUI interactiva real de `claude` (jamas `claude -p`) a traves
|
||||||
|
de un mitmproxy que captura el SSE de `POST api.anthropic.com/v1/messages`, y emite el texto
|
||||||
|
exacto del modelo, token a token, como NDJSON.
|
||||||
|
|
||||||
|
Es la culminacion de la exploracion: parsear el render (`claude_pipe`) es heuristico y lento
|
||||||
|
(warmup + idle ciegos, artefactos del spinner, truncacion por scroll). Interceptar la red da el
|
||||||
|
texto **exacto** del modelo, en **streaming real**, y sabe **exactamente** cuando termina
|
||||||
|
(`message_stop`), eliminando el idle ciego.
|
||||||
|
|
||||||
|
## Como funciona
|
||||||
|
|
||||||
|
```
|
||||||
|
mitmdump + tee_anthropic_sse ── captura el SSE de /v1/messages → NDJSON
|
||||||
|
claude_pipe (PTY, dirige la TUI) ── lanza claude interactivo y teclea el prompt
|
||||||
|
claude_wire (este runner) ── lee el NDJSON del proxy, emite text_delta + result,
|
||||||
|
mata todo en cuanto llega message_stop
|
||||||
|
```
|
||||||
|
|
||||||
|
1. Arranca `mitmdump` con el addon `tee_anthropic_sse_py_cybersecurity` (`FN_WIRE_ONLY_TOOLS=1`
|
||||||
|
para aislar la respuesta principal de las llamadas auxiliares de titulo/clasificador).
|
||||||
|
2. Dirige la TUI interactiva con `claude_pipe` por el proxy (`HTTPS_PROXY` + `NODE_EXTRA_CA_CERTS`
|
||||||
|
apuntando a la CA de mitmproxy). El output parseado de `claude_pipe` se ignora — solo se usa
|
||||||
|
para lanzar claude y teclear el prompt.
|
||||||
|
3. Lee el NDJSON que emite el addon, sigue el primer stream con `has_tools`, emite cada
|
||||||
|
`text_delta`, y al `message_stop` emite el `result` y mata mitmdump + claude_pipe.
|
||||||
|
|
||||||
|
El texto sale **del cable**, no de la pantalla.
|
||||||
|
|
||||||
|
## Prerrequisitos
|
||||||
|
|
||||||
|
- `mitmproxy` instalado (`mitmdump` en el PATH): `uv tool install mitmproxy`.
|
||||||
|
- La CA de mitmproxy generada y confiada: arrancar `mitmdump` una vez crea
|
||||||
|
`~/.mitmproxy/mitmproxy-ca-cert.pem`. claude la acepta via `NODE_EXTRA_CA_CERTS`.
|
||||||
|
- El binario `claude_pipe` compilado (driver de la TUI): `apps/claude_pipe/claude_pipe`.
|
||||||
|
|
||||||
|
## Ejemplo
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd apps/claude_wire
|
||||||
|
go build -o claude_wire .
|
||||||
|
|
||||||
|
./claude_wire --prompt "di tres palabras: uno dos tres" --cwd /home/enmanuel/fn_registry
|
||||||
|
# {"type":"text_delta","text":"T"}
|
||||||
|
# {"type":"text_delta","text":"res palabras: uno dos tres."}
|
||||||
|
# {"type":"result","subtype":"success","result":"Tres palabras: uno dos tres."}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Flags
|
||||||
|
|
||||||
|
| Flag | Default | Que hace |
|
||||||
|
|---|---|---|
|
||||||
|
| `--prompt` | — | Prompt a enviar (o arg posicional al final). |
|
||||||
|
| `--cwd` | `~/fn_registry` | Directorio donde corre claude (MCP aprobados → sin dialogo de arranque). |
|
||||||
|
| `--port` | `8901` | Puerto del mitmproxy. |
|
||||||
|
| `--root` | `~/fn_registry` | Raiz del registry (para localizar el addon por defecto). |
|
||||||
|
| `--addon` | `<root>/python/functions/cybersecurity/tee_anthropic_sse.py` | Addon mitmproxy. |
|
||||||
|
| `--ca` | `~/.mitmproxy/mitmproxy-ca-cert.pem` | CA de mitmproxy para `NODE_EXTRA_CA_CERTS`. |
|
||||||
|
| `--pipe` | `apps/claude_pipe/claude_pipe` | Binario que dirige la TUI. |
|
||||||
|
| `--warmup` | `5s` | Espera de `claude_pipe` para que cargue la TUI antes de teclear. |
|
||||||
|
| `--max` | `120s` | Timeout duro. |
|
||||||
|
|
||||||
|
## Comparativa (por que existe)
|
||||||
|
|
||||||
|
| | `claude_pipe` (parsear TUI) | `claude_wire` (interceptar red) |
|
||||||
|
|---|---|---|
|
||||||
|
| Texto | heuristico, artefactos | **exacto, byte a byte** |
|
||||||
|
| Streaming | snapshots ~150ms | **token real (content_block_delta)** |
|
||||||
|
| Fin de respuesta | idle ciego (4s) | **message_stop exacto** |
|
||||||
|
| Latencia medida | ~15s | **~9s** |
|
||||||
|
| Robustez | fragil (UI cambia) | **protocolo API estable** |
|
||||||
|
|
||||||
|
## Cuando usarla
|
||||||
|
|
||||||
|
- Cuando quieras el texto exacto del modelo desde la TUI interactiva, en streaming real, sin
|
||||||
|
parsear el render ni pagar el idle ciego.
|
||||||
|
- Como backend de un chat: emite el mismo NDJSON que `claude_pipe --stream`, mas rapido y exacto.
|
||||||
|
|
||||||
|
## Gotchas
|
||||||
|
|
||||||
|
- **Requiere mitmproxy + CA confiada** (`NODE_EXTRA_CA_CERTS`). Si claude empezara a hacer TLS
|
||||||
|
pinning, dejaria de funcionar (hoy no lo hace).
|
||||||
|
- **Depende de `claude_pipe`** como driver de la TUI (el PTY). No reimplementa el pilotaje.
|
||||||
|
- **Sigue heredando el warmup** de `claude_pipe` (esperar a que la TUI cargue antes de teclear);
|
||||||
|
ahi esta el grueso de la latencia restante. Una deteccion de "TUI lista" lo reduciria mas.
|
||||||
|
- **Una interaccion dispara varias /v1/messages** (respuesta + titulo/clasificador en haiku); el
|
||||||
|
addon filtra por `has_tools` para seguir solo la principal.
|
||||||
|
- **Es trafico de tu propia cuenta y maquina** — observabilidad local, no acceso remoto.
|
||||||
|
- **Linux/Unix** (PTY POSIX heredado de `claude_pipe`).
|
||||||
@@ -0,0 +1,241 @@
|
|||||||
|
// Command claude_wire gets claude's answer by intercepting the model's network
|
||||||
|
// stream — the SSE response from api.anthropic.com — instead of parsing the
|
||||||
|
// terminal render. It drives the real interactive claude TUI (never `claude -p`)
|
||||||
|
// through a mitmproxy that tees the /v1/messages SSE, and emits the exact model
|
||||||
|
// text token by token as NDJSON.
|
||||||
|
//
|
||||||
|
// Why this beats parsing the TUI render:
|
||||||
|
// - Exact text, byte for byte (no heuristics, no spinner artifacts, no scroll
|
||||||
|
// truncation).
|
||||||
|
// - Real token-level streaming (the API's content_block_delta events).
|
||||||
|
// - No blind idle wait: the message_stop event tells us precisely when the
|
||||||
|
// answer finished, so there is no 4s idle tail.
|
||||||
|
// - Stable protocol (Anthropic SSE) instead of a UI that changes between
|
||||||
|
// claude versions.
|
||||||
|
//
|
||||||
|
// Pipeline:
|
||||||
|
//
|
||||||
|
// mitmdump + tee_anthropic_sse addon ── captures /v1/messages SSE → NDJSON
|
||||||
|
// claude_pipe (drives the TUI via PTY) ── sends the prompt, keeps claude alive
|
||||||
|
// this runner ── reads the proxy NDJSON, emits text_delta
|
||||||
|
// + result, kills everything on message_stop
|
||||||
|
//
|
||||||
|
// claude_wire does NOT use `claude -p`. The TUI is driven exactly as a human would;
|
||||||
|
// the text is read off the wire, not off the screen.
|
||||||
|
//
|
||||||
|
// Output (NDJSON, same shape as claude_pipe --stream):
|
||||||
|
//
|
||||||
|
// {"type":"text_delta","text":"..."}
|
||||||
|
// {"type":"result","subtype":"success","is_error":false,"result":"<full answer>"}
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// wireEvent is one NDJSON line emitted by the tee_anthropic_sse addon.
|
||||||
|
type wireEvent struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
StreamID int `json:"stream_id"`
|
||||||
|
Model string `json:"model"`
|
||||||
|
HasTools bool `json:"has_tools"`
|
||||||
|
Text string `json:"text"`
|
||||||
|
StopReason string `json:"stop_reason"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// outEvent is one NDJSON line this runner emits to its own stdout.
|
||||||
|
type outEvent struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Text string `json:"text,omitempty"`
|
||||||
|
Subtype string `json:"subtype,omitempty"`
|
||||||
|
IsError bool `json:"is_error,omitempty"`
|
||||||
|
Result string `json:"result,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var (
|
||||||
|
prompt = arg("--prompt", "")
|
||||||
|
cwd = arg("--cwd", "/home/enmanuel/fn_registry")
|
||||||
|
port = arg("--port", "8901")
|
||||||
|
root = arg("--root", "/home/enmanuel/fn_registry")
|
||||||
|
addon = arg("--addon", "")
|
||||||
|
caPath = arg("--ca", os.Getenv("HOME")+"/.mitmproxy/mitmproxy-ca-cert.pem")
|
||||||
|
pipeBin = arg("--pipe", "/home/enmanuel/fn_registry/apps/claude_pipe/claude_pipe")
|
||||||
|
warmup = arg("--warmup", "5s")
|
||||||
|
maxStr = arg("--max", "120s")
|
||||||
|
)
|
||||||
|
// The prompt may also be a trailing positional arg.
|
||||||
|
if prompt == "" {
|
||||||
|
if p := positional(); p != "" {
|
||||||
|
prompt = p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if prompt == "" {
|
||||||
|
fmt.Fprintln(os.Stderr, "claude_wire: no prompt (use --prompt or a positional arg)")
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
if addon == "" {
|
||||||
|
addon = filepath.Join(root, "python/functions/cybersecurity/tee_anthropic_sse.py")
|
||||||
|
}
|
||||||
|
|
||||||
|
maxDur, err := time.ParseDuration(maxStr)
|
||||||
|
if err != nil {
|
||||||
|
maxDur = 120 * time.Second
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), maxDur)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// 1. Start mitmdump with the SSE tee addon. FN_WIRE_ONLY_TOOLS isolates the
|
||||||
|
// main Claude Code response (has_tools) from title/classifier calls.
|
||||||
|
mitm := exec.CommandContext(ctx, "mitmdump", "-p", port, "-s", addon, "-q")
|
||||||
|
mitm.Env = append(os.Environ(), "FN_WIRE_ONLY_TOOLS=1")
|
||||||
|
mitm.Stderr = os.Stderr
|
||||||
|
ndjson, err := mitm.StdoutPipe()
|
||||||
|
if err != nil {
|
||||||
|
fail("mitmdump stdout pipe", err)
|
||||||
|
}
|
||||||
|
if err := mitm.Start(); err != nil {
|
||||||
|
fail("start mitmdump", err)
|
||||||
|
}
|
||||||
|
defer kill(mitm)
|
||||||
|
|
||||||
|
// 2. Wait for the proxy to listen.
|
||||||
|
if !waitPort("127.0.0.1:"+port, 10*time.Second) {
|
||||||
|
fail("proxy did not come up", fmt.Errorf("port %s", port))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Drive the interactive claude TUI through the proxy with claude_pipe. Its
|
||||||
|
// own parsed output is irrelevant here — we only need it to launch claude
|
||||||
|
// and type the prompt. A long idle keeps it from cutting before message_stop;
|
||||||
|
// we kill it as soon as the wire reports the answer is done.
|
||||||
|
pipe := exec.CommandContext(ctx, pipeBin,
|
||||||
|
"--cwd", cwd, "--warmup", warmup, "--idle", "30s", "--max", maxStr,
|
||||||
|
"--format", "text", prompt)
|
||||||
|
pipe.Env = append(os.Environ(),
|
||||||
|
"HTTPS_PROXY=http://127.0.0.1:"+port,
|
||||||
|
"HTTP_PROXY=http://127.0.0.1:"+port,
|
||||||
|
"NODE_EXTRA_CA_CERTS="+caPath,
|
||||||
|
"SSL_CERT_FILE="+caPath,
|
||||||
|
"REQUESTS_CA_BUNDLE="+caPath,
|
||||||
|
)
|
||||||
|
pipe.Stdout = nil
|
||||||
|
pipe.Stderr = nil
|
||||||
|
if err := pipe.Start(); err != nil {
|
||||||
|
fail("start claude_pipe", err)
|
||||||
|
}
|
||||||
|
defer kill(pipe)
|
||||||
|
|
||||||
|
// 4. Read the proxy NDJSON. The addon already filtered to the main stream, so
|
||||||
|
// we follow the first stream we see and stop at its message_stop.
|
||||||
|
enc := json.NewEncoder(os.Stdout)
|
||||||
|
sc := bufio.NewScanner(ndjson)
|
||||||
|
sc.Buffer(make([]byte, 1024*1024), 1024*1024)
|
||||||
|
|
||||||
|
var answer strings.Builder
|
||||||
|
mainStream := 0
|
||||||
|
|
||||||
|
for sc.Scan() {
|
||||||
|
line := strings.TrimSpace(sc.Text())
|
||||||
|
if line == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var ev wireEvent
|
||||||
|
if json.Unmarshal([]byte(line), &ev) != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch ev.Type {
|
||||||
|
case "message_start":
|
||||||
|
if mainStream == 0 && ev.HasTools {
|
||||||
|
mainStream = ev.StreamID
|
||||||
|
}
|
||||||
|
case "text_delta":
|
||||||
|
if ev.StreamID == mainStream {
|
||||||
|
answer.WriteString(ev.Text)
|
||||||
|
_ = enc.Encode(outEvent{Type: "text_delta", Text: ev.Text})
|
||||||
|
}
|
||||||
|
case "message_stop":
|
||||||
|
if ev.StreamID == mainStream {
|
||||||
|
_ = enc.Encode(outEvent{
|
||||||
|
Type: "result",
|
||||||
|
Subtype: "success",
|
||||||
|
IsError: answer.Len() == 0,
|
||||||
|
Result: answer.String(),
|
||||||
|
})
|
||||||
|
return // defers kill mitmdump + claude_pipe
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stream ended without a message_stop (timeout / claude died). Emit whatever
|
||||||
|
// we have so the consumer is not left hanging.
|
||||||
|
_ = enc.Encode(outEvent{
|
||||||
|
Type: "result",
|
||||||
|
Subtype: "incomplete",
|
||||||
|
IsError: answer.Len() == 0,
|
||||||
|
Result: answer.String(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- tiny flag/util helpers (no external deps) ---
|
||||||
|
|
||||||
|
func arg(name, def string) string {
|
||||||
|
for i, a := range os.Args[1:] {
|
||||||
|
if a == name && i+2 <= len(os.Args)-1 {
|
||||||
|
return os.Args[i+2]
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(a, name+"=") {
|
||||||
|
return strings.TrimPrefix(a, name+"=")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
|
||||||
|
// positional returns the last argument if it is not a flag or a flag value.
|
||||||
|
func positional() string {
|
||||||
|
args := os.Args[1:]
|
||||||
|
for i := len(args) - 1; i >= 0; i-- {
|
||||||
|
a := args[i]
|
||||||
|
if strings.HasPrefix(a, "--") {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
// Skip if this is the value of a preceding flag.
|
||||||
|
if i > 0 && strings.HasPrefix(args[i-1], "--") && !strings.Contains(args[i-1], "=") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitPort(addr string, timeout time.Duration) bool {
|
||||||
|
deadline := time.Now().Add(timeout)
|
||||||
|
for time.Now().Before(deadline) {
|
||||||
|
c, err := net.DialTimeout("tcp", addr, 300*time.Millisecond)
|
||||||
|
if err == nil {
|
||||||
|
_ = c.Close()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
time.Sleep(200 * time.Millisecond)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func kill(c *exec.Cmd) {
|
||||||
|
if c != nil && c.Process != nil {
|
||||||
|
_ = c.Process.Kill()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func fail(what string, err error) {
|
||||||
|
fmt.Fprintf(os.Stderr, "claude_wire: %s: %v\n", what, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user