Skip to content

computer: Send Git's network requests through a host fetch - #245

Open
mattzcarey wants to merge 1 commit into
mainfrom
git-host-auth
Open

mattzcarey wants to merge 1 commit into
mainfrom
git-host-auth

Conversation

@mattzcarey

@mattzcarey mattzcarey commented Oct 16, 2026 •

Copy link
Copy Markdown
Member

The Git commands the model runs, the worker-shell git command and codemode:git, have no way to authenticate (#218). Host code can pass headers or onAuth on each typed call, but a model running git push against a private repository has to put a token on the command line, where it ends up in the transcript.

createGitClient() now takes a fetch, and every request a clone, fetch, pull or push makes goes through it. That covers the typed API, cli(), the shell's git command and codemode:git, since the last two already run through the host's Git client. A host wraps fetch to add credentials, which then never reach argv, the remote URL, .git/config or the output:

const gitFetch: typeof fetch = async (input, init) => {
  const request = new Request(input, init);
  const url = new URL(request.url);
  if (url.origin === "https://github.com" && url.pathname.startsWith("/acme/") && !request.headers.has("Authorization")) {
    const authed = new Request(request, { redirect: "manual" });
    authed.headers.set("Authorization", `Bearer ${env.GITHUB_TOKEN}`);
    return fetch(authed);
  }
  return fetch(request);
};

const ws = new Workspace({ storage: ctx.storage, git: createGitClient({ fetch: gitFetch }) });

An earlier version of this change added a Git-specific auth resolver. Taking a plain fetch instead keeps the policy with the host: which repositories get a token, whether a caller's Authorization header wins, and how redirects are handled. It also lets a service binding's fetch be passed straight down. A host that wants read-only tokens for fetches and write tokens for pushes can tell them apart by the Git service in the URL (git-upload-pack or git-receive-pack). Request and response bodies are streamed, so packfiles aren't held in memory. Without fetch, Git uses isomorphic-git's own transport as before.

http.test.ts covers the streaming adapter: uploads, status and headers, empty responses, cancelling when Git stops reading, and fetch failures. It also drives a real Workspace through a typed clone, a CLI clone (checking the token isn't in the output), and typed fetch, pull and a CLI push, using a fetch that adds a token and answers 401.

npm test --workspace @cloudflare/computer -- src/git/http.test.ts

The "Host Fetch and authentication" section of docs/13_git_interface.md and the README's Git section describe the option. A minor changeset is included.

@changeset-bot

changeset-bot Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: e43b0bd

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 5 packages
Name Type
@cloudflare/computer Minor
@cloudflare/dofs Minor
@cloudflare/computer-rpc Minor
@cloudflare/computerd Minor
@cloudflare/remote-exec Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pkg-pr-new

pkg-pr-new Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@cloudflare/computer@245

commit: e43b0bd

@aron-cf

aron-cf commented Nov 10, 2026

Copy link
Copy Markdown
Collaborator

@ask-gardener can you look at whether this should be implemented as a custom fetcher instead. Ideally we'd just provide the fetch implementation directly, this would allow fetchers to be passed down the stack.

@github-actions

Copy link
Copy Markdown
Contributor

@aron-cf Yes—after tracing the client, I recommend an injected fetch implementation rather than a Git-specific auth resolver.

  • createGitClient already shares one HTTP transport across clone/fetch/pull/push; shell Git and ws:git delegate to that client.
  • The existing http option is an isomorphic-git request() transport, documented as test-only—not Fetch-compatible. Expose fetch and bridge it with a thin request/response adapter, including streaming bodies.
  • Keep credential injection in the host-provided fetch wrapper, preserving explicit Authorization and enforcing HTTPS, destination scoping and redirect safety. Fetch/push policy can inspect Git’s service query/path, as auth.ts already does.
  • No code changes or tests run; this was a design assessment, not execution of the PR body’s test instructions.

@aron-cf

aron-cf commented Nov 10, 2026

Copy link
Copy Markdown
Collaborator

@ask-gardener can you implement that on this branch? Or alternatively take this PR, make those changes and implement a fresh PR.

@github-actions

Copy link
Copy Markdown
Contributor

@aron-cf Implemented the custom Fetch approach directly on git-host-auth in c466d28.

  • Replaced the Git-specific auth resolver with createGitClient({ fetch }), including streaming request/response adaptation. The default transport is unchanged.
  • Updated tests, README, authentication guidance and changeset. Credential scoping and redirect policy belong to the host fetcher.
  • Checks passed: all 346 Git tests, production TypeScript check, focused lint/format checks and whitespace validation.
  • No separate PR opened. This token's push does not start PR checks; those still need a maintainer-triggered run.

@aron-cf
aron-cf marked this pull request as ready for review November 10, 2026 16:03
@aron-cf

aron-cf commented Nov 10, 2026

Copy link
Copy Markdown
Collaborator

@ask-gardener can you resolve the merge conflicts and if possible update the PR description

The Git commands the model runs, the worker-shell `git` command and
codemode:git, had no way to authenticate. Host code could pass
`headers` or `onAuth` on each typed call, but a model running
`git push` against a private repository had to put a token on the
command line, where it ends up in the transcript.

createGitClient() now takes `fetch`. Every request a clone, fetch,
pull or push makes goes through it, from the typed API, cli(), the
shell command and codemode:git alike, since the last two already run
through the host's Git client. A wrapper can add credentials, which
then never reach argv, the remote URL, .git/config or output, and a
service binding's fetch can be passed down as it is. Which requests
get credentials, whether a caller's Authorization header wins, and
how redirects are handled stay with the wrapper; the docs show one
that scopes a token to a GitHub organisation and won't follow a
redirect with it.

A small adapter bridges isomorphic-git's iterable request and
response bodies to Fetch streams, so packfiles aren't buffered, and
cancels the response when Git stops reading. Without `fetch`, Git
uses isomorphic-git's own transport as before.
@aron-cf aron-cf changed the title computer: Hold Git credentials on the host with an auth resolver computer: Send Git's network requests through a host fetch Nov 10, 2026

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Devin Review found 3 potential issues.

Devin Review

// Required by Node's Fetch for streaming uploads; ignored by Workers.
init.duplex = "half";
}
const response = await fetcher(url, init);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Failed fetch leaves upload streams open

When a host fetch rejects a Git POST, withFetch leaves its upload stream uncancelled. The stream can already have pulled from the pack iterator, leaving it open after the failed push.

Learn more

A Git push supplies an iterable pack body, and the adapter wraps it in a ReadableStream that can pull before the host fetch settles. If the supplied fetch rejects without consuming that body, the adapter returns immediately; its cancel() handler never runs and the underlying iterator never receives return(). This is distinct from cancellation of the response stream, which only exists after fetch resolves.

Example: A host fetch wrapper rejects a POST while refreshing its token. The stream has pulled its first pack chunk, but the rejected push never closes the pack iterator.

Recommended fix: Hold the upload stream separately and cancel it when fetcher(url, init) rejects. Cover rejection before and after the upload begins with a generator that records its cleanup.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

* credentials, whether a caller's `Authorization` header wins, and how
* redirects are handled. Defaults to isomorphic-git's own transport.
*/
fetch?: typeof globalThis.fetch;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 New Git option lacks consumer example

The new fetch option changes the public createGitClient API, but no runnable example uses it. Repository guidance requires updating examples alongside public API changes.

Learn more

The repository treats its examples as real consumers of the public API and explicitly requires updating them when that API changes. The new option is demonstrated in documentation, but no runnable example exercises it.

Example: A developer starting from the worker-shell example sees createGitClient() without a host fetch and cannot use its shell Git command against a private remote without configuring authentication separately.

Recommended fix: Update a Git-enabled example to pass a scoped host fetch wrapper and demonstrate the new option in a runnable consumer.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

request.headers.set("Authorization", "Bearer host-token");
}
requests.push(request);
return new Response(null, { status: 401, statusText: "Unauthorized" });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔍 Successful Git transfers remain untested

All Workspace cases stop at a 401 discovery response, before any pack transfer. The upload test uses Response to consume bytes rather than sending a Fetch request. A successful clone or push against a local HTTP server would exercise the missing integration.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

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.

2 participants