Referencia de la API HTTP
El demonio ccmux expone un HTTP y un JSON API para enumerar sesiones, leer datos de proyectos y crear clientes remotos. Esta referencia cubre la configuración de la conexión, el modelo de confianza, los puntos finales y los tipos de respuesta.
Las rutas y tipos se mantienen en el ccmux source. Es posible que las versiones anteriores del demonio no admitan todos los puntos finales documentados aquí; verifique el punto final de salud para la versión en ejecución.
Transporte y accesibilidad
Las mismas rutas se atienden en dos transportes:
| Transporte | Dirección | Predeterminado |
|---|---|---|
| Conector Unix local | ~/.local/state/ccmux/ccmuxd.sock (modo 0600) |
siempre encendido |
| tailnet HTTP | http://TAILNET-IPv4:PORT (puerto predeterminado 7474) |
desactivado |
Para acceder a un demonio desde otro dispositivo, en el demonio anfitrión: establecer
daemon.listen_tailnet = true (opcionalmente daemon.tailnet_port) en
~/.config/ccmux/config.toml y reinicie ccmuxd. La dirección de enlace es la del host. tailscale ip -4. Si Tailscale no se está ejecutando, el oyente de tailnet no se inicia silenciosamente (solo se sirve el socket Unix).
- Todas las rutas están bajo
/v1/. no hay/v2. - Los cuerpos de solicitud/respuesta son
application/json— excepto/v1/events(text/event-stream) y el punto final adjunto (una actualización de WebSocket). - Sin TLS. El túnel WireGuard de Tailscale es el límite de cifrado + identidad; el oyente HTTP es simplemente HTTP vinculado a la IP de la tailnet.
- Los cuerpos de solicitud tienen un límite de 64 KiB.
Modelo de autenticación y confianza: lea esto primero
No existe autenticación a nivel de aplicación en el HTTP API. Sin token de portador, sin clave API, sin firma por solicitud, sin lista de IP permitidas. El límite de confianza es tu tailnet Tailscale: cualquier host que pueda alcanzar el demonio
100.x.x.x:7474 puede llamar a todos los puntos finales: listar/crear/eliminar/renombrar/enviar claves, adjuntar a una terminal interactiva completay leer notas/conversaciones/uso.
Asegure esto con ACL Tailscale, no autenticación a nivel de aplicación. Trate todo lo que pueda enrutarse a la IP de la tailnet del demonio como de plena confianza.
Dos puntos finales se validan por solicitud y solo para iniciar la confianza del dispositivo + inserción, no para proteger el API:
POST /v1/pairrequiere un token de emparejamiento válido y de un solo uso más una clave pública SSH analizable. Al canjearlo, se instala esa clave en el servidor del host.~/.ssh/authorized_keys.POST /v1/pair-tokenes Solo socket Unix (nunca en la tailnet) y además regresa503a menos quelisten_tailnetestá activado.
Forma de error
Los errores son texto sin formato (text/plain): un mensaje de una sola línea, no un sobre JSON. Compruebe el código de estadoy leer el cuerpo como una cadena para el mensaje humano. Las respuestas exitosas JSON son application/json.
| Código | Significado |
|---|---|
200 |
OK (cuerpo JSON) |
204 |
OK, sin cuerpo (eliminar/enviar claves/registro de dispositivo) |
400 |
entrada incorrecta/fallo de validación |
401 |
token de emparejamiento no válido o caducado (/v1/pair solamente) |
404 |
no encontrado — o este demonio es anterior al punto final |
405 |
método HTTP incorrecto |
500 |
servidor / tmux / falla del andamio |
503 |
función no disponible (p. ej. /v1/pair-token con listen_tailnet desactivado) |
El API evoluciona agregando Campos JSON (todos los campos opcionales usan omitempty) y agregando rutas. Tratar un 404 en un punto final completo como "este demonio es más antiguo que esa característica" y se degrada suavemente.
Resumen de puntos finales
| Método | Ruta | Respuesta |
|---|---|---|
GET |
/v1/health |
HealthInfo |
GET |
/v1/peers |
[]PeerInfo |
GET |
/v1/sessions |
[]SessionState |
POST |
/v1/sessions |
SessionState (crear o adjuntar) |
POST |
/v1/sessions/bare |
NewBareSessionResponse |
POST |
/v1/sessions/NAME/kill |
204 |
POST |
/v1/sessions/NAME/rename |
SessionState |
POST |
/v1/sessions/NAME/send-keys |
204 |
GET |
/v1/sessions/NAME/preview |
PreviewResponse |
GET |
/v1/sessions/NAME/attach |
WebSocket (PTY interactivo) |
GET |
/v1/projects |
[]ProjectInfo |
POST |
/v1/projects |
NewProjectResponse |
GET |
/v1/conversations |
[]Conversation |
GET |
/v1/usage?window=… |
AgentUsage |
GET |
/v1/notes?project=…[&file=…] |
[]NoteEntry o NoteContent |
GET |
/v1/notes/search?project=…&q=… |
[]SearchHit |
GET |
/v1/events |
Flujo SSE de SessionEvent |
POST |
/v1/pair-token (solo Unix) |
PairTokenResponse |
POST |
/v1/pair |
PairResponse |
POST |
/v1/devices |
204 |
POST |
/v1/devices/test |
204 |
(Donde una ruta muestra NAME es el segmento de ruta del nombre de sesión tmux, es decir. /v1/sessions/{name}/kill.)
Detalles del punto final
Sesiones
GET /v1/sessions→[]SessionState. Cada sesión que gestiona este demonio, con funciones derivadas del demonio.state(active/idle/needs_input/error/unknown).POST /v1/sessions(NewSessionRequest,projectrequerido) →SessionState. Cree o adjunte una sesión de agente vinculada al proyecto (idempotente en el nombre tmux).pathpor defecto esPROJECTS_ROOT/PROJECTen el host del demonio.POST /v1/sessions/bare(NewBareSessionRequest) →NewBareSessionResponse. Una sesión de solo shell (sin proyecto, sin andamio).POST /v1/sessions/NAME/kill→204. Emite unkilledEvento SSE.POST /v1/sessions/NAME/rename(RenameRequest) →SessionState.NAMEes el nombre actual; el cuerpo lleva el nuevo.POST /v1/sessions/NAME/send-keys(SendKeysRequest) →204. Pasó atmux send-keys(por ejemplo, escriba una respuesta +\n).GET /v1/sessions/NAME/preview?lines=N→PreviewResponse. Últimas N líneas del panel activo como texto sin formato (ANSI eliminado).Nes1..200, predeterminado24. Un vistazo ligero sin abrir el enchufe de conexión.
Terminal interactivo — GET /v1/sessions/NAME/attach (WebSocket)
Actualice a un WebSocket conectado a un servidor real tmux attach-session en un PTY: un verdadero terminal interactivo (salida en vivo, entrada, cambio de tamaño) sin ssh/mosh. Usos coder/websocket encuadre.
- cliente → servidor, marco binario: bytes estándar sin formato (pulsaciones de teclas).
- cliente → servidor, marco de texto: JSON
{"cols":N,"rows":N}para cambiar el tamaño. - servidor → cliente, marco binario: bytes de salida PTY sin procesar.
El tamaño de PTY inicial es 80x24 hasta el primer cambio de tamaño. El servidor hace ping cada 25 segundos con una fecha límite de 10 segundos: responda a los pings o espere un desmontaje.
InsecureSkipVerify está configurado (no Origin comprobar). Cerrar solo el enchufe
se separa; la sesión tmux continúa ejecutándose. Prefiero esto a las encuestas
/preview para una vista interactiva.
Proyectos
GET /v1/projects→[]ProjectInfo. Proyectos descubiertos en la raíz de proyectos del demonio, etiquetados con su nombre de host.POST /v1/projects(NewProjectRequest,namerequerido) →NewProjectResponse. Crea un nuevo proyecto (solo directorio, noCLAUDE.md/git) e inicia una sesión de agente.namedebe ser un único segmento de ruta no oculto (no/,\, sin interlineado.).
Conversaciones, uso, notas
GET /v1/conversations→[]Conversation. Transcripciones de agentes anteriores, las más recientes primero;ides el UUID propio del agente (su--resumeidentificación).GET /v1/usage?window=DURATION→AgentUsage. Token por agente + costo durante una ventana móvil (duración Go como2h,24h; predeterminado5h).GET /v1/notes?project=NAME→[]NoteEntry; con&file=REL→NoteContent.filedebe ser relativo al proyecto.mdruta, no...GET /v1/notes/search?project=NAME&q=QUERY→[]SearchHit. Respaldado por Ripgrep. (Demonios más antiguos404— tratar como “búsqueda no disponible”).
Actualizaciones en vivo — GET /v1/events (SSE)
text/event-stream. cada uno data: el marco es un JSON SessionEvent; kind es
created / killed / state_change / needs_input. Latidos del corazón: : connected en abierto, : ping cada 20 segundos (líneas de comentarios que comienzan con : son ignorables). Un event: drops / data: N frame significa que te perdiste N eventos y debes volver a buscarlos /v1/sessions para resincronizar.
Salud y descubrimiento
GET /v1/health→HealthInfo. Sondeo de vida + identidad.GET /v1/peers→[]PeerInfo. Cada par de tailnet + si cada uno ejecuta ccmuxd (devuelve[], no500, cuando Tailscale está ausente).
Emparejamiento y push (móvil)
Flujo opcional para push nativo (APN/iOS, FCM/Android) e instalación de una clave SSH para que el dispositivo pueda ssh/mosh adjuntar. No necesario utilizar los puntos finales de lectura/actuación a través de la tailnet.
POST /v1/pair-token(Solo socket Unix) →PairTokenResponse. Acuña un token de una sola vez (hexadecimal de 128 bits, de un solo uso, TTL de 5 minutos) + unccmux://pair?…enlace profundo.503silisten_tailnetestá desactivado.POST /v1/pair(PairRequest) →PairResponse. Canjee un token: instale la clave pública SSH del dispositivo y, opcionalmente, registre un token push en línea.401en token no válido/caducado. Accesible en la tailnet.POST /v1/devices(RegisterDeviceRequest) →204. Registre/actualice un token de inserción en un host ya emparejado. Dispositivo identificado por su SSHpublic_key(almacenado solo como hash SHA-256).provideresapns(predeterminado) ofcm; Necesidades de APNenv=development/production, FCM necesita vacíoenv.POST /v1/devices/test({"public_key":"…"}) →204. Empuje de verificación al dispositivo para esa clave.
Activa dos transiciones: una sesión entrando needs_inputy
active → idle (“agente finalizado”). La identificación de la sesión del push es
local/SESSIONNAME. Los APN y FCM son desactivado de forma predeterminada y necesita configuración del lado del servidor; El enrutamiento FCM existe pero la entrega de Android aún no está conectada.
Tipos
Copiado de internal/daemon/protocol.go (estructuras Go con sus etiquetas JSON).
type SessionState struct {
Name string `json:"name"`
Host string `json:"host"` // "local" or a remote host name
Project string `json:"project"`
Path string `json:"path"` // session working directory
State string `json:"state"` // active|idle|needs_input|error|unknown
Attached bool `json:"attached"`
Windows int `json:"windows"`
Created time.Time `json:"created"`
LastChange time.Time `json:"last_change"`
PromptCount int `json:"prompt_count"`
Agent string `json:"agent,omitempty"`
}
type HealthInfo struct {
OK bool `json:"ok"`
Hostname string `json:"hostname"`
Version string `json:"version"`
Sessions int `json:"sessions"`
SleepMode string `json:"sleep_mode"` // off|safe|dangerous|very_dangerous
}
type SessionEvent struct {
At time.Time `json:"at"`
Kind string `json:"kind"` // state_change|created|killed|needs_input
Session SessionState `json:"session"`
}
type NewSessionRequest struct {
Project string `json:"project"`
Path string `json:"path"` // defaults to PROJECTS_ROOT/PROJECT
Continue bool `json:"continue"` // start the agent with --continue
Name string `json:"name,omitempty"`
Agent string `json:"agent,omitempty"`
}
type NewBareSessionRequest struct {
Name string `json:"name,omitempty"`
Path string `json:"path,omitempty"`
Agent string `json:"agent,omitempty"`
}
type NewBareSessionResponse struct {
Session string `json:"session"`
Path string `json:"path"`
Host string `json:"host"`
}
type NewProjectRequest struct {
Name string `json:"name"`
Agent string `json:"agent,omitempty"`
}
type NewProjectResponse struct {
Session string `json:"session"`
Path string `json:"path"`
Host string `json:"host"`
}
type ProjectInfo struct {
Name string `json:"name"`
Host string `json:"host"`
Path string `json:"path"` // absolute on the daemon host
HasGit bool `json:"has_git"`
HasCM bool `json:"has_cm"`
HasAgents bool `json:"has_agents,omitempty"`
HasDocs bool `json:"has_docs"`
Agent string `json:"agent,omitempty"`
Modified time.Time `json:"modified"`
}
type PeerInfo struct {
Hostname string `json:"hostname"`
Addr string `json:"addr"` // tailnet IPv4
OS string `json:"os"`
Online bool `json:"online"`
RunsCCMuxd bool `json:"runs_ccmuxd"`
Port *int `json:"port,omitempty"`
}
type Conversation struct {
ID string `json:"id"` // agent UUID (its --resume id)
Agent string `json:"agent"`
Project string `json:"project,omitempty"`
Path string `json:"path,omitempty"`
Preview string `json:"preview,omitempty"`
Modified time.Time `json:"modified"`
}
type AgentUsage struct {
Claude UsageSummary `json:"claude"`
Codex UsageSummary `json:"codex"`
Antigravity UsageSummary `json:"antigravity"`
}
type UsageSummary struct {
HasData bool `json:"has_data"`
WindowSeconds int `json:"window_seconds"`
Prompts int `json:"prompts"`
InputTokens int `json:"input_tokens"`
OutputTokens int `json:"output_tokens"`
EstimatedCost float64 `json:"estimated_cost"` // USD
}
type NoteEntry struct {
Rel string `json:"rel"`
Dir string `json:"dir"`
Display string `json:"display"`
Modified time.Time `json:"modified"`
}
type NoteContent struct {
Rel string `json:"rel"`
Content string `json:"content"`
}
type SearchHit struct {
Rel string `json:"rel"`
LineNum int `json:"line_num"`
Snippet string `json:"snippet"`
}
type PreviewResponse struct {
Lines int `json:"lines"`
Content string `json:"content"`
}
type RenameRequest struct{ Name string `json:"name"` }
type SendKeysRequest struct{ Keys string `json:"keys"` }
type PairRequest struct {
Token string `json:"token"`
PublicKey string `json:"public_key"`
DeviceToken string `json:"device_token,omitempty"`
APNsEnv string `json:"apns_env,omitempty"` // development|production
}
type PairResponse struct{ Hostname string `json:"hostname"`; Version string `json:"version"` }
type PairTokenResponse struct{ Token string `json:"token"`; URL string `json:"url"` }
type RegisterDeviceRequest struct {
Token string `json:"token"`
Env string `json:"env,omitempty"` // apns: development|production; fcm: empty
PublicKey string `json:"public_key"`
Provider string `json:"provider,omitempty"` // apns (default) | fcm
}
// POST /v1/devices/test body is { "public_key": "..." }
Reglas de validación para reflejar el lado del cliente
- tmux session names no debe contener
/,\, o:. - nombres de proyectos (para
POST /v1/projects) debe ser un único segmento de ruta no oculto. - rutas de archivos de notas debe ser relativo al proyecto, no
.., que termina en.md. - los cuerpos de solicitud tienen un límite de 64 KiB.
Ejemplos
# Health (over the tailnet)
curl -s http://100.75.64.20:7474/v1/health | jq
# List sessions
curl -s http://100.75.64.20:7474/v1/sessions | jq
# Type a reply into a session and press Enter
curl -s -X POST http://100.75.64.20:7474/v1/sessions/c-auth/send-keys \
-H 'content-type: application/json' \
-d '{"keys":"yes, ship it\n"}'
# Peek at the last 40 lines of a pane
curl -s 'http://100.75.64.20:7474/v1/sessions/c-auth/preview?lines=40' | jq -r .content
# Subscribe to live events
curl -sN http://100.75.64.20:7474/v1/events
El cliente de referencia Go en
internal/daemon/client.go
es el consumidor canónico y un mapa útil de método → punto final.
Ayuda a mejorar ccmux
¿Encontraste un error, una guía poco clara o una traducción mejorable? Todas las aportaciones son bienvenidas. Abre una incidencia o envía una solicitud de incorporación de cambios.
¿Detectó un error o algo desactualizado? Editar esta página en GitHub.