These docs track the main branch and may describe unreleased features. The stable documentation lives at docs.docker.com.

Contributing

Docker Agent is open source. Here's how to set up your development environment and contribute.

Development Setup

Prerequisites

Platform Support

macOS and Linux are fully supported for development. On Windows, use task build-local to build via Docker.

Build from Source

# Clone and build
git clone https://github.com/docker/docker-agent.git
cd docker-agent
task build

# Set API keys
export OPENAI_API_KEY=your_key_here
export ANTHROPIC_API_KEY=your_key_here

# Run an example
./bin/docker-agent run examples/code.yaml

Development Commands

Command Description
task build Build the binary to ./bin/docker-agent
task test Run all tests (clears API keys for determinism)
task lint Run golangci-lint, custom checks, and module tidiness checks
task format Format code
task dev Run lint, test, and build in parallel
task build-local Build for local platform via Docker
task cross Cross-platform builds (all architectures)

Debugging TUI Tests

The headless TUI harness can save each captured frame for inspection:

mkdir -p .cache/tui-test
go test -count=1 -v -artifacts -outputdir="$PWD/.cache/tui-test" ./e2e/tui -tuitest.frames

-tuitest.frames writes numbered text files in a separate frames-* directory for each driver, under the test's testing.TB.ArtifactDir(). The test log prints the exact path. -artifacts retains these directories after the tests finish; without it, dumps are temporary and removed during test cleanup. Use -count=1 to capture fresh frames rather than reuse a cached test result.

With the command above, retained dumps are under .cache/tui-test/_artifacts/. For CI jobs that enable frame dumping, upload this directory even when tests fail. Dumps are no longer written alongside golden files in testdata/frames; -tuitest.update still updates golden files in testdata/.

For an approximate live view instead, use go test -v ./e2e/tui -tuitest.live.

Dogfooding

Use Docker Agent to work on Docker Agent! The project includes a specialized developer agent:

cd docker-agent
docker agent run ./golang_developer.yaml

This agent is an expert Go developer that understands the Docker Agent codebase. Ask it questions, request fixes, or have it implement features.

Core Concepts

Code Style

The project uses golangci-lint with strict rules. As long as task lint passes, the code is stylistically acceptable.

Key conventions:

Profiling the lint runner

Benchmark the full command with a warm Go build cache:

hyperfine --warmup 1 --runs 5 'go run ./lint .'

For CPU and allocation profiles of the in-process runner:

mkdir -p .cache/lint-profile
go test ./lint -run '^$' -bench '^BenchmarkLintRepository$' -benchtime=1x \
  -cpuprofile="$PWD/.cache/lint-profile/cpu.pprof" \
  -memprofile="$PWD/.cache/lint-profile/mem.pprof" \
  -o "$PWD/.cache/lint-profile/lint.test"
go tool pprof -top .cache/lint-profile/cpu.pprof
go tool pprof -alloc_space -top .cache/lint-profile/mem.pprof

BenchmarkChannelDirection isolates channel analysis on already-loaded packages. Go benchmark timings exclude compilation and may benefit from in-process caches; use the full command to confirm end-to-end improvements. Allocation totals are cumulative, not peak memory, and profiles do not include go/packages subprocesses.

Lint rules

task lint runs the shared and project-specific cops selected in lint/main.go. Reusable checks come from rubocop-go (pinned in go.mod); project-specific checks and frozen-config exclusions stay in lint/. Cop IDs and //rubocop:disable annotations are unchanged. Add shared checks by their constructors, not by enabling the entire upstream catalog. 40 of the 42 shared opt-in cops are currently selected. Lint/ContextFirstParameter and Lint/NoContextField remain disabled: existing public constructor signatures and intentionally owned lifecycle or telemetry contexts would require suppressions or unrelated refactoring.

Execution adapters and candidate prefilters also come from rubocop-go. runner.WithDirectoryTypes retains inactive and partially typed constructor checks; prog.FromFileWithTests shares the test-package cache for slice checks. The runner opts into WithSharedProgramLoad to share production/test type-checking. Program cops must keep source files, package data, and the build environment unchanged during analysis; file-cop coverage still includes inactive files.

CancellationLoop, ClosedChannelLoop, ChannelStateCheck, CancellationSafeResultSend, NonblockingChannelReplacement, and SlicesConcat use the upstream implementations and tests. Shared-cop behavior and version compatibility are tested in rubocop-go; only project-owned rules, exclusions, and registration checks are tested here.

Lint/ChannelDirection suggests directional raw-channel parameters and results. It follows requirements through locals, fields, direct calls, returns, and closure captures, including internal/external tests. Closing requires send capability; receiving and closing requires both. Connected declarations may need coordinated changes, so there is no autofix. Generated and frozen declarations retain their original contracts. Unknown calls, interface escapes, reflective type arguments, function values, and unsupported structural conversions conservatively retain capabilities. Tuple flows preserve both channel operations and aggregate escapes. The check assumes the loaded program is the consumer: review exported API changes for downstream users and validate other build targets. It covers the active build, not every build-tag or platform variant; it does not prove channel ownership or send/close race safety.

Lint/CancellationLoop warns when a context.Context.Done() case can return to the same select in an unconditional loop, including an unlabelled break that only exits the select. Lint/ClosedChannelLoop follows explicit comma-ok closure checks and warns when the closed channel can be polled again without exiting or replacing/disabling it. These checks use local control flow and stable channel expressions; they do not prove aliases, helper side effects, conditional loops, or arbitrary cancellation protocols.

Lint/ChannelStateCheck warns about potentially blocking channel operations guarded by a len/cap check on the same channel. The observation does not reserve capacity or a value; use select or explicit synchronization. Nonblocking select communications are excluded. Matching covers local guards and resolved storage expressions, not channel aliases or indexed containers; labels discard inferred guards rather than assuming jump paths are safe. All three checks include tests, skip generated files and frozen configs, and provide no automatic fixes.

Lint/HTTPRequestWithContext enforces contextual HTTP request construction outside frozen config versions. It inspects production files only; matching is syntactic, so aliases and shadowing require care. Lint/NoFatalOutsideMain reserves log.Fatal* for package main; tests are exempt.

The following modernization cops inspect resolved production, internal/external tests, and test-only packages, excluding generated files and frozen configs:

These checks follow active build constraints, skip ill-typed candidate packages, and gate suggestions on the target module/file's Go and stdlib versions, not the linter toolchain. Suggestions require behavior review, not automatic rewriting. Rubocop exclusions and suppressions do not configure golangci-lint's overlapping modernize checks.

Lint/FieldsSeq flags strings.Fields slices used only for one value-only range, including loops with an empty-input fallback. Use strings.FieldsSeq, evaluate its input at the original location, and track whether any word was yielded when preserving that fallback. Indexing, repeated traversal, capacity/count uses, mutable byte inputs, and FieldsFunc callbacks are intentionally excluded.

Lint/FieldsSeqLookup complements it by flagging guarded first-field reads and slices.Contains(strings.Fields(...), ...). Stop iteration once the first field or match is found. Preserve empty-input fallbacks, and evaluate the input and membership search value once, in their original order, before iterating. Unguarded indexing, other slice uses, and exact field-count checks are excluded.

Lint/ConstructorCommandExec flags command setup and resolved exec.Cmd execution methods in constructors, including aliased imports and typed test packages. Unrelated methods named Run or Start are excluded. Move process work to an explicit operation.

Lint/NoStdoutInLibraries checks non-main production packages under pkg/ for fmt.Print* and fmt.Fprint* calls targeting os.Stdout; use a caller-provided writer instead. CLI packages and tests remain exempt.

Lint/SortStableFunc recommends slices.SortStableFunc with cmp.Compare for reflection-based sort.SliceStable calls comparing integer or string keys, including short-circuiting tie-breaks. Reverse comparison arguments for descending keys and preserve stable ties. Floating-point keys (NaN ordering), side-effecting comparators, generated code, tests, and frozen configs are excluded. The cop reports suggestions, not automatic rewrites.

Lint/CutPrefix flags strings.HasPrefix paired with TrimPrefix or equivalent prefix slicing, including early-exit guards and switch cases. It matches resolved stdlib calls and stable arguments, stopping at intervening calls or mutations. Preserve short-circuit evaluation and assignments to existing variables when using strings.CutPrefix; a redundant check may only need TrimPrefix. Generated files and frozen config versions are excluded.

Lint/CutSuffix flags an adjacent strings.HasSuffix check and matching TrimSuffix or suffix-only slice, including early-return/continue guards. Only constant or local identifier inputs are matched; compound conditions, intervening work, and effectful expressions are excluded. Preserve original values, assignment scope, and evaluation order when introducing the cut result. Both cut cops also cover immediate bytes.HasPrefix/HasSuffix plus matching trimming or slicing on local unnamed byte slices. Preserve nilness, capacity, and backing-array aliasing.

Lint/NewExpr flags a fresh local declared only to return its address, recommending new(expr) instead. It requires the declaration and return &x to be adjacent, the variable to have no other uses, and skips composite literals (&T{...} stays clearer than new(T{...})) and any file that shadows the builtin new.

Lint/SplitTrimJoin flags strings.Split followed by suffix-only trimming and a strings.Join that needlessly materializes and rejoins the string; use strings.CutLast instead. Only adjacent, local patterns with a single-byte separator are matched, and predicates that mutate or retain a pointer into the slice are excluded.

Lint/SlicesConcat flags nested append chains and adjacent local append sequences that combine slices into fresh storage (nil, empty literals, make, or slices.Clone). It skips in-place appends, single-slice copies, mixed named slice types, scalar appends, and inputs with calls or other side effects. Review nil versus non-nil empty results, capacity assumptions, and allocation panics before replacing a match; Concat returns nil for an empty result. Keep a reasoned //rubocop:disable Lint/SlicesConcat where those semantics matter. The cop uses resolved types, inspects production packages, excludes generated and frozen config files, and never rewrites code automatically.

Lint/PointerHelper recommends native new expressions for AWS scalar pointer helpers. Preserve explicit numeric conversions; slice/map and dereference helpers are not replacements for new.

Lint/ReflectFields covers paired reflect.Value.Field(i) and Value.Type().Field(i) loops that the upstream iterator analyzer misses. It excludes receiver mutation/escape, unrelated index uses, and unsafe callbacks.

Lint/StdlibUUID recommends stdlib UUIDs for random strings and canonical literal-to-string conversions. It preserves UUIDv5 and compatibility parsers, and skips recommendations when production or test code configures Google UUID randomness. General parsers and values exposing the Google UUID type are excluded.

Lint/URLClone recommends url.URL.Clone for equivalent nil-safe deep copies or copies guarded by User == nil. General shallow copies are excluded because Clone also copies userinfo.

Lint/JSONMarshalWrite flags a single buffered encoding followed by newline trimming. Review the suggested jsonv2.MarshalWrite migration with v1 defaults, explicit HTML escaping, unchanged evaluation order, and discarded partial output on error. Streaming encoders and indentation are excluded.

Lint/BenchmarkLoop covers simple ResetTimer/b.N loops, including sub-benchmarks and test-only packages. Review measurement and compiler effects before adopting b.Loop; parallel, timer-sensitive, and escaping benchmark handles are excluded. These cops report suggestions, never automatic rewrites. The modernization cops above inspect production packages; the UUID randomness guard additionally reads tests.

Lint/SlicesClone checks production and test code for append-to-empty slice copies and adjacent fresh-local make(len(src)) / copy pairs. It resolves builtin calls and slice types, skips generated files and frozen configs, and excludes buffer reuse, explicit capacities, and effectful repeated sources. Suggestions require review: slices.Clone preserves source nilness and named slice types, whereas the old idioms may normalize empty results or change the type. Preserve those behaviors and any observable capacity contract when migrating.

Test output/artifact lifetimes and in-memory HTTP compatibility require review; there are no blanket rules for t.Output, ArtifactDir, or NewTestServer.

Correctness cops

Lint/CancellationSafeResultSend detects unbuffered, local result sends whose sole receiver can return on context cancellation. It checks inline goroutines and simple aliases, excluding escaped channels and extra receivers. Buffered channels and cancellation-aware send selects are not reported. Every returning cancellation arm must be covered by the producer. Straight-line context copies and WithValue preserve identity; escaped contexts and unresolved context constructors are excluded. Buffering alone is only sufficient for a one-shot result, not an arbitrary stream.

Lint/NonblockingChannelReplacement flags a failed nonblocking send followed by an adjacent blocking drain/resend on the same local or stable field channel. Another consumer can empty the queue after the failed send. Nonblocking drains, indexed receivers, and effectful send expressions are excluded. The check does not prove ownership of a queue; exclusive-access protocols need review.

Lint/BoundedCommandPipeLifetime flags synchronous shell stdout/stderr pipe reads between Start and Wait without recognized cancellation cleanup. CommandContext killing the shell does not close pipes inherited by descendants; WaitDelay alone cannot bound a caller-owned read before Wait. The check recognizes literal -c shells and shellpath.DetectShell, local aliases, and explicit cancellation-driven pipe close or SIGKILL of a configured process group. Replaced cancellation callbacks and stopped AfterFunc registrations no longer provide protection. Conditional calls do not establish unconditional cleanup or completion. It does not prove helper, cross-block, or asynchronous read lifetimes.

Lint/SilentLimitedRead follows local io.ReadAll(io.LimitReader(...)) results into complete-file writes. It checks that overflow cannot reach those sinks; a typical guard reads limit+1 and rejects lengths greater than limit. Preview-only uses, transformed or escaped buffers, and conditionally evaluated sinks are excluded. Mutable or escaped limits are outside the stable-bound analysis. File-size snapshots and HTTP Content-Length metadata alone do not establish a bound on a later read. The analysis is local, not a proof of stream completeness. JSON decoding alone is not a complete-file sink: a truncated valid JSON object either fails decoding or loses only trailing whitespace, so it is outside this cop's scope.

All four correctness cops inspect resolved production code, exclude generated files and frozen configs, and support ordinary rubocop suppressions. They report existing issues without automatically changing application behavior.

Opening Issues

File issues on the GitHub issue tracker. Please:

See also

Troubleshooting — Common issues and debug mode. Telemetry — What data is collected and how to opt out.

Submitting Pull Requests

  1. Fork the repository and create a branch for your changes
  2. Write your code following the style and testing guidelines above
  3. Test your changes: run task lint and task test
  4. Sign off your commits with git commit -s (DCO required)
  5. Open a pull request against the main branch
Tip

Use the dogfooding agent (docker agent run ./golang_developer.yaml) to help write and review your changes before submitting.

Sign Your Work

All contributions require a Developer Certificate of Origin (DCO) sign-off:

# Set the identity used for the sign-off
$ git config user.name "Your Name"
$ git config user.email "your.email@example.com"

# Add a DCO sign-off to this commit
$ git commit -s -m "Your commit message"

-s adds a Signed-off-by trailer; it does not cryptographically sign the commit. To also sign with a GPG or SSH key, configure Git signing and use git commit -S -s.

Community

Find us on Slack for questions and discussions.

Code of Conduct

We want to keep the Docker Agent community welcoming, inclusive, and collaborative. Key guidelines:

The governance for this repository is handled by Docker Inc.

Reviewer-only Gemini integration fixtures

Gemini HTTP/SSE fixtures, recorded CLI flows, and real SDK/session composition checks are off by default, including fixtures that use only loopback or an in-memory transport. Isolated adapter, configuration, policy, and metadata units remain enabled, as do non-Gemini subtests.

Only reviewers on their own non-CI workstation may enable these fixtures with DOCKER_AGENT_RUN_GEMINI_INTEGRATION_TESTS=1 (exactly 1). Any nonempty GITHUB_ACTIONS value, even false, disables them regardless of opt-in. Do not unset that variable to bypass the guard in GitHub Actions. No workflow or test selection changes are needed: the guard applies to PRs (including drafts), main and tag pushes, merge groups, manual dispatches, and the WASM test lane.

Before opting in, deny external network egress while allowing loopback, keep all checked-in cassettes, and do not supply live API credentials. The E2E proxy uses ModeRecordOnce: a missing cassette can attempt a real upstream request. The OpenCode fixture also relies on a transport redirect to stay local. Opt-in permits existing offline fixtures only, not cassette recording or paid calls.

The following commands are for reviewer execution only, not for implementers or project CI. Run them from the repository root after checking out the PR head:

# Confirm GITHUB_ACTIONS is empty on your own workstation; never bypass it in CI.
printf 'GITHUB_ACTIONS=%s\n' "$GITHUB_ACTIONS"
export DOCKER_AGENT_RUN_GEMINI_INTEGRATION_TESTS=1

go test -count=1 ./pkg/model/provider/gemini
go test -count=1 ./pkg/model/provider -run '^TestGatewayProviderRequests$/^google$'
go test -count=1 ./pkg/runtime -run '^TestRunStream_ImageOutputGuard_RejectsBeforeDispatch$'
go test -count=1 ./pkg/tui/page/chat -run '^TestImageOutputGuard_RuntimeToTUI_RejectsBeforeDispatchAndClearsSpinner$'
go test -count=1 ./e2e -run '^TestExec_Gemini($|_)'
go test -count=1 ./e2e -run '^TestDebug_Title$/^Google_'

# Requires Node for the Go WASM test runner.
GOOS=js GOARCH=wasm go test -count=1 -tags docker_agent_wasm_cloud \
  -exec="$(go env GOROOT)/lib/wasm/go_js_wasm_exec" ./cmd/wasm \
  -run '^TestCloud(SessionsUseSessionEnvOnly|TokensReachRequests)$/.*gemini'
GOOS=js GOARCH=wasm go test -count=1 \
  -exec="$(go env GOROOT)/lib/wasm/go_js_wasm_exec" ./cmd/wasm \
  -run '^TestGoogleFactory(RejectsVertexWithoutCloudTag|KeepsGeminiAPIWithoutCloudTag)$'

Expected: selected fixtures run rather than skip and their existing assertions pass. They cover streaming keepalives/tool replay, request wire shape, auth and transport wrapping, embeddings, image-output guards, runtime/TUI error propagation, CLI tools/title/sampling, and WASM session/token flows. These are existing regressions, not live-backend entitlement or new model acceptance tests. Record head SHA, OS/Go/Node versions, command, pass/fail/skip and sanitized evidence. A skip is not a pass, and pure unit success is not integration evidence.

For the preparation change, no Gemini integration fixture was executed by the implementer, even to verify the skip path. Validation is limited to pure gate units, source placement review, compile-only checks, static analysis and builds. Future fixture additions must be guarded before server, proxy, transport, SDK flow or browser-session setup, with pure cases and non-Gemini rows kept separate.