Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d4c7cb2021 | |||
| 25400fedca | |||
| 82bb833099 | |||
| 61beb7bfa8 | |||
| b4be5bcfed | |||
| ceaba0778e | |||
| e24cc5e6da | |||
| 259dee2ea8 | |||
| 8e0516a19c | |||
| 770fdb377c |
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"ignores": ["PLAN.md"],
|
||||
}
|
||||
@@ -90,7 +90,6 @@ func TestExpRpty(t *testing.T) {
|
||||
wantLabel := "coder.devcontainers.TestExpRpty.Container"
|
||||
|
||||
client, workspace, agentToken := setupWorkspaceForAgent(t)
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
pool, err := dockertest.NewPool("")
|
||||
require.NoError(t, err, "Could not connect to docker")
|
||||
ct, err := pool.RunWithOptions(&dockertest.RunOptions{
|
||||
@@ -128,14 +127,15 @@ func TestExpRpty(t *testing.T) {
|
||||
clitest.SetupConfig(t, client, root)
|
||||
pty := ptytest.New(t).Attach(inv)
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
cmdDone := tGo(t, func() {
|
||||
err := inv.WithContext(ctx).Run()
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
pty.ExpectMatch(" #")
|
||||
pty.ExpectMatchContext(ctx, " #")
|
||||
pty.WriteLine("hostname")
|
||||
pty.ExpectMatch(ct.Container.Config.Hostname)
|
||||
pty.ExpectMatchContext(ctx, ct.Container.Config.Hostname)
|
||||
pty.WriteLine("exit")
|
||||
<-cmdDone
|
||||
})
|
||||
|
||||
+3
-3
@@ -2052,7 +2052,6 @@ func TestSSH_Container(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client, workspace, agentToken := setupWorkspaceForAgent(t)
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
pool, err := dockertest.NewPool("")
|
||||
require.NoError(t, err, "Could not connect to docker")
|
||||
ct, err := pool.RunWithOptions(&dockertest.RunOptions{
|
||||
@@ -2087,14 +2086,15 @@ func TestSSH_Container(t *testing.T) {
|
||||
clitest.SetupConfig(t, client, root)
|
||||
ptty := ptytest.New(t).Attach(inv)
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
cmdDone := tGo(t, func() {
|
||||
err := inv.WithContext(ctx).Run()
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
ptty.ExpectMatch(" #")
|
||||
ptty.ExpectMatchContext(ctx, " #")
|
||||
ptty.WriteLine("hostname")
|
||||
ptty.ExpectMatch(ct.Container.Config.Hostname)
|
||||
ptty.ExpectMatchContext(ctx, ct.Container.Config.Hostname)
|
||||
ptty.WriteLine("exit")
|
||||
<-cmdDone
|
||||
})
|
||||
|
||||
+11
@@ -118,12 +118,23 @@ AI BRIDGE OPTIONS:
|
||||
requests (requires the "oauth2" and "mcp-server-http" experiments to
|
||||
be enabled).
|
||||
|
||||
--aibridge-max-concurrency int, $CODER_AIBRIDGE_MAX_CONCURRENCY (default: 0)
|
||||
Maximum number of concurrent AI Bridge requests. Set to 0 to disable
|
||||
(unlimited).
|
||||
|
||||
--aibridge-openai-base-url string, $CODER_AIBRIDGE_OPENAI_BASE_URL (default: https://api.openai.com/v1/)
|
||||
The base URL of the OpenAI API.
|
||||
|
||||
--aibridge-openai-key string, $CODER_AIBRIDGE_OPENAI_KEY
|
||||
The key to authenticate against the OpenAI API.
|
||||
|
||||
--aibridge-rate-limit int, $CODER_AIBRIDGE_RATE_LIMIT (default: 0)
|
||||
Maximum number of AI Bridge requests per rate window. Set to 0 to
|
||||
disable rate limiting.
|
||||
|
||||
--aibridge-rate-window duration, $CODER_AIBRIDGE_RATE_WINDOW (default: 1m)
|
||||
Duration of the rate limiting window for AI Bridge requests.
|
||||
|
||||
CLIENT OPTIONS:
|
||||
These options change the behavior of how clients interact with the Coder.
|
||||
Clients include the Coder CLI, Coder Desktop, IDE extensions, and the web UI.
|
||||
|
||||
+11
@@ -742,6 +742,17 @@ aibridge:
|
||||
# (token, prompt, tool use).
|
||||
# (default: 60d, type: duration)
|
||||
retention: 1440h0m0s
|
||||
# Maximum number of concurrent AI Bridge requests. Set to 0 to disable
|
||||
# (unlimited).
|
||||
# (default: 0, type: int)
|
||||
max_concurrency: 0
|
||||
# Maximum number of AI Bridge requests per rate window. Set to 0 to disable rate
|
||||
# limiting.
|
||||
# (default: 0, type: int)
|
||||
rate_limit: 0
|
||||
# Duration of the rate limiting window for AI Bridge requests.
|
||||
# (default: 1m, type: duration)
|
||||
rate_window: 1m0s
|
||||
# Configure data retention policies for various database tables. Retention
|
||||
# policies automatically purge old data to reduce database size and improve
|
||||
# performance. Setting a retention duration to 0 disables automatic purging for
|
||||
|
||||
Generated
+12
-2
@@ -1800,7 +1800,7 @@ const docTemplate = `{
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Organizations"
|
||||
"Enterprise"
|
||||
],
|
||||
"summary": "Add new license",
|
||||
"operationId": "add-new-license",
|
||||
@@ -1836,7 +1836,7 @@ const docTemplate = `{
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Organizations"
|
||||
"Enterprise"
|
||||
],
|
||||
"summary": "Update license entitlements",
|
||||
"operationId": "update-license-entitlements",
|
||||
@@ -11877,9 +11877,19 @@ const docTemplate = `{
|
||||
"inject_coder_mcp_tools": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"max_concurrency": {
|
||||
"description": "Overload protection settings.",
|
||||
"type": "integer"
|
||||
},
|
||||
"openai": {
|
||||
"$ref": "#/definitions/codersdk.AIBridgeOpenAIConfig"
|
||||
},
|
||||
"rate_limit": {
|
||||
"type": "integer"
|
||||
},
|
||||
"rate_window": {
|
||||
"type": "integer"
|
||||
},
|
||||
"retention": {
|
||||
"type": "integer"
|
||||
}
|
||||
|
||||
Generated
+12
-2
@@ -1570,7 +1570,7 @@
|
||||
],
|
||||
"consumes": ["application/json"],
|
||||
"produces": ["application/json"],
|
||||
"tags": ["Organizations"],
|
||||
"tags": ["Enterprise"],
|
||||
"summary": "Add new license",
|
||||
"operationId": "add-new-license",
|
||||
"parameters": [
|
||||
@@ -1602,7 +1602,7 @@
|
||||
}
|
||||
],
|
||||
"produces": ["application/json"],
|
||||
"tags": ["Organizations"],
|
||||
"tags": ["Enterprise"],
|
||||
"summary": "Update license entitlements",
|
||||
"operationId": "update-license-entitlements",
|
||||
"responses": {
|
||||
@@ -10543,9 +10543,19 @@
|
||||
"inject_coder_mcp_tools": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"max_concurrency": {
|
||||
"description": "Overload protection settings.",
|
||||
"type": "integer"
|
||||
},
|
||||
"openai": {
|
||||
"$ref": "#/definitions/codersdk.AIBridgeOpenAIConfig"
|
||||
},
|
||||
"rate_limit": {
|
||||
"type": "integer"
|
||||
},
|
||||
"rate_window": {
|
||||
"type": "integer"
|
||||
},
|
||||
"retention": {
|
||||
"type": "integer"
|
||||
}
|
||||
|
||||
@@ -3391,6 +3391,37 @@ Write out the current server config as YAML to stdout.`,
|
||||
YAML: "retention",
|
||||
Annotations: serpent.Annotations{}.Mark(annotationFormatDuration, "true"),
|
||||
},
|
||||
{
|
||||
Name: "AI Bridge Max Concurrency",
|
||||
Description: "Maximum number of concurrent AI Bridge requests. Set to 0 to disable (unlimited).",
|
||||
Flag: "aibridge-max-concurrency",
|
||||
Env: "CODER_AIBRIDGE_MAX_CONCURRENCY",
|
||||
Value: &c.AI.BridgeConfig.MaxConcurrency,
|
||||
Default: "0",
|
||||
Group: &deploymentGroupAIBridge,
|
||||
YAML: "max_concurrency",
|
||||
},
|
||||
{
|
||||
Name: "AI Bridge Rate Limit",
|
||||
Description: "Maximum number of AI Bridge requests per rate window. Set to 0 to disable rate limiting.",
|
||||
Flag: "aibridge-rate-limit",
|
||||
Env: "CODER_AIBRIDGE_RATE_LIMIT",
|
||||
Value: &c.AI.BridgeConfig.RateLimit,
|
||||
Default: "0",
|
||||
Group: &deploymentGroupAIBridge,
|
||||
YAML: "rate_limit",
|
||||
},
|
||||
{
|
||||
Name: "AI Bridge Rate Window",
|
||||
Description: "Duration of the rate limiting window for AI Bridge requests.",
|
||||
Flag: "aibridge-rate-window",
|
||||
Env: "CODER_AIBRIDGE_RATE_WINDOW",
|
||||
Value: &c.AI.BridgeConfig.RateWindow,
|
||||
Default: "1m",
|
||||
Group: &deploymentGroupAIBridge,
|
||||
YAML: "rate_window",
|
||||
Annotations: serpent.Annotations{}.Mark(annotationFormatDuration, "true"),
|
||||
},
|
||||
// Retention settings
|
||||
{
|
||||
Name: "Audit Logs Retention",
|
||||
@@ -3461,6 +3492,10 @@ type AIBridgeConfig struct {
|
||||
Bedrock AIBridgeBedrockConfig `json:"bedrock" typescript:",notnull"`
|
||||
InjectCoderMCPTools serpent.Bool `json:"inject_coder_mcp_tools" typescript:",notnull"`
|
||||
Retention serpent.Duration `json:"retention" typescript:",notnull"`
|
||||
// Overload protection settings.
|
||||
MaxConcurrency serpent.Int64 `json:"max_concurrency" typescript:",notnull"`
|
||||
RateLimit serpent.Int64 `json:"rate_limit" typescript:",notnull"`
|
||||
RateWindow serpent.Duration `json:"rate_window" typescript:",notnull"`
|
||||
}
|
||||
|
||||
type AIBridgeOpenAIConfig struct {
|
||||
|
||||
@@ -4,3 +4,10 @@
|
||||
# Redirect old offline anchor fragments to new airgap anchors
|
||||
/install/offline#offline-docs /install/airgap#airgap-docs 301
|
||||
/install/offline#offline-container-images /install/airgap#airgap-container-images 301
|
||||
|
||||
# Redirect old devcontainers folder to envbuilder
|
||||
/admin/templates/managing-templates/devcontainers /admin/templates/managing-templates/envbuilder 301
|
||||
/admin/templates/managing-templates/devcontainers/index /admin/templates/managing-templates/envbuilder 301
|
||||
/admin/templates/managing-templates/devcontainers/add-devcontainer /admin/templates/managing-templates/envbuilder/add-envbuilder 301
|
||||
/admin/templates/managing-templates/devcontainers/devcontainer-security-caching /admin/templates/managing-templates/envbuilder/envbuilder-security-caching 301
|
||||
/admin/templates/managing-templates/devcontainers/devcontainer-releases-known-issues /admin/templates/managing-templates/envbuilder/envbuilder-releases-known-issues 301
|
||||
|
||||
@@ -510,9 +510,9 @@ resource "kubernetes_pod" "workspace" {
|
||||
## Get help
|
||||
|
||||
- **Examples**: Review real-world examples from the [official Coder templates](https://registry.coder.com/contributors/coder?tab=templates):
|
||||
- [AWS EC2 (Devcontainer)](https://registry.coder.com/templates/aws-devcontainer) - AWS EC2 VMs with devcontainer support
|
||||
- [Docker (Devcontainer)](https://registry.coder.com/templates/docker-devcontainer) - Envbuilder containers with dev container support
|
||||
- [Kubernetes (Devcontainer)](https://registry.coder.com/templates/kubernetes-devcontainer) - Envbuilder pods on Kubernetes
|
||||
- [AWS EC2 (Devcontainer)](https://registry.coder.com/templates/aws-devcontainer) - AWS EC2 VMs with Envbuilder
|
||||
- [Docker (Devcontainer)](https://registry.coder.com/templates/docker-devcontainer) - Docker-in-Docker with Dev Containers integration
|
||||
- [Kubernetes (Devcontainer)](https://registry.coder.com/templates/kubernetes-devcontainer) - Kubernetes pods with Envbuilder
|
||||
- [Docker Containers](https://registry.coder.com/templates/docker) - Basic Docker container workspaces
|
||||
- [AWS EC2 (Linux)](https://registry.coder.com/templates/aws-linux) - AWS EC2 VMs for Linux development
|
||||
- [Google Compute Engine (Linux)](https://registry.coder.com/templates/gcp-vm-container) - GCP VM instances
|
||||
|
||||
+1
-1
@@ -52,7 +52,7 @@ For any information not strictly contained in these sections, check out our
|
||||
### Development containers (dev containers)
|
||||
|
||||
- A
|
||||
[Development Container](./templates/managing-templates/devcontainers/index.md)
|
||||
[Development Container](./templates/extending-templates/devcontainers.md)
|
||||
is an open-source specification for defining development environments (called
|
||||
dev containers). It is generally stored in VCS alongside associated source
|
||||
code. It can reference an existing base image, or a custom Dockerfile that
|
||||
|
||||
@@ -25,7 +25,9 @@ This is the recommended approach for most use cases.
|
||||
|
||||
### Project Discovery
|
||||
|
||||
Enable automatic discovery of Dev Containers in Git repositories. Project discovery automatically scans Git repositories for `.devcontainer/devcontainer.json` or `.devcontainer.json` files and surfaces them in the Coder UI. See the [Environment Variables](#environment-variables) section for detailed configuration options.
|
||||
Alternatively, enable automatic discovery of Dev Containers in Git repositories.
|
||||
The agent scans for `devcontainer.json` files and surfaces them in the Coder UI.
|
||||
See [Environment Variables](#environment-variables) for configuration options.
|
||||
|
||||
## Install the Dev Containers CLI
|
||||
|
||||
@@ -36,7 +38,7 @@ to ensure the `@devcontainers/cli` is installed in your workspace:
|
||||
```terraform
|
||||
module "devcontainers-cli" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "dev.registry.coder.com/modules/devcontainers-cli/coder"
|
||||
source = "registry.coder.com/coder/devcontainers-cli/coder"
|
||||
agent_id = coder_agent.dev.id
|
||||
}
|
||||
```
|
||||
@@ -72,6 +74,10 @@ resource "coder_devcontainer" "my-repository" {
|
||||
> module to ensure your repository is cloned into the workspace folder and ready
|
||||
> for automatic startup.
|
||||
|
||||
For multi-repo workspaces, define multiple `coder_devcontainer` resources, each
|
||||
pointing to a different repository. Each one runs as a separate sub-agent with
|
||||
its own terminal and apps in the dashboard.
|
||||
|
||||
## Enable Dev Containers Integration
|
||||
|
||||
Dev Containers integration is **enabled by default** in Coder 2.24.0 and later.
|
||||
@@ -119,15 +125,17 @@ by setting this to `false`.
|
||||
|
||||
Enables automatic discovery of Dev Containers in Git repositories.
|
||||
|
||||
When enabled, the agent will:
|
||||
When enabled, the agent scans the configured working directory (set via the
|
||||
`directory` attribute in `coder_agent`, typically the user's home directory) for
|
||||
Git repositories. If the directory itself is a Git repository, it searches that
|
||||
project. Otherwise, it searches immediate subdirectories for Git repositories.
|
||||
|
||||
- Scan the agent directory for Git repositories
|
||||
- Look for `.devcontainer/devcontainer.json` or `.devcontainer.json` files
|
||||
- Surface discovered Dev Containers automatically in the Coder UI
|
||||
- Respect `.gitignore` patterns during discovery
|
||||
For each repository found, the agent looks for `devcontainer.json` files in the
|
||||
[standard locations](../../../user-guides/devcontainers/index.md#add-a-devcontainerjson)
|
||||
and surfaces discovered Dev Containers in the Coder UI. Discovery respects
|
||||
`.gitignore` patterns.
|
||||
|
||||
You can disable automatic discovery by setting this to `false` if you prefer to
|
||||
use only the `coder_devcontainer` resource for explicit configuration.
|
||||
Set to `false` if you prefer explicit configuration via `coder_devcontainer`.
|
||||
|
||||
### CODER_AGENT_DEVCONTAINERS_DISCOVERY_AUTOSTART_ENABLE
|
||||
|
||||
@@ -142,67 +150,33 @@ always auto-start regardless of this setting.
|
||||
|
||||
## Per-Container Customizations
|
||||
|
||||
Individual Dev Containers can be customized using the `customizations.coder` block
|
||||
in your `devcontainer.json` file. These customizations allow you to control
|
||||
container-specific behavior without modifying your template.
|
||||
> [!NOTE]
|
||||
>
|
||||
> Dev container sub-agents are created dynamically after workspace provisioning,
|
||||
> so Terraform resources like
|
||||
> [`coder_script`](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/script)
|
||||
> and [`coder_app`](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/app)
|
||||
> cannot currently be attached to them. Modules from the
|
||||
> [Coder registry](https://registry.coder.com) that depend on these resources
|
||||
> are also not currently supported for sub-agents.
|
||||
>
|
||||
> To add tools to dev containers, use
|
||||
> [dev container features](../../../user-guides/devcontainers/working-with-dev-containers.md#dev-container-features).
|
||||
> For Coder-specific apps, use the
|
||||
> [`apps` customization](../../../user-guides/devcontainers/customizing-dev-containers.md#custom-apps).
|
||||
|
||||
### Ignore Specific Containers
|
||||
Developers can customize individual dev containers using the `customizations.coder`
|
||||
block in their `devcontainer.json` file. Available options include:
|
||||
|
||||
Use the `ignore` option to hide a Dev Container from Coder completely:
|
||||
- `ignore` — Hide a dev container from Coder completely
|
||||
- `autoStart` — Control whether the container starts automatically (requires
|
||||
`CODER_AGENT_DEVCONTAINERS_DISCOVERY_AUTOSTART_ENABLE` to be enabled)
|
||||
- `name` — Set a custom agent name
|
||||
- `displayApps` — Control which built-in apps appear
|
||||
- `apps` — Define custom applications
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "My Dev Container",
|
||||
"image": "mcr.microsoft.com/devcontainers/base:ubuntu",
|
||||
"customizations": {
|
||||
"coder": {
|
||||
"ignore": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
When `ignore` is set to `true`:
|
||||
|
||||
- The Dev Container won't appear in the Coder UI
|
||||
- Coder won't manage or monitor the container
|
||||
|
||||
This is useful when you have Dev Containers in your repository that you don't
|
||||
want Coder to manage.
|
||||
|
||||
### Per-Container Auto-Start
|
||||
|
||||
Control whether individual Dev Containers should auto-start using the
|
||||
`autoStart` option:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "My Dev Container",
|
||||
"image": "mcr.microsoft.com/devcontainers/base:ubuntu",
|
||||
"customizations": {
|
||||
"coder": {
|
||||
"autoStart": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Important**: The `autoStart` option only applies when global auto-start is
|
||||
enabled via `CODER_AGENT_DEVCONTAINERS_DISCOVERY_AUTOSTART_ENABLE=true`. If
|
||||
the global setting is disabled, containers won't auto-start regardless of this
|
||||
setting.
|
||||
|
||||
When `autoStart` is set to `true`:
|
||||
|
||||
- The Dev Container automatically builds and starts during workspace
|
||||
initialization
|
||||
- Works on a per-container basis (you can enable it for some containers but not
|
||||
others)
|
||||
|
||||
When `autoStart` is set to `false` or omitted:
|
||||
|
||||
- The Dev Container is discovered and shown in the UI
|
||||
- Users must manually start it via the UI
|
||||
For the full reference, see
|
||||
[Customizing dev containers](../../../user-guides/devcontainers/customizing-dev-containers.md).
|
||||
|
||||
## Complete Template Example
|
||||
|
||||
@@ -232,7 +206,7 @@ resource "coder_agent" "dev" {
|
||||
|
||||
module "devcontainers-cli" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "dev.registry.coder.com/modules/devcontainers-cli/coder"
|
||||
source = "registry.coder.com/coder/devcontainers-cli/coder"
|
||||
agent_id = coder_agent.dev.id
|
||||
}
|
||||
|
||||
@@ -243,9 +217,10 @@ resource "coder_devcontainer" "my-repository" {
|
||||
}
|
||||
```
|
||||
|
||||
### Alternative: Project Discovery Mode
|
||||
### Alternative: Project Discovery with Autostart
|
||||
|
||||
You can enable automatic starting of discovered Dev Containers:
|
||||
By default, discovered containers appear in the dashboard but developers must
|
||||
manually start them. To have them start automatically, enable autostart:
|
||||
|
||||
```terraform
|
||||
resource "docker_container" "workspace" {
|
||||
@@ -261,11 +236,11 @@ resource "docker_container" "workspace" {
|
||||
}
|
||||
```
|
||||
|
||||
With this configuration:
|
||||
With autostart enabled:
|
||||
|
||||
- Project discovery is enabled (default behavior)
|
||||
- Discovered containers are automatically started (via the env var)
|
||||
- The `coder_devcontainer` resource is **not** required
|
||||
- Discovered containers automatically build and start during workspace
|
||||
initialization
|
||||
- The `coder_devcontainer` resource is not required
|
||||
- Developers can work with multiple projects seamlessly
|
||||
|
||||
> [!NOTE]
|
||||
@@ -273,8 +248,16 @@ With this configuration:
|
||||
> When using project discovery, you still need to install the devcontainers CLI
|
||||
> using the module or in your base image.
|
||||
|
||||
## Example Template
|
||||
|
||||
The [Docker (Dev Containers)](https://github.com/coder/coder/tree/main/examples/templates/docker-devcontainer)
|
||||
starter template demonstrates Dev Containers integration using Docker-in-Docker.
|
||||
It includes the `devcontainers-cli` module, `git-clone` module, and the
|
||||
`coder_devcontainer` resource.
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [Dev Containers Integration](../../../user-guides/devcontainers/index.md)
|
||||
- [Customizing Dev Containers](../../../user-guides/devcontainers/customizing-dev-containers.md)
|
||||
- [Working with Dev Containers](../../../user-guides/devcontainers/working-with-dev-containers.md)
|
||||
- [Troubleshooting Dev Containers](../../../user-guides/devcontainers/troubleshooting-dev-containers.md)
|
||||
|
||||
@@ -48,11 +48,10 @@ needs of different teams.
|
||||
|
||||
- [Image management](./managing-templates/image-management.md): Learn how to
|
||||
create and publish images for use within Coder workspaces & templates.
|
||||
- [Dev Container support](./managing-templates/devcontainers/index.md): Enable
|
||||
dev containers to allow teams to bring their own tools into Coder workspaces.
|
||||
- [Early Access Dev Containers](../../user-guides/devcontainers/index.md): Try our
|
||||
new direct devcontainers integration (distinct from Envbuilder-based
|
||||
approach).
|
||||
- [Dev Containers integration](./extending-templates/devcontainers.md): Enable
|
||||
native dev containers support using `@devcontainers/cli` and Docker.
|
||||
- [Envbuilder](./managing-templates/envbuilder/index.md): Alternative approach
|
||||
for environments without Docker access.
|
||||
- [Template hardening](./extending-templates/resource-persistence.md#-bulletproofing):
|
||||
Configure your template to prevent certain resources from being destroyed
|
||||
(e.g. user disks).
|
||||
|
||||
+6
-7
@@ -1,10 +1,9 @@
|
||||
# Add a dev container template to Coder
|
||||
# Add an Envbuilder template
|
||||
|
||||
A Coder administrator adds a dev container-compatible template to Coder
|
||||
(Envbuilder). This allows the template to prompt for the developer for their dev
|
||||
container repository's URL as a
|
||||
[parameter](../../extending-templates/parameters.md) when they create their
|
||||
workspace. Envbuilder clones the repo and builds a container from the
|
||||
A Coder administrator adds an Envbuilder-compatible template to Coder. This
|
||||
allows the template to prompt the developer for their dev container repository's
|
||||
URL as a [parameter](../../extending-templates/parameters.md) when they create
|
||||
their workspace. Envbuilder clones the repo and builds a container from the
|
||||
`devcontainer.json` specified in the repo.
|
||||
|
||||
You can create template files through the Coder dashboard, CLI, or you can
|
||||
@@ -143,4 +142,4 @@ Lifecycle scripts are managed by project developers.
|
||||
|
||||
## Next steps
|
||||
|
||||
- [Dev container security and caching](./devcontainer-security-caching.md)
|
||||
- [Envbuilder security and caching](./envbuilder-security-caching.md)
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
# Dev container releases and known issues
|
||||
# Envbuilder releases and known issues
|
||||
|
||||
## Release channels
|
||||
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
# Dev container security and caching
|
||||
# Envbuilder security and caching
|
||||
|
||||
Ensure Envbuilder can only pull pre-approved images and artifacts by configuring
|
||||
it with your existing HTTP proxies, firewalls, and artifact managers.
|
||||
@@ -26,7 +26,7 @@ of caching:
|
||||
|
||||
- Caches the entire image, skipping the build process completely (except for
|
||||
post-build
|
||||
[lifecycle scripts](./add-devcontainer.md#dev-container-lifecycle-scripts)).
|
||||
[lifecycle scripts](./add-envbuilder.md#dev-container-lifecycle-scripts)).
|
||||
|
||||
Note that caching requires push access to a registry, and may require approval
|
||||
from relevant infrastructure team(s).
|
||||
@@ -62,5 +62,5 @@ You may also wish to consult a
|
||||
|
||||
## Next steps
|
||||
|
||||
- [Dev container releases and known issues](./devcontainer-releases-known-issues.md)
|
||||
- [Envbuilder releases and known issues](./envbuilder-releases-known-issues.md)
|
||||
- [Dotfiles](../../../../user-guides/workspace-dotfiles.md)
|
||||
+15
-6
@@ -1,9 +1,18 @@
|
||||
# Dev containers
|
||||
# Envbuilder
|
||||
|
||||
A Development Container is an
|
||||
[open-source specification](https://containers.dev/implementors/spec/) for
|
||||
defining containerized development environments which are also called
|
||||
development containers (dev containers).
|
||||
Envbuilder is an open-source tool that builds development environments from
|
||||
[dev container](https://containers.dev/implementors/spec/) configuration files.
|
||||
Unlike the [native Dev Containers integration](../../extending-templates/devcontainers.md),
|
||||
Envbuilder transforms the workspace image itself rather than running containers
|
||||
inside the workspace.
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> For most use cases, we recommend the
|
||||
> [native Dev Containers integration](../../extending-templates/devcontainers.md),
|
||||
> which uses the standard `@devcontainers/cli` and Docker. Envbuilder is an
|
||||
> alternative for environments where Docker is not available or for
|
||||
> administrator-controlled dev container workflows.
|
||||
|
||||
Dev containers provide developers with increased autonomy and control over their
|
||||
Coder cloud development environments.
|
||||
@@ -119,4 +128,4 @@ of the Coder control plane and even run within a CI/CD pipeline.
|
||||
|
||||
## Next steps
|
||||
|
||||
- [Add a dev container template](./add-devcontainer.md)
|
||||
- [Add an Envbuilder template](./add-envbuilder.md)
|
||||
@@ -70,4 +70,5 @@ specific tooling for their projects. The [Dev Container](https://containers.dev)
|
||||
specification allows developers to define their projects dependencies within a
|
||||
`devcontainer.json` in their Git repository.
|
||||
|
||||
- [Learn how to integrate Dev Containers with Coder](./devcontainers/index.md)
|
||||
- [Configure a template for Dev Containers](../extending-templates/devcontainers.md) (recommended)
|
||||
- [Learn about Envbuilder](./envbuilder/index.md) (alternative for environments without Docker)
|
||||
|
||||
@@ -96,5 +96,6 @@ coder templates delete <template-name>
|
||||
## Next steps
|
||||
|
||||
- [Image management](./image-management.md)
|
||||
- [Devcontainer templates](./devcontainers/index.md)
|
||||
- [Dev Containers integration](../extending-templates/devcontainers.md) (recommended)
|
||||
- [Envbuilder](./envbuilder/index.md) (alternative for environments without Docker)
|
||||
- [Change management](./change-management.md)
|
||||
|
||||
@@ -60,3 +60,65 @@ needs.
|
||||
|
||||
For configuration options and details, see [Data Retention](./setup.md#data-retention)
|
||||
in the AI Bridge setup guide.
|
||||
|
||||
## Tracing
|
||||
|
||||
AI Bridge supports tracing via [OpenTelemetry](https://opentelemetry.io/),
|
||||
providing visibility into request processing, upstream API calls, and MCP server
|
||||
interactions.
|
||||
|
||||
### Enabling Tracing
|
||||
|
||||
AI Bridge tracing is enabled when tracing is enabled for the Coder server.
|
||||
To enable tracing set `CODER_TRACE_ENABLE` environment variable or
|
||||
[--trace](https://coder.com/docs/reference/cli/server#--trace) CLI flag:
|
||||
|
||||
```sh
|
||||
export CODER_TRACE_ENABLE=true
|
||||
```
|
||||
|
||||
```sh
|
||||
coder server --trace
|
||||
```
|
||||
|
||||
### What is Traced
|
||||
|
||||
AI Bridge creates spans for the following operations:
|
||||
|
||||
| Span Name | Description |
|
||||
|---------------------------------------------|------------------------------------------------------|
|
||||
| `CachedBridgePool.Acquire` | Acquiring a request bridge instance from the pool |
|
||||
| `Intercept` | Top-level span for processing an intercepted request |
|
||||
| `Intercept.CreateInterceptor` | Creating the request interceptor |
|
||||
| `Intercept.ProcessRequest` | Processing the request through the bridge |
|
||||
| `Intercept.ProcessRequest.Upstream` | Forwarding the request to the upstream AI provider |
|
||||
| `Intercept.ProcessRequest.ToolCall` | Executing a tool call requested by the AI model |
|
||||
| `Intercept.RecordInterception` | Recording creating interception record |
|
||||
| `Intercept.RecordPromptUsage` | Recording prompt/message data |
|
||||
| `Intercept.RecordTokenUsage` | Recording token consumption |
|
||||
| `Intercept.RecordToolUsage` | Recording tool/function calls |
|
||||
| `Intercept.RecordInterceptionEnded` | Recording the interception as completed |
|
||||
| `ServerProxyManager.Init` | Initializing MCP server proxy connections |
|
||||
| `StreamableHTTPServerProxy.Init` | Setting up HTTP-based MCP server proxies |
|
||||
| `StreamableHTTPServerProxy.Init.fetchTools` | Fetching available tools from MCP servers |
|
||||
|
||||
Example trace of an interception using Jaeger backend:
|
||||
|
||||

|
||||
|
||||
### Capturing Logs in Traces
|
||||
|
||||
> **Note:** Enabling log capture may generate a large volume of trace events.
|
||||
|
||||
To include log messages as trace events, enable trace log capture
|
||||
by setting `CODER_TRACE_LOGS` environment variable or using
|
||||
[--trace-logs](https://coder.com/docs/reference/cli/server#--trace-logs) flag:
|
||||
|
||||
```sh
|
||||
export CODER_TRACE_ENABLE=true
|
||||
export CODER_TRACE_LOGS=true
|
||||
```
|
||||
|
||||
```sh
|
||||
coder server --trace --trace-logs
|
||||
```
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 412 KiB |
+18
-13
@@ -331,6 +331,11 @@
|
||||
"description": "Access dev containers via SSH, your IDE, or web terminal.",
|
||||
"path": "./user-guides/devcontainers/working-with-dev-containers.md"
|
||||
},
|
||||
{
|
||||
"title": "Customizing dev containers",
|
||||
"description": "Configure custom agent names, apps, and display options in devcontainer.json.",
|
||||
"path": "./user-guides/devcontainers/customizing-dev-containers.md"
|
||||
},
|
||||
{
|
||||
"title": "Troubleshooting dev containers",
|
||||
"description": "Diagnose and resolve common issues with dev containers in your Coder workspace.",
|
||||
@@ -527,24 +532,24 @@
|
||||
"path": "./admin/templates/managing-templates/change-management.md"
|
||||
},
|
||||
{
|
||||
"title": "Dev containers",
|
||||
"description": "Learn about using development containers in templates",
|
||||
"path": "./admin/templates/managing-templates/devcontainers/index.md",
|
||||
"title": "Envbuilder",
|
||||
"description": "Build dev containers using Envbuilder for environments without Docker",
|
||||
"path": "./admin/templates/managing-templates/envbuilder/index.md",
|
||||
"children": [
|
||||
{
|
||||
"title": "Add a dev container template",
|
||||
"description": "How to add a dev container template to Coder",
|
||||
"path": "./admin/templates/managing-templates/devcontainers/add-devcontainer.md"
|
||||
"title": "Add an Envbuilder template",
|
||||
"description": "How to add an Envbuilder dev container template to Coder",
|
||||
"path": "./admin/templates/managing-templates/envbuilder/add-envbuilder.md"
|
||||
},
|
||||
{
|
||||
"title": "Dev container security and caching",
|
||||
"description": "Configure dev container authentication and caching",
|
||||
"path": "./admin/templates/managing-templates/devcontainers/devcontainer-security-caching.md"
|
||||
"title": "Envbuilder security and caching",
|
||||
"description": "Configure Envbuilder authentication and caching",
|
||||
"path": "./admin/templates/managing-templates/envbuilder/envbuilder-security-caching.md"
|
||||
},
|
||||
{
|
||||
"title": "Dev container releases and known issues",
|
||||
"description": "Dev container releases and known issues",
|
||||
"path": "./admin/templates/managing-templates/devcontainers/devcontainer-releases-known-issues.md"
|
||||
"title": "Envbuilder releases and known issues",
|
||||
"description": "Envbuilder releases and known issues",
|
||||
"path": "./admin/templates/managing-templates/envbuilder/envbuilder-releases-known-issues.md"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -943,7 +948,7 @@
|
||||
},
|
||||
{
|
||||
"title": "AI Bridge",
|
||||
"description": "Centralized LLM and MCP proxy for platform teams",
|
||||
"description": "AI Gateway for Enterprise Governance \u0026 Observability",
|
||||
"path": "./ai-coder/ai-bridge/index.md",
|
||||
"icon_path": "./images/icons/api.svg",
|
||||
"state": ["premium", "beta"],
|
||||
|
||||
Generated
+87
@@ -727,6 +727,93 @@ Status Code **200**
|
||||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||
|
||||
## Add new license
|
||||
|
||||
### Code samples
|
||||
|
||||
```shell
|
||||
# Example request using curl
|
||||
curl -X POST http://coder-server:8080/api/v2/licenses \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'Accept: application/json' \
|
||||
-H 'Coder-Session-Token: API_KEY'
|
||||
```
|
||||
|
||||
`POST /licenses`
|
||||
|
||||
> Body parameter
|
||||
|
||||
```json
|
||||
{
|
||||
"license": "string"
|
||||
}
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
| Name | In | Type | Required | Description |
|
||||
|--------|------|--------------------------------------------------------------------|----------|---------------------|
|
||||
| `body` | body | [codersdk.AddLicenseRequest](schemas.md#codersdkaddlicenserequest) | true | Add license request |
|
||||
|
||||
### Example responses
|
||||
|
||||
> 201 Response
|
||||
|
||||
```json
|
||||
{
|
||||
"claims": {},
|
||||
"id": 0,
|
||||
"uploaded_at": "2019-08-24T14:15:22Z",
|
||||
"uuid": "095be615-a8ad-4c33-8e9c-c7612fbf6c9f"
|
||||
}
|
||||
```
|
||||
|
||||
### Responses
|
||||
|
||||
| Status | Meaning | Description | Schema |
|
||||
|--------|--------------------------------------------------------------|-------------|------------------------------------------------|
|
||||
| 201 | [Created](https://tools.ietf.org/html/rfc7231#section-6.3.2) | Created | [codersdk.License](schemas.md#codersdklicense) |
|
||||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||
|
||||
## Update license entitlements
|
||||
|
||||
### Code samples
|
||||
|
||||
```shell
|
||||
# Example request using curl
|
||||
curl -X POST http://coder-server:8080/api/v2/licenses/refresh-entitlements \
|
||||
-H 'Accept: application/json' \
|
||||
-H 'Coder-Session-Token: API_KEY'
|
||||
```
|
||||
|
||||
`POST /licenses/refresh-entitlements`
|
||||
|
||||
### Example responses
|
||||
|
||||
> 201 Response
|
||||
|
||||
```json
|
||||
{
|
||||
"detail": "string",
|
||||
"message": "string",
|
||||
"validations": [
|
||||
{
|
||||
"detail": "string",
|
||||
"field": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Responses
|
||||
|
||||
| Status | Meaning | Description | Schema |
|
||||
|--------|--------------------------------------------------------------|-------------|--------------------------------------------------|
|
||||
| 201 | [Created](https://tools.ietf.org/html/rfc7231#section-6.3.2) | Created | [codersdk.Response](schemas.md#codersdkresponse) |
|
||||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||
|
||||
## Delete license
|
||||
|
||||
### Code samples
|
||||
|
||||
Generated
+3
@@ -176,10 +176,13 @@ curl -X GET http://coder-server:8080/api/v2/deployment/config \
|
||||
},
|
||||
"enabled": true,
|
||||
"inject_coder_mcp_tools": true,
|
||||
"max_concurrency": 0,
|
||||
"openai": {
|
||||
"base_url": "string",
|
||||
"key": "string"
|
||||
},
|
||||
"rate_limit": 0,
|
||||
"rate_window": 0,
|
||||
"retention": 0
|
||||
}
|
||||
},
|
||||
|
||||
Generated
-87
@@ -1,92 +1,5 @@
|
||||
# Organizations
|
||||
|
||||
## Add new license
|
||||
|
||||
### Code samples
|
||||
|
||||
```shell
|
||||
# Example request using curl
|
||||
curl -X POST http://coder-server:8080/api/v2/licenses \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'Accept: application/json' \
|
||||
-H 'Coder-Session-Token: API_KEY'
|
||||
```
|
||||
|
||||
`POST /licenses`
|
||||
|
||||
> Body parameter
|
||||
|
||||
```json
|
||||
{
|
||||
"license": "string"
|
||||
}
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
| Name | In | Type | Required | Description |
|
||||
|--------|------|--------------------------------------------------------------------|----------|---------------------|
|
||||
| `body` | body | [codersdk.AddLicenseRequest](schemas.md#codersdkaddlicenserequest) | true | Add license request |
|
||||
|
||||
### Example responses
|
||||
|
||||
> 201 Response
|
||||
|
||||
```json
|
||||
{
|
||||
"claims": {},
|
||||
"id": 0,
|
||||
"uploaded_at": "2019-08-24T14:15:22Z",
|
||||
"uuid": "095be615-a8ad-4c33-8e9c-c7612fbf6c9f"
|
||||
}
|
||||
```
|
||||
|
||||
### Responses
|
||||
|
||||
| Status | Meaning | Description | Schema |
|
||||
|--------|--------------------------------------------------------------|-------------|------------------------------------------------|
|
||||
| 201 | [Created](https://tools.ietf.org/html/rfc7231#section-6.3.2) | Created | [codersdk.License](schemas.md#codersdklicense) |
|
||||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||
|
||||
## Update license entitlements
|
||||
|
||||
### Code samples
|
||||
|
||||
```shell
|
||||
# Example request using curl
|
||||
curl -X POST http://coder-server:8080/api/v2/licenses/refresh-entitlements \
|
||||
-H 'Accept: application/json' \
|
||||
-H 'Coder-Session-Token: API_KEY'
|
||||
```
|
||||
|
||||
`POST /licenses/refresh-entitlements`
|
||||
|
||||
### Example responses
|
||||
|
||||
> 201 Response
|
||||
|
||||
```json
|
||||
{
|
||||
"detail": "string",
|
||||
"message": "string",
|
||||
"validations": [
|
||||
{
|
||||
"detail": "string",
|
||||
"field": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Responses
|
||||
|
||||
| Status | Meaning | Description | Schema |
|
||||
|--------|--------------------------------------------------------------|-------------|--------------------------------------------------|
|
||||
| 201 | [Created](https://tools.ietf.org/html/rfc7231#section-6.3.2) | Created | [codersdk.Response](schemas.md#codersdkresponse) |
|
||||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||
|
||||
## Get organizations
|
||||
|
||||
### Code samples
|
||||
|
||||
Generated
+23
-8
@@ -390,24 +390,30 @@
|
||||
},
|
||||
"enabled": true,
|
||||
"inject_coder_mcp_tools": true,
|
||||
"max_concurrency": 0,
|
||||
"openai": {
|
||||
"base_url": "string",
|
||||
"key": "string"
|
||||
},
|
||||
"rate_limit": 0,
|
||||
"rate_window": 0,
|
||||
"retention": 0
|
||||
}
|
||||
```
|
||||
|
||||
### Properties
|
||||
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
|--------------------------|----------------------------------------------------------------------|----------|--------------|-------------|
|
||||
| `anthropic` | [codersdk.AIBridgeAnthropicConfig](#codersdkaibridgeanthropicconfig) | false | | |
|
||||
| `bedrock` | [codersdk.AIBridgeBedrockConfig](#codersdkaibridgebedrockconfig) | false | | |
|
||||
| `enabled` | boolean | false | | |
|
||||
| `inject_coder_mcp_tools` | boolean | false | | |
|
||||
| `openai` | [codersdk.AIBridgeOpenAIConfig](#codersdkaibridgeopenaiconfig) | false | | |
|
||||
| `retention` | integer | false | | |
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
|--------------------------|----------------------------------------------------------------------|----------|--------------|-------------------------------|
|
||||
| `anthropic` | [codersdk.AIBridgeAnthropicConfig](#codersdkaibridgeanthropicconfig) | false | | |
|
||||
| `bedrock` | [codersdk.AIBridgeBedrockConfig](#codersdkaibridgebedrockconfig) | false | | |
|
||||
| `enabled` | boolean | false | | |
|
||||
| `inject_coder_mcp_tools` | boolean | false | | |
|
||||
| `max_concurrency` | integer | false | | Overload protection settings. |
|
||||
| `openai` | [codersdk.AIBridgeOpenAIConfig](#codersdkaibridgeopenaiconfig) | false | | |
|
||||
| `rate_limit` | integer | false | | |
|
||||
| `rate_window` | integer | false | | |
|
||||
| `retention` | integer | false | | |
|
||||
|
||||
## codersdk.AIBridgeInterception
|
||||
|
||||
@@ -700,10 +706,13 @@
|
||||
},
|
||||
"enabled": true,
|
||||
"inject_coder_mcp_tools": true,
|
||||
"max_concurrency": 0,
|
||||
"openai": {
|
||||
"base_url": "string",
|
||||
"key": "string"
|
||||
},
|
||||
"rate_limit": 0,
|
||||
"rate_window": 0,
|
||||
"retention": 0
|
||||
}
|
||||
}
|
||||
@@ -2860,10 +2869,13 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
|
||||
},
|
||||
"enabled": true,
|
||||
"inject_coder_mcp_tools": true,
|
||||
"max_concurrency": 0,
|
||||
"openai": {
|
||||
"base_url": "string",
|
||||
"key": "string"
|
||||
},
|
||||
"rate_limit": 0,
|
||||
"rate_window": 0,
|
||||
"retention": 0
|
||||
}
|
||||
},
|
||||
@@ -3382,10 +3394,13 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
|
||||
},
|
||||
"enabled": true,
|
||||
"inject_coder_mcp_tools": true,
|
||||
"max_concurrency": 0,
|
||||
"openai": {
|
||||
"base_url": "string",
|
||||
"key": "string"
|
||||
},
|
||||
"rate_limit": 0,
|
||||
"rate_window": 0,
|
||||
"retention": 0
|
||||
}
|
||||
},
|
||||
|
||||
Generated
+33
@@ -1771,6 +1771,39 @@ Whether to inject Coder's MCP tools into intercepted AI Bridge requests (require
|
||||
|
||||
Length of time to retain data such as interceptions and all related records (token, prompt, tool use).
|
||||
|
||||
### --aibridge-max-concurrency
|
||||
|
||||
| | |
|
||||
|-------------|----------------------------------------------|
|
||||
| Type | <code>int</code> |
|
||||
| Environment | <code>$CODER_AIBRIDGE_MAX_CONCURRENCY</code> |
|
||||
| YAML | <code>aibridge.max_concurrency</code> |
|
||||
| Default | <code>0</code> |
|
||||
|
||||
Maximum number of concurrent AI Bridge requests. Set to 0 to disable (unlimited).
|
||||
|
||||
### --aibridge-rate-limit
|
||||
|
||||
| | |
|
||||
|-------------|-----------------------------------------|
|
||||
| Type | <code>int</code> |
|
||||
| Environment | <code>$CODER_AIBRIDGE_RATE_LIMIT</code> |
|
||||
| YAML | <code>aibridge.rate_limit</code> |
|
||||
| Default | <code>0</code> |
|
||||
|
||||
Maximum number of AI Bridge requests per rate window. Set to 0 to disable rate limiting.
|
||||
|
||||
### --aibridge-rate-window
|
||||
|
||||
| | |
|
||||
|-------------|------------------------------------------|
|
||||
| Type | <code>duration</code> |
|
||||
| Environment | <code>$CODER_AIBRIDGE_RATE_WINDOW</code> |
|
||||
| YAML | <code>aibridge.rate_window</code> |
|
||||
| Default | <code>1m</code> |
|
||||
|
||||
Duration of the rate limiting window for AI Bridge requests.
|
||||
|
||||
### --audit-logs-retention
|
||||
|
||||
| | |
|
||||
|
||||
+1
-23
@@ -558,29 +558,7 @@ primary purpose of this feature is to warn and discourage users from downloading
|
||||
confidential resources to their local machines.
|
||||
|
||||
For more advanced security needs, consider adopting an endpoint security
|
||||
solution. These are third-party tools that operate at the operating system or
|
||||
network level to monitor and control data movement, providing stronger
|
||||
enforcement than application-layer controls.
|
||||
|
||||
Endpoint security solutions typically fall into these categories:
|
||||
|
||||
- **Data Loss Prevention (DLP)**: Software that monitors, detects, and blocks
|
||||
sensitive data transfers based on content inspection and policy rules.
|
||||
- **Endpoint Detection and Response (EDR)**: Agents installed on user machines
|
||||
that can monitor file operations and network activity in real-time.
|
||||
- **Network-level DLP**: Solutions that inspect traffic at the network boundary
|
||||
to prevent unauthorized data exfiltration.
|
||||
|
||||
Examples of endpoint security products include:
|
||||
|
||||
- [CrowdStrike Falcon](https://www.crowdstrike.com/products/endpoint-security/)
|
||||
- [Microsoft Defender for Endpoint](https://www.microsoft.com/en-us/security/business/endpoint-security/microsoft-defender-endpoint)
|
||||
- [Palo Alto Cortex XDR](https://www.paloaltonetworks.com/cortex/cortex-xdr)
|
||||
- [Symantec Endpoint Security](https://www.broadcom.com/products/cybersecurity/endpoint)
|
||||
|
||||
These tools can enforce data protection policies regardless of how users attempt
|
||||
to transfer files, complementing Coder's built-in controls with OS-level and
|
||||
network-level enforcement.
|
||||
solution.
|
||||
|
||||
## How do I change the access URL for my Coder server?
|
||||
|
||||
|
||||
@@ -0,0 +1,309 @@
|
||||
# Customizing dev containers
|
||||
|
||||
Coder supports custom configuration in your `devcontainer.json` file through the
|
||||
`customizations.coder` block. These options let you control how Coder interacts
|
||||
with your dev container without requiring template changes.
|
||||
|
||||
## Ignore a dev container
|
||||
|
||||
Use the `ignore` option to hide a dev container from Coder completely:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "My Dev Container",
|
||||
"image": "mcr.microsoft.com/devcontainers/base:ubuntu",
|
||||
"customizations": {
|
||||
"coder": {
|
||||
"ignore": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
When `ignore` is set to `true`:
|
||||
|
||||
- The dev container won't appear in the Coder UI
|
||||
- Coder won't manage or monitor the container
|
||||
|
||||
This is useful for dev containers in your repository that you don't want Coder
|
||||
to manage.
|
||||
|
||||
## Auto-start
|
||||
|
||||
Control whether your dev container should auto-start using the `autoStart`
|
||||
option:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "My Dev Container",
|
||||
"image": "mcr.microsoft.com/devcontainers/base:ubuntu",
|
||||
"customizations": {
|
||||
"coder": {
|
||||
"autoStart": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
When `autoStart` is set to `true`, the dev container automatically builds and
|
||||
starts during workspace initialization.
|
||||
|
||||
When `autoStart` is set to `false` or omitted, the dev container is discovered
|
||||
and shown in the UI, but users must manually start it.
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> The `autoStart` option only takes effect when your template administrator has
|
||||
> enabled [`CODER_AGENT_DEVCONTAINERS_DISCOVERY_AUTOSTART_ENABLE`](../../admin/templates/extending-templates/devcontainers.md#coder_agent_devcontainers_discovery_autostart_enable).
|
||||
> If this setting is disabled at the template level, containers won't auto-start
|
||||
> regardless of this option.
|
||||
|
||||
## Custom agent name
|
||||
|
||||
Each dev container gets an agent name derived from the workspace folder path by
|
||||
default. You can set a custom name using the `name` option:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "My Dev Container",
|
||||
"image": "mcr.microsoft.com/devcontainers/base:ubuntu",
|
||||
"customizations": {
|
||||
"coder": {
|
||||
"name": "my-custom-agent"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The name must contain only lowercase letters, numbers, and hyphens. This name
|
||||
appears in `coder ssh` commands and the dashboard (e.g.,
|
||||
`coder ssh my-workspace.my-custom-agent`).
|
||||
|
||||
## Display apps
|
||||
|
||||
Control which built-in Coder apps appear for your dev container using
|
||||
`displayApps`:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "My Dev Container",
|
||||
"image": "mcr.microsoft.com/devcontainers/base:ubuntu",
|
||||
"customizations": {
|
||||
"coder": {
|
||||
"displayApps": {
|
||||
"web_terminal": true,
|
||||
"ssh_helper": true,
|
||||
"port_forwarding_helper": true,
|
||||
"vscode": true,
|
||||
"vscode_insiders": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Available display apps:
|
||||
|
||||
| App | Description | Default |
|
||||
|--------------------------|------------------------------|---------|
|
||||
| `web_terminal` | Web-based terminal access | `true` |
|
||||
| `ssh_helper` | SSH connection helper | `true` |
|
||||
| `port_forwarding_helper` | Port forwarding interface | `true` |
|
||||
| `vscode` | VS Code Desktop integration | `true` |
|
||||
| `vscode_insiders` | VS Code Insiders integration | `false` |
|
||||
|
||||
## Custom apps
|
||||
|
||||
Define custom applications for your dev container using the `apps` array:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "My Dev Container",
|
||||
"image": "mcr.microsoft.com/devcontainers/base:ubuntu",
|
||||
"customizations": {
|
||||
"coder": {
|
||||
"apps": [
|
||||
{
|
||||
"slug": "zed",
|
||||
"displayName": "Zed Editor",
|
||||
"url": "zed://ssh/${localEnv:CODER_WORKSPACE_AGENT_NAME}.${localEnv:CODER_WORKSPACE_NAME}.${localEnv:CODER_WORKSPACE_OWNER_NAME}.coder${containerWorkspaceFolder}",
|
||||
"external": true,
|
||||
"icon": "/icon/zed.svg",
|
||||
"order": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This example adds a Zed Editor button that opens the dev container directly in
|
||||
the Zed desktop app via its SSH remote feature.
|
||||
|
||||
Each app supports the following properties:
|
||||
|
||||
| Property | Type | Description |
|
||||
|---------------|---------|---------------------------------------------------------------|
|
||||
| `slug` | string | Unique identifier for the app (required) |
|
||||
| `displayName` | string | Human-readable name shown in the UI |
|
||||
| `url` | string | URL to open (supports variable interpolation) |
|
||||
| `command` | string | Command to run instead of opening a URL |
|
||||
| `icon` | string | Path to an icon (e.g., `/icon/code.svg`) |
|
||||
| `openIn` | string | `"tab"` or `"slim-window"` (default: `"slim-window"`) |
|
||||
| `share` | string | `"owner"`, `"authenticated"`, `"organization"`, or `"public"` |
|
||||
| `external` | boolean | Open as external URL (e.g., for desktop apps) |
|
||||
| `group` | string | Group name for organizing apps in the UI |
|
||||
| `order` | number | Sort order for display |
|
||||
| `hidden` | boolean | Hide the app from the UI |
|
||||
| `subdomain` | boolean | Use subdomain-based access |
|
||||
| `healthCheck` | object | Health check configuration (see below) |
|
||||
|
||||
### Health checks
|
||||
|
||||
Configure health checks to monitor app availability:
|
||||
|
||||
```json
|
||||
{
|
||||
"customizations": {
|
||||
"coder": {
|
||||
"apps": [
|
||||
{
|
||||
"slug": "web-server",
|
||||
"displayName": "Web Server",
|
||||
"url": "http://localhost:8080",
|
||||
"healthCheck": {
|
||||
"url": "http://localhost:8080/healthz",
|
||||
"interval": 5,
|
||||
"threshold": 2
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Health check properties:
|
||||
|
||||
| Property | Type | Description |
|
||||
|-------------|--------|-------------------------------------------------|
|
||||
| `url` | string | URL to check for health status |
|
||||
| `interval` | number | Seconds between health checks |
|
||||
| `threshold` | number | Number of failures before marking app unhealthy |
|
||||
|
||||
## Variable interpolation
|
||||
|
||||
App URLs and other string values support variable interpolation for dynamic
|
||||
configuration.
|
||||
|
||||
### Environment variables
|
||||
|
||||
Use `${localEnv:VAR_NAME}` to reference environment variables, with optional
|
||||
default values:
|
||||
|
||||
```json
|
||||
{
|
||||
"customizations": {
|
||||
"coder": {
|
||||
"apps": [
|
||||
{
|
||||
"slug": "my-app",
|
||||
"url": "http://${localEnv:HOST:127.0.0.1}:${localEnv:PORT:8080}"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Coder-provided variables
|
||||
|
||||
Coder provides these environment variables automatically:
|
||||
|
||||
| Variable | Description |
|
||||
|-------------------------------------|------------------------------------|
|
||||
| `CODER_WORKSPACE_NAME` | Name of the workspace |
|
||||
| `CODER_WORKSPACE_OWNER_NAME` | Username of the workspace owner |
|
||||
| `CODER_WORKSPACE_AGENT_NAME` | Name of the dev container agent |
|
||||
| `CODER_WORKSPACE_PARENT_AGENT_NAME` | Name of the parent workspace agent |
|
||||
| `CODER_URL` | URL of the Coder deployment |
|
||||
| `CONTAINER_ID` | Docker container ID |
|
||||
|
||||
### Dev container variables
|
||||
|
||||
Standard dev container variables are also available:
|
||||
|
||||
| Variable | Description |
|
||||
|-------------------------------|--------------------------------------------|
|
||||
| `${containerWorkspaceFolder}` | Workspace folder path inside the container |
|
||||
| `${localWorkspaceFolder}` | Workspace folder path on the host |
|
||||
|
||||
### Session token
|
||||
|
||||
Use `$SESSION_TOKEN` in external app URLs to include the user's session token:
|
||||
|
||||
```json
|
||||
{
|
||||
"customizations": {
|
||||
"coder": {
|
||||
"apps": [
|
||||
{
|
||||
"slug": "custom-ide",
|
||||
"displayName": "Custom IDE",
|
||||
"url": "custom-ide://open?token=$SESSION_TOKEN&folder=${containerWorkspaceFolder}",
|
||||
"external": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Feature options as environment variables
|
||||
|
||||
When your dev container uses features, Coder exposes feature options as
|
||||
environment variables. The format is `FEATURE_<FEATURE_NAME>_OPTION_<OPTION_NAME>`.
|
||||
|
||||
For example, with this feature configuration:
|
||||
|
||||
```json
|
||||
{
|
||||
"features": {
|
||||
"ghcr.io/coder/devcontainer-features/code-server:1": {
|
||||
"port": 9090
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Coder creates `FEATURE_CODE_SERVER_OPTION_PORT=9090`, which you can reference in
|
||||
your apps:
|
||||
|
||||
```json
|
||||
{
|
||||
"features": {
|
||||
"ghcr.io/coder/devcontainer-features/code-server:1": {
|
||||
"port": 9090
|
||||
}
|
||||
},
|
||||
"customizations": {
|
||||
"coder": {
|
||||
"apps": [
|
||||
{
|
||||
"slug": "code-server",
|
||||
"displayName": "Code Server",
|
||||
"url": "http://localhost:${localEnv:FEATURE_CODE_SERVER_OPTION_PORT:8080}",
|
||||
"icon": "/icon/code.svg"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Next steps
|
||||
|
||||
- [Working with dev containers](./working-with-dev-containers.md) — SSH, IDE
|
||||
integration, and port forwarding
|
||||
- [Troubleshooting dev containers](./troubleshooting-dev-containers.md) —
|
||||
Diagnose common issues
|
||||
@@ -1,87 +1,137 @@
|
||||
# Dev Containers Integration
|
||||
|
||||
The Dev Containers integration enables seamless creation and management of Dev
|
||||
Containers in Coder workspaces. This feature leverages the
|
||||
The Dev Containers integration enables seamless creation and management of dev
|
||||
containers in Coder workspaces. This feature leverages the
|
||||
[`@devcontainers/cli`](https://github.com/devcontainers/cli) and
|
||||
[Docker](https://www.docker.com) to provide a streamlined development
|
||||
experience.
|
||||
|
||||
This implementation is different from the existing
|
||||
[Envbuilder-based Dev Containers](../../admin/templates/managing-templates/devcontainers/index.md)
|
||||
offering.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Coder version 2.24.0 or later
|
||||
- Coder CLI version 2.24.0 or later
|
||||
- **Linux or macOS workspace**, Dev Containers are not supported on Windows
|
||||
- A template with:
|
||||
- Dev Containers integration enabled
|
||||
- A Docker-compatible workspace image
|
||||
- Appropriate permissions to execute Docker commands inside your workspace
|
||||
- Docker available inside your workspace
|
||||
- The `@devcontainers/cli` installed in your workspace
|
||||
|
||||
## How It Works
|
||||
|
||||
The Dev Containers integration utilizes the `devcontainer` command from
|
||||
[`@devcontainers/cli`](https://github.com/devcontainers/cli) to manage Dev
|
||||
Containers within your Coder workspace.
|
||||
This command provides comprehensive functionality for creating, starting, and managing Dev Containers.
|
||||
|
||||
Dev environments are configured through a standard `devcontainer.json` file,
|
||||
which allows for extensive customization of your development setup.
|
||||
|
||||
When a workspace with the Dev Containers integration starts:
|
||||
|
||||
1. The workspace initializes the Docker environment.
|
||||
1. The integration detects repositories with a `.devcontainer` directory or a
|
||||
`devcontainer.json` file.
|
||||
1. The integration builds and starts the Dev Container based on the
|
||||
configuration.
|
||||
1. Your workspace automatically detects the running Dev Container.
|
||||
Dev Containers integration is enabled by default. Your workspace needs Docker
|
||||
(via Docker-in-Docker or a mounted socket) and the devcontainers CLI. Most
|
||||
templates with Dev Containers support include both—see
|
||||
[Configure a template for dev containers](../../admin/templates/extending-templates/devcontainers.md)
|
||||
for setup details.
|
||||
|
||||
## Features
|
||||
|
||||
### Available Now
|
||||
- Automatic dev container detection from repositories
|
||||
- Seamless container startup during workspace initialization
|
||||
- Change detection with outdated status indicator
|
||||
- On-demand container rebuild via dashboard button
|
||||
- Integrated IDE experience with VS Code
|
||||
- Direct SSH access to containers
|
||||
- Automatic port detection
|
||||
|
||||
- Automatic Dev Container detection from repositories
|
||||
- Seamless Dev Container startup during workspace initialization
|
||||
- Dev Container change detection and dirty state indicators
|
||||
- On-demand Dev Container recreation via rebuild button
|
||||
- Integrated IDE experience in Dev Containers with VS Code
|
||||
- Direct service access in Dev Containers
|
||||
- SSH access to Dev Containers
|
||||
- Automatic port detection for container ports
|
||||
## Getting started
|
||||
|
||||
### Add a devcontainer.json
|
||||
|
||||
Add a `devcontainer.json` file to your repository. This file defines your
|
||||
development environment. You can place it in:
|
||||
|
||||
- `.devcontainer/devcontainer.json` (recommended)
|
||||
- `.devcontainer.json` (root of repository)
|
||||
- `.devcontainer/<folder>/devcontainer.json` (for multiple configurations)
|
||||
|
||||
The third option allows monorepos to define multiple dev container
|
||||
configurations in separate sub-folders. See the
|
||||
[Dev Container specification](https://containers.dev/implementors/spec/#devcontainerjson)
|
||||
for details.
|
||||
|
||||
Here's a minimal example:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "My Dev Container",
|
||||
"image": "mcr.microsoft.com/devcontainers/base:ubuntu"
|
||||
}
|
||||
```
|
||||
|
||||
For more configuration options, see the
|
||||
[Dev Container specification](https://containers.dev/).
|
||||
|
||||
### Start your dev container
|
||||
|
||||
Coder automatically discovers dev container configurations in your repositories
|
||||
and displays them in your workspace dashboard. From there, you can start a dev
|
||||
container with a single click.
|
||||
|
||||
If your template administrator has configured automatic startup (via the
|
||||
`coder_devcontainer` Terraform resource or autostart settings), your dev
|
||||
container will build and start automatically when the workspace starts.
|
||||
|
||||
### Connect to your dev container
|
||||
|
||||
Once running, your dev container appears as a sub-agent in your workspace
|
||||
dashboard. You can connect via:
|
||||
|
||||
- **Web terminal** in the Coder dashboard
|
||||
- **SSH** using `coder ssh <workspace>.<agent>`
|
||||
- **VS Code** using the "Open in VS Code Desktop" button
|
||||
|
||||
See [Working with dev containers](./working-with-dev-containers.md) for detailed
|
||||
connection instructions.
|
||||
|
||||
## How it works
|
||||
|
||||
The Dev Containers integration uses the `devcontainer` command from
|
||||
[`@devcontainers/cli`](https://github.com/devcontainers/cli) to manage
|
||||
containers within your Coder workspace.
|
||||
|
||||
When a workspace with Dev Containers integration starts:
|
||||
|
||||
1. The workspace initializes the Docker environment.
|
||||
1. The integration detects repositories with dev container configurations.
|
||||
1. Detected dev containers appear in the Coder dashboard.
|
||||
1. If auto-start is configured (via `coder_devcontainer` or autostart settings),
|
||||
the integration builds and starts the dev container automatically.
|
||||
1. Coder creates a sub-agent for the running container, enabling direct access.
|
||||
|
||||
Without auto-start, users can manually start discovered dev containers from the
|
||||
dashboard.
|
||||
|
||||
### Agent naming
|
||||
|
||||
Each dev container gets its own agent name, derived from the workspace folder
|
||||
path. For example, a dev container with workspace folder `/home/coder/my-app`
|
||||
will have an agent named `my-app`.
|
||||
|
||||
Agent names are sanitized to contain only lowercase alphanumeric characters and
|
||||
hyphens. You can also set a
|
||||
[custom agent name](./customizing-dev-containers.md#custom-agent-name)
|
||||
in your `devcontainer.json`.
|
||||
|
||||
## Limitations
|
||||
|
||||
The Dev Containers integration has the following limitations:
|
||||
- **Linux and macOS only** — Dev Containers are not supported on Windows
|
||||
workspaces
|
||||
- Changes to `devcontainer.json` require manual rebuild using the dashboard
|
||||
button
|
||||
- The `forwardPorts` property in `devcontainer.json` with `host:port` syntax
|
||||
(e.g., `"db:5432"`) for Docker Compose sidecar containers is not yet
|
||||
supported. For single-container dev containers, use `coder port-forward` to
|
||||
access ports directly on the sub-agent.
|
||||
- Some advanced dev container features may have limited support
|
||||
|
||||
- **Not supported on Windows**
|
||||
- Changes to the `devcontainer.json` file require manual container recreation
|
||||
using the rebuild button
|
||||
- Some Dev Container features may not work as expected
|
||||
> [!NOTE]
|
||||
> If your template uses Envbuilder rather than Docker-based dev containers, see
|
||||
> the [Envbuilder documentation](../../admin/templates/managing-templates/envbuilder/index.md).
|
||||
|
||||
## Comparison with Envbuilder-based Dev Containers
|
||||
## Next steps
|
||||
|
||||
| Feature | Dev Containers Integration | Envbuilder Dev Containers |
|
||||
|----------------|----------------------------------------|----------------------------------------------|
|
||||
| Implementation | Direct `@devcontainers/cli` and Docker | Coder's Envbuilder |
|
||||
| Target users | Individual developers | Platform teams and administrators |
|
||||
| Configuration | Standard `devcontainer.json` | Terraform templates with Envbuilder |
|
||||
| Management | User-controlled | Admin-controlled |
|
||||
| Requirements | Docker access in workspace | Compatible with more restricted environments |
|
||||
|
||||
Choose the appropriate solution based on your team's needs and infrastructure
|
||||
constraints. For additional details on Envbuilder's Dev Container support, see
|
||||
the
|
||||
[Envbuilder Dev Container spec support documentation](https://github.com/coder/envbuilder/blob/main/docs/devcontainer-spec-support.md).
|
||||
|
||||
## Next Steps
|
||||
|
||||
- Explore the [Dev Container specification](https://containers.dev/) to learn
|
||||
more about advanced configuration options
|
||||
- Read about [Dev Container features](https://containers.dev/features) to
|
||||
enhance your development environment
|
||||
- Check the
|
||||
[VS Code dev containers documentation](https://code.visualstudio.com/docs/devcontainers/containers)
|
||||
for IDE-specific features
|
||||
- [Working with dev containers](./working-with-dev-containers.md) — SSH, IDE
|
||||
integration, and port forwarding
|
||||
- [Customizing dev containers](./customizing-dev-containers.md) — Custom agent
|
||||
names, apps, and display options
|
||||
- [Troubleshooting dev containers](./troubleshooting-dev-containers.md) —
|
||||
Diagnose common issues
|
||||
- [Dev Container specification](https://containers.dev/) — Advanced
|
||||
configuration options
|
||||
- [Dev Container features](https://containers.dev/features) — Enhance your
|
||||
environment with pre-built tools
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Troubleshooting dev containers
|
||||
|
||||
## Dev Container Not Starting
|
||||
## Dev container not starting
|
||||
|
||||
If your dev container fails to start:
|
||||
|
||||
@@ -10,7 +10,108 @@ If your dev container fails to start:
|
||||
- `/tmp/coder-startup-script.log`
|
||||
- `/tmp/coder-script-[script_id].log`
|
||||
|
||||
1. Verify that Docker is running in your workspace.
|
||||
1. Ensure the `devcontainer.json` file is valid.
|
||||
1. Verify Docker is available in your workspace (see below).
|
||||
1. Ensure the `devcontainer.json` file is valid JSON.
|
||||
1. Check that the repository has been cloned correctly.
|
||||
1. Verify the resource limits in your workspace are sufficient.
|
||||
|
||||
## Docker not available
|
||||
|
||||
Dev containers require Docker, either via a running daemon (Docker-in-Docker) or
|
||||
a mounted socket from the host. Your template determines which approach is used.
|
||||
|
||||
**If using Docker-in-Docker**, check that the daemon is running:
|
||||
|
||||
```console
|
||||
sudo service docker status
|
||||
sudo service docker start # if not running
|
||||
```
|
||||
|
||||
**If using a mounted socket**, verify the socket exists and is accessible:
|
||||
|
||||
```console
|
||||
ls -la /var/run/docker.sock
|
||||
docker ps # test access
|
||||
```
|
||||
|
||||
If you get permission errors, your user may need to be in the `docker` group.
|
||||
|
||||
## Finding your dev container agent
|
||||
|
||||
Use `coder show` to list all agents in your workspace, including dev container
|
||||
sub-agents:
|
||||
|
||||
```console
|
||||
coder show <workspace>
|
||||
```
|
||||
|
||||
The agent name is derived from the workspace folder path. For details on how
|
||||
names are generated, see [Agent naming](./index.md#agent-naming).
|
||||
|
||||
## SSH connection issues
|
||||
|
||||
If `coder ssh <workspace>.<agent>` fails:
|
||||
|
||||
1. Verify the agent name using `coder show <workspace>`.
|
||||
1. Check that the dev container is running:
|
||||
|
||||
```console
|
||||
docker ps
|
||||
```
|
||||
|
||||
1. Check the workspace agent logs for container-related errors:
|
||||
|
||||
```console
|
||||
grep -i container /tmp/coder-agent.log
|
||||
```
|
||||
|
||||
## VS Code connection issues
|
||||
|
||||
VS Code connects to dev containers through the Coder extension. The extension
|
||||
uses the sub-agent information to route connections through the parent workspace
|
||||
agent to the dev container. If VS Code fails to connect:
|
||||
|
||||
1. Ensure you have the latest Coder VS Code extension.
|
||||
1. Verify the dev container is running in the Coder dashboard.
|
||||
1. Check the parent workspace agent is healthy.
|
||||
1. Try restarting the dev container from the dashboard.
|
||||
|
||||
## Dev container features not working
|
||||
|
||||
If features from your `devcontainer.json` aren't being applied:
|
||||
|
||||
1. Rebuild the container to ensure features are installed fresh.
|
||||
1. Check the container build output for feature installation errors.
|
||||
1. Verify the feature reference format is correct:
|
||||
|
||||
```json
|
||||
{
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/node:1": {}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Slow container startup
|
||||
|
||||
If your dev container takes a long time to start:
|
||||
|
||||
1. **Use a pre-built image** instead of building from a Dockerfile. This avoids
|
||||
the image build step, though features and lifecycle scripts still run.
|
||||
1. **Minimize features**. Each feature executes as a separate Docker layer
|
||||
during the image build, which is typically the slowest part. Changing
|
||||
`devcontainer.json` invalidates the layer cache, causing features to
|
||||
reinstall on rebuild.
|
||||
1. **Check lifecycle scripts**. Commands in `postStartCommand` run on every
|
||||
container start. Commands in `postCreateCommand` run once per build, so
|
||||
they execute again after each rebuild.
|
||||
|
||||
## Getting more help
|
||||
|
||||
If you continue to experience issues:
|
||||
|
||||
1. Collect logs from `/tmp/coder-agent.log` (both workspace and container).
|
||||
1. Note the exact error messages.
|
||||
1. Check [Coder GitHub issues](https://github.com/coder/coder/issues) for
|
||||
similar problems.
|
||||
1. Contact your Coder administrator for template-specific issues.
|
||||
|
||||
@@ -5,93 +5,151 @@ visual representation of the running environment:
|
||||
|
||||

|
||||
|
||||
## SSH Access
|
||||
## SSH access
|
||||
|
||||
You can SSH into your dev container directly using the Coder CLI:
|
||||
Each dev container has its own agent name, derived from the workspace folder
|
||||
(e.g., `/home/coder/my-project` becomes `my-project`). You can find agent names
|
||||
in your workspace dashboard, or see
|
||||
[Agent naming](./index.md#agent-naming) for details on how names are generated.
|
||||
|
||||
### Using the Coder CLI
|
||||
|
||||
The simplest way to SSH into a dev container is using `coder ssh` with the
|
||||
workspace and agent name:
|
||||
|
||||
```console
|
||||
coder ssh --container keen_dijkstra my-workspace
|
||||
coder ssh <workspace>.<agent>
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> SSH access is not yet compatible with the `coder config-ssh` command for use
|
||||
> with OpenSSH. You would need to manually modify your SSH config to include the
|
||||
> `--container` flag in the `ProxyCommand`.
|
||||
For example, to connect to a dev container with agent name `my-project` in
|
||||
workspace `my-workspace`:
|
||||
|
||||
## Web Terminal Access
|
||||
```console
|
||||
coder ssh my-workspace.my-project
|
||||
```
|
||||
|
||||
To SSH into the main workspace agent instead of the dev container:
|
||||
|
||||
```console
|
||||
coder ssh my-workspace
|
||||
```
|
||||
|
||||
### Using OpenSSH (config-ssh)
|
||||
|
||||
You can also use standard OpenSSH tools after generating SSH config entries with
|
||||
`coder config-ssh`:
|
||||
|
||||
```console
|
||||
coder config-ssh
|
||||
```
|
||||
|
||||
This creates a wildcard SSH host entry that matches all your workspaces and
|
||||
their agents, including dev container sub-agents. You can then connect using:
|
||||
|
||||
```console
|
||||
ssh my-project.my-workspace.me.coder
|
||||
```
|
||||
|
||||
The default hostname suffix is `.coder`. If your organization uses a different
|
||||
suffix, adjust the hostname accordingly. The suffix can be configured via
|
||||
[`coder config-ssh --hostname-suffix`](../../reference/cli/config-ssh.md) or
|
||||
by your deployment administrator.
|
||||
|
||||
This method works with any SSH client, IDE remote extensions, `rsync`, `scp`,
|
||||
and other tools that use SSH.
|
||||
|
||||
## Web terminal access
|
||||
|
||||
Once your workspace and dev container are running, you can use the web terminal
|
||||
in the Coder interface to execute commands directly inside the dev container.
|
||||
|
||||

|
||||
|
||||
## IDE Integration (VS Code)
|
||||
## IDE integration (VS Code)
|
||||
|
||||
You can open your dev container directly in VS Code by:
|
||||
|
||||
1. Selecting "Open in VS Code Desktop" from the Coder web interface
|
||||
2. Using the Coder CLI with the container flag:
|
||||
1. Selecting **Open in VS Code Desktop** from the dev container agent in the
|
||||
Coder web interface.
|
||||
1. Using the Coder CLI:
|
||||
|
||||
```console
|
||||
coder open vscode --container keen_dijkstra my-workspace
|
||||
```
|
||||
```console
|
||||
coder open vscode <workspace>.<agent>
|
||||
```
|
||||
|
||||
While optimized for VS Code, other IDEs with dev containers support may also
|
||||
For example:
|
||||
|
||||
```console
|
||||
coder open vscode my-workspace.my-project
|
||||
```
|
||||
|
||||
VS Code will automatically detect the dev container environment and connect
|
||||
appropriately.
|
||||
|
||||
While optimized for VS Code, other IDEs with dev container support may also
|
||||
work.
|
||||
|
||||
## Port Forwarding
|
||||
## Port forwarding
|
||||
|
||||
During the early access phase, port forwarding is limited to ports defined via
|
||||
Since dev containers run as sub-agents, you can forward ports directly to them
|
||||
using standard Coder port forwarding:
|
||||
|
||||
```console
|
||||
coder port-forward <workspace>.<agent> --tcp 8080
|
||||
```
|
||||
|
||||
For example, to forward port 8080 from a dev container with agent name
|
||||
`my-project`:
|
||||
|
||||
```console
|
||||
coder port-forward my-workspace.my-project --tcp 8080
|
||||
```
|
||||
|
||||
This forwards port 8080 on your local machine directly to port 8080 in the dev
|
||||
container. Coder also automatically detects ports opened inside the container.
|
||||
|
||||
### Exposing ports on the parent workspace
|
||||
|
||||
If you need to expose dev container ports through the parent workspace agent
|
||||
(rather than the sub-agent), you can use the
|
||||
[`appPort`](https://containers.dev/implementors/json_reference/#image-specific)
|
||||
in your `devcontainer.json` file.
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> Support for automatic port forwarding via the `forwardPorts` property in
|
||||
> `devcontainer.json` is planned for a future release.
|
||||
|
||||
For example, with this `devcontainer.json` configuration:
|
||||
property in your `devcontainer.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"appPort": ["8080:8080", "4000:3000"]
|
||||
"appPort": ["8080:8080", "4000:3000"]
|
||||
}
|
||||
```
|
||||
|
||||
You can forward these ports to your local machine using:
|
||||
This maps container ports to the parent workspace, which can then be forwarded
|
||||
using the main workspace agent.
|
||||
|
||||
```console
|
||||
coder port-forward my-workspace --tcp 8080,4000
|
||||
```
|
||||
## Dev container features
|
||||
|
||||
This forwards port 8080 (local) -> 8080 (agent) -> 8080 (dev container) and port
|
||||
4000 (local) -> 4000 (agent) -> 3000 (dev container).
|
||||
|
||||
## Dev Container Features
|
||||
|
||||
You can use standard dev container features in your `devcontainer.json` file.
|
||||
Coder also maintains a
|
||||
You can use standard [dev container features](https://containers.dev/features)
|
||||
in your `devcontainer.json` file. Coder also maintains a
|
||||
[repository of features](https://github.com/coder/devcontainer-features) to
|
||||
enhance your development experience.
|
||||
|
||||
Currently available features include [code-server](https://github.com/coder/devcontainer-features/blob/main/src/code-server).
|
||||
|
||||
To use the code-server feature, add the following to your `devcontainer.json`:
|
||||
For example, the
|
||||
[code-server](https://github.com/coder/devcontainer-features/blob/main/src/code-server)
|
||||
feature from the [Coder features repository](https://github.com/coder/devcontainer-features):
|
||||
|
||||
```json
|
||||
{
|
||||
"features": {
|
||||
"ghcr.io/coder/devcontainer-features/code-server:1": {
|
||||
"port": 13337,
|
||||
"host": "0.0.0.0"
|
||||
}
|
||||
},
|
||||
"appPort": ["13337:13337"]
|
||||
"features": {
|
||||
"ghcr.io/coder/devcontainer-features/code-server:1": {
|
||||
"port": 13337,
|
||||
"host": "0.0.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> Remember to include the port in the `appPort` section to ensure proper port
|
||||
> forwarding.
|
||||
## Rebuilding dev containers
|
||||
|
||||
When you modify your `devcontainer.json`, you need to rebuild the container for
|
||||
changes to take effect. Coder detects changes and shows an **Outdated** status
|
||||
next to the dev container.
|
||||
|
||||
Click **Rebuild** to recreate your dev container with the updated configuration.
|
||||
|
||||
@@ -7,7 +7,7 @@ These are intended for end-user flows only. If you are an administrator, please
|
||||
refer to our docs on configuring [templates](../admin/index.md) or the
|
||||
[control plane](../admin/index.md).
|
||||
|
||||
Check out our [early access features](../install/releases/feature-stages.md) for upcoming
|
||||
functionality, including [Dev Containers integration](../user-guides/devcontainers/index.md).
|
||||
Check out [Dev Containers integration](./devcontainers/index.md) for running
|
||||
containerized development environments in your Coder workspace.
|
||||
|
||||
<children></children>
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"cdr.dev/slog"
|
||||
@@ -32,7 +33,11 @@ type Server struct {
|
||||
// A pool of [aibridge.RequestBridge] instances, which service incoming requests.
|
||||
requestBridgePool Pooler
|
||||
|
||||
// overloadProtection provides rate limiting and concurrency control.
|
||||
overloadProtection *OverloadProtection
|
||||
|
||||
logger slog.Logger
|
||||
tracer trace.Tracer
|
||||
wg sync.WaitGroup
|
||||
|
||||
// initConnectionCh will receive when the daemon connects to coderd for the
|
||||
@@ -48,7 +53,7 @@ type Server struct {
|
||||
shutdownOnce sync.Once
|
||||
}
|
||||
|
||||
func New(ctx context.Context, pool Pooler, rpcDialer Dialer, logger slog.Logger) (*Server, error) {
|
||||
func New(ctx context.Context, pool Pooler, rpcDialer Dialer, logger slog.Logger, tracer trace.Tracer, overloadCfg *OverloadConfig) (*Server, error) {
|
||||
if rpcDialer == nil {
|
||||
return nil, xerrors.Errorf("nil rpcDialer given")
|
||||
}
|
||||
@@ -56,6 +61,7 @@ func New(ctx context.Context, pool Pooler, rpcDialer Dialer, logger slog.Logger)
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
daemon := &Server{
|
||||
logger: logger,
|
||||
tracer: tracer,
|
||||
clientDialer: rpcDialer,
|
||||
clientCh: make(chan DRPCClient),
|
||||
lifecycleCtx: ctx,
|
||||
@@ -65,6 +71,16 @@ func New(ctx context.Context, pool Pooler, rpcDialer Dialer, logger slog.Logger)
|
||||
requestBridgePool: pool,
|
||||
}
|
||||
|
||||
// Initialize overload protection if configured.
|
||||
if overloadCfg != nil {
|
||||
daemon.overloadProtection = NewOverloadProtection(*overloadCfg, logger)
|
||||
logger.Info(ctx, "overload protection enabled",
|
||||
slog.F("max_concurrency", overloadCfg.MaxConcurrency),
|
||||
slog.F("rate_limit", overloadCfg.RateLimit),
|
||||
slog.F("rate_window", overloadCfg.RateWindow),
|
||||
)
|
||||
}
|
||||
|
||||
daemon.wg.Add(1)
|
||||
go daemon.connect()
|
||||
|
||||
@@ -143,7 +159,7 @@ func (s *Server) GetRequestHandler(ctx context.Context, req Request) (http.Handl
|
||||
return nil, xerrors.New("nil requestBridgePool")
|
||||
}
|
||||
|
||||
reqBridge, err := s.requestBridgePool.Acquire(ctx, req, s.Client, NewMCPProxyFactory(s.logger, s.Client))
|
||||
reqBridge, err := s.requestBridgePool.Acquire(ctx, req, s.Client, NewMCPProxyFactory(s.logger, s.tracer, s.Client))
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("acquire request bridge: %w", err)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"slices"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -13,8 +14,13 @@ import (
|
||||
promtest "github.com/prometheus/client_golang/prometheus/testutil"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tidwall/gjson"
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
||||
"go.opentelemetry.io/otel/sdk/trace/tracetest"
|
||||
|
||||
"github.com/coder/aibridge"
|
||||
aibtracing "github.com/coder/aibridge/tracing"
|
||||
"github.com/coder/coder/v2/coderd/coderdtest"
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/coderd/database/dbauthz"
|
||||
@@ -28,6 +34,8 @@ import (
|
||||
"github.com/coder/coder/v2/testutil"
|
||||
)
|
||||
|
||||
var testTracer = otel.Tracer("aibridged_test")
|
||||
|
||||
// TestIntegration is not an exhaustive test against the upstream AI providers' SDKs (see coder/aibridge for those).
|
||||
// This test validates that:
|
||||
// - intercepted requests can be authenticated/authorized
|
||||
@@ -35,11 +43,17 @@ import (
|
||||
// - responses can be returned as expected
|
||||
// - interceptions are logged, as well as their related prompt, token, and tool calls
|
||||
// - MCP server configurations are returned as expected
|
||||
// - tracing spans are properly recorded
|
||||
func TestIntegration(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
sr := tracetest.NewSpanRecorder()
|
||||
tp := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(sr))
|
||||
tracer := tp.Tracer(t.Name())
|
||||
defer func() { _ = tp.Shutdown(t.Context()) }()
|
||||
|
||||
// Create mock MCP server.
|
||||
var mcpTokenReceived string
|
||||
mockMCPServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -169,13 +183,13 @@ func TestIntegration(t *testing.T) {
|
||||
|
||||
logger := testutil.Logger(t)
|
||||
providers := []aibridge.Provider{aibridge.NewOpenAIProvider(aibridge.OpenAIConfig{BaseURL: mockOpenAI.URL})}
|
||||
pool, err := aibridged.NewCachedBridgePool(aibridged.DefaultPoolOptions, providers, nil, logger)
|
||||
pool, err := aibridged.NewCachedBridgePool(aibridged.DefaultPoolOptions, providers, logger, nil, tracer)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Given: aibridged is started.
|
||||
srv, err := aibridged.New(t.Context(), pool, func(ctx context.Context) (aibridged.DRPCClient, error) {
|
||||
return aiBridgeClient, nil
|
||||
}, logger)
|
||||
}, logger, tracer, nil)
|
||||
require.NoError(t, err, "create new aibridged")
|
||||
t.Cleanup(func() {
|
||||
_ = srv.Shutdown(ctx)
|
||||
@@ -256,6 +270,44 @@ func TestIntegration(t *testing.T) {
|
||||
|
||||
// Then: the MCP server was initialized.
|
||||
require.Contains(t, mcpTokenReceived, authLink.OAuthAccessToken, "mock MCP server not requested")
|
||||
|
||||
// Then: verify tracing spans were recorded.
|
||||
spans := sr.Ended()
|
||||
require.NotEmpty(t, spans)
|
||||
i := slices.IndexFunc(spans, func(s sdktrace.ReadOnlySpan) bool { return s.Name() == "CachedBridgePool.Acquire" })
|
||||
require.NotEqual(t, -1, i, "span named 'CachedBridgePool.Acquire' not found")
|
||||
|
||||
expectAttrs := []attribute.KeyValue{
|
||||
attribute.String(aibtracing.InitiatorID, user.ID.String()),
|
||||
attribute.String(aibtracing.APIKeyID, keyID),
|
||||
}
|
||||
require.Equal(t, spans[i].Attributes(), expectAttrs)
|
||||
|
||||
// Check for aibridge spans.
|
||||
spanNames := make(map[string]bool)
|
||||
for _, span := range spans {
|
||||
spanNames[span.Name()] = true
|
||||
}
|
||||
|
||||
expectedAibridgeSpans := []string{
|
||||
"CachedBridgePool.Acquire",
|
||||
"ServerProxyManager.Init",
|
||||
"StreamableHTTPServerProxy.Init",
|
||||
"StreamableHTTPServerProxy.Init.fetchTools",
|
||||
"Intercept",
|
||||
"Intercept.CreateInterceptor",
|
||||
"Intercept.RecordInterception",
|
||||
"Intercept.ProcessRequest",
|
||||
"Intercept.ProcessRequest.Upstream",
|
||||
"Intercept.RecordPromptUsage",
|
||||
"Intercept.RecordTokenUsage",
|
||||
"Intercept.RecordToolUsage",
|
||||
"Intercept.RecordInterceptionEnded",
|
||||
}
|
||||
|
||||
for _, expectedSpan := range expectedAibridgeSpans {
|
||||
require.Contains(t, spanNames, expectedSpan)
|
||||
}
|
||||
}
|
||||
|
||||
// TestIntegrationWithMetrics validates that Prometheus metrics are correctly incremented
|
||||
@@ -324,13 +376,13 @@ func TestIntegrationWithMetrics(t *testing.T) {
|
||||
providers := []aibridge.Provider{aibridge.NewOpenAIProvider(aibridge.OpenAIConfig{BaseURL: mockOpenAI.URL})}
|
||||
|
||||
// Create pool with metrics.
|
||||
pool, err := aibridged.NewCachedBridgePool(aibridged.DefaultPoolOptions, providers, metrics, logger)
|
||||
pool, err := aibridged.NewCachedBridgePool(aibridged.DefaultPoolOptions, providers, logger, metrics, testTracer)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Given: aibridged is started.
|
||||
srv, err := aibridged.New(ctx, pool, func(ctx context.Context) (aibridged.DRPCClient, error) {
|
||||
return aiBridgeClient, nil
|
||||
}, logger)
|
||||
}, logger, testTracer, nil)
|
||||
require.NoError(t, err, "create new aibridged")
|
||||
t.Cleanup(func() {
|
||||
_ = srv.Shutdown(ctx)
|
||||
|
||||
@@ -41,7 +41,7 @@ func newTestServer(t *testing.T) (*aibridged.Server, *mock.MockDRPCClient, *mock
|
||||
pool,
|
||||
func(ctx context.Context) (aibridged.DRPCClient, error) {
|
||||
return client, nil
|
||||
}, logger)
|
||||
}, logger, testTracer, nil)
|
||||
require.NoError(t, err, "create new aibridged")
|
||||
t.Cleanup(func() {
|
||||
srv.Shutdown(context.Background())
|
||||
@@ -290,7 +290,7 @@ func TestRouting(t *testing.T) {
|
||||
aibridge.NewOpenAIProvider(aibridge.OpenAIConfig{BaseURL: openaiSrv.URL}),
|
||||
aibridge.NewAnthropicProvider(aibridge.AnthropicConfig{BaseURL: antSrv.URL}, nil),
|
||||
}
|
||||
pool, err := aibridged.NewCachedBridgePool(aibridged.DefaultPoolOptions, providers, nil, logger)
|
||||
pool, err := aibridged.NewCachedBridgePool(aibridged.DefaultPoolOptions, providers, logger, nil, testTracer)
|
||||
require.NoError(t, err)
|
||||
conn := &mockDRPCConn{}
|
||||
client.EXPECT().DRPCConn().AnyTimes().Return(conn)
|
||||
@@ -309,7 +309,7 @@ func TestRouting(t *testing.T) {
|
||||
// Given: aibridged is started.
|
||||
srv, err := aibridged.New(t.Context(), pool, func(ctx context.Context) (aibridged.DRPCClient, error) {
|
||||
return client, nil
|
||||
}, logger)
|
||||
}, logger, testTracer, nil)
|
||||
require.NoError(t, err, "create new aibridged")
|
||||
t.Cleanup(func() {
|
||||
_ = srv.Shutdown(testutil.Context(t, testutil.WaitShort))
|
||||
|
||||
@@ -19,8 +19,19 @@ var (
|
||||
ErrConnect = xerrors.New("could not connect to coderd")
|
||||
ErrUnauthorized = xerrors.New("unauthorized")
|
||||
ErrAcquireRequestHandler = xerrors.New("failed to acquire request handler")
|
||||
ErrOverloaded = xerrors.New("server is overloaded")
|
||||
)
|
||||
|
||||
// Handler returns an http.Handler that wraps the server with any configured
|
||||
// overload protection (rate limiting and concurrency control).
|
||||
func (s *Server) Handler() http.Handler {
|
||||
var handler http.Handler = s
|
||||
if s.overloadProtection != nil {
|
||||
handler = s.overloadProtection.WrapHandler(handler)
|
||||
}
|
||||
return handler
|
||||
}
|
||||
|
||||
// ServeHTTP is the entrypoint for requests which will be intercepted by AI Bridge.
|
||||
// This function will validate that the given API key may be used to perform the request.
|
||||
//
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"cdr.dev/slog"
|
||||
@@ -28,30 +29,32 @@ type MCPProxyBuilder interface {
|
||||
// The SessionKey from [Request] is used to authenticate against the Coder MCP server.
|
||||
//
|
||||
// NOTE: the [mcp.ServerProxier] instance may be proxying one or more MCP servers.
|
||||
Build(ctx context.Context, req Request) (mcp.ServerProxier, error)
|
||||
Build(ctx context.Context, req Request, tracer trace.Tracer) (mcp.ServerProxier, error)
|
||||
}
|
||||
|
||||
var _ MCPProxyBuilder = &MCPProxyFactory{}
|
||||
|
||||
type MCPProxyFactory struct {
|
||||
logger slog.Logger
|
||||
tracer trace.Tracer
|
||||
clientFn ClientFunc
|
||||
}
|
||||
|
||||
func NewMCPProxyFactory(logger slog.Logger, clientFn ClientFunc) *MCPProxyFactory {
|
||||
func NewMCPProxyFactory(logger slog.Logger, tracer trace.Tracer, clientFn ClientFunc) *MCPProxyFactory {
|
||||
return &MCPProxyFactory{
|
||||
logger: logger,
|
||||
tracer: tracer,
|
||||
clientFn: clientFn,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MCPProxyFactory) Build(ctx context.Context, req Request) (mcp.ServerProxier, error) {
|
||||
func (m *MCPProxyFactory) Build(ctx context.Context, req Request, tracer trace.Tracer) (mcp.ServerProxier, error) {
|
||||
proxiers, err := m.retrieveMCPServerConfigs(ctx, req)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("resolve configs: %w", err)
|
||||
}
|
||||
|
||||
return mcp.NewServerProxyManager(proxiers), nil
|
||||
return mcp.NewServerProxyManager(proxiers, tracer), nil
|
||||
}
|
||||
|
||||
func (m *MCPProxyFactory) retrieveMCPServerConfigs(ctx context.Context, req Request) (map[string]mcp.ServerProxier, error) {
|
||||
@@ -173,7 +176,6 @@ func (m *MCPProxyFactory) newStreamableHTTPServerProxy(cfg *proto.MCPServerConfi
|
||||
// The proxy could then use its interface to retrieve a new access token and re-establish a connection.
|
||||
// For now though, the short TTL of this cache should mostly mask this problem.
|
||||
srv, err := mcp.NewStreamableHTTPServerProxy(
|
||||
m.logger.Named(fmt.Sprintf("mcp-server-proxy-%s", cfg.GetId())),
|
||||
cfg.GetId(),
|
||||
cfg.GetUrl(),
|
||||
// See https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization#token-requirements.
|
||||
@@ -182,6 +184,8 @@ func (m *MCPProxyFactory) newStreamableHTTPServerProxy(cfg *proto.MCPServerConfi
|
||||
},
|
||||
allowlist,
|
||||
denylist,
|
||||
m.logger.Named(fmt.Sprintf("mcp-server-proxy-%s", cfg.GetId())),
|
||||
m.tracer,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("create streamable HTTP MCP server proxy: %w", err)
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.opentelemetry.io/otel"
|
||||
|
||||
"github.com/coder/coder/v2/enterprise/aibridged/proto"
|
||||
"github.com/coder/coder/v2/testutil"
|
||||
@@ -42,7 +43,7 @@ func TestMCPRegex(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
logger := testutil.Logger(t)
|
||||
f := NewMCPProxyFactory(logger, nil)
|
||||
f := NewMCPProxyFactory(logger, otel.Tracer("aibridged_test"), nil)
|
||||
|
||||
_, err := f.newStreamableHTTPServerProxy(&proto.MCPServerConfig{
|
||||
Id: "mock",
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
package aibridged
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/httprate"
|
||||
|
||||
"cdr.dev/slog"
|
||||
"github.com/coder/coder/v2/coderd/httpapi"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
)
|
||||
|
||||
// OverloadConfig configures overload protection for the AI Bridge server.
|
||||
type OverloadConfig struct {
|
||||
// MaxConcurrency is the maximum number of concurrent requests allowed.
|
||||
// Set to 0 to disable concurrency limiting.
|
||||
MaxConcurrency int64
|
||||
|
||||
// RateLimit is the maximum number of requests per RateWindow.
|
||||
// Set to 0 to disable rate limiting.
|
||||
RateLimit int64
|
||||
|
||||
// RateWindow is the duration of the rate limiting window.
|
||||
RateWindow time.Duration
|
||||
}
|
||||
|
||||
// OverloadProtection provides middleware for protecting the AI Bridge server
|
||||
// from overload conditions.
|
||||
type OverloadProtection struct {
|
||||
config OverloadConfig
|
||||
logger slog.Logger
|
||||
|
||||
// concurrencyLimiter tracks the number of concurrent requests.
|
||||
currentConcurrency atomic.Int64
|
||||
|
||||
// rateLimiter is the rate limiting middleware.
|
||||
rateLimiter func(http.Handler) http.Handler
|
||||
}
|
||||
|
||||
// NewOverloadProtection creates a new OverloadProtection instance.
|
||||
func NewOverloadProtection(config OverloadConfig, logger slog.Logger) *OverloadProtection {
|
||||
op := &OverloadProtection{
|
||||
config: config,
|
||||
logger: logger.Named("overload"),
|
||||
}
|
||||
|
||||
// Initialize rate limiter if configured.
|
||||
if config.RateLimit > 0 && config.RateWindow > 0 {
|
||||
op.rateLimiter = httprate.Limit(
|
||||
int(config.RateLimit),
|
||||
config.RateWindow,
|
||||
httprate.WithKeyFuncs(httprate.KeyByIP),
|
||||
httprate.WithLimitHandler(func(w http.ResponseWriter, r *http.Request) {
|
||||
httpapi.Write(r.Context(), w, http.StatusTooManyRequests, codersdk.Response{
|
||||
Message: "AI Bridge rate limit exceeded. Please try again later.",
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
return op
|
||||
}
|
||||
|
||||
// ConcurrencyMiddleware returns a middleware that limits concurrent requests.
|
||||
// Returns nil if concurrency limiting is disabled.
|
||||
func (op *OverloadProtection) ConcurrencyMiddleware() func(http.Handler) http.Handler {
|
||||
if op.config.MaxConcurrency <= 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
current := op.currentConcurrency.Add(1)
|
||||
defer op.currentConcurrency.Add(-1)
|
||||
|
||||
if current > op.config.MaxConcurrency {
|
||||
op.logger.Warn(r.Context(), "ai bridge concurrency limit exceeded",
|
||||
slog.F("current", current),
|
||||
slog.F("max", op.config.MaxConcurrency),
|
||||
)
|
||||
httpapi.Write(r.Context(), w, http.StatusServiceUnavailable, codersdk.Response{
|
||||
Message: "AI Bridge is currently at capacity. Please try again later.",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// RateLimitMiddleware returns a middleware that limits the rate of requests.
|
||||
// Returns nil if rate limiting is disabled.
|
||||
func (op *OverloadProtection) RateLimitMiddleware() func(http.Handler) http.Handler {
|
||||
return op.rateLimiter
|
||||
}
|
||||
|
||||
// CurrentConcurrency returns the current number of concurrent requests.
|
||||
func (op *OverloadProtection) CurrentConcurrency() int64 {
|
||||
return op.currentConcurrency.Load()
|
||||
}
|
||||
|
||||
// WrapHandler wraps the given handler with all enabled overload protection
|
||||
// middleware.
|
||||
func (op *OverloadProtection) WrapHandler(handler http.Handler) http.Handler {
|
||||
// Apply rate limiting first (cheaper check).
|
||||
if op.rateLimiter != nil {
|
||||
handler = op.rateLimiter(handler)
|
||||
}
|
||||
|
||||
// Then apply concurrency limiting.
|
||||
if concurrencyMW := op.ConcurrencyMiddleware(); concurrencyMW != nil {
|
||||
handler = concurrencyMW(handler)
|
||||
}
|
||||
|
||||
return handler
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
package aibridged_test
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"cdr.dev/slog"
|
||||
"cdr.dev/slog/sloggers/slogtest"
|
||||
"github.com/coder/coder/v2/enterprise/aibridged"
|
||||
"github.com/coder/coder/v2/testutil"
|
||||
)
|
||||
|
||||
func TestOverloadProtection_ConcurrencyLimit(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).Leveled(slog.LevelDebug)
|
||||
|
||||
t.Run("allows_requests_within_limit", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
op := aibridged.NewOverloadProtection(aibridged.OverloadConfig{
|
||||
MaxConcurrency: 5,
|
||||
}, logger)
|
||||
|
||||
var handlerCalls atomic.Int32
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
handlerCalls.Add(1)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
wrapped := op.WrapHandler(handler)
|
||||
|
||||
// Make 5 requests in sequence - all should succeed.
|
||||
for i := 0; i < 5; i++ {
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
wrapped.ServeHTTP(rec, req)
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
}
|
||||
|
||||
assert.Equal(t, int32(5), handlerCalls.Load())
|
||||
})
|
||||
|
||||
t.Run("rejects_requests_over_limit", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
op := aibridged.NewOverloadProtection(aibridged.OverloadConfig{
|
||||
MaxConcurrency: 2,
|
||||
}, logger)
|
||||
|
||||
// Create a handler that blocks until we release it.
|
||||
blocked := make(chan struct{})
|
||||
var handlerCalls atomic.Int32
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
handlerCalls.Add(1)
|
||||
<-blocked
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
wrapped := op.WrapHandler(handler)
|
||||
|
||||
// Start 2 requests that will block.
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < 2; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
wrapped.ServeHTTP(rec, req)
|
||||
}()
|
||||
}
|
||||
|
||||
// Wait for the handlers to be called.
|
||||
require.Eventually(t, func() bool {
|
||||
return handlerCalls.Load() == 2
|
||||
}, testutil.WaitShort, testutil.IntervalFast)
|
||||
|
||||
// Make a third request - it should be rejected.
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
wrapped.ServeHTTP(rec, req)
|
||||
assert.Equal(t, http.StatusServiceUnavailable, rec.Code)
|
||||
|
||||
// Verify current concurrency is 2.
|
||||
assert.Equal(t, int64(2), op.CurrentConcurrency())
|
||||
|
||||
// Unblock the handlers.
|
||||
close(blocked)
|
||||
wg.Wait()
|
||||
|
||||
// Verify concurrency is back to 0.
|
||||
assert.Equal(t, int64(0), op.CurrentConcurrency())
|
||||
})
|
||||
|
||||
t.Run("disabled_when_zero", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
op := aibridged.NewOverloadProtection(aibridged.OverloadConfig{
|
||||
MaxConcurrency: 0, // Disabled.
|
||||
}, logger)
|
||||
|
||||
assert.Nil(t, op.ConcurrencyMiddleware())
|
||||
})
|
||||
}
|
||||
|
||||
func TestOverloadProtection_RateLimit(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).Leveled(slog.LevelDebug)
|
||||
|
||||
t.Run("allows_requests_within_limit", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
op := aibridged.NewOverloadProtection(aibridged.OverloadConfig{
|
||||
RateLimit: 5,
|
||||
RateWindow: time.Minute,
|
||||
}, logger)
|
||||
|
||||
var handlerCalls atomic.Int32
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
handlerCalls.Add(1)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
wrapped := op.WrapHandler(handler)
|
||||
|
||||
// Make 5 requests - all should succeed.
|
||||
for i := 0; i < 5; i++ {
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
wrapped.ServeHTTP(rec, req)
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
}
|
||||
|
||||
assert.Equal(t, int32(5), handlerCalls.Load())
|
||||
})
|
||||
|
||||
t.Run("rejects_requests_over_limit", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
op := aibridged.NewOverloadProtection(aibridged.OverloadConfig{
|
||||
RateLimit: 2,
|
||||
RateWindow: time.Minute,
|
||||
}, logger)
|
||||
|
||||
var handlerCalls atomic.Int32
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
handlerCalls.Add(1)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
wrapped := op.WrapHandler(handler)
|
||||
|
||||
// Make 3 requests - first 2 should succeed, 3rd should be rate limited.
|
||||
for i := 0; i < 3; i++ {
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
wrapped.ServeHTTP(rec, req)
|
||||
|
||||
if i < 2 {
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
} else {
|
||||
assert.Equal(t, http.StatusTooManyRequests, rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
assert.Equal(t, int32(2), handlerCalls.Load())
|
||||
})
|
||||
|
||||
t.Run("disabled_when_zero", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
op := aibridged.NewOverloadProtection(aibridged.OverloadConfig{
|
||||
RateLimit: 0, // Disabled.
|
||||
}, logger)
|
||||
|
||||
assert.Nil(t, op.RateLimitMiddleware())
|
||||
})
|
||||
}
|
||||
|
||||
func TestOverloadProtection_Combined(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).Leveled(slog.LevelDebug)
|
||||
|
||||
t.Run("both_limits_applied", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
op := aibridged.NewOverloadProtection(aibridged.OverloadConfig{
|
||||
MaxConcurrency: 10,
|
||||
RateLimit: 3,
|
||||
RateWindow: time.Minute,
|
||||
}, logger)
|
||||
|
||||
var handlerCalls atomic.Int32
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
handlerCalls.Add(1)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
wrapped := op.WrapHandler(handler)
|
||||
|
||||
// Make 4 requests - first 3 should succeed, 4th should be rate limited.
|
||||
for i := 0; i < 4; i++ {
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
wrapped.ServeHTTP(rec, req)
|
||||
|
||||
if i < 3 {
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
} else {
|
||||
assert.Equal(t, http.StatusTooManyRequests, rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
assert.Equal(t, int32(3), handlerCalls.Load())
|
||||
})
|
||||
}
|
||||
@@ -7,13 +7,15 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/dgraph-io/ristretto/v2"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
"golang.org/x/xerrors"
|
||||
"tailscale.com/util/singleflight"
|
||||
|
||||
"cdr.dev/slog"
|
||||
|
||||
"github.com/coder/aibridge"
|
||||
"github.com/coder/aibridge/mcp"
|
||||
"github.com/coder/aibridge/tracing"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -52,12 +54,13 @@ type CachedBridgePool struct {
|
||||
singleflight *singleflight.Group[string, *aibridge.RequestBridge]
|
||||
|
||||
metrics *aibridge.Metrics
|
||||
tracer trace.Tracer
|
||||
|
||||
shutDownOnce sync.Once
|
||||
shuttingDownCh chan struct{}
|
||||
}
|
||||
|
||||
func NewCachedBridgePool(options PoolOptions, providers []aibridge.Provider, metrics *aibridge.Metrics, logger slog.Logger) (*CachedBridgePool, error) {
|
||||
func NewCachedBridgePool(options PoolOptions, providers []aibridge.Provider, logger slog.Logger, metrics *aibridge.Metrics, tracer trace.Tracer) (*CachedBridgePool, error) {
|
||||
cache, err := ristretto.NewCache(&ristretto.Config[string, *aibridge.RequestBridge]{
|
||||
NumCounters: options.MaxItems * 10, // Docs suggest setting this 10x number of keys.
|
||||
MaxCost: options.MaxItems * cacheCost, // Up to n instances.
|
||||
@@ -85,13 +88,13 @@ func NewCachedBridgePool(options PoolOptions, providers []aibridge.Provider, met
|
||||
return &CachedBridgePool{
|
||||
cache: cache,
|
||||
providers: providers,
|
||||
logger: logger,
|
||||
options: options,
|
||||
metrics: metrics,
|
||||
tracer: tracer,
|
||||
logger: logger,
|
||||
|
||||
singleflight: &singleflight.Group[string, *aibridge.RequestBridge]{},
|
||||
|
||||
metrics: metrics,
|
||||
|
||||
shuttingDownCh: make(chan struct{}),
|
||||
}, nil
|
||||
}
|
||||
@@ -100,7 +103,15 @@ func NewCachedBridgePool(options PoolOptions, providers []aibridge.Provider, met
|
||||
//
|
||||
// Each returned [*aibridge.RequestBridge] is safe for concurrent use.
|
||||
// Each [*aibridge.RequestBridge] is stateful because it has MCP clients which maintain sessions to the configured MCP server.
|
||||
func (p *CachedBridgePool) Acquire(ctx context.Context, req Request, clientFn ClientFunc, mcpProxyFactory MCPProxyBuilder) (http.Handler, error) {
|
||||
func (p *CachedBridgePool) Acquire(ctx context.Context, req Request, clientFn ClientFunc, mcpProxyFactory MCPProxyBuilder) (_ http.Handler, outErr error) {
|
||||
spanAttrs := []attribute.KeyValue{
|
||||
attribute.String(tracing.InitiatorID, req.InitiatorID.String()),
|
||||
attribute.String(tracing.APIKeyID, req.APIKeyID),
|
||||
}
|
||||
ctx, span := p.tracer.Start(ctx, "CachedBridgePool.Acquire", trace.WithAttributes(spanAttrs...))
|
||||
defer tracing.EndSpanErr(span, &outErr)
|
||||
ctx = tracing.WithRequestBridgeAttributesInContext(ctx, spanAttrs)
|
||||
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, xerrors.Errorf("acquire: %w", err)
|
||||
}
|
||||
@@ -124,10 +135,12 @@ func (p *CachedBridgePool) Acquire(ctx context.Context, req Request, clientFn Cl
|
||||
// expire after the original TTL; we can extend the TTL on each Acquire() call.
|
||||
// For now, we need to let the instance expiry to keep the MCP connections fresh.
|
||||
|
||||
span.AddEvent("cache_hit")
|
||||
return bridge, nil
|
||||
}
|
||||
|
||||
recorder := aibridge.NewRecorder(p.logger.Named("recorder"), func() (aibridge.Recorder, error) {
|
||||
span.AddEvent("cache_miss")
|
||||
recorder := aibridge.NewRecorder(p.logger.Named("recorder"), p.tracer, func() (aibridge.Recorder, error) {
|
||||
client, err := clientFn()
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("acquire client: %w", err)
|
||||
@@ -145,7 +158,7 @@ func (p *CachedBridgePool) Acquire(ctx context.Context, req Request, clientFn Cl
|
||||
err error
|
||||
)
|
||||
|
||||
mcpServers, err = mcpProxyFactory.Build(ctx, req)
|
||||
mcpServers, err = mcpProxyFactory.Build(ctx, req, p.tracer)
|
||||
if err != nil {
|
||||
p.logger.Warn(ctx, "failed to create MCP server proxiers", slog.Error(err))
|
||||
// Don't fail here; MCP server injection can gracefully degrade.
|
||||
@@ -158,7 +171,7 @@ func (p *CachedBridgePool) Acquire(ctx context.Context, req Request, clientFn Cl
|
||||
}
|
||||
}
|
||||
|
||||
bridge, err := aibridge.NewRequestBridge(ctx, p.providers, recorder, mcpServers, p.metrics, p.logger)
|
||||
bridge, err := aibridge.NewRequestBridge(ctx, p.providers, recorder, mcpServers, p.logger, p.metrics, p.tracer)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("create new request bridge: %w", err)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
"go.uber.org/mock/gomock"
|
||||
|
||||
"cdr.dev/slog/sloggers/slogtest"
|
||||
@@ -30,7 +31,7 @@ func TestPool(t *testing.T) {
|
||||
mcpProxy := mcpmock.NewMockServerProxier(ctrl)
|
||||
|
||||
opts := aibridged.PoolOptions{MaxItems: 1, TTL: time.Second}
|
||||
pool, err := aibridged.NewCachedBridgePool(opts, nil, nil, logger)
|
||||
pool, err := aibridged.NewCachedBridgePool(opts, nil, logger, nil, testTracer)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { pool.Shutdown(context.Background()) })
|
||||
|
||||
@@ -120,6 +121,6 @@ func newMockMCPFactory(proxy *mcpmock.MockServerProxier) *mockMCPFactory {
|
||||
return &mockMCPFactory{proxy: proxy}
|
||||
}
|
||||
|
||||
func (m *mockMCPFactory) Build(ctx context.Context, req aibridged.Request) (mcp.ServerProxier, error) {
|
||||
func (m *mockMCPFactory) Build(ctx context.Context, req aibridged.Request, tracer trace.Tracer) (mcp.ServerProxier, error) {
|
||||
return m.proxy, nil
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
|
||||
"github.com/coder/aibridge"
|
||||
"github.com/coder/coder/v2/coderd/tracing"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/enterprise/aibridged"
|
||||
"github.com/coder/coder/v2/enterprise/coderd"
|
||||
@@ -35,17 +36,29 @@ func newAIBridgeDaemon(coderAPI *coderd.API) (*aibridged.Server, error) {
|
||||
|
||||
reg := prometheus.WrapRegistererWithPrefix("coder_aibridged_", coderAPI.PrometheusRegistry)
|
||||
metrics := aibridge.NewMetrics(reg)
|
||||
tracer := coderAPI.TracerProvider.Tracer(tracing.TracerName)
|
||||
|
||||
// Create pool for reusable stateful [aibridge.RequestBridge] instances (one per user).
|
||||
pool, err := aibridged.NewCachedBridgePool(aibridged.DefaultPoolOptions, providers, metrics, logger.Named("pool")) // TODO: configurable size.
|
||||
pool, err := aibridged.NewCachedBridgePool(aibridged.DefaultPoolOptions, providers, logger.Named("pool"), metrics, tracer) // TODO: configurable size.
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("create request pool: %w", err)
|
||||
}
|
||||
|
||||
// Configure overload protection if any limits are set.
|
||||
var overloadCfg *aibridged.OverloadConfig
|
||||
bridgeCfg := coderAPI.DeploymentValues.AI.BridgeConfig
|
||||
if bridgeCfg.MaxConcurrency.Value() > 0 || bridgeCfg.RateLimit.Value() > 0 {
|
||||
overloadCfg = &aibridged.OverloadConfig{
|
||||
MaxConcurrency: bridgeCfg.MaxConcurrency.Value(),
|
||||
RateLimit: bridgeCfg.RateLimit.Value(),
|
||||
RateWindow: bridgeCfg.RateWindow.Value(),
|
||||
}
|
||||
}
|
||||
|
||||
// Create daemon.
|
||||
srv, err := aibridged.New(ctx, pool, func(dialCtx context.Context) (aibridged.DRPCClient, error) {
|
||||
return coderAPI.CreateInMemoryAIBridgeServer(dialCtx)
|
||||
}, logger)
|
||||
}, logger, tracer, overloadCfg)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("start in-memory aibridge daemon: %w", err)
|
||||
}
|
||||
|
||||
@@ -119,12 +119,23 @@ AI BRIDGE OPTIONS:
|
||||
requests (requires the "oauth2" and "mcp-server-http" experiments to
|
||||
be enabled).
|
||||
|
||||
--aibridge-max-concurrency int, $CODER_AIBRIDGE_MAX_CONCURRENCY (default: 0)
|
||||
Maximum number of concurrent AI Bridge requests. Set to 0 to disable
|
||||
(unlimited).
|
||||
|
||||
--aibridge-openai-base-url string, $CODER_AIBRIDGE_OPENAI_BASE_URL (default: https://api.openai.com/v1/)
|
||||
The base URL of the OpenAI API.
|
||||
|
||||
--aibridge-openai-key string, $CODER_AIBRIDGE_OPENAI_KEY
|
||||
The key to authenticate against the OpenAI API.
|
||||
|
||||
--aibridge-rate-limit int, $CODER_AIBRIDGE_RATE_LIMIT (default: 0)
|
||||
Maximum number of AI Bridge requests per rate window. Set to 0 to
|
||||
disable rate limiting.
|
||||
|
||||
--aibridge-rate-window duration, $CODER_AIBRIDGE_RATE_WINDOW (default: 1m)
|
||||
Duration of the rate limiting window for AI Bridge requests.
|
||||
|
||||
CLIENT OPTIONS:
|
||||
These options change the behavior of how clients interact with the Coder.
|
||||
Clients include the Coder CLI, Coder Desktop, IDE extensions, and the web UI.
|
||||
|
||||
@@ -59,7 +59,7 @@ var Keys = map[string]ed25519.PublicKey{"2022-08-12": ed25519.PublicKey(key20220
|
||||
// @Security CoderSessionToken
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Tags Organizations
|
||||
// @Tags Enterprise
|
||||
// @Param request body codersdk.AddLicenseRequest true "Add license request"
|
||||
// @Success 201 {object} codersdk.License
|
||||
// @Router /licenses [post]
|
||||
@@ -163,7 +163,7 @@ func (api *API) postLicense(rw http.ResponseWriter, r *http.Request) {
|
||||
// @ID update-license-entitlements
|
||||
// @Security CoderSessionToken
|
||||
// @Produce json
|
||||
// @Tags Organizations
|
||||
// @Tags Enterprise
|
||||
// @Success 201 {object} codersdk.Response
|
||||
// @Router /licenses/refresh-entitlements [post]
|
||||
func (api *API) postRefreshEntitlements(rw http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -474,9 +474,9 @@ require (
|
||||
|
||||
require (
|
||||
github.com/anthropics/anthropic-sdk-go v1.19.0
|
||||
github.com/brianvoe/gofakeit/v7 v7.9.0
|
||||
github.com/brianvoe/gofakeit/v7 v7.12.1
|
||||
github.com/coder/agentapi-sdk-go v0.0.0-20250505131810-560d1d88d225
|
||||
github.com/coder/aibridge v0.2.2
|
||||
github.com/coder/aibridge v0.3.0
|
||||
github.com/coder/aisdk-go v0.0.9
|
||||
github.com/coder/boundary v1.0.1-0.20250925154134-55a44f2a7945
|
||||
github.com/coder/preview v1.0.4
|
||||
|
||||
@@ -844,8 +844,8 @@ github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl
|
||||
github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
github.com/bramvdbogaerde/go-scp v1.5.0 h1:a9BinAjTfQh273eh7vd3qUgmBC+bx+3TRDtkZWmIpzM=
|
||||
github.com/bramvdbogaerde/go-scp v1.5.0/go.mod h1:on2aH5AxaFb2G0N5Vsdy6B0Ml7k9HuHSwfo1y0QzAbQ=
|
||||
github.com/brianvoe/gofakeit/v7 v7.9.0 h1:6NsaMy9D5ZKVwIZ1V8L//J2FrOF3546FcXDElWLx994=
|
||||
github.com/brianvoe/gofakeit/v7 v7.9.0/go.mod h1:QXuPeBw164PJCzCUZVmgpgHJ3Llj49jSLVkKPMtxtxA=
|
||||
github.com/brianvoe/gofakeit/v7 v7.12.1 h1:df1tiI4SL1dR5Ix4D/r6a3a+nXBJ/OBGU5jEKRBmmqg=
|
||||
github.com/brianvoe/gofakeit/v7 v7.12.1/go.mod h1:QXuPeBw164PJCzCUZVmgpgHJ3Llj49jSLVkKPMtxtxA=
|
||||
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
|
||||
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
|
||||
github.com/bytecodealliance/wasmtime-go/v3 v3.0.2 h1:3uZCA/BLTIu+DqCfguByNMJa2HVHpXvjfy0Dy7g6fuA=
|
||||
@@ -919,8 +919,8 @@ github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f h1:Y8xYupdHxryycyPlc9Y
|
||||
github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f/go.mod h1:HlzOvOjVBOfTGSRXRyY0OiCS/3J1akRGQQpRO/7zyF4=
|
||||
github.com/coder/agentapi-sdk-go v0.0.0-20250505131810-560d1d88d225 h1:tRIViZ5JRmzdOEo5wUWngaGEFBG8OaE1o2GIHN5ujJ8=
|
||||
github.com/coder/agentapi-sdk-go v0.0.0-20250505131810-560d1d88d225/go.mod h1:rNLVpYgEVeu1Zk29K64z6Od8RBP9DwqCu9OfCzh8MR4=
|
||||
github.com/coder/aibridge v0.2.2 h1:bTsz4nBD3znxkgrzzz7D5Ymx9nFUfvR500kevVN3xrA=
|
||||
github.com/coder/aibridge v0.2.2/go.mod h1:2T0RSnIX1WTqFajzXsaNsoNe6mmNsNeCTxiHBWEsFnE=
|
||||
github.com/coder/aibridge v0.3.0 h1:z5coky9A5uXOr+zjgmsynal8PVYBMmxE9u1vcIzs4t8=
|
||||
github.com/coder/aibridge v0.3.0/go.mod h1:ENnl6VhU8Qot5OuVYqs7V4vXII11oKBWgWKrgIJbRAs=
|
||||
github.com/coder/aisdk-go v0.0.9 h1:Vzo/k2qwVGLTR10ESDeP2Ecek1SdPfZlEjtTfMveiVo=
|
||||
github.com/coder/aisdk-go v0.0.9/go.mod h1:KF6/Vkono0FJJOtWtveh5j7yfNrSctVTpwgweYWSp5M=
|
||||
github.com/coder/boundary v1.0.1-0.20250925154134-55a44f2a7945 h1:hDUf02kTX8EGR3+5B+v5KdYvORs4YNfDPci0zCs+pC0=
|
||||
|
||||
@@ -145,6 +145,8 @@ type outExpecter struct {
|
||||
runeReader *bufio.Reader
|
||||
}
|
||||
|
||||
// Deprecated: use ExpectMatchContext instead.
|
||||
// This uses a background context, so will not respect the test's context.
|
||||
func (e *outExpecter) ExpectMatch(str string) string {
|
||||
return e.expectMatchContextFunc(str, e.ExpectMatchContext)
|
||||
}
|
||||
|
||||
+2
-2
@@ -93,11 +93,11 @@
|
||||
"lucide-react": "0.555.0",
|
||||
"monaco-editor": "0.55.1",
|
||||
"pretty-bytes": "6.1.1",
|
||||
"react": "19.2.0",
|
||||
"react": "19.2.1",
|
||||
"react-color": "2.19.3",
|
||||
"react-confetti": "6.4.0",
|
||||
"react-date-range": "1.4.0",
|
||||
"react-dom": "19.2.0",
|
||||
"react-dom": "19.2.1",
|
||||
"react-markdown": "9.1.0",
|
||||
"react-query": "npm:@tanstack/react-query@5.77.0",
|
||||
"react-resizable-panels": "3.0.6",
|
||||
|
||||
Generated
+516
-516
File diff suppressed because it is too large
Load Diff
Generated
+6
@@ -33,6 +33,12 @@ export interface AIBridgeConfig {
|
||||
readonly bedrock: AIBridgeBedrockConfig;
|
||||
readonly inject_coder_mcp_tools: boolean;
|
||||
readonly retention: number;
|
||||
/**
|
||||
* Overload protection settings.
|
||||
*/
|
||||
readonly max_concurrency: number;
|
||||
readonly rate_limit: number;
|
||||
readonly rate_window: number;
|
||||
}
|
||||
|
||||
// From codersdk/aibridge.go
|
||||
|
||||
@@ -24,6 +24,7 @@ const ALLOWED_EXTERNAL_APP_PROTOCOLS = [
|
||||
"jetbrains:",
|
||||
"kiro:",
|
||||
"positron:",
|
||||
"antigravity:",
|
||||
];
|
||||
|
||||
type GetVSCodeHrefParams = {
|
||||
|
||||
+55
-34
@@ -24,16 +24,27 @@
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:root {
|
||||
--color-white: #FFFFFF;
|
||||
--color-warning: #F3722C;
|
||||
--color-surface-primary: #090B0B;
|
||||
--color-zinc-900: #18181B;
|
||||
--color-zinc-400: #A1A1AA;
|
||||
--color-zinc-800: #27272A;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
background-color: #05060b;
|
||||
color: #a1a1aa;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: sans-serif;
|
||||
font-size: 16px;
|
||||
height: 100%;
|
||||
|
||||
background: var(--color-surface-primary);
|
||||
color: var(--color-zinc-400);
|
||||
|
||||
font-family: system-ui, sans-serif;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
header {
|
||||
@@ -41,37 +52,38 @@
|
||||
}
|
||||
|
||||
.container {
|
||||
--side-padding: 24px;
|
||||
--side-padding: 1.5rem;
|
||||
width: 100%;
|
||||
max-width: calc(500px + var(--side-padding) * 2);
|
||||
max-width: calc(31.25rem + var(--side-padding) * 2);
|
||||
padding: 0 var(--side-padding);
|
||||
}
|
||||
|
||||
.coder-svg {
|
||||
width: 80px;
|
||||
margin-bottom: 24px;
|
||||
width: 5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
margin-bottom: 8px;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 400;
|
||||
color: white;
|
||||
color: var(--color-white);
|
||||
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
p,
|
||||
li {
|
||||
line-height: 140%;
|
||||
font-size: 14px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
margin-top: 24px;
|
||||
gap: 0.75rem;
|
||||
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.button-group a,
|
||||
@@ -79,29 +91,37 @@
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 6px 16px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #2c3854;
|
||||
text-decoration: none;
|
||||
|
||||
padding: 0.5rem 1rem;
|
||||
|
||||
border-radius: 0.375rem;
|
||||
border: 1px solid var(--color-zinc-800);
|
||||
background: none;
|
||||
font-size: inherit;
|
||||
color: inherit;
|
||||
width: 200px;
|
||||
height: 42px;
|
||||
|
||||
color: var(--color-white);
|
||||
text-decoration: none;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 400;
|
||||
|
||||
width: 12.5rem;
|
||||
height: 2.5rem;
|
||||
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.button-group a:hover,
|
||||
.button-group button:hover {
|
||||
border-color: hsl(222, 31%, 40%);
|
||||
background-color: var(--color-zinc-900);
|
||||
border-color: var(--color-zinc-800);
|
||||
}
|
||||
|
||||
.warning {
|
||||
margin-top: 24px;
|
||||
border: 1px solid rgb(243, 140, 89);
|
||||
background: rgb(13, 19, 33);
|
||||
border: 1px solid var(--color-warning);
|
||||
background: var(--color-zinc-900);
|
||||
width: 100%;
|
||||
padding: 24px;
|
||||
|
||||
margin: 1.5rem 0 0;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.warning-title {
|
||||
@@ -111,15 +131,16 @@
|
||||
}
|
||||
|
||||
.warning-title h3 {
|
||||
margin-left: 12px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: white;
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
color: var(--color-white);
|
||||
|
||||
margin-left: 0.75rem;
|
||||
}
|
||||
|
||||
.warning li {
|
||||
padding-top: 10px;
|
||||
margin-left: 30px;
|
||||
padding-top: 0.625rem;
|
||||
margin-left: 1.875rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
Reference in New Issue
Block a user