mirror of
https://github.com/coder/coder.git
synced 2026-06-03 04:58:23 +00:00
ee855f9618
Replace hardcoded paths for instruction files, skills, and MCP config
with
values read from `CODER_AGENT_EXP_*` environment variables. Template
authors
configure paths via the existing `coder_agent` `env` block. The agent
resolves `~`, relative, and absolute paths locally, then serves the
resolved config over `GET /api/v0/context-config`. `chatd` fetches this
once per workspace attach and falls back to today's defaults for older
agents.
All path env vars are comma-separated, allowing multiple directories:
| Env Var | Default | Controls |
|---|---|---|
| `CODER_AGENT_EXP_INSTRUCTIONS_DIRS` | `~/.coder` | Dirs containing the
instruction file |
| `CODER_AGENT_EXP_INSTRUCTIONS_FILE` | `AGENTS.md` | Instruction file
name |
| `CODER_AGENT_EXP_SKILLS_DIRS` | `.agents/skills` | Skills directories
|
| `CODER_AGENT_EXP_SKILL_META_FILE` | `SKILL.md` | Skill metadata file
name |
| `CODER_AGENT_EXP_MCP_CONFIG_FILES` | `.mcp.json` | MCP config files |
### Example
```hcl
resource "coder_agent" "main" {
os = "linux"
arch = "amd64"
env = {
CODER_AGENT_EXP_INSTRUCTIONS_DIRS = "/opt/company/agent-config,~/.coder"
CODER_AGENT_EXP_INSTRUCTIONS_FILE = "CLAUDE.md"
CODER_AGENT_EXP_SKILLS_DIRS = "/opt/company/ai-skills,.agents/skills"
CODER_AGENT_EXP_MCP_CONFIG_FILES = "/opt/company/mcp.json,.mcp.json"
}
}
```
<details>
<summary>Implementation Details</summary>
### Architecture
Follows the same pattern as MCP tool discovery:
agent resolves locally → exposes via HTTP → chatd consumes.
**Agent-side** (`agent/agentcontextconfig/`):
- `ResolvePath` / `ResolvePaths` handle `~`, relative, and absolute path
forms; returns `""` for relative paths when baseDir is empty
- `Config` reads env vars, falls back to defaults, resolves all paths
- `GET /api/v0/context-config` serves the resolved config as JSON
**chatd-side** (`coderd/x/chatd/`):
- Calls `conn.ContextConfig()` once on first workspace attach
- Falls back to hardcoded defaults on 404 (older agents)
- Iterates instruction dirs, skills dirs using resolved absolute paths
- `LSRelativityRoot` everywhere — no more home/root juggling
### Key design decisions
- **`EXP_` prefix**: env vars use `CODER_AGENT_EXP_*` to indicate
experimental status
- **Plural names**: comma-separated vars use plural names (`DIRS`,
`FILES`); single-value vars use singular (`FILE`)
- **Defaults in `workspacesdk`**: default constants live in
`codersdk/workspacesdk/` so both agent and server reference them without
cross-layer imports
- **`skillMetaFile` persistence**: stored on context-file parts via
`ContextFileSkillMetaFile` and restored on subsequent chat turns so
custom values survive across turns
- **Working dir dedup**: `slices.Contains` guard prevents reading the
same instruction file from both `InstructionsDirs` and the working
directory
- **MCP server dedup**: first-occurrence-wins dedup prevents leaking
duplicate connections from overlapping config files
- **ResolvePath safety**: returns `""` for relative paths when `baseDir`
is empty, so `ResolvePaths` filters them out
### Files changed
| File | Change |
|---|---|
| `agent/agentcontextconfig/` | New package — path resolution + HTTP
endpoint |
| `codersdk/workspacesdk/agentconn.go` | `ContextConfigResponse` type,
default constants, client method |
| `agent/agent.go` + `agent/api.go` | Wire up endpoint, pass config to
MCP |
| `agent/x/agentmcp/manager.go` | Accept `[]string` MCP config paths,
dedup by name |
| `coderd/x/chatd/chatd.go` | Fetch config, thread through, named
returns |
| `coderd/x/chatd/instruction.go` | Accept configurable dir + file name,
`skillMetaFileFromParts` |
| `coderd/x/chatd/chattool/skill.go` | Accept configurable dirs + meta
file |
| `codersdk/chats.go` | `ContextFileSkillMetaFile` field for persistence
|
### Test coverage
- `TestConfig` (4 cases): defaults, custom env vars, whitespace
trimming, comma-separated dirs
- `TestResolvePath` / `TestResolvePaths`: including empty baseDir edge
case
- `TestPersistInstructionFilesFallbackOnOlderAgent`: backward-compat
path when `ContextConfig` returns 404
- `TestChatMessagePartVariantTags`: updated exclusion list for new
internal field
### Backward compatibility
Older agents return 404 for the new endpoint. `chatd` catches this and
falls back to today's defaults via `readHomeInstructionFile` (using
`LSRelativityHome`). Existing workspaces work with no changes.
</details>
108 lines
3.5 KiB
Go
108 lines
3.5 KiB
Go
package agent
|
|
|
|
import (
|
|
"net/http"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/google/uuid"
|
|
|
|
"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() http.Handler {
|
|
r := chi.NewRouter()
|
|
r.Use(
|
|
httpmw.Recover(a.logger),
|
|
tracing.StatusWriterMiddleware,
|
|
loggermw.Logger(a.logger),
|
|
)
|
|
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/desktop", a.desktopAPI.Routes())
|
|
r.Mount("/api/v0/mcp", a.mcpAPI.Routes())
|
|
r.Mount("/api/v0/context-config", a.contextConfigAPI.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("/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,
|
|
})
|
|
}
|