Diffinery
A git forge for teams of people and AI agents, running entirely on Cloudflare. Git hosting, pull requests and reviews, issues, GitHub-Actions-compatible CI, an MCP server for agents, and sandboxes, with no servers to run.
- Drop-in for GitHub workflows.
.github/workflows/*.ymlruns as-is: same triggers, expressions, matrices,needs, outputs, caches, artifacts, secrets, OIDC,actions/checkout,setup-node/python/go, and remote JavaScript and composite actions. - Built for agents. Agents are first-class accounts owned by a person or
team, with their own tokens and limits. They can't push or merge to protected
branches, and their pull requests always need a human approval. Every commit
they make is stamped with who they act for. They work through MCP
(
/mcp) or the REST API, and can start sandboxes to build and test. - Secure by default. People sign in with passkeys, with no passwords to
leak or phish. Invites, roles and branch protection decide what they can do. Pushes are scanned for
leaked credentials and blocked if they contain any; nothing from a blocked
push is kept. A repository admin (a person, never an agent) can push anyway
with
git push -o allow-secrets=<reason>, which is audited. A file can opt out with adiffinery:allow-secretcomment, for test fixtures. Secrets use envelope encryption, and every sensitive action goes to an append-only audit log.
How it fits on Cloudflare
| Piece | Cloudflare product |
|---|---|
| HTTP, git smart HTTP, REST API, MCP, web UI | Workers (Hono) |
| One repository: refs, objects, PRs, issues, reviews, checks, runs | Durable Object with SQLite (RepoDO), one per repository |
| Large objects, Git LFS, logs, caches, artifacts, event archive | R2 |
| Accounts, orgs, tokens, invites, notifications, audit log | D1, read from nearby replicas through the Sessions API (each browser keeps a bookmark so it reads its own writes) |
| Webhooks, notifications, audit fan-out | Queues (with a dead-letter queue) |
| Workflow runs (durable, survive restarts, 6 h job timeout) | Workflows |
| Job runners and agent sandboxes | Containers (Ubuntu 24.04 image in containers/runner) |
Scheduled workflows (on: schedule) | Cron Triggers |
| Live run logs and PR updates | Durable Object WebSockets (hibernation) |
| Sign-in | Passkeys (WebAuthn) and sessions in D1. Cloudflare Access guards only /auth/access, the email recovery path |
apps/diffinery the Worker (src/index.ts), D1 migrations, wrangler config, e2e test
apps/cli the `diffinery` command-line client
containers/runner runner agent + sandbox server, and the container image
packages/git-core git objects, packfiles, deltas, diff/merge (pure TypeScript)
packages/workflow-yaml GitHub Actions workflow parser, expressions, matrix, cron
packages/shared types shared by the Worker and the runner
scripts bootstrap.sh, ensure-secrets.sh, render-config.mjs
Deploying
Diffinery deploys itself: this repository lives at
diffinery.com/codician-team/diffinery, and every push to main there runs
.github/workflows/deploy.yml on Diffinery's hosted runners, followed by the
live test (.github/workflows/live-test.yml). GitHub stays the upstream for
now: .github/workflows/sync-to-diffinery.yml copies every branch and tag, and
the secrets and variables below, to Diffinery on each push, then waits for the
Actions runs there and fails if any of them fail. Running Deploy by hand on
GitHub still deploys, as a fallback.
The workflow needs two repository secrets:
CLOUDFLARE_API_TOKEN: account permissions Workers Scripts, D1, R2, Queues, Workflows, Containers and Access: Apps and Policies (all Edit), and zone permission Workers Routes Edit ondiffinery.com.CLOUDFLARE_ACCOUNT_ID
On each deploy, scripts/bootstrap.sh creates anything that is missing: the D1
database, R2 bucket, queues and one Access application that covers only
/auth/access (email-based account recovery). It then applies D1 migrations. wrangler deploy
builds and pushes the runner image. On the first deploy, ensure-secrets.sh
generates MASTER_KEY and the OIDC signing key inside the job, so neither
value is ever printed.
If the token can't read your Zero Trust organization, set a repository
variable ACCESS_TEAM_DOMAIN (for example codician.cloudflareaccess.com).
Email recovery uses Access's email one-time PIN, which works without any setup. You can also add GitHub as a login method under Zero Trust → Settings → Authentication.
Signing in and inviting people
Everyone signs in with a passkey. There are no passwords.
- First sign-in for admins. The emails in
BOOTSTRAP_ADMINS(inapps/diffinery/wrangler.jsonc) become site admins. Openhttps://diffinery.com/auth/access, confirm your email with the one-time code, and add a passkey on the page that follows. - Inviting someone. An admin or org owner creates an invite from
Settings → Site administration, an organization's People tab, or
diffinery invite friend@example.com --org codician. The person opens the link, picks a username and creates a passkey, and they're in. - Lost passkey. A site admin clicks Sign-in link next to the person in
Settings → Site administration and sends them the one-time link, which
works for 24 hours. People can also recover through
/auth/accessby email. - More devices. Add passkeys under Settings → Passkeys and sessions, or keep one in a password manager that syncs. The same page signs out other browsers.
- Repository and org roles (read, triage, write, maintain, admin) decide what each person can do.
Nobody needs a Cloudflare account. Git and the CLI use a personal token from Settings → Tokens.
git clone https://diffinery.com/codician/website.git # username: anything, password: your dfy_ token
diffinery login && diffinery auth setup-git # or let the CLI handle credentials
Agents
Create an agent under Settings → Agents or with diffinery agent create reviewer-bot --org codician.
Then point any MCP client at it:
{ "mcpServers": { "diffinery": { "type": "http", "url": "https://diffinery.com/mcp", "headers": { "Authorization": "Bearer dfy_…" } } } }
Tools include get_file, list_tree, commit_files (no git client needed),
create_branch, create_pull_request (with an intent for reviewers),
review_pull_request, merge_pull_request (policy-guarded), issues,
list_workflow_runs and get_job_logs, rerun_workflow, and
sandbox_create/sandbox_exec for running builds and tests.
Agent guardrails apply everywhere: git, REST, MCP and the web UI.
- An agent's role is capped at write, and never exceeds its owner's role.
- Agents can't push to the default branch or protected branches, and can't
merge there. Turn this on per branch with
agentsMayPushoragentsMayMerge. - A pull request opened by an agent needs at least one human approval, and approvals from agents don't count toward it.
- Agents can't change branch protection or read or set secrets.
Workflows
Put workflow files in .github/workflows/ (or .diffinery/workflows/).
Supported triggers are push, pull_request, pull_request_target,
merge_group, workflow_dispatch (with inputs), schedule and workflow_run
(up to three levels deep, like GitHub), with branch, tag and path filters.
Pull requests behave as on GitHub:
pull_requestruns check out the test merge commit,refs/pull/<n>/merge(the head merged into the current base), withGITHUB_REFset to that ref. While a pull request conflicts with its base, itspull_requestworkflows don't run.refs/pull/<n>/headandrefs/pull/<n>/mergecan be fetched but not pushed.pull_request_targetruns the base branch's own workflow on the base commit, with the repository's secrets, so a pull request can't change what runs. Check out the pull request's code yourself only if you trust it.- Both report their checks on the pull request's head commit.
opened,synchronize,reopenedandclosed(withpull_request.merged) fire. - Merge queue: turn on Require a merge queue in a branch protection rule
(or
"mergeQueue": {"method": "squash"}through the API). "Merge when ready" then adds a pull request to the queue. Each entry gets a merge group branch,gh-readonly-queue/<base>/pr-<n>-<head sha>, holding the base, every pull request ahead of it and its own changes, andmerge_group(andpush) workflows run on it. Once its required checks pass and the entries ahead have merged, the base fast-forwards to exactly the tested commit. A group that fails, conflicts or exceeds the check timeout leaves the queue with a comment saying why, and the groups behind it are rebuilt. The queue is at/<owner>/<repo>/queue/<branch>; the API isPUT/DELETE /repos/{owner}/{repo}/pulls/{n}/merge-queueandGET /repos/{owner}/{repo}/merge-queue/{branch},diffinery pr mergequeues automatically, and the MCPmerge_pull_requesttool does too.
Supported workflow features:
needsand job outputs,ifwithsuccess(),failure(),always()andcancelled(),strategy.matrixwith include and exclude,fail-fastandmax-parallel,concurrencywithcancel-in-progress, andtimeout-minutes.continue-on-error,defaults.run,envat every level,$GITHUB_OUTPUT,$GITHUB_ENV,$GITHUB_PATH,$GITHUB_STEP_SUMMARY, and masking of secrets and::add-mask::values.secrets.*,vars.*,GITHUB_TOKEN(a per-job token), andpermissions: id-token: writefor OIDC (issuerhttps://diffinery.com).runs-on: ubuntu-latest(and anyubuntu-*orlinuxlabel) runs in a Cloudflare Container.runs-on: [self-hosted, …]jobs wait for a runner you start withdiffinery runner -R owner/repo --labels gpu,macos. Use that for macOS, GPUs or private networks.- Reusable workflows:
jobs.<id>.uses: ./.github/workflows/x.ymlfrom the same commit, orowner/repo/.github/workflows/x.yml@reffrom another repository on this instance (public or internal ones, or private ones with the same owner).with,secrets(a mapping orinherit),on.workflow_calloutputs and nesting up to four levels deep all work. A matrix on the calling job isn't supported yet. - Docker, on hosted runners and on self-hosted runners with Docker installed:
a job
container:(steps and JavaScript actions run inside it),services:(reachable by name from the job container, or onlocalhostthrough their mapped ports, with health checks awaited), Docker container actions (docker://imagesteps and actions withruns.using: docker, built from their Dockerfile), and plaindocker build/docker run/docker composein steps. Hosted runners start the Docker daemon the first time a job uses it and pull Docker Hub images throughmirror.gcr.io.
Not supported yet: actions/github-script,
Windows and macOS hosted runners, and
deepen-not / partial clone filters. A job that uses an unsupported feature fails
with a clear message instead of misbehaving.
Developing
pnpm install
pnpm -r build && pnpm -r typecheck && pnpm -r test
pnpm --filter @diffinery/app db:migrate:local
pnpm dev # http://localhost:8787, signed in as dev@diffinery.local
pnpm --filter @diffinery/app e2e # wrangler dev + real git + the runner agent + Chromium
The end-to-end test does the following:
- Pushes a repository with real
git. - Runs its CI through the actual runner agent.
- Checks secret scanning and branch protection.
- Has an agent open a pull request that needs a human approval and a green check, then merges it.
- Runs pull request workflows on test merge commits,
pull_request_targetfrom the base branch, and a merge queue withmerge_groupruns, including a group that fails and leaves the queue. - Clones shallow, then deepens the clone.
- Drives the MCP server.
- In Chromium with a virtual authenticator, joins from an invite with a new passkey, adds and removes passkeys, signs out and in, and uses an admin sign-in link.
Team preferences for product and design decisions are in docs/PREFERENCES.md.