Files
coder/agent/api.go
Asher cc0c868c55 feat: add client_session_id to ssh connections (#29701)
Add a new /tcp endpoint on the agent that is used to pass along
the client session ID (with the existing baggage mechanisms). The agent
then can upgrade that into the desired connection type, based on the
port. For now that is only the standard SSH port and reconnecting pty.

The `ssh`, `ssh --stdio`, `ssh --stdio` over Coder Connect, `exp rpty`,
and web terminal paths are all updated to use this new endpoint, and
fall back to the old method when the endpoint is not available.

That ID is added to the log context, and is also used to populate the
session ID column in the connection logs table.

There are some logs related to a connection that will not be tagged with
the ID (like the "accepted connection" line) because they happen before
the http handler gets to run.
2026-10-02 16:40:24 -08:00

121 lines
4.1 KiB
Go

package agent
import (
"net/http"
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
"github.com/coder/coder/v2/agent/agentchat"
"github.com/coder/coder/v2/coderd/httpapi"
"github.com/coder/coder/v2/coderd/httpmw/loggermw"
"github.com/coder/coder/v2/coderd/tracing"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/codersdk/workspacesdk"
"github.com/coder/coder/v2/httpmw"
)
func (a *agent) apiHandler(upgradeListener *httpUpgrader) http.Handler {
r := chi.NewRouter()
r.Use(
httpmw.Recover(a.logger),
tracing.StatusWriterMiddleware,
// Reuse the coderd tracing middleware with a noop tracer (nil provider):
// it emits no spans or telemetry on the agent and only enriches the log
// context with client_session_id. Tracks the API, debug (for
// support-bundle correlation), and root routes.
tracing.Middleware(nil, []string{"/", "/api", "/api/**", "/debug/**"}, "agent"),
loggermw.Logger(a.logger, nil),
agentchat.Middleware,
a.toolCalls.Middleware,
)
r.Get("/", func(rw http.ResponseWriter, r *http.Request) {
httpapi.Write(r.Context(), rw, http.StatusOK, codersdk.Response{
Message: "Hello from the agent!",
})
})
r.Mount("/api/v0", a.filesAPI.Routes())
r.Mount("/api/v0/git", a.gitAPI.Routes())
r.Mount("/api/v0/processes", a.processAPI.Routes())
r.Mount("/api/v0/tool-calls", a.toolCalls.Routes())
r.Mount("/api/v0/desktop", a.desktopAPI.Routes())
r.Mount("/api/v0/mcp", a.mcpAPI.Routes())
r.Mount("/api/v0/context-config", a.contextConfigAPI.Routes())
if a.contextAPI != nil {
r.Mount("/api/v0/context", a.contextAPI.Routes())
}
if a.devcontainers {
r.Mount("/api/v0/containers", a.containerAPI.Routes())
} else if manifest := a.manifest.Load(); manifest != nil && manifest.ParentID != uuid.Nil {
r.HandleFunc("/api/v0/containers", func(w http.ResponseWriter, r *http.Request) {
httpapi.Write(r.Context(), w, http.StatusForbidden, codersdk.Response{
Message: "Dev Container feature not supported.",
Detail: "Dev Container integration inside other Dev Containers is explicitly not supported.",
})
})
} else {
r.HandleFunc("/api/v0/containers", func(w http.ResponseWriter, r *http.Request) {
httpapi.Write(r.Context(), w, http.StatusForbidden, codersdk.Response{
Message: "Dev Container feature not enabled.",
Detail: "To enable this feature, set CODER_AGENT_DEVCONTAINERS_ENABLE=true in your template.",
})
})
}
promHandler := PrometheusMetricsHandler(a.prometheusRegistry, a.logger)
r.Get("/api/v0/listening-ports", a.listeningPortsHandler.handler)
r.Get("/api/v0/netcheck", a.HandleNetcheck)
r.Get("/api/v0/tcp/{port}", upgradeListener.handler)
r.Get("/debug/logs", a.HandleHTTPDebugLogs)
r.Get("/debug/magicsock", a.HandleHTTPDebugMagicsock)
r.Get("/debug/magicsock/debug-logging/{state}", a.HandleHTTPMagicsockDebugLoggingState)
r.Get("/debug/manifest", a.HandleHTTPDebugManifest)
r.Get("/debug/prometheus", promHandler.ServeHTTP)
return r
}
type ListeningPortsGetter interface {
GetListeningPorts() ([]codersdk.WorkspaceAgentListeningPort, error)
}
type listeningPortsHandler struct {
// In production code, this is set to an osListeningPortsGetter, but it can be overridden for
// testing.
getter ListeningPortsGetter
ignorePorts map[int]string
}
// handler returns a list of listening ports. This is tested by coderd's
// TestWorkspaceAgentListeningPorts test.
func (lp *listeningPortsHandler) handler(rw http.ResponseWriter, r *http.Request) {
ports, err := lp.getter.GetListeningPorts()
if err != nil {
httpapi.Write(r.Context(), rw, http.StatusInternalServerError, codersdk.Response{
Message: "Could not scan for listening ports.",
Detail: err.Error(),
})
return
}
filteredPorts := make([]codersdk.WorkspaceAgentListeningPort, 0, len(ports))
for _, port := range ports {
if port.Port < workspacesdk.AgentMinimumListeningPort {
continue
}
// Ignore ports that we've been told to ignore.
if _, ok := lp.ignorePorts[int(port.Port)]; ok {
continue
}
filteredPorts = append(filteredPorts, port)
}
httpapi.Write(r.Context(), rw, http.StatusOK, codersdk.WorkspaceAgentListeningPortsResponse{
Ports: filteredPorts,
})
}