mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: make agent context paths configurable via env vars (#23878)
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>
This commit is contained in:
@@ -61,6 +61,7 @@ type AgentConn interface {
|
||||
AwaitReachable(ctx context.Context) bool
|
||||
CallMCPTool(ctx context.Context, req CallMCPToolRequest) (CallMCPToolResponse, error)
|
||||
Close() error
|
||||
ContextConfig(ctx context.Context) (ContextConfigResponse, error)
|
||||
DebugLogs(ctx context.Context) ([]byte, error)
|
||||
DebugMagicsock(ctx context.Context) ([]byte, error)
|
||||
DebugManifest(ctx context.Context) ([]byte, error)
|
||||
@@ -946,6 +947,30 @@ type MCPToolInfo struct {
|
||||
Required []string `json:"required"`
|
||||
}
|
||||
|
||||
// Default values for context configuration. These are used
|
||||
// by the agent when env vars are unset and by the server as
|
||||
// fallbacks for older agents that don't support the
|
||||
// context-config endpoint.
|
||||
const (
|
||||
DefaultInstructionsDir = "~/.coder"
|
||||
DefaultInstructionsFile = "AGENTS.md"
|
||||
DefaultSkillsDir = ".agents/skills"
|
||||
DefaultSkillMetaFile = "SKILL.md"
|
||||
DefaultMCPConfigFile = ".mcp.json"
|
||||
)
|
||||
|
||||
// ContextConfigResponse is the response from the agent's
|
||||
// context configuration endpoint. Directory fields contain
|
||||
// fully resolved absolute paths. File name fields contain
|
||||
// basenames.
|
||||
type ContextConfigResponse struct {
|
||||
InstructionsDirs []string `json:"instructions_dirs"`
|
||||
InstructionsFile string `json:"instructions_file"`
|
||||
SkillsDirs []string `json:"skills_dirs"`
|
||||
SkillMetaFile string `json:"skill_meta_file"`
|
||||
MCPConfigFiles []string `json:"mcp_config_files"`
|
||||
}
|
||||
|
||||
// CallMCPToolRequest is the request body for proxying an MCP
|
||||
// tool call through the workspace agent.
|
||||
type CallMCPToolRequest struct {
|
||||
@@ -1018,6 +1043,23 @@ func (c *agentConn) ListMCPTools(ctx context.Context) (ListMCPToolsResponse, err
|
||||
return resp, json.NewDecoder(res.Body).Decode(&resp)
|
||||
}
|
||||
|
||||
// ContextConfig returns the resolved context configuration from
|
||||
// the workspace agent.
|
||||
func (c *agentConn) ContextConfig(ctx context.Context) (ContextConfigResponse, error) {
|
||||
ctx, span := tracing.StartSpan(ctx)
|
||||
defer span.End()
|
||||
res, err := c.apiRequest(ctx, http.MethodGet, "/api/v0/context-config", nil)
|
||||
if err != nil {
|
||||
return ContextConfigResponse{}, xerrors.Errorf("do request: %w", err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return ContextConfigResponse{}, codersdk.ReadBodyAsError(res)
|
||||
}
|
||||
var resp ContextConfigResponse
|
||||
return resp, json.NewDecoder(res.Body).Decode(&resp)
|
||||
}
|
||||
|
||||
// CallMCPTool proxies a tool call to an MCP server running in
|
||||
// the workspace.
|
||||
func (c *agentConn) CallMCPTool(ctx context.Context, req CallMCPToolRequest) (CallMCPToolResponse, error) {
|
||||
|
||||
@@ -17,18 +17,19 @@ import (
|
||||
reflect "reflect"
|
||||
time "time"
|
||||
|
||||
slog "cdr.dev/slog/v3"
|
||||
codersdk "github.com/coder/coder/v2/codersdk"
|
||||
healthsdk "github.com/coder/coder/v2/codersdk/healthsdk"
|
||||
workspacesdk "github.com/coder/coder/v2/codersdk/workspacesdk"
|
||||
wsjson "github.com/coder/coder/v2/codersdk/wsjson"
|
||||
tailnet "github.com/coder/coder/v2/tailnet"
|
||||
uuid "github.com/google/uuid"
|
||||
gomock "go.uber.org/mock/gomock"
|
||||
ssh "golang.org/x/crypto/ssh"
|
||||
gonet "gvisor.dev/gvisor/pkg/tcpip/adapters/gonet"
|
||||
ipnstate "tailscale.com/ipn/ipnstate"
|
||||
speedtest "tailscale.com/net/speedtest"
|
||||
|
||||
slog "cdr.dev/slog/v3"
|
||||
codersdk "github.com/coder/coder/v2/codersdk"
|
||||
healthsdk "github.com/coder/coder/v2/codersdk/healthsdk"
|
||||
workspacesdk "github.com/coder/coder/v2/codersdk/workspacesdk"
|
||||
wsjson "github.com/coder/coder/v2/codersdk/wsjson"
|
||||
tailnet "github.com/coder/coder/v2/tailnet"
|
||||
)
|
||||
|
||||
// MockAgentConn is a mock of AgentConn interface.
|
||||
@@ -113,6 +114,21 @@ func (mr *MockAgentConnMockRecorder) ConnectDesktopVNC(ctx any) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConnectDesktopVNC", reflect.TypeOf((*MockAgentConn)(nil).ConnectDesktopVNC), ctx)
|
||||
}
|
||||
|
||||
// ContextConfig mocks base method.
|
||||
func (m *MockAgentConn) ContextConfig(ctx context.Context) (workspacesdk.ContextConfigResponse, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "ContextConfig", ctx)
|
||||
ret0, _ := ret[0].(workspacesdk.ContextConfigResponse)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// ContextConfig indicates an expected call of ContextConfig.
|
||||
func (mr *MockAgentConnMockRecorder) ContextConfig(ctx any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContextConfig", reflect.TypeOf((*MockAgentConn)(nil).ContextConfig), ctx)
|
||||
}
|
||||
|
||||
// DebugLogs mocks base method.
|
||||
func (m *MockAgentConn) DebugLogs(ctx context.Context) ([]byte, error) {
|
||||
m.ctrl.T.Helper()
|
||||
|
||||
Reference in New Issue
Block a user