mirror of
https://github.com/docker/docker-agent.git
synced 2026-11-04 10:02:15 +00:00
feat(sandbox): support runtime.network_allowlist in agent config
Agents that talk to endpoints the auto-installer can't infer (custom
MCP servers, third-party APIs, registries not covered by the aqua
resolver, ...) saw a default-deny 403 from the sandbox proxy on
first contact, then had to fall back to the wider conservative host
union the kit uses on resolution failures.
Let agent authors declare those hosts explicitly:
runtime:
sandbox: true
network_allowlist:
- api.example.com
- registry.npmjs.org
The list is unioned with the gateway and tool-install hosts the
runner already opens automatically and printed before launch so
users can audit exactly which holes the run punched in the
default-deny policy. Commas and whitespace are rejected so a single
entry can't smuggle several rules into the policy engine.
This commit is contained in:
6 files changed
+159
-11
No files matched your search
@@ -1410,6 +1410,13 @@
|
||||
"sandbox": {
|
||||
"type": "boolean",
|
||||
"description": "When true, run the agent inside a Docker sandbox by default — equivalent to passing --sandbox on the command line. An explicit --sandbox=false on the CLI still wins."
|
||||
},
|
||||
"network_allowlist": {
|
||||
"type": "array",
|
||||
"description": "Hosts to add to the sandbox's default-deny network proxy when this agent runs in a sandbox. Each entry is a hostname with an optional ':port' suffix (e.g. 'api.example.com', 'registry.npmjs.org:443'). Unioned with the gateway and tool-install hosts the runner already opens automatically. Use this for hosts the auto-installer can't infer (custom MCP endpoints, third-party APIs, registries not covered by the aqua resolver) instead of relying on the wider fallback host set.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
|
||||
+56
-11
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
"github.com/docker/docker-agent/pkg/config"
|
||||
latestcfg "github.com/docker/docker-agent/pkg/config/latest"
|
||||
"github.com/docker/docker-agent/pkg/environment"
|
||||
"github.com/docker/docker-agent/pkg/paths"
|
||||
"github.com/docker/docker-agent/pkg/sandbox"
|
||||
@@ -31,18 +32,38 @@ import (
|
||||
// returns false so the caller falls through to the normal path,
|
||||
// which will surface a proper error from the eventual load.
|
||||
func peekAgentSandbox(ctx context.Context, agentRef string) bool {
|
||||
cfg := loadAgentConfig(ctx, agentRef)
|
||||
return cfg != nil && cfg.Runtime != nil && cfg.Runtime.Sandbox
|
||||
}
|
||||
|
||||
// agentNetworkAllowlist returns the hostnames the agent declared in
|
||||
// runtime.network_allowlist. Same best-effort contract as
|
||||
// peekAgentSandbox: any failure to load the config returns nil so
|
||||
// the run continues with the inferred host set only.
|
||||
func agentNetworkAllowlist(ctx context.Context, agentRef string) []string {
|
||||
cfg := loadAgentConfig(ctx, agentRef)
|
||||
if cfg == nil || cfg.Runtime == nil {
|
||||
return nil
|
||||
}
|
||||
return cfg.Runtime.NetworkAllowlist
|
||||
}
|
||||
|
||||
// loadAgentConfig is the shared best-effort loader behind
|
||||
// peekAgentSandbox / agentNetworkAllowlist. Returns nil on any
|
||||
// resolve or load failure.
|
||||
func loadAgentConfig(ctx context.Context, agentRef string) *latestcfg.Config {
|
||||
if agentRef == "" {
|
||||
return false
|
||||
return nil
|
||||
}
|
||||
source, err := config.Resolve(agentRef, nil)
|
||||
if err != nil {
|
||||
return false
|
||||
return nil
|
||||
}
|
||||
cfg, err := config.Load(ctx, source)
|
||||
if err != nil {
|
||||
return false
|
||||
return nil
|
||||
}
|
||||
return cfg.Runtime != nil && cfg.Runtime.Sandbox
|
||||
return cfg
|
||||
}
|
||||
|
||||
// runInSandbox delegates the current command to a Docker sandbox.
|
||||
@@ -105,8 +126,11 @@ func runInSandbox(ctx context.Context, cmd *cobra.Command, args []string, runCon
|
||||
}
|
||||
}
|
||||
|
||||
agentHosts := agentNetworkAllowlist(ctx, agentRef)
|
||||
|
||||
printModelsGateway(cmd.OutOrStdout(), runConfig.ModelsGateway)
|
||||
printToolInstallAllowance(cmd.OutOrStdout(), kitResult)
|
||||
printAgentNetworkAllowlist(cmd.OutOrStdout(), agentHosts)
|
||||
|
||||
name, err := backend.Ensure(ctx, wd, extras, template, configDir)
|
||||
if err != nil {
|
||||
@@ -126,7 +150,7 @@ func runInSandbox(ctx context.Context, cmd *cobra.Command, args []string, runCon
|
||||
if kitResult != nil {
|
||||
toolHosts = kitResult.ToolInstallHosts
|
||||
}
|
||||
allowSandboxHosts(ctx, backend, name, runConfig.ModelsGateway, toolHosts)
|
||||
allowSandboxHosts(ctx, backend, name, runConfig.ModelsGateway, toolHosts, agentHosts)
|
||||
|
||||
// Resolve env vars the agent needs and forward them into the sandbox.
|
||||
// Docker Desktop proxies well-known API keys automatically; this handles
|
||||
@@ -202,11 +226,12 @@ func dockerAgentArgs(cmd *cobra.Command, args []string, configDir string) []stri
|
||||
|
||||
// allowSandboxHosts adds per-sandbox allow-network rules for every
|
||||
// host the in-sandbox runtime is known to need: the configured
|
||||
// models gateway (when set) and the package hosts the auto-installer
|
||||
// models gateway (when set), the package hosts the auto-installer
|
||||
// reaches for (when the kit build identified at least one
|
||||
// auto-installable toolset). The default sandbox proxy denies all of
|
||||
// them; without this, the inner agent's first request returns a
|
||||
// misleading "403 Blocked by network policy".
|
||||
// auto-installable toolset), and any extra hosts the agent author
|
||||
// declared in runtime.network_allowlist. The default sandbox proxy
|
||||
// denies all of them; without this, the inner agent's first request
|
||||
// returns a misleading "403 Blocked by network policy".
|
||||
//
|
||||
// Holes are punched only when the corresponding feature is in play:
|
||||
// - the gateway host is added only when gatewayURL is non-empty;
|
||||
@@ -216,15 +241,20 @@ func dockerAgentArgs(cmd *cobra.Command, args []string, configDir string) []stri
|
||||
// module proxy + toolchain bootstrap for go_install packages,
|
||||
// GitHub release hosts for github_release packages). When a
|
||||
// lookup failed, the kit folds in [toolinstall.FallbackHosts]
|
||||
// so the run can still succeed.
|
||||
// so the run can still succeed;
|
||||
// - the agent-declared hosts come straight from the YAML and are
|
||||
// unioned with the inferred set so authors can add hosts the
|
||||
// resolver doesn't know about (custom MCP endpoints, third-party
|
||||
// APIs, ...).
|
||||
//
|
||||
// Best-effort: a malformed gateway URL or a backend that doesn't
|
||||
// support per-sandbox policies is logged at debug level and the run
|
||||
// proceeds. The user will then see a network-policy 403 from the
|
||||
// inner and we surface that diagnostic verbatim.
|
||||
func allowSandboxHosts(ctx context.Context, backend *sandbox.Backend, name, gatewayURL string, toolInstallHosts []string) {
|
||||
func allowSandboxHosts(ctx context.Context, backend *sandbox.Backend, name, gatewayURL string, toolInstallHosts, agentHosts []string) {
|
||||
var hosts []string
|
||||
hosts = append(hosts, toolInstallHosts...)
|
||||
hosts = append(hosts, agentHosts...)
|
||||
|
||||
if gatewayURL != "" {
|
||||
if h := gatewayHostPort(gatewayURL); h != "" {
|
||||
@@ -426,3 +456,18 @@ func printToolInstallAllowance(w io.Writer, kitResult *kit.Result) {
|
||||
fmt.Fprintf(w, " ! %s (using fallback host set)\n", e.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// printAgentNetworkAllowlist prints the host(s) the agent's config
|
||||
// asked us to add to the sandbox proxy. Surfacing them next to the
|
||||
// kit / gateway lines makes it obvious which holes were punched by
|
||||
// the agent author vs auto-discovered, so an unexpected 403 has a
|
||||
// short list of suspects.
|
||||
func printAgentNetworkAllowlist(w io.Writer, hosts []string) {
|
||||
if len(hosts) == 0 {
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(w, "Agent network allowlist: allowlisting %d host(s) declared in runtime.network_allowlist:\n", len(hosts))
|
||||
for _, h := range hosts {
|
||||
fmt.Fprintf(w, " - %s\n", h)
|
||||
}
|
||||
}
|
||||
@@ -295,3 +295,51 @@ func TestPeekAgentSandbox(t *testing.T) {
|
||||
assert.False(t, peekAgentSandbox(t.Context(), "/nonexistent/agent.yaml"))
|
||||
})
|
||||
}
|
||||
|
||||
func TestAgentNetworkAllowlist(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "agent.yaml")
|
||||
yamlBody := "runtime:\n" +
|
||||
" network_allowlist:\n" +
|
||||
" - api.example.com\n" +
|
||||
" - registry.npmjs.org:443\n" +
|
||||
"agents:\n root:\n model: openai/gpt-4o\n description: t\n instruction: t\n"
|
||||
require.NoError(t, os.WriteFile(path, []byte(yamlBody), 0o600))
|
||||
|
||||
assert.Equal(t, []string{"api.example.com", "registry.npmjs.org:443"}, agentNetworkAllowlist(t.Context(), path))
|
||||
}
|
||||
|
||||
func TestPrintAgentNetworkAllowlist(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
hosts []string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
hosts: nil,
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "single host",
|
||||
hosts: []string{"api.example.com"},
|
||||
want: "Agent network allowlist: allowlisting 1 host(s) declared in runtime.network_allowlist:\n" +
|
||||
" - api.example.com\n",
|
||||
},
|
||||
{
|
||||
name: "multiple",
|
||||
hosts: []string{"a.example.com", "b.example.com"},
|
||||
want: "Agent network allowlist: allowlisting 2 host(s) declared in runtime.network_allowlist:\n" +
|
||||
" - a.example.com\n" +
|
||||
" - b.example.com\n",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var buf strings.Builder
|
||||
printAgentNetworkAllowlist(&buf, tt.hosts)
|
||||
assert.Equal(t, tt.want, buf.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -92,6 +92,31 @@ The rule is the same as for aliases: an explicit `--sandbox=false` on the
|
||||
CLI overrides the config default, so you can debug an agent on the host
|
||||
without editing its YAML.
|
||||
|
||||
### Declare a network allowlist
|
||||
|
||||
The runner already opens the [tool install hosts](#network-allowlist) and
|
||||
the [models gateway](#how-it-works) automatically, but agents that talk
|
||||
to endpoints those resolvers can't infer (custom MCP servers, third-party
|
||||
APIs, registries not covered by the aqua resolver) would still see a 403
|
||||
from the sandbox proxy on first contact.
|
||||
|
||||
Declare those hosts in `runtime.network_allowlist` and they are unioned
|
||||
with the inferred set, so the agent can reach them on its first request:
|
||||
|
||||
```yaml
|
||||
# agent.yaml
|
||||
runtime:
|
||||
sandbox: true
|
||||
network_allowlist:
|
||||
- api.example.com
|
||||
- registry.npmjs.org
|
||||
```
|
||||
|
||||
Each entry is a hostname with an optional `:port` suffix. Commas and
|
||||
whitespace are rejected to keep a single entry from smuggling several
|
||||
rules into the policy engine. The runner prints the resulting allowlist
|
||||
before launch so you can audit exactly which hosts the run opens up.
|
||||
|
||||
## Example
|
||||
|
||||
```yaml
|
||||
|
||||
@@ -3,6 +3,14 @@ runtime:
|
||||
# to remember --sandbox on the command line. An explicit
|
||||
# --sandbox=false on the CLI still wins.
|
||||
sandbox: true
|
||||
# Hosts the agent's tools need to reach. Unioned with the
|
||||
# auto-discovered tool-install hosts and the configured models
|
||||
# gateway, then added to the sandbox proxy's default-deny
|
||||
# allowlist. Use this for endpoints the aqua-based resolver
|
||||
# can't infer.
|
||||
network_allowlist:
|
||||
- api.example.com
|
||||
- registry.npmjs.org
|
||||
|
||||
agents:
|
||||
root:
|
||||
|
||||
@@ -39,6 +39,21 @@ type RuntimeDefaults struct {
|
||||
// default — equivalent to passing --sandbox on the command line.
|
||||
// Useful for agents that always need filesystem/network isolation.
|
||||
Sandbox bool `json:"sandbox,omitempty" yaml:"sandbox,omitempty"`
|
||||
|
||||
// NetworkAllowlist is the list of hosts that should be added to
|
||||
// the sandbox's default-deny network proxy when this agent runs in
|
||||
// a sandbox. Each entry is a hostname with an optional ":port"
|
||||
// suffix (e.g. "api.example.com", "registry.npmjs.org:443"). The
|
||||
// list is unioned with the gateway and tool-install hosts the
|
||||
// runner already opens automatically; commas and whitespace are
|
||||
// rejected to keep a single entry from smuggling several rules
|
||||
// into the policy engine.
|
||||
//
|
||||
// Use this when an agent's tools call hosts the auto-installer
|
||||
// can't infer (custom MCP endpoints, third-party APIs, registries
|
||||
// not covered by the aqua resolver, etc.) instead of relying on
|
||||
// the wider fallback host set the kit uses on resolution failures.
|
||||
NetworkAllowlist []string `json:"network_allowlist,omitempty" yaml:"network_allowlist,omitempty"`
|
||||
}
|
||||
|
||||
// MCPToolset is a reusable MCP server definition stored in the top-level
|
||||
|
||||
Reference in new issue
Block a user