Skip to content

Conversation

@doringeman
Copy link
Contributor

@doringeman doringeman commented Jan 22, 2026

Instead of copying ~/.docker/config.json into the container, credentials are now resolved on the CLI side using the Docker credential helpers and sent with pull/push requests. This simplifies container setup and avoids exposing the full Docker config inside the container.

To test following the next steps, you'd need to either connect to Offload or use _MODEL_RUNNER_TREAT_DESKTOP_AS_MOBY=1.

make docker-build DOCKER_IMAGE=docker/model-runner:test-docker-cli-auth
MODEL_RUNNER_CONTROLLER_VERSION=test-docker-cli-auth MODEL_RUNNER_CONTROLLER_VARIANT=cpu docker model start-runner
$ docker model package --gguf /Users/dorin/.cache/huggingface/hub/models--HuggingFaceTB--SmolLM2-360M-Instruct-GGUF/snapshots/593b5a2e04c8f3e4ee880263f93e0bd2901ad47f/smollm2-360m-instruct-q8_0.gguf --push doringeman657/my-smollm2
Adding GGUF file from "/Users/dorin/.cache/huggingface/hub/models--HuggingFaceTB--SmolLM2-360M-Instruct-GGUF/snapshots/593b5a2e04c8f3e4ee880263f93e0bd2901ad47f/smollm2-360m-instruct-q8_0.gguf"
Pushing model to registry...
WARN[0001] reference for unknown type: application/vnd.docker.ai.gguf.v3
Uploaded: 368.50 MB
Model pushed successfully

Resolves #560.
I'd merge this once we get #582 merged as well.

Instead of copying ~/.docker/config.json into the container, credentials are now resolved on the CLI side using the Docker credential helpers and sent with pull/push requests. This simplifies container setup and avoids exposing the full Docker config inside the container.

Signed-off-by: Dorin Geman <dorin.geman@docker.com>
Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues, and left some high level feedback:

  • In Client.Pull/Push, consider threading the request context down into resolveCredentials instead of using context.Background() so timeouts/cancellations and tracing are respected during credential resolution.
  • In handlePushModel, JSON decoding errors for ModelPushRequest are silently ignored and the request proceeds; it would be clearer to return a 400 Bad Request (and/or log) when the body is present but invalid JSON.
  • In exchangeForToken, treating a registry with no WWW-Authenticate realm as an error ("no auth required") causes a fallback to username/password; consider treating this as a non-error and using anonymous access instead of basic auth when no auth is required.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `Client.Pull`/`Push`, consider threading the request context down into `resolveCredentials` instead of using `context.Background()` so timeouts/cancellations and tracing are respected during credential resolution.
- In `handlePushModel`, JSON decoding errors for `ModelPushRequest` are silently ignored and the request proceeds; it would be clearer to return a 400 Bad Request (and/or log) when the body is present but invalid JSON.
- In `exchangeForToken`, treating a registry with no `WWW-Authenticate` realm as an error (`"no auth required"`) causes a fallback to username/password; consider treating this as a non-error and using anonymous access instead of basic auth when no auth is required.

## Individual Comments

### Comment 1
<location> `cmd/cli/desktop/desktop.go:98` </location>
<code_context>
+		return "", fmt.Errorf("no auth required")
+	}
+
+	scope := ref.Scope(remote.PushScope)
+	token, err := remote.Exchange(ctx, ref.Context().Registry, auth, nil, []string{scope}, pr)
+	if err != nil {
</code_context>

<issue_to_address>
**🚨 issue (security):** Using PushScope for all token exchanges may be too restrictive for pull-only operations

resolveCredentials is used for both pull and push, but exchangeForToken always requests remote.PushScope. For pull-only callers this can both fail unnecessarily and request overly broad (write) tokens. Consider making the scope a parameter (pull vs push) or deriving it from the caller so we only request the minimum required scope.
</issue_to_address>

### Comment 2
<location> `pkg/distribution/distribution/client.go:240-249` </location>
<code_context>
+func (c *Client) PullModel(ctx context.Context, reference string, progressWriter io.Writer, creds *Credentials) error {
</code_context>

<issue_to_address>
**suggestion (bug_risk):** Authentication precedence differs between PullModel and PushModel and may be surprising

In PullModel, basic auth currently takes precedence over BearerToken, while PushModel does the opposite. This inconsistent precedence means callers who set both fields get different auth behavior between pull and push. Consider standardizing the precedence across both methods or rejecting calls where multiple auth fields are set to avoid confusion and unintended auth schemes.

Suggested implementation:

```golang
 // PullModel pulls a model from a registry and returns the local file path
func (c *Client) PullModel(ctx context.Context, reference string, progressWriter io.Writer, creds *Credentials) error {
	// Ensure callers don't mix auth mechanisms; this matches PushModel behavior and avoids
	// surprising precedence differences when multiple credential fields are set.
	if creds != nil {
		hasBearer := creds.BearerToken != ""
		// NOTE: adjust these field names if your Credentials type uses different ones
		hasBasic := creds.Username != "" || creds.Password != ""

		if hasBearer && hasBasic {
			return fmt.Errorf("multiple authentication methods configured: use either bearer token or basic auth, not both")
		}
	}

	// Store original reference before normalization (needed for case-sensitive HuggingFace API)
	originalReference := reference
	// Normalize the model reference
	reference = c.normalizeModelName(reference)
	c.log.Infoln("Starting model pull:", utils.SanitizeForLog(reference))

```

1. Ensure the `Credentials` struct has `Username` and `Password` fields; if they differ (e.g. `User` / `Pass`), update the `hasBasic` line accordingly.
2. Add the `fmt` package to the imports in `pkg/distribution/distribution/client.go`:
   - `import "fmt"`
3. For full consistency with `PushModel`, confirm that `PushModel` either:
   - Uses the same mutual-exclusion rule (rejecting mixed auth), or
   - If it assumes a specific precedence (e.g. bearer over basic), document that and potentially mirror that behavior here instead of rejecting.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

return "", fmt.Errorf("no auth required")
}

scope := ref.Scope(remote.PushScope)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚨 issue (security): Using PushScope for all token exchanges may be too restrictive for pull-only operations

resolveCredentials is used for both pull and push, but exchangeForToken always requests remote.PushScope. For pull-only callers this can both fail unnecessarily and request overly broad (write) tokens. Consider making the scope a parameter (pull vs push) or deriving it from the caller so we only request the minimum required scope.

Comment on lines +240 to 249
func (c *Client) PullModel(ctx context.Context, reference string, progressWriter io.Writer, creds *Credentials) error {
// Store original reference before normalization (needed for case-sensitive HuggingFace API)
originalReference := reference
// Normalize the model reference
reference = c.normalizeModelName(reference)
c.log.Infoln("Starting model pull:", utils.SanitizeForLog(reference))

// Handle bearer token for registry authentication
var token string
if len(bearerToken) > 0 && bearerToken[0] != "" {
token = bearerToken[0]
}

// HuggingFace references always use native pull (download raw files from HF Hub)
if isHuggingFaceReference(originalReference) {
c.log.Infoln("Using native HuggingFace pull for:", utils.SanitizeForLog(reference))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): Authentication precedence differs between PullModel and PushModel and may be surprising

In PullModel, basic auth currently takes precedence over BearerToken, while PushModel does the opposite. This inconsistent precedence means callers who set both fields get different auth behavior between pull and push. Consider standardizing the precedence across both methods or rejecting calls where multiple auth fields are set to avoid confusion and unintended auth schemes.

Suggested implementation:

 // PullModel pulls a model from a registry and returns the local file path
func (c *Client) PullModel(ctx context.Context, reference string, progressWriter io.Writer, creds *Credentials) error {
	// Ensure callers don't mix auth mechanisms; this matches PushModel behavior and avoids
	// surprising precedence differences when multiple credential fields are set.
	if creds != nil {
		hasBearer := creds.BearerToken != ""
		// NOTE: adjust these field names if your Credentials type uses different ones
		hasBasic := creds.Username != "" || creds.Password != ""

		if hasBearer && hasBasic {
			return fmt.Errorf("multiple authentication methods configured: use either bearer token or basic auth, not both")
		}
	}

	// Store original reference before normalization (needed for case-sensitive HuggingFace API)
	originalReference := reference
	// Normalize the model reference
	reference = c.normalizeModelName(reference)
	c.log.Infoln("Starting model pull:", utils.SanitizeForLog(reference))
  1. Ensure the Credentials struct has Username and Password fields; if they differ (e.g. User / Pass), update the hasBasic line accordingly.
  2. Add the fmt package to the imports in pkg/distribution/distribution/client.go:
    • import "fmt"
  3. For full consistency with PushModel, confirm that PushModel either:
    • Uses the same mutual-exclusion rule (rejecting mixed auth), or
    • If it assumes a specific precedence (e.g. bearer over basic), document that and potentially mirror that behavior here instead of rejecting.

Copy link
Contributor

@ilopezluna ilopezluna left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we should enable TLS by default. What do you think?

Copy link
Contributor

@ilopezluna ilopezluna left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make -C cmd/cli docs :D

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

OCI registry auth fails with remote Docker daemons due to backend credential resolution

2 participants