mirror of
https://github.com/coder/coder.git
synced 2026-06-06 14:38:23 +00:00
fb6bf3a568
## Problem The `agentproc` process manager spawns processes with only `os.Environ()`, missing agent-level environment variables like `GIT_ASKPASS`, `CODER_*`, and `GIT_SSH_COMMAND` that are injected by the agent's `updateCommandEnv` function. This means processes started through the HTTP process API (used by chat tools) cannot authenticate git operations via the Coder gitaskpass helper. By contrast, SSH sessions get the full agent environment because the SSH server calls `updateCommandEnv` via its `UpdateEnv` config hook. ## Fix Wire the agent's `updateCommandEnv` hook into the process manager so all spawned processes receive the full agent environment. The hook is: - Passed as a parameter through `NewAPI` → `newManager` - Called in `manager.start()` with `os.Environ()` as the base, producing the same enriched env that SSH sessions get - Gracefully falls back to `os.Environ()` if the hook returns an error Request-level env vars (`req.Env`, set by chat tools) are still appended last and take precedence. ## Changes - `agent/agentproc/process.go`: Add `updateEnv` field to manager, call it when building process env - `agent/agentproc/api.go`: Accept `updateEnv` parameter in `NewAPI` - `agent/agent.go`: Pass `a.updateCommandEnv` when creating the process API - `agent/agentproc/api_test.go`: Add `UpdateEnvHook` and `UpdateEnvHookOverriddenByReqEnv` tests Co-authored-by: Coder <coder@coder.com>
176 lines
4.4 KiB
Go
176 lines
4.4 KiB
Go
package agentproc
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
|
|
"cdr.dev/slog/v3"
|
|
"github.com/coder/coder/v2/agent/agentexec"
|
|
"github.com/coder/coder/v2/coderd/httpapi"
|
|
"github.com/coder/coder/v2/codersdk"
|
|
"github.com/coder/coder/v2/codersdk/workspacesdk"
|
|
)
|
|
|
|
// API exposes process-related operations through the agent.
|
|
type API struct {
|
|
logger slog.Logger
|
|
manager *manager
|
|
}
|
|
|
|
// NewAPI creates a new process API handler.
|
|
func NewAPI(logger slog.Logger, execer agentexec.Execer, updateEnv func(current []string) (updated []string, err error)) *API {
|
|
return &API{
|
|
logger: logger,
|
|
manager: newManager(logger, execer, updateEnv),
|
|
}
|
|
}
|
|
|
|
// Close shuts down the process manager, killing all running
|
|
// processes.
|
|
func (api *API) Close() error {
|
|
return api.manager.Close()
|
|
}
|
|
|
|
// Routes returns the HTTP handler for process-related routes.
|
|
func (api *API) Routes() http.Handler {
|
|
r := chi.NewRouter()
|
|
r.Post("/start", api.handleStartProcess)
|
|
r.Get("/list", api.handleListProcesses)
|
|
r.Get("/{id}/output", api.handleProcessOutput)
|
|
r.Post("/{id}/signal", api.handleSignalProcess)
|
|
return r
|
|
}
|
|
|
|
// handleStartProcess starts a new process.
|
|
func (api *API) handleStartProcess(rw http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
|
|
var req workspacesdk.StartProcessRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
|
Message: "Request body must be valid JSON.",
|
|
Detail: err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
if req.Command == "" {
|
|
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
|
Message: "Command is required.",
|
|
})
|
|
return
|
|
}
|
|
|
|
proc, err := api.manager.start(req)
|
|
if err != nil {
|
|
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
|
Message: "Failed to start process.",
|
|
Detail: err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
httpapi.Write(ctx, rw, http.StatusOK, workspacesdk.StartProcessResponse{
|
|
ID: proc.id,
|
|
Started: true,
|
|
})
|
|
}
|
|
|
|
// handleListProcesses lists all tracked processes.
|
|
func (api *API) handleListProcesses(rw http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
|
|
infos := api.manager.list()
|
|
httpapi.Write(ctx, rw, http.StatusOK, workspacesdk.ListProcessesResponse{
|
|
Processes: infos,
|
|
})
|
|
}
|
|
|
|
// handleProcessOutput returns the output of a process.
|
|
func (api *API) handleProcessOutput(rw http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
|
|
id := chi.URLParam(r, "id")
|
|
proc, ok := api.manager.get(id)
|
|
if !ok {
|
|
httpapi.Write(ctx, rw, http.StatusNotFound, codersdk.Response{
|
|
Message: fmt.Sprintf("Process %q not found.", id),
|
|
})
|
|
return
|
|
}
|
|
|
|
output, truncated := proc.output()
|
|
info := proc.info()
|
|
|
|
httpapi.Write(ctx, rw, http.StatusOK, workspacesdk.ProcessOutputResponse{
|
|
Output: output,
|
|
Truncated: truncated,
|
|
Running: info.Running,
|
|
ExitCode: info.ExitCode,
|
|
})
|
|
}
|
|
|
|
// handleSignalProcess sends a signal to a running process.
|
|
func (api *API) handleSignalProcess(rw http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
|
|
id := chi.URLParam(r, "id")
|
|
|
|
var req workspacesdk.SignalProcessRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
|
Message: "Request body must be valid JSON.",
|
|
Detail: err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
if req.Signal == "" {
|
|
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
|
Message: "Signal is required.",
|
|
})
|
|
return
|
|
}
|
|
|
|
if req.Signal != "kill" && req.Signal != "terminate" {
|
|
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
|
Message: fmt.Sprintf(
|
|
"Unsupported signal %q. Use \"kill\" or \"terminate\".",
|
|
req.Signal,
|
|
),
|
|
})
|
|
return
|
|
}
|
|
|
|
if err := api.manager.signal(id, req.Signal); err != nil {
|
|
switch {
|
|
case errors.Is(err, errProcessNotFound):
|
|
httpapi.Write(ctx, rw, http.StatusNotFound, codersdk.Response{
|
|
Message: fmt.Sprintf("Process %q not found.", id),
|
|
})
|
|
case errors.Is(err, errProcessNotRunning):
|
|
httpapi.Write(ctx, rw, http.StatusConflict, codersdk.Response{
|
|
Message: fmt.Sprintf(
|
|
"Process %q is not running.", id,
|
|
),
|
|
})
|
|
default:
|
|
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
|
Message: "Failed to signal process.",
|
|
Detail: err.Error(),
|
|
})
|
|
}
|
|
return
|
|
}
|
|
|
|
httpapi.Write(ctx, rw, http.StatusOK, codersdk.Response{
|
|
Message: fmt.Sprintf(
|
|
"Signal %q sent to process %q.", req.Signal, id,
|
|
),
|
|
})
|
|
}
|