Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Grovedock

One root. Thousands of agents.

Grovedock is an open protocol and reference server for source control at agent scale. Commits, rebases and history run on the server, so hundreds of agents land on one branch at once with no merge queue, a clone fetches only the files you ask for, and CI reads files without checking anything out. It runs beside GitHub, or instead of it.

Grovedock was called Pando until October 2026; commands and packages keep the pando name until 3.6. The name is inspired by Pando, the aspen grove in Utah: some 47,000 trunks on one root. A grove is many agents on one history, and a dock is where their changes land.

Start here

Also: migrate from GitHub, the CLI reference, the server reference, and the live console demo, which simulates an organisation of agents in your browser.

How it differs from git

gitGrovedock
Clonedownloads all historydownloads one tree, lazily, only the paths you ask for
Commitbuilt locally, pushed as packsthe server builds it from the list of changed files
Concurrent pushes to mainone wins, the rest fetch, rebase and retrythe server rebases changes that touch different files and lands them all (how)
Reading a file in CIclone firstone call: pando cat main path
log, blame, diffneed local historyserver APIs; ancestry in O(log n)
IntegritySHA-1BLAKE3, verified at every cache tier

Where git wins: working offline with full history. Grovedock needs the server for history, commits and landing. The benchmarks give the measured numbers, the method and the limits.

Plain git keeps working against a Grovedock server through git-remote-pando; see Coming from git.

Try it in 5 minutes

This page takes you from nothing to a landed change in about five minutes.

Install

brew install kabirnarang39/tap/pando

Other ways:

# macOS and Linux, verifies the release checksum
curl -fsSL https://raw.githubusercontent.com/kabirnarang39/pando/main/install.sh | sh

# Docker (a server on the network needs a token; pick your own secret)
docker run -p 7420:7420 -e PANDO_TOKENS="me:$(openssl rand -hex 24)" ghcr.io/kabirnarang39/pando:latest

# From source
cargo install --git https://github.com/kabirnarang39/pando pando-vcs pando-server

Windows: download the .zip from the releases page.

You get three programs: pando (the client), pandod (server, cache and node daemon) and git-remote-pando (lets plain git talk to a Grovedock server).

Run a server

pandod server --listen 127.0.0.1:7420

With no options the server keeps everything in ./pando-data/ on SQLite and accepts requests without authentication, so it listens only on 127.0.0.1.

Note

That is fine on a laptop. Before anyone else connects, give it tokens or single sign-on: see Deploy and Security and audit.

Import a git repository

pando import-git ~/code/my-repo --server http://127.0.0.1:7420

Import is deterministic and incremental: run it again later and it uploads only new commits. pando export-git goes the other way, with identical trees, and pando mirror keeps a git host such as GitHub or Bitbucket up to date continuously.

Make and land a change

pando clone http://127.0.0.1:7420 work
cd work
echo hello > hello.txt
pando commit -m "Say hello"
pando land

pando commit creates the commit on the server from the files you changed. pando land moves main to include it. If someone landed first and your change touches different files, the server rebases it for you; there is nothing to pull and retry.

Files matched by .gitignore or .pandoignore (same syntax) are left out. .sapignore, the old name, still works but is deprecated.

To check out part of a large repository, add --path:

pando clone http://127.0.0.1:7420 api --path services/api

Read without a clone

export PANDO_SERVER=http://127.0.0.1:7420
pando cat main services/api/config.yaml
pando log services/api
pando ls main services

Inside a workspace these commands use the workspace’s server when PANDO_SERVER is unset, so it is only needed elsewhere. --server always wins; a PANDO_SERVER that names the workspace’s own server without a repository keeps the workspace’s repository.

Sign in to a shared server

A team server with browser sign-in gives the CLI a token without anyone handing one out:

pando login https://pando.example.com     # opens the browser; the token is saved for this server
pando whoami
pando logout                              # revokes it

Commands use --token, then PANDO_TOKEN, then the token pando login saved. How sign-in is set up on the server is in Sign-in, organisations, invitations.

Mount instead of checking out

A mount shows a whole revision at once and downloads a file only when something reads it, which suits big repositories and CI jobs that touch a few files:

mkdir ~/main && pando mount ~/main --rev main      # Ctrl-C to unmount

It works on Linux (FUSE), macOS (built in, nothing to install) and Windows (the built-in Projected File System; enable it once with Enable-WindowsOptionalFeature -Online -FeatureName Client-ProjFS). Mounts are read-only; use pando clone to make changes. If your Mac’s management policy refuses the mount, run pando mount ~/main --serve-only and have an administrator run the command it prints.

Lock files that cannot be merged

Art, models, video and other binaries cannot be merged, so two people editing one at the same time lose someone’s work. Lock the file first:

pando lock art/hero.psd          # others can still read it, but cannot land changes to it
pando locks art                  # who holds what
pando unlock art/hero.psd        # or --force to release someone else's lock

A land that changes a file someone else has locked is refused with rejected: `art/hero.psd` is locked by alice. Locks need write access to the path, and every lock and unlock is recorded in the audit log.

Next

Coming from git

You can move to Grovedock one person at a time. git users and pando users work on the same branches of the same server.

Keep using git

With git-remote-pando on your PATH (every install method puts it next to pando), git talks to a Grovedock server directly:

git clone pando::http://127.0.0.1:7420 repo
git push          # lands on the server; if main moved, the server rebases instead of rejecting
git pull --rebase

Fetches are incremental. Set PANDO_PUSH=strict to get git’s reject-if-behind behaviour instead of a server-side rebase.

Command map

gitpandoNotes
git clone URLpando clone URL DIRadd --path services/api to check out part of the repository
git statuspando statusalias st
git add + git commit -mpando commit -m MSG [PATHS]no staging area; pass paths to commit a subset
git pushpando landalias push; the server rebases if main moved
git pullpando syncalias pull
git switch -c x / git switch xpando checkout -b x / pando checkout x
git log -- pathpando log pathanswered by the server, no local history
git diff / git diff A Bpando diff / pando diff A B
git show REVpando show REV
git blame fpando blame f
git show REV:pathpando cat REV pathworks without a clone
git ls-tree REV dirpando ls REV [dir]works without a clone
git branchpando refsalias branch
git merge-base A Bpando merge-base A B
(none)pando find PATTERNpath search on the server
git lfs lock f / git lfs unlock fpando lock f / pando unlock fexclusive locks for files that cannot be merged
git lfs lockspando locks [dir]
(none)pando mount DIR --rev maina read-only view, files fetched on first read (Linux, macOS, Windows)

Moving history in and out

pando import-git ~/code/my-repo --server http://127.0.0.1:7420   # incremental; rerun any time
pando export-git ./mirror.git                                     # first-parent history, identical trees

Keep GitHub, GitLab or Bitbucket up to date

pando mirror pushes every landed change to an existing git host, so CI, code search and issue links keep working there. See Mirror to a git host.

Submodules are not imported. Commits are imported with their authors, messages and dates, so pando log and pando blame show the original history.

What works differently

  • No staging area. pando commit takes paths instead of git add.
  • No local history. log, blame and diff between revisions ask the server. You need the server to be reachable; this is the main trade-off against git.
  • Landing replaces push. If main moved, the server rebases your change when it touches different files. You only resolve conflicts that are real.

Agents and MCP

Remote MCP: any agent, no checkout

pando mcp --http serves a whole Grovedock server to agents anywhere over the Model Context Protocol (2026-07-28, and the earlier versions from 2024-11-05, all served statelessly). An agent needs a URL and a token, nothing else: it reads, searches, writes and lands files, opens and reviews change requests, reports CI checks and reads its inbox, without cloning anything.

pando mcp --http 0.0.0.0:7430 --server https://pando.example.com --allowed-host mcp.example.com
pando token create claude --as agent/claude-01 --repo lumen/fleet --role write   # one token per agent

Connect an agent with the URL and its token:

claude mcp add --transport http pando https://mcp.example.com/mcp \
  --header "Authorization: Bearer $PANDO_TOKEN"                  # Claude Code

Claude Desktop, Cursor, ChatGPT, Gemini CLI and other clients that speak MCP over streamable HTTP take the same URL and header. Clients on versions before 2026-07-28 start with initialize as usual; the gateway answers it without keeping a session.

  • The token decides everything. The gateway passes each request’s token to the server and holds no credentials of its own, so an agent can do exactly what its token allows: limited to some repositories, a highest role, an expiry, every action in the audit log.
  • Protected branches still hold. When an agent writes to one, the change goes to a new review/ branch and a change request opens, asking the person the agent answers to.
  • Stateless: run as many gateways as you like behind a load balancer; nothing is shared.
ToolWhat it does
pando_repositoriesrepositories you can read, with today’s activity
pando_read_file, pando_list_directory, pando_find_filesread code at any branch or change
pando_history, pando_blame, pando_diffwho changed what, and when
pando_writewrite or delete files and land them as one change (or open a change request on a protected branch)
pando_change_requests, pando_change_requestchange requests and what blocks each
pando_open_change_request, pando_comment, pando_reviewpropose, discuss, approve or request changes
pando_land_change_requestland once nothing blocks it
pando_report_checkreport a CI result on a change request
pando_issues, pando_issue, pando_open_issue, pando_comment_on_issue, pando_update_issuework to do: read, open, discuss, close, assign
pando_inboxwhat needs you: assigned issues, requested reviews, changes requested on yours

The agent loop

  1. Someone assigns an issue to an agent (pando issue assign 12 agent/fix-01, or in the console). An issue webhook can wake the agent’s runner at once.
  2. The agent calls pando_inbox, reads the issue with pando_issue, and writes the fix on a branch with pando_write.
  3. It opens a change request saying “Fixes #12” with pando_open_change_request; the person it answers to is asked to review.
  4. After approval and passing checks it lands with pando_land_change_request, and the issue closes itself.

Local MCP, in a workspace

pando mcp serves Grovedock as Model Context Protocol tools over stdio, so coding agents read, commit and land without shelling out:

claude mcp add pando -- pando mcp      # Claude Code; other MCP clients work the same way

Run it inside a workspace. The tools:

ToolWhat it does
pando_catread a file at any revision
pando_lslist a directory at any revision
pando_findfind files whose path contains a string
pando_loghistory, optionally for one path
pando_blamewhich change last touched each line
pando_diffdiff between two revisions
pando_statusuncommitted changes in the workspace
pando_commitcreate a changeset from workspace changes
pando_landland committed work on the branch
pando_syncupdate the workspace to the branch head

Many agents on one branch

Agents can all land on main at once. The server rebases changes that touch different files and lands them in batches, so an agent never has to pull, rebase and retry. A change that really conflicts is refused with the conflicting paths, and the branch is untouched. On one laptop, 1,000 agents landing at once sustain about 500 changes a second; see Benchmarks.

SDKs

Read files, list directories, search and inspect history from your own tools, without a clone. Every client verifies content against its BLAKE3 id.

LanguagePackageExample
Pythonsdk/pythonClient(url).read_file("src/lib.rs")
TypeScriptsdk/typescriptawait new Client(url).readFile("src/lib.rs")
Gosdk/goc.ReadFile(ctx, "main", "src/lib.rs")

For any other language, generate a client from pando.proto. Object encodings are in pando_objects.proto.

Migrate from GitHub

One command moves a repository from GitHub, GitLab, Bitbucket or any git host:

export PANDO_GITHUB_TOKEN=$(gh auth token)   # private repositories, and GitHub's rate limit
pando migrate https://github.com/acme/fleet --to https://pando.example.com/acme/fleet
pando migrate git@gitlab.com:acme/fleet.git --to https://pando.example.com/acme/fleet
pando migrate https://github.example.com/acme/fleet --to https://pando.example.com/acme/fleet \
  --github-api https://github.example.com/api/v3          # GitHub Enterprise Server

What comes across

Historyevery commit on every branch, with its author, message and date, and merges; the files at each branch tip are byte-identical to git’s
Pull requests (GitHub)each open pull request becomes a change request from a github/pr-N branch into its base, with its title, description and a link back
Issues (GitHub)open and closed, oldest first, with every comment, labels, and closed as done or not planned
The repositorycreated if it doesn’t exist, with its GitHub description

Commit ids change (Grovedock addresses content its own way); each change keeps its git message and author. Issues, pull requests and comments are posted by whoever runs the migration, each naming its original GitHub author with a link to the original. Run it again later to bring across new commits; history is imported incrementally.

Keep the old host current while you move

From GitHub, migrate ends by printing the pando mirror command for the repository you came from (from other hosts, write it the same way):

pando mirror --to https://github.com/acme/fleet --server https://pando.example.com/acme/fleet --every 10

With the mirror running, CI, code search and issue links on the old host keep working while people and agents move to Grovedock at their own pace. Stopping the mirror is the whole rollback.

Tokens

  • A GitHub token is needed for private repositories and to stay under GitHub’s rate limit (60 requests an hour without one). Give it in PANDO_GITHUB_TOKEN (or GITHUB_TOKEN), or in a file with --github-token-file. --github-token works but warns, since other users of the machine can see command lines. The token is sent only to the repository’s own host and passed to git through its environment; source URLs are printed without credentials.
  • --no-issues and --no-pull-requests skip those.
  • The destination uses your Grovedock token (PANDO_TOKEN), which needs to be allowed to create repositories in the organisation (an organisation administrator, or a site administrator for a new organisation).

Mirror to a git host

Run Grovedock beside GitHub, GitLab or Bitbucket instead of replacing them. pando mirror keeps a git repository current with a branch, so CI, code search, issue links and anything else attached to your git host keep working while agents land in Grovedock:

pando mirror --to git@github.com:acme/fleet.git --every 5        # runs until stopped
pando mirror --to https://bitbucket.org/acme/fleet.git           # once, for cron or CI
  • Every landed change becomes one git commit with its author, message and date. The same history always makes the same commits, so a mirror can be rebuilt from scratch at any time.
  • It is incremental: each round pushes only what landed since the last one.
  • It never force-pushes. If the destination branch has commits the mirror did not make, it stops and says so; protect the branch on your git host so only the mirror writes to it.
  • Credentials are git’s own: an SSH key, or a credential helper or deploy token. Tokens in the URL are never printed.
  • The local copy lives in your cache directory (--dir to choose).

Options

OptionDefaultMeaning
--to URL(required)where to push: any URL git accepts
--server URL$PANDO_SERVER, or the workspace’s serverthe Grovedock server, with /OWNER/NAME on a server that hosts many repositories
--rev REVmainthe Grovedock branch to mirror
--git-ref REFmainthe branch to update on the git host
--every SECONDSoncemirror again every SECONDS until interrupted
--dir DIRyour cache directorythe mirror’s local bare git repository

Rolling back

Stopping the mirror is the whole rollback: the git host has every change that landed in Grovedock, as ordinary commits, and people can go back to pushing there. To move a repository the other way, from a git host into Grovedock with its pull requests and issues, see Migrate from GitHub.

Organisations and access

On a server that hosts many repositories (pandod server --platform, see Deploying Grovedock), people and agents belong to organisations, get roles on repositories, and land on protected branches through reviewed change requests. This page covers who may do what, and how review works. Signing in from a browser, invitations and multi-tenant servers are in Sign-in, organisations, invitations.

Who may do what

RoleReadWrite (land, push objects, lock)Administer (access, settings)
Read, Triageyes
Write, Maintainyesyes
Adminyesyesyes
  • A person’s role on a repository is the highest of: their organisation role, grants to them, and grants to their teams. Internal and public repositories are readable by everyone with an account on the server. Repositories are private unless made otherwise.
  • --platform-admins administer the whole server: they create organisations and act as administrators everywhere. An organisation’s administrators add members, teams and repositories.
  • An agent (agent/NAME) answers to the person who issued its first token and works with that person’s role, or with its own if it has been given one.
  • A token acts as you or as an agent you answer for. It can be limited to some repositories and to a highest role, and expires within a year (90 days by default). The server stores only a hash; tokens start with pando_, so secret scanners can find leaked ones.
  • People who sign in with single sign-on or a static token get an account automatically; give them a role with pando org member.
  • Someone who can’t read a repository is told it does not exist, so private names don’t leak. Every change to access is in the audit log.
  • With no static tokens, single sign-on or platform tokens, the server is open, for local development; it refuses to listen beyond 127.0.0.1 that way unless PANDO_ALLOW_ANONYMOUS=1.
  • Caches and node daemons serve the default repository only.

Change requests and protected branches

pando protect set main --approvals 1 --check build --bypass admins   # repository admins
pando checkout -b lower-rate && $EDITOR tax.rs && pando commit -m "Lower the rate" && pando land
pando cr create --title "Lower the tax rate" --reviewer mira@lumen.dev
pando cr review 7 approve -m "Matches the regulation"                # mira
pando cr check 7 build passed --url https://ci.example.com/runs/91   # CI, with a write token
pando cr land 7
  • A protected branch is updated only by those its protection lets bypass review (a login, a team org/team, or admins), and by agents when it lets agents land directly. Everyone else lands through a change request: a direct land opens one for them (from a review/… branch) and says so.
  • A change request lands when nothing blocks it: enough approvals from people other than its author, every required check passed, no requested changes, no conflicts, not a draft. Each blocker is shown in words. With “a new version needs new approvals” (the default), pushing to the branch makes earlier approvals and requested changes stale.
  • It lands as one changeset, rebased onto the target’s tip file by file like any other land, with the title, description and approvers in its message. pando cr rebase replays a change request on its target’s tip when they changed different files.
  • Checks belong to a version of the change: CI reports them with pando cr check (or the ReportCheck call) and a new push starts with none.
  • A change is reviewed and checked by someone else (ADR-030). The author, the person an agent author answers to, and every agent that person answers for count as one person: none of them can approve or request changes, their approvals never count, and they can’t report the change’s checks. Give CI its own account or token (for example bot/ci).
  • Reading needs Read; commenting needs Read; opening a change request needs Triage; approving, requesting changes, reporting checks and landing need Write; protecting branches needs Admin.

Grovedock platform: hosting, review and the web app

Grovedock’s web app grows from a read-only console into a full hosting platform: organisations, repositories, people and agents, change requests with review, protected branches and the activity of an agent fleet, on top of the Grovedock server. This page records which flows it covers, how they compare with GitHub’s, and the order they are built in.

How the flows were mapped

GitHub’s public pages for a busy repository (tokio-rs/tokio) were captured in a scripted browser and their structure read from the accessibility tree: repository home, pull request list, a pull request’s conversation and changed files, commits, a commit, blame, a file, issues, an issue, compare, releases, actions, branches and the organisation page. Pages that need an account (creating repositories and organisations, settings, rulesets, tokens, notifications, submitting a review, the merge queue) were not observed; their flows come from GitHub’s documentation and are marked documented below. Nothing from GitHub’s code, design or assets is reused; only the list of jobs people do.

What GitHub’s pages are made of (observed)

PageWhat is on it
Pull request listquery box with qualifier syntax (is:pr state:open); filters for author, label, project, milestone, reviews, assignee; sort; comfortable or compact density; check status per row (“91/92”); labels; stack position (“2 of 2”)
Pull requestsplit into Conversation, Commits, Checks and Files changed tabs; sidebar forms for reviewers, assignees, labels, projects, milestone and linked issues; participants
Files changedcommit range picker (“Changes from all commits”), file filter, conversations, jump to file, diff settings, collapse and expand all
Commitsbranch, author and date filters; grouped by day; copy SHA; expand description
Commitfile tree with filter; one collapsible region per file with copy-path and options; commit comments
Blame and filebranch picker, path breadcrumbs, raw/blame/history actions, symbols
Issues and issuethe same query-and-filters list; timeline with sidebar metadata
Compare, releases, actions, branches, organisationbase/head pickers; release notes with assets; workflow runs; branch list with ahead/behind; organisation repositories and people

Where GitHub is weak, and what Grovedock does instead

GitHub todayGrovedock
A pull request is four tabs; reviewing means switching between the conversation, the diff and checksOne review page: the file tree, the diff, comments and checks side by side, with keyboard navigation through files and threads
Large diffs are cut off (“Load diff”, files hidden) and big pull requests get slowDiffs render only what is on screen and highlight off the main thread; nothing is hidden behind a button except deliberately huge generated files
Lists go stale until you reloadEverything live: lists, review state and branch tips update as they change, without moving what you are reading
Agents are ordinary users; a fleet opening hundreds of pull requests floods the listAgents are first-class: marked as agents, grouped by fleet, with a timeline per agent and filters for people versus agents
Merge conflicts surface late; the merge queue is a separate featureGrovedock lands with path-scoped conflict checks (1,012 lands/s at 200 agents against git’s 0.5, measured); a change request merges by landing, and conflicts are known per path before anyone reviews
Branch protection and rulesets live in settings pages far from the codeProtection is shown where it applies: on the branch, on the change request and in the land result, with the reason a land was refused
Monorepo work: slow clones, history of a directory is slowClone in 0.46 s instead of 19.1 s (git/git), mount any revision, and path history is a server query
Notifications are one stream for everythingA review inbox: what waits on you, what you wait on, and what agents need approved, in that order

Flows, in build order

Each phase ships with server and web app together, tests, docs and a demo that runs in the browser.

  1. Repositories and organisations (done in 3.1). One server hosts many repositories, each with its own storage (a {repo} template in the storage URLs). Requests name their repository; a repository call that names none is refused. Organisation and repository pages, repository creation, a home page with your repositories and their activity.
  2. Coexistence with GitHub, GitLab and Bitbucket (done in 3.1: pando mirror). A continuous mirror of every landed change to an existing host, so CI, integrations and audit archives keep working while agents land in Grovedock. This is how an enterprise adopts Grovedock without replacing anything.
  3. People, agents and access (done in 3.2, ADR-023). Accounts from the identity provider (OIDC is already supported), personal and agent tokens, organisation members, teams, and per-repository roles (read, write, maintain, admin). Agents are accounts marked as agents and owned by a person or team.
  4. Change requests and review (done in 3.3, ADR-024). Open a change request from a branch, review with comments on lines, suggestions, approvals and requests for changes; merge by landing. Protected branches with required approvals and checks; an agent’s land on a protected branch becomes a change request automatically.
  5. Inbox, checks and integrations (done in 3.3, ADR-025). Review inbox and notifications, check runs reported by CI, webhooks, and an audit view.
  6. Issues and releases (documented flows). Issues with the same live lists, labels and linking to change requests (done in 3.3, ADR-027); releases with notes and assets (not yet).

Multi-tenant servers

A server that strangers share (a hosted service) starts with --multi-tenant (PANDO_MULTI_TENANT=1, with --platform; ADR-032). It is off by default, and a company’s own server behaves as before. With it:

  • Internal repositories are readable by members of the owning organisation (directly or through a team), not by every account on the server. Public repositories stay readable by every account; private ones only by those given access.
  • Members and grants name people who already have an account (they have signed in once). Anyone else is refused with “LOGIN has no Pando account yet; ask them to sign in first, or send an invitation”. Agents (agent/NAME) are recorded as before.
  • Organisations may be created by any person (not agents), who becomes their administrator, up to --max-orgs-per-account (default 3) organisations they administer. Organisations and accounts share one namespace: a name that is someone’s login is taken, and service names (admin, api, login, settings, new, explore, www, mail, support, help, security, status, docs, pando) are reserved. Site administrators are not capped.
  • Webhooks keep the default refusal of loopback and private networks; --webhook-allow-private can’t be combined with it.
  • The server refuses to start without authentication.

The cross-tenant suite (every_method_keeps_tenants_apart in crates/pando/tests/isolation.rs) gates every release: it calls every method in the protocol with one organisation’s tokens (a person, an agent, tokens limited to a repository, and someone in no organisation) at another organisation’s repositories, change requests, issues, webhooks, protections, locks, teams and tokens, and each must be refused as missing or forbidden or show nothing of theirs. A method added to the protocol without an entry in its table fails it.

Plans and limits

Every platform server records what each organisation uses (ADR-035); a multi-tenant server also enforces limits on it. A company’s own server (without --multi-tenant) records usage but limits nothing.

  • Active agents. An agent (agent/NAME or bot/NAME) is active in an organisation for a calendar month (UTC) once it lands a change in one of its repositories: a Land that succeeded, a CompareAndSwap that moved the branch, or a change request it wrote or landed. A refused land is an error and a lost compare-and-swap moves nothing; neither counts. People are never active agents.
  • Seats are the people (not agents) who are members of the organisation.
  • Storage is measured hourly per repository (its SQLite database and write-ahead log, plus its objects when they live in an object store), with each month’s peak per organisation. Postgres metadata isn’t measured.

Limits. An organisation’s limits are seats, active agents, storage and private repositories, each a number or unlimited, plus a plan name shown to its administrators and a suspension flag. Organisations without limits of their own take --default-org-limits, e.g. --default-org-limits seats=5,agents=20,storage=1GiB,private_repos=unlimited,plan=Free (multi-tenant only; without it they are unlimited). With --multi-tenant:

LimitWhat happens
SuspendedWrites are refused with PermissionDenied (“organisation ORG is suspended; contact support”); reads work.
Active agentsA new agent’s land is refused with ResourceExhausted (“N of N active agents this month; upgrade at URL or wait until DATE”); agents already active this month keep landing.
StorageOver the limit, writes are refused with ResourceExhausted; reads work.
SeatsAdding a person beyond the limit is refused (changing a member’s role isn’t).
Private repositoriesCreating one beyond the limit, or making one private, is refused.

Storage is measured hourly, so an organisation can pass its limit by up to an hour of pushes, and new agents landing at the same moment can pass the active-agent limit by a few.

Managing plans. Organisation administrators see their plan and use with pando org usage ORG or on the console’s Usage & plan tab (each limit with a bar and what happens when it is reached), which shows a Change plan button when the server has --billing-url. Every page of a suspended organisation shows a banner saying so, with a Contact support link when the server has --support-url (a mailto: or web address). Site administrators change limits with pando admin limits set ORG --seats N --agents N --storage 10GiB --private-repos unlimited --plan NAME (flags not given stay as they are), and suspend or restore an organisation with pando admin limits suspend|restore ORG. A billing service does the same through the SetLimits and SetSuspended RPCs, which set absolute values, so repeating a call is harmless. Nothing is ever deleted when limits fall: an organisation over a new limit keeps its data and can read it.

Support licences. pandod server --license FILE checks a support licence at startup (Ed25519-signed, checked offline) and logs whether it is valid, in its grace period or expired; pandod license verify FILE checks one by hand, and GetLicense reports it to site administrators. A licence never turns anything off. Releases up to 3.5.0 don’t carry the signing key yet, so they report every licence as invalid.

UX audit of the web app (2026-10-17)

Fixed in this round: the review page can collapse files; landing asks for confirmation and names the branch; a conflict offers a rebase, which re-applies the change when edits don’t overlap and otherwise names the files; errors start with a capital and say what to do next; the code view has a visible “Go to file”; live views load an hour of history; the command menu no longer loses the first key typed; effects can no longer hand React a Promise as cleanup.

Still open, in priority order:

  1. Open a change request from the app. Compare two branches and open one (GitHub’s “Compare & pull request”). Today agents open them by landing on a protected branch.
  2. Profiles. Clicking a person or agent should open their page: activity, repositories, owned agents.
  3. Create a repository and an organisation from the app.
  4. Mark files as viewed in a review, with progress, so large reviews can be resumed.
  5. Syntax highlighting inside diffs.
  6. Search inside files. The server’s Find matches paths only.
  7. Edit settings (protection, access) once the server implements them; the demo shows them read-only.
  8. Notifications beyond the inbox: email or webhook delivery.

Update (3.5.0): 1, 3 and 7 have shipped: the console opens change requests, creates repositories and organisations, and edits protection and access.

Sign-in

People sign in from a browser with Google (or any OpenID Connect provider) or with a link sent by email. pandod server --platform … --login-listen 127.0.0.1:7430 serves the pages under /login/*; put it behind the same host name as the console (Caddy routes /login/* to it). A successful sign-in creates an ordinary platform token, “browser session” (30 days, Admin ceiling, every repository), and sends the browser to {public-url}{next}#pando_session=TOKEN. The console reads the token from the fragment and wipes it from the address bar. Revoking the token signs the browser out (ADR-031).

Google. In Google Cloud Console, create an OAuth client of type “Web application” with the authorized redirect URI {public-url}/login/oidc/google/callback, then:

export PANDO_OIDC_GOOGLE_CLIENT_ID=….apps.googleusercontent.com
export PANDO_OIDC_GOOGLE_CLIENT_SECRET_FILE=/run/secrets/google-client-secret
pandod server --platform sqlite:///var/lib/pando/registry.db --repos-metadata … \
  --login-listen 127.0.0.1:7430 --public-url https://pando.example.com \
  --oidc google=https://accounts.google.com

Any other OpenID Connect provider works the same way: --oidc ID=ISSUER_URL with PANDO_OIDC_<ID>_CLIENT_ID and PANDO_OIDC_<ID>_CLIENT_SECRET (or _FILE), and the redirect URI {public-url}/login/oidc/ID/callback. A provider must report email_verified: true for a new account to be created; some Microsoft Entra ID setups don’t send that claim, so their users can’t sign up (link them to an account instead, below).

Email links. --smtp-url smtps://smtp.example.com:465 (or smtp://HOST:587?tls=required), --mail-from "Grovedock <login@example.com>", with PANDO_SMTP_USERNAME and PANDO_SMTP_PASSWORD (or _FILE). A link works once, within 15 minutes; opening it shows a “Sign in” button, so mail scanners that fetch links don’t use it up. The form answers the same whether or not the address may sign in. Add --turnstile-site-key and PANDO_TURNSTILE_SECRET to put a Cloudflare Turnstile challenge on it.

Who may sign up. --signup waitlist (the default) lets only listed addresses and domains create accounts; open lets anyone; closed lets nobody. People with an account can always sign in. Site administrators manage the list:

pando admin signup allow mira@example.com
pando admin signup allow @example.com
pando admin signup list
pando admin signup remove @example.com

Accounts. The first sign-in creates a person’s account, named after the email’s local part (mira, then mira-2… if taken by an account or organisation; site administrators’ logins and names like admin, api, login and settings are never handed out). A sign-in is never attached to an existing account because the email matches. To let the first administrator (created with pandod token) sign in from a browser, a site administrator links an identity:

pando admin identity link kabir --provider email --subject kabir@example.com

GET /login/providers lists the sign-in buttons for the console: {"providers":[{"id":"google","name":"Google","url":"/login/oidc/google"}],"email":true}.

Terms. Before a first sign-in creates an account, the person must tick “I am 18 or older and accept the Terms and Privacy Policy”; without it no account is made. The time is kept on the account (accepted_terms_at_ms). Link your documents with --terms-url and --privacy-url (PANDO_TERMS_URL, PANDO_PRIVACY_URL). Accounts made before this, and existing accounts signing in, are not asked.

The CLI. pando login [SERVER_URL] signs the CLI in through the same pages (the OAuth device flow, RFC 8628): it prints a link and a code like BCDF-GHJK, opens the browser, and waits while you sign in and approve “CLI sign-in from … as you”. It then saves a 90-day token, “cli on HOST” (Admin ceiling), in ~/.config/pando/credentials.json ($XDG_CONFIG_HOME/pando if set; under the user profile on Windows), readable only by you and kept per server. Commands use --token, then PANDO_TOKEN, then that file. pando whoami shows who you are; pando logout [SERVER_URL] revokes the token and forgets it. The endpoints are POST /login/device/code, GET /login/device and POST /login/device/token; only the device code’s hash is stored, for 10 minutes, and the token is returned once. The approve page shows the code: approve only a code your own terminal shows.

Invitations. Organisation administrators invite by email, from the console’s People tab or the API (CreateInvitation, ListInvitations, RevokeInvitation). An invitation is a link, {public-url}/login/invite/TOKEN, valid 7 days and once; it is emailed when the server sends mail, and always returned to the administrator to share by hand. Whoever opens it signs in with any provider, creating an account if needed (the waitlist doesn’t apply to an invitation), and presses “Join ORG”: they become a member with the invited role. The link is the authority: the address it was sent to is shown on the page but need not be the one they sign in with (ADR-033). On a --multi-tenant server this is how people who have never signed in are added. Invitations need --login-listen.

The web app’s foundation

React 19 with the React Compiler, React Aria for accessible components, TanStack Router and Query, Connect-ES over gRPC-Web, and Shiki in a worker. Every view’s state is in its URL. The server contract is written first (protobuf), implemented by an in-browser demo server so the whole app can be built and tested, then implemented by pandod.

Plans and limits

Grovedock is open source and has no feature gates: everything on this page is a limit you can see and, where it says so, change.

Built-in limits

WhatLimitChange it with
One call’s running time600 s--request-timeout
Calls in progress, whole server4,096 (more are refused with Unavailable, and clients back off)--max-in-flight
Calls in progress, one connection256
Calls in progress, one account (platform servers)128 (more are refused with ResourceExhausted)--max-calls-per-principal
One request message64 MiB on platform servers, 260 MiB otherwise; files of 8 MiB and more upload in chunks, so this never limits file size--max-request-mb
Repositories open at once (platform servers)256; one unused for 10 minutes closes--max-open-repos, --repo-idle-minutes
Organisations one person may create (multi-tenant servers)3--max-orgs-per-account
Access token lifetime90 days by default, a year at mostpando token create --days
Browser session30 daysrevoke the token to sign out
Email sign-in link15 minutes, once
Webhooks per repository20, keeping each one’s last 200 deliveries
Pre-land check10 s, then the land is refused--land-hook-timeout

Plans

A platform server can record what each organisation uses (people, active agents, storage, private repositories) and, on a multi-tenant server, enforce a limit on each. How usage is counted and what happens at a limit is in Sign-in, organisations, invitations.

Grovedock Cloud, a hosted Grovedock, is coming. Until then, everything runs on your own machines with the same binary; see Deploying Grovedock.

Webhooks

A repository on a platform server (pandod server --platform) can call URLs when something happens in it, so CI, chat and issue trackers react to Grovedock the way they react to GitHub.

pando webhook add lumen/fleet https://ci.example.com/hooks/pando --event push --event change_request
pando webhook ping ID        # sends a ping now and shows the answer
pando webhook log ID         # recent deliveries

Or in the console: repository Settings → Webhooks (repository administrators).

Events

EventSent whenaction
pusha branch is created or moved (a land, a push, a rebase)created, updated
change_requesta change request is opened, closed or landedopened, closed, landed
reviewsomeone reviewsapproved, requested changes, commented
commentsomeone comments on a change request or an issuecreated
checkCI reports a checkreported
issuean issue is opened, closed, reopened or assignedopened, closed, reopened, assigned
pingyou ping the webhookping

A webhook with no events listed receives every event.

Requests

POST with a JSON body and these headers:

HeaderValue
X-Pando-Eventthe event, e.g. push
X-Pando-Deliverya unique id for this delivery; retries keep it
X-Pando-Signature-256sha256= and the hex HMAC-SHA256 of the body, keyed with the webhook’s secret

Every body has event, action, delivery, repository (owner, name, full_name), sender and at_ms. A push adds ref, before, after and head (id, author, message); change request events add change_request (number, title, author, state, source, target, source_tip, base, draft), and a landing adds landed_as; check adds check (name, state, summary, details_url) and the change request; issue adds issue (number, title, author, state, labels, assignees).

Verifying the signature

The signature is computed exactly as GitHub computes X-Hub-Signature-256, so existing libraries work. Compare in constant time, over the raw body:

// Node
const crypto = require("node:crypto");
const ok = (secret, body, header) => {
  const want = "sha256=" + crypto.createHmac("sha256", secret).update(body).digest("hex");
  return header.length === want.length && crypto.timingSafeEqual(Buffer.from(header), Buffer.from(want));
};
# Python
import hashlib, hmac
def ok(secret: bytes, body: bytes, header: str) -> bool:
    want = "sha256=" + hmac.new(secret, body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(want, header)
// Go
func ok(secret, body []byte, header string) bool {
	m := hmac.New(sha256.New, secret)
	m.Write(body)
	return hmac.Equal([]byte("sha256="+hex.EncodeToString(m.Sum(nil))), []byte(header))
}

Delivery

  • Answer with any 2xx within 10 seconds. Anything else is retried with growing gaps (20 s up to about 20 minutes), eight attempts in all; redirects are not followed.
  • Delivery is at least once. Deliveries are queued in the registry and claimed with a lease, so a restart loses none; but a server that stops after posting and before recording the answer posts that delivery again. Receivers dedupe by the X-Pando-Delivery id (also the payload’s delivery field).
  • Each webhook gets at most 2 deliveries in flight, and each delivery at most 30 seconds, so a slow or dead endpoint delays only its own deliveries.
  • Finished deliveries (delivered, or given up after eight attempts) are deleted after 30 days.
  • Webhooks never call link-local, unspecified, multicast or reserved addresses, or cloud metadata services (169.254.169.254, 100.100.100.200, fd00:ec2::254), including IPv4 addresses written as IPv6 (mapped, NAT64, 6to4). By default they don’t call loopback or private networks (RFC 1918, 100.64.0.0/10, fc00::/7) either; a self-hosted server whose CI lives there starts with --webhook-allow-private. The address is checked when the webhook is saved and again at every delivery, and the delivery connects only to the address it checked, so a DNS answer that changes in between can’t redirect it.
  • A failed delivery reports the HTTP status, or only “unreachable”: never why a connection failed, so webhooks can’t be used to probe the server’s network.
  • Each repository may have up to 20 webhooks; the last 200 deliveries of each are kept.

The web console

The console is a web app for watching and reviewing work on a Grovedock server: browse code with blame and history, follow every change as it lands, see who (person or agent) landed what, review change requests, and see which files are locked. It runs entirely in the browser and talks to pandod directly over gRPC-Web; there is no backend of its own.

Try it: the live demo runs a simulated organisation in your browser: four repositories, a dozen agents landing changes every few seconds, change requests with reviews, checks and conflicts. Nothing leaves your machine.

Connect it to your server

  1. Allow the site that serves the console to call your server:

    PANDO_TOKENS="me:$(openssl rand -hex 24)" pandod server --listen 0.0.0.0:7440 --web-origins https://console.example.com
    

    --web-origins (or PANDO_WEB_ORIGINS) takes a comma-separated list. Browser access is the web feature flag, on by default; turn it off with PANDO_FLAGS=web=off.

  2. Open the console, choose Connection, and enter the server’s address and, if the server requires one, a token. The token stays in the tab unless you choose to remember it.

Sign in

When the console is served from the same address as a server with sign-in pages (pandod server --login-listen, behind one proxy that routes /login/* to them), the Connection screen shows Sign in with Google (one button per configured provider) and an email-link form instead of the address and token fields; Use a token instead brings those back. Signing in creates your account the first time and returns you to the console signed in: the session arrives once in the address (#pando_session=…), is kept as this site’s connection, and is wiped from the address bar and history straight away. Sign out, in the account menu, revokes that session’s token on the server and forgets it in the browser. The email form shows a Cloudflare Turnstile check only when the server is configured with a site key; only then does the console load Cloudflare’s script.

The console’s Content Security Policy allows exactly that: scripts and frames from https://challenges.cloudflare.com, and forms posting to the console’s own origin or any https: address. A host that sends its own CSP header must allow the same.

Set up an organisation

Someone signed in who belongs to no organisation lands in a three-step set-up, also reachable from New organisation in the sidebar:

  1. Name your organisation: its short name (used in addresses) and a display name. A taken, reserved or invalid name, or reaching the most organisations the server lets one person create, is explained in place.
  2. Invite teammates (optional): an email address and a role per person. Each invitation is a single-use link valid for 7 days; it is emailed when the server sends mail, and shown to copy either way.
  3. Connect an agent (optional): a token for an agent (agent/NAME, answering to you) or for yourself, write access for 90 days, shown once, with the MCP address (default SERVER/mcp; change it to wherever your pando mcp --http gateway answers, see Agents) and a ready-to-paste claude mcp add command.

Later, the organisation’s People tab invites people by email and lists invitations not yet accepted (expired ones are marked), each with Revoke.

A link of the form https://console.example.com/?server=https://pando.example.com opens the console already pointed at a server.

Host it yourself

The console is static files. Every release attaches them as pando-console-vVERSION.tar.gz (with a .sha256 and the same signed provenance as the binaries), or build them yourself:

cd console && pnpm install && pnpm build   # into console/dist

From pandod itself. --console-dir DIR (PANDO_CONSOLE_DIR) serves the files at / on the server’s own address, next to gRPC and gRPC-Web, so no other web server and no --web-origins are needed:

curl -LO https://github.com/kabirnarang39/pando/releases/download/v3.5.0/pando-console-v3.5.0.tar.gz
tar xzf pando-console-v3.5.0.tar.gz
pandod server --platform sqlite:///var/lib/pando/platform.db … --console-dir pando-console-v3.5.0

Every GET is a file, and a path that isn’t one is index.html. Files under assets/ (named by their content hash) are cached for a year; everything else, index.html included, is checked on each visit, so a new release shows at once. Each file carries a Content Security Policy header that adds frame-ancestors 'none' to the build’s own policy and keeps connect-src to the server, plus nosniff, X-Frame-Options: DENY, Referrer-Policy: no-referrer and Cross-Origin-Opener-Policy: same-origin. It needs the web flag (on by default). Put TLS (and HSTS, compression) in front with any proxy, or use --tls-cert.

From a web server. Serve the unpacked directory from anywhere (Caddy, nginx, an object store, a CDN, GitHub Pages) and send the same headers. With Caddy, proxying gRPC to pandod on the same host name:

pando.example.com {
  @grpc header Content-Type application/grpc*
  handle @grpc {
    reverse_proxy h2c://127.0.0.1:7420
  }
  handle {
    root * /srv/pando-console
    header Content-Security-Policy "connect-src 'self'; frame-ancestors 'none'; base-uri 'none'; object-src 'none'; form-action 'self'"
    header X-Content-Type-Options nosniff
    @assets path /assets/*
    header @assets Cache-Control "public, max-age=31536000, immutable"
    @app not path /assets/*
    header @app Cache-Control no-cache
    encode zstd gzip
    try_files {path} /index.html
    file_server
  }
}

form-action must allow 'self': the email sign-in form posts to /login/.

URLs use the fragment (/#/server/repo/changes), so no server-side routing is needed and deep links work from any path.

What works where

Featurepandodpandod --platformComing next
Code, blame, file history, search by pathyesyes
Changes as they land, word-level diffsyesyes
Locks, branch switching, fleet timelineyesyes
Many repositories, grouped by ownerone repositoryyes
People, agents, teams, roles and tokensyes (3.2)
Sign in, new organisations, invitationsyes, with --login-listen
Change requests, reviews, line comments, land gate, rebaseyes (3.3)
Inbox, activity, protected branchesyes (3.3)

The console asks the server what it supports and shows only that: a server without hosting shows its one repository, and a server without review hides reviews and the inbox. The demo shows everything. The platform contract is pando_platform.proto; the plan is in Platform.

Keyboard

KeysDoes
⌘K or Ctrl K, /search files, repositories, reviews and actions
tgo to a file in this repository
g then h, i, c, l, r, khome, inbox, code, changes, reviews, locks
j, knext and previous item in a list
n, pnext and previous file in a review
areview the change request
b, h, ublame, file history, unified or split diff
?all shortcuts

Performance and privacy

  • The first screen needs about 167 KB of gzipped JavaScript; each screen after that loads its own code on first visit (and is preloaded on hover). The build fails if the first screen grows past its budget.
  • Syntax highlighting runs in a worker. Parts of a file far from the screen are plain text until they come near it, yet every line stays in the page, so the browser’s find works: a 50,000-line (1.7 MB) file opens within 1.5 seconds in the end-to-end tests. Files over 1 MB show without colours, and say so.
  • Fonts and icons are bundled: the console makes no request to any third party.
  • Untrusted content (READMEs, descriptions, comments) is rendered without HTML: markdown is turned into elements, raw HTML shows as text, and only http(s), mailto and relative links are followed.

Deploying Grovedock

A single pandod server with SQLite is enough for a team on one machine. For more, run stateless servers on Postgres or CockroachDB with objects in S3, GCS or Azure, and put a regional cache and a node daemon between CI machines and the servers.

The whole topology with Docker Compose

The repository’s docker-compose.yml starts Postgres, an S3-compatible store, a server, a regional cache and a node daemon. They listen on the network, so they need a token: put one in .env next to the compose file first (docker compose refuses to start without it):

echo "PANDO_TOKEN=$(openssl rand -hex 24)" > .env
docker compose up -d
export $(cat .env)
pando clone http://localhost:7420 work --objects http://localhost:7422

--objects sends file reads through the node daemon; commits and lands still go to the server.

For free, on one machine

deploy/free-tier sets up a production server on Oracle Cloud’s Always Free ARM machine: HTTPS by Caddy, a hardened systemd service, and hourly incremental backups to a free Neon database and R2 bucket. It costs nothing until usage outgrows the free tiers, and moves to Postgres and object storage with pandod copy when it does.

Hosting many repositories

One pandod can host any number of repositories, each with its own storage, for a whole company: organisations, members, teams, per-repository roles and access tokens.

# 1. The first administrator's token, written straight into the registry (shown once).
pandod token --platform sqlite:///var/lib/pando/platform.db --account kabir > ~/.pando-admin-token

# 2. The server. With a platform token in the registry, every call needs a token.
pandod server --listen 0.0.0.0:7420 \
  --platform sqlite:///var/lib/pando/platform.db \
  --repos-metadata 'sqlite:///var/lib/pando/repos/{repo}.db' \
  --repos-objects 'file:///var/lib/pando/objects/{repo_id}' \
  --platform-admins kabir

# 3. An organisation, a repository, people and agents.
export PANDO_TOKEN=$(cat ~/.pando-admin-token) PANDO_SERVER=https://pando.example.com
pando org create lumen
pando repo create lumen/fleet
pando org member lumen mira@lumen.dev write
pando team set lumen/platform --member mira@lumen.dev
pando repo grant lumen/fleet lumen/platform admin
pando token create perf-fleet --as agent/perf-05 --repo lumen/fleet --role write
  • --platform is the registry: which repositories exist, and who may use them. --repos-metadata and --repos-objects are templates: {repo} becomes OWNER/NAME and {repo_id} becomes OWNER--NAME. Each repository gets its own database and its own landing queues, so one busy repository never slows another, and a repository can be backed up, moved or deleted on its own.
  • Clients name a repository by adding /OWNER/NAME to the server address; the web console lists them. A repository call that names none is refused: the server’s own --metadata repository is not reachable in this mode. To keep a single-repository server’s history, create the repository and copy it in with pandod copy.
  • --multi-tenant (PANDO_MULTI_TENANT=1) is for a server that strangers share; see Multi-tenant servers. --max-orgs-per-account (default 3) caps how many organisations a person may create there.
  • Webhooks never call loopback, private networks or cloud metadata addresses. --webhook-allow-private lets them call loopback and private networks (self-hosted CI on an internal network); it can’t be combined with --multi-tenant.

The web console

Each release ships the console as pando-console-vVERSION.tar.gz. Unpack it and pass --console-dir to pandod server to serve it from the server’s own address, or serve it from your proxy; see Host it yourself.

Signing in from a browser

On a platform server, --login-listen ADDR serves sign-in pages (Google or any OpenID Connect provider, and email links) that hand the console an ordinary platform token. Run it on a loopback address and route /login/* to it from the reverse proxy that serves the console, on the same host name as --public-url:

pando.example.com {
  handle /login/* {
    reverse_proxy 127.0.0.1:7430
  }
  # … the console and gRPC-Web routes
}
SettingFlag / environment
Public address--public-url / PANDO_PUBLIC_URL (https; http only for localhost)
Provider--oidc google=https://accounts.google.com (repeatable); PANDO_OIDC_GOOGLE_CLIENT_ID, PANDO_OIDC_GOOGLE_CLIENT_SECRET or …_FILE
Email--smtp-url / PANDO_SMTP_URL, --mail-from / PANDO_MAIL_FROM; PANDO_SMTP_USERNAME, PANDO_SMTP_PASSWORD or …_FILE
Bot challenge--turnstile-site-key; PANDO_TURNSTILE_SECRET or …_FILE (unset: none)
Who may sign up--signup waitlist|open|closed / PANDO_SIGNUP (default waitlist, managed with pando admin signup)

The proxy must set X-Forwarded-For, which the per-address request limits use. With sign-in on, the server always requires a token, even before any exists. Setup, the waitlist and linking the first administrator are described in the platform notes.

Pre-land checks

With --land-hook, the server sends each proposed land to your URL before it moves the branch: who is landing, the branch, the changeset and its paths. Any 2xx answer lets it land; anything else, including a timeout, refuses it. Use it to require green CI or an approved review. The payload is described in Architecture.

Running a hosted service

Taking a repository down

pandod repo --platform sqlite:///var/lib/pando/platform.db disable acme/leaky --reason "takedown request 2026-118"
pandod repo --platform sqlite:///var/lib/pando/platform.db enable acme/leaky

disable refuses every read and write of the repository, for everyone including administrators, with “disabled by the operator: REASON”, and hides it from listings. Nothing is deleted. It works while the server runs, which notices within 5 seconds. Both are recorded in the audit log (disable_repository, enable_repository) when the command is given --audit-log (pandod --audit-log FILE repo …).

Deleting a repository

pandod repo --platform sqlite:///var/lib/pando/platform.db delete acme/old \
  --repos-metadata 'sqlite:///var/lib/pando/repos/{repo}.db' \
  --repos-objects 'file:///var/lib/pando/objects/{repo_id}' --confirm acme/old

This deletes the registry rows (change requests, reviews, issues, webhooks and their deliveries, grants, protections) and the repository’s SQLite database and file:// object directory. Objects in a cloud bucket and Postgres metadata are named in the output for you to remove. It cannot be undone: take a pandod backup first. Platform tokens limited to the repository keep its name in their list; revoke them if the name will be reused.

Purging a leaked secret

History is content-addressed, so a committed secret can’t be edited out in place. Rotate the secret first (that is what actually protects you), then:

  1. Disable the repository (pandod repo disable) so nobody fetches it meanwhile.
  2. Rewrite the history without the secret: pando export-git to a git repository, remove the file or string with git filter-repo, and pando import-git into a new repository.
  3. Delete the old repository (pandod repo delete; on Windows, stop the server first, because Windows won’t delete files a running server holds open); recreate it under the same name if you want to, and import the rewritten history there.
  4. Ask everyone with a checkout to clone again; old checkouts still hold the secret.

Deleting an object while other changesets still reference it is not supported: there is no object garbage collection yet, so objects a deleted branch alone referenced stay in storage until the repository is deleted. Plan capacity accordingly.

Next steps

Security and audit

How a server decides who is calling, what they may do, and what it records. Report vulnerabilities as described in the security policy; the security model is there too.

Warning

A server without tokens, single sign-on or platform tokens accepts every caller. It refuses to listen beyond 127.0.0.1 in that state unless you pass --allow-anonymous; never do that on a network you don’t control.

Authentication and permissions

Without --tokens, --oidc-issuer or platform tokens the server accepts everyone, and so it listens only on a loopback address unless --allow-anonymous (PANDO_ALLOW_ANONYMOUS=1) says otherwise. In production, set at least one. With OpenID Connect, the claim named by --oidc-claim (for example email) becomes the principal that access rules refer to.

Every --oidc-require CLAIM=VALUE (repeatable, or comma-separated in PANDO_OIDC_REQUIRE) must match the token exactly; booleans and numbers compare as written (email_verified=true). Issuers that sign tokens for anyone refuse to start without a rule that binds your tenant: GitHub Actions (https://token.actions.githubusercontent.com) needs repository_owner, repository_owner_id, repository, repository_id, enterprise or enterprise_id; Google (https://accounts.google.com) needs hd. For other providers, pin the tenant the same way (tid=…, org_id=…) when the issuer serves more than your organisation.

pandod server --oidc-issuer https://token.actions.githubusercontent.com --oidc-audience pando \
  --oidc-require repository_owner=acme ...

A token is refused when its principal is a --platform-admins login (unless that login is also given with --oidc-admin), or starts with agent/ or bot/, so an identity provider that lets people choose their name can’t make anyone an administrator or an agent. Unknown signing keys refresh the key set at most once a minute.

Access rules grant read and write per directory and per branch. Principals who can read only part of the repository get path-scoped checkouts, search and history, with changes outside their scope removed. --acl FILE (or PANDO_ACL) names a JSON file of rules:

{
  "default":    { "write": ["*"], "refs": ["user/*"] },
  "principals": {
    "release-bot": { "refs": ["main", "release/*"] },
    "api-team":    { "write": ["services/api", "libs/api-client"], "refs": ["main"] }
  }
}

A principal’s grants are the union of default and its own entry. read and write list path prefixes (* is everything); refs lists branch names or prefix/*. When no entry mentions read, everyone may read everything.

Audit log

--audit-log FILE (or PANDO_AUDIT_LOG) appends one JSON line per event: who read or changed what, from which address, with what result. It is off by default because it grows with every request. Events go to one file per UTC day, FILE.YYYY-MM-DD; files older than --audit-retention-days (default 365) are deleted. Behind a reverse proxy on the same host, add --trust-proxy so the address recorded is the client’s, not the proxy’s.

Retention for hosted services. Keep at least 365 days. In India, CERT-In’s 2022 directions require 180 days of logs kept within India, and the DPDP Rules require one year from May 2027; other jurisdictions have their own rules, so check yours. Copy the daily files to durable storage in the required region (the same backup job as the databases works) rather than relying on one disk.

Clocks. Timestamps are UTC (RFC 3339) from the host clock, so keep it synchronised: run systemd-timesyncd or chrony against reliable servers. CERT-In asks Indian service providers to synchronise with the NIC (samay1.nic.in, samay2.nic.in) or NPL (time.nplindia.org) NTP servers; elsewhere use your cloud’s time service or pool.ntp.org.

{"timestamp":"2026-10-16T09:06:02.695Z","kind":"land","principal":"alice","ref":"main","changeset":"0844d2dc…","landed":"0844d2dc…","rebased":false}
{"timestamp":"2026-10-16T09:06:02.702Z","kind":"read_file","principal":"ci","rev":"main","path":"README"}
{"timestamp":"2026-10-16T09:06:02.702Z","kind":"rpc","principal":"ci","peer":"10.0.4.17","method":"/pando.v1.Browse/ReadFile","status":"0","ms":0}
{"timestamp":"2026-10-16T09:06:58.112Z","kind":"auth_failed","peer":"10.0.9.2","presented":true,"reason":"invalid token"}
kindRecorded for
rpcevery authenticated request: principal, client address, method, gRPC status, duration
auth_failedevery rejected request: client address, whether a token was presented, why (never the token)
landeach land: branch, changeset, and the landed id or the refusal
set_refbranch moves through the refs API
read_file, list_dir, checkout, findcontent reads, with revision and paths
log, blamehistory queries, with revision and path
lock, unlockfile locks taken and released
account_created, sign_in, sign_up, accept_invitation, create_token, …changes to accounts, tokens, organisations, access, reviews and webhooks (platform servers), with a target

Audit events never appear in the normal log, and the normal log never appears in the audit file.

Backup and restore

Two commands cover it: pandod copy moves one repository between stores (a backup, a restore, or a migration to another backend), and pandod backup snapshots a whole platform server while it runs.

Backups, restores and moving between backends

pandod copy copies a repository from one store to another. The same command makes a backup, restores one, and moves a repository to another backend:

# Back up the production database to a SQLite file
pandod copy --from-metadata postgres://pando@db/pando --from-objects s3://pando-objects \
            --to-metadata sqlite:///backups/pando.db

# Move a single-node server from SQLite to Postgres and S3
pandod copy --from-metadata sqlite:///var/lib/pando/meta.db \
            --to-metadata postgres://pando@db/pando --to-objects s3://pando-objects

It is safe while the source is serving: branch pointers are read first and written to the destination last, and everything they point to was stored before them, so the copy is always consistent. Objects are copied as stored, so compacted history stays compact, and whole objects are verified against their hashes on the way. Anything the destination already holds is skipped, so running the same copy again only moves what is new: on git/git’s history (84,050 changesets, 330,000 objects) a first copy to SQLite takes 13 s and a repeat 3.5 s. Run it on a schedule for incremental backups. System pointers such as the compaction lease are not copied.

Backing up a platform server

pandod copy moves one repository. A platform server also keeps a registry (accounts, tokens, organisations, change requests, reviews, checks, issues, protections, webhooks) and one database per repository. pandod backup snapshots all of them while the server runs:

pandod backup --platform sqlite:///var/lib/pando/platform.db \
  --repos-metadata 'sqlite:///var/lib/pando/repos/{repo}.db' \
  --repos-objects 'file:///var/lib/pando/objects/{repo_id}' \
  --to /backups/pando-$(date +%F)          # or s3://bucket/prefix, gs://..., az://...

The backup holds platform.db, repos/OWNER--NAME.db for every repository the registry lists, default.db when --metadata is SQLite, and manifest.json, which records where each file belongs and where objects live. Each database is copied with SQLite’s VACUUM INTO, one consistent transaction each. Objects stored inside the metadata (--repos-objects metadata) are in the copy. Objects stored apart are only ever added, never changed, and are not copied: back that store up with its provider’s tools (bucket versioning or replication), and don’t delete objects from it. Repositories on Postgres are refused; use pg_dump for those. To an object store, files go under pando-backup-<time>/ with the provider’s usual credential environment variables.

Restore (the server must be stopped):

  1. Stop pandod.
  2. Copy platform.db to the --platform path.
  3. For each entry in manifest.json → repositories, copy its file to the path in its metadata (without sqlite://); for default, likewise.
  4. Delete any -wal and -shm files left next to the files you replaced.
  5. Start pandod. Tokens, members, change requests and history are as they were at the backup.

For a point-in-time copy between backups, run Litestream next to the server to stream platform.db and each repository database to object storage continuously; take pandod backup snapshots as well, since they cover databases created after Litestream was configured.

Scaling and operations

One pandod server on SQLite is enough for a team on one machine. This page covers the processes you add as load grows, storage maintenance, and monitoring.

Processes

ProcessCommandRuns whereState
Serverpandod serverbehind a load balancer, any number; with --platform, exactly one (see below)none; everything is in the stores
Regional cachepandod cache --upstream URLone or more per regionmemory only
Node daemonpandod daemon --upstream URLone per CI machinememory and local disk
Compactionpandod compact --metadata URLa scheduled jobnone; safe while serving

A platform server (--platform) is single-node: its registry (accounts, tokens, change requests, reviews, issues, protections, webhooks) is one SQLite connection in one process. Run exactly one active pandod server --platform per registry, and never put the registry on a network filesystem; scale reads with regional caches instead. Without --platform, servers over Postgres and an object store can run behind a load balancer in any number.

Objects are immutable and named by their BLAKE3 hash, so caches never need invalidating and every tier verifies what it receives. Only branch pointers change, and only the server changes them.

Storage maintenance

Run pandod compact on a schedule. It stores old file versions as deltas against newer ones and returns space to the filesystem; on git/git’s history this takes the store from 4.6 GB to 369 MB. Runs are incremental and safe while the server is serving.

Monitoring

--metrics ADDR serves three endpoints on its own port, without authentication (keep the port private to your network and monitoring):

PathAnswer
/metricsPrometheus metrics: request counts and latencies per RPC (pando_rpc_total, pando_rpc_seconds; methods the protocol does not define are labelled unknown), lands, refused lands, attempts and batch sizes (pando_land_*), and objects warmed by node daemons (pando_prefetch_objects_total)
/healthzliveness: 200 ok while the process answers
/readyzreadiness: 200 ready, or 503 not ready when the registry can’t be used or a local data directory (the metadata, objects and registry directories) can’t take a synced write, and from the moment shutdown starts. Checked every 5 seconds; the reason goes to the log

The standard gRPC health check (grpc.health.v1.Health) needs no token either and reports NOT_SERVING whenever /readyz would answer 503, for load balancers that speak gRPC. --log-json writes structured logs.

On SIGTERM or Ctrl-C the server stops being ready, finishes the calls in progress (lands included), stops the webhook worker (deliveries in flight finish or are retried after a restart) and exits. Give it the longest call you allow (--request-timeout) before a hard kill.

CLI reference

Every pando command and option, as printed by --help. For a task-by-task guide, start with Try it in 5 minutes; for git equivalents, see Coming from git.

pando clone

Create a workspace and check out a ref (lazily, and sparsely with --path)

Usage: pando clone [OPTIONS] <SERVER> <DIR>

Arguments:
  <SERVER>
  <DIR>

Options:
      --ref <REF_NAME>     [default: main]
      --token <TOKEN>      Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
      --path <PATHS>       Only materialise these paths (repeatable)
      --objects <OBJECTS>  Fetch objects through a node daemon or regional cache [env: PANDO_OBJECTS=]
  -h, --help               Print help
  -V, --version            Print version

pando status

Show working-copy changes

Usage: pando status [OPTIONS] [PATHS]...

Arguments:
  [PATHS]...

Options:
      --token <TOKEN>  Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
  -h, --help           Print help
  -V, --version        Print version

pando commit

Create a changeset on the server from working-copy changes (all, or PATHS)

Usage: pando commit [OPTIONS] --message <MESSAGE> [PATHS]...

Arguments:
  [PATHS]...

Options:
  -m, --message <MESSAGE>
      --token <TOKEN>      Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
  -h, --help               Print help
  -V, --version            Print version

pando land

Land the committed work on the ref (server-side rebase + atomic ref update)

Usage: pando land [OPTIONS]

Options:
      --token <TOKEN>  Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
  -h, --help           Print help
  -V, --version        Print version

pando sync

Update the working copy to the ref head (or REV)

Usage: pando sync [OPTIONS] [REV]

Arguments:
  [REV]

Options:
      --token <TOKEN>  Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
  -h, --help           Print help
  -V, --version        Print version

pando checkout

Switch to a ref or changeset; -b creates a ref at the current base

Usage: pando checkout [OPTIONS] <TARGET>

Arguments:
  <TARGET>

Options:
  -b
      --token <TOKEN>  Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
  -h, --help           Print help
  -V, --version        Print version

pando log

Changeset history (server-side; no local history needed)

Usage: pando log [OPTIONS] [PATH]

Arguments:
  [PATH]  Limit to a path

Options:
      --rev <REV>
      --token <TOKEN>  Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
  -n, --limit <LIMIT>  [default: 20]
      --oneline
  -h, --help           Print help
  -V, --version        Print version

pando diff

Diff the working copy, or two revisions (`pando diff A B`)

Usage: pando diff [OPTIONS] [REVS]...

Arguments:
  [REVS]...

Options:
      --name-only
      --token <TOKEN>  Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
  -h, --help           Print help
  -V, --version        Print version

pando show

Show a changeset and its patch against its first parent

Usage: pando show [OPTIONS] [REV]

Arguments:
  [REV]

Options:
      --token <TOKEN>  Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
  -h, --help           Print help
  -V, --version        Print version

pando cat

Print a file at a revision without checking anything out

Usage: pando cat [OPTIONS] <REV> <PATH>

Arguments:
  <REV>
  <PATH>

Options:
      --token <TOKEN>  Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
  -h, --help           Print help
  -V, --version        Print version

pando lock

Lock files for exclusive editing (for files that cannot be merged). Others cannot land changes to them until you unlock

Usage: pando lock [OPTIONS] <PATHS>...

Arguments:
  <PATHS>...

Options:
      --token <TOKEN>  Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
  -h, --help           Print help
  -V, --version        Print version

pando unlock

Release your locks; --force releases someone else's (needs write access)

Usage: pando unlock [OPTIONS] <PATHS>...

Arguments:
  <PATHS>...

Options:
      --force
      --token <TOKEN>  Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
  -h, --help           Print help
  -V, --version        Print version

pando locks

List locks, optionally under a directory

Usage: pando locks [OPTIONS] [PREFIX]

Arguments:
  [PREFIX]  [default: ""]

Options:
      --token <TOKEN>  Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
  -h, --help           Print help
  -V, --version        Print version

pando ls

List a directory at a revision without checking anything out

Usage: pando ls [OPTIONS] <REV> [PATH]

Arguments:
  <REV>
  [PATH]  [default: ""]

Options:
      --token <TOKEN>  Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
  -h, --help           Print help
  -V, --version        Print version

pando find

Find files by path substring at a revision (server-side)

Usage: pando find [OPTIONS] <PATTERN>

Arguments:
  <PATTERN>

Options:
      --rev <REV>      [default: main]
      --token <TOKEN>  Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
  -h, --help           Print help
  -V, --version        Print version

pando blame

Show which changeset last changed each line of a file

Usage: pando blame [OPTIONS] <PATH>

Arguments:
  <PATH>

Options:
      --rev <REV>      [default: main]
      --token <TOKEN>  Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
  -h, --help           Print help
  -V, --version        Print version

pando refs

List refs

Usage: pando refs [OPTIONS]

Options:
      --token <TOKEN>  Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
  -h, --help           Print help
  -V, --version        Print version

pando merge-base

Best common ancestor of two revisions

Usage: pando merge-base [OPTIONS] <A> <B>

Arguments:
  <A>
  <B>

Options:
      --token <TOKEN>  Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
  -h, --help           Print help
  -V, --version        Print version

pando import-git

Import a git repository's history

Usage: pando import-git [OPTIONS] <GIT_DIR>

Arguments:
  <GIT_DIR>

Options:
      --server <SERVER>
      --token <TOKEN>      Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
      --git-ref <GIT_REF>  [default: HEAD]
      --ref <REF_NAME>     [default: main]
      --force
  -h, --help               Print help
  -V, --version            Print version

pando mount

Mount a revision read-only; files are fetched on first read (Linux, macOS, Windows)

Usage: pando mount [OPTIONS] <DIR>

Arguments:
  <DIR>

Options:
      --server <SERVER>
      --token <TOKEN>        Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
      --objects <OBJECTS>    Daemon or cache serving objects
      --rev <REV>            [default: main]
      --cache-mb <CACHE_MB>  In-memory cache for file contents [default: 256]
      --serve-only           Serve the checkout over NFS on localhost and print the command that mounts it, instead of mounting (for machines where only an administrator may mount, or Linux machines without FUSE)
  -h, --help                 Print help
  -V, --version              Print version

pando mcp

Serve Grovedock as MCP tools over stdio for AI agents (Claude Code, Codex, Cursor...)

Usage: pando mcp [OPTIONS]

Options:
      --server <SERVER>

      --token <TOKEN>
          Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
      --http <ADDR>
          Serve the whole server to remote agents over HTTP at this address (e.g. 0.0.0.0:7430), instead of one workspace over stdio. Each request's bearer token is passed through to the server
      --allowed-host <ALLOWED_HOSTS>
          Host names clients use to reach the HTTP endpoint (repeatable); default: loopback only, against DNS rebinding
      --allowed-origin <ALLOWED_ORIGINS>
          Browser origins allowed to call the HTTP endpoint (repeatable), e.g. https://app.example.com:443; default: none (requests with an Origin are refused)
  -h, --help
          Print help
  -V, --version
          Print version

pando login

Sign in to a server from the browser; the token is saved in ~/.config/pando/credentials.json

Usage: pando login [OPTIONS] [SERVER]

Arguments:
  [SERVER]  The server, e.g. https://pando.example.com; default $PANDO_SERVER or the workspace's

Options:
      --token <TOKEN>  Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
  -h, --help           Print help
  -V, --version        Print version

pando logout

Revoke the token `pando login` saved for a server, and forget it

Usage: pando logout [OPTIONS] [SERVER]

Arguments:
  [SERVER]

Options:
      --token <TOKEN>  Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
  -h, --help           Print help
  -V, --version        Print version

pando whoami

Show who you are signed in as

Usage: pando whoami [OPTIONS] [SERVER]

Arguments:
  [SERVER]

Options:
      --token <TOKEN>  Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
  -h, --help           Print help
  -V, --version        Print version

pando repo

Create and list repositories on a server that hosts many (`pandod server --platform`)

Usage: pando repo [OPTIONS] <COMMAND>

Commands:
  create  Create OWNER/NAME. Then: `pando clone SERVER/OWNER/NAME`
  list    List repositories, optionally of one owner
  grant   Give a person, agent or team (ORG/TEAM) a role on a repository; `none` removes it. With only the repository, lists who has access
  help    Print this message or the help of the given subcommand(s)

Options:
      --token <TOKEN>  Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
  -h, --help           Print help
  -V, --version        Print version

pando repo create

Create OWNER/NAME. Then: `pando clone SERVER/OWNER/NAME`

Usage: pando repo create [OPTIONS] <NAME>

Arguments:
  <NAME>

Options:
      --description <DESCRIPTION>  [default: ""]
      --token <TOKEN>              Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
      --server <SERVER>
  -h, --help                       Print help
  -V, --version                    Print version

pando repo list

List repositories, optionally of one owner

Usage: pando repo list [OPTIONS]

Options:
      --owner <OWNER>    [default: ""]
      --token <TOKEN>    Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
      --server <SERVER>
  -h, --help             Print help
  -V, --version          Print version

pando repo grant

Give a person, agent or team (ORG/TEAM) a role on a repository; `none` removes it. With only the repository, lists who has access

Usage: pando repo grant [OPTIONS] <REPO> [PRINCIPAL] [ROLE]

Arguments:
  <REPO>


  [PRINCIPAL]


  [ROLE]
          Possible values:
          - read
          - triage
          - write
          - maintain
          - admin
          - none:     Removes the member or grant

Options:
      --server <SERVER>


      --token <TOKEN>
          Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved

          [env: PANDO_TOKEN]

  -h, --help
          Print help (see a summary with '-h')

  -V, --version
          Print version

pando org

Organisations and their members

Usage: pando org [OPTIONS] <COMMAND>

Commands:
  list     Organisations you belong to (all of them, for site administrators)
  create   Create an organisation; you become its administrator. Site administrators only
  members  List an organisation's members and their roles
  member   Add a member, change their role, or remove them with `none`
  usage    The organisation's plan, limits and use: active agents this month, people, storage. Organisation administrators
  help     Print this message or the help of the given subcommand(s)

Options:
      --server <SERVER>
      --token <TOKEN>    Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
  -h, --help             Print help
  -V, --version          Print version

pando org list

Organisations you belong to (all of them, for site administrators)

Usage: pando org list [OPTIONS]

Options:
      --server <SERVER>
      --token <TOKEN>    Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
  -h, --help             Print help
  -V, --version          Print version

pando org create

Create an organisation; you become its administrator. Site administrators only

Usage: pando org create [OPTIONS] <NAME>

Arguments:
  <NAME>

Options:
      --description <DESCRIPTION>  [default: ""]
      --server <SERVER>
      --token <TOKEN>              Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
  -h, --help                       Print help
  -V, --version                    Print version

pando org members

List an organisation's members and their roles

Usage: pando org members [OPTIONS] <ORG>

Arguments:
  <ORG>

Options:
      --server <SERVER>
      --token <TOKEN>    Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
  -h, --help             Print help
  -V, --version          Print version

pando org member

Add a member, change their role, or remove them with `none`

Usage: pando org member [OPTIONS] <ORG> <LOGIN> <ROLE>

Arguments:
  <ORG>


  <LOGIN>


  <ROLE>
          Possible values:
          - read
          - triage
          - write
          - maintain
          - admin
          - none:     Removes the member or grant

Options:
      --server <SERVER>


      --token <TOKEN>
          Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved

          [env: PANDO_TOKEN]

  -h, --help
          Print help (see a summary with '-h')

  -V, --version
          Print version

pando org usage

The organisation's plan, limits and use: active agents this month, people, storage. Organisation administrators

Usage: pando org usage [OPTIONS] <ORG>

Arguments:
  <ORG>

Options:
      --month <MONTH>    A past month, YYYY-MM (default: this month, UTC) [default: ""]
      --server <SERVER>
      --token <TOKEN>    Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
  -h, --help             Print help
  -V, --version          Print version

pando team

Teams within an organisation, for granting access to many people at once

Usage: pando team [OPTIONS] <COMMAND>

Commands:
  list    List an organisation's teams
  set     Create a team, or replace its members: `pando team set lumen/platform --member mira`
  delete  Delete a team and its grants
  help    Print this message or the help of the given subcommand(s)

Options:
      --server <SERVER>
      --token <TOKEN>    Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
  -h, --help             Print help
  -V, --version          Print version

pando team list

List an organisation's teams

Usage: pando team list [OPTIONS] <ORG>

Arguments:
  <ORG>

Options:
      --server <SERVER>
      --token <TOKEN>    Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
  -h, --help             Print help
  -V, --version          Print version

pando team set

Create a team, or replace its members: `pando team set lumen/platform --member mira`

Usage: pando team set [OPTIONS] <TEAM>

Arguments:
  <TEAM>  ORG/TEAM

Options:
      --member <MEMBERS>  A member of the organisation (repeatable)
      --server <SERVER>
      --token <TOKEN>     Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
      --name <NAME>       [default: ""]
  -h, --help              Print help
  -V, --version           Print version

pando team delete

Delete a team and its grants

Usage: pando team delete [OPTIONS] <TEAM>

Arguments:
  <TEAM>  ORG/TEAM

Options:
      --server <SERVER>
      --token <TOKEN>    Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
  -h, --help             Print help
  -V, --version          Print version

pando cr

Change requests: propose a branch, review it, land it

Usage: pando cr [OPTIONS] <COMMAND>

Commands:
  create   Propose a branch for landing on another: `pando cr create --title "…"`
  list     List change requests
  view     Show one, with what still blocks it
  comment  Comment, on the change or on a line of a file
  review   Approve, request changes, or comment
  check    Report a CI check on the current version: `pando cr check 12 build passed --url …`
  land     Land it, once nothing blocks it
  rebase   Replay it on the target's tip
  ready    Mark a draft ready for review
  close
  reopen
  help     Print this message or the help of the given subcommand(s)

Options:
      --server <SERVER>  The repository, SERVER/OWNER/NAME; defaults to the workspace's
      --token <TOKEN>    Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
  -h, --help             Print help
  -V, --version          Print version

pando cr create

Propose a branch for landing on another: `pando cr create --title "…"`

Usage: pando cr create [OPTIONS] --title <TITLE>

Options:
      --server <SERVER>       The repository, SERVER/OWNER/NAME; defaults to the workspace's
      --title <TITLE>
      --token <TOKEN>         Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
      --body <BODY>           [default: ""]
      --source <SOURCE>       The branch to land; defaults to the workspace's branch
      --target <TARGET>       Where to land it; defaults to the repository's default branch [default: ""]
      --draft
      --reviewer <REVIEWERS>  Ask someone to review (repeatable)
  -h, --help                  Print help
  -V, --version               Print version

pando cr list

List change requests

Usage: pando cr list [OPTIONS]

Options:
      --server <SERVER>  The repository, SERVER/OWNER/NAME; defaults to the workspace's
      --state <STATE>    [default: open] [possible values: open, draft, landed, closed, all]
      --token <TOKEN>    Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
      --author <AUTHOR>  Only this author; `agent/*` for every agent [default: ""]
  -h, --help             Print help
  -V, --version          Print version

pando cr view

Show one, with what still blocks it

Usage: pando cr view [OPTIONS] <NUMBER>

Arguments:
  <NUMBER>

Options:
      --server <SERVER>  The repository, SERVER/OWNER/NAME; defaults to the workspace's
      --token <TOKEN>    Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
  -h, --help             Print help
  -V, --version          Print version

pando cr comment

Comment, on the change or on a line of a file

Usage: pando cr comment [OPTIONS] --message <MESSAGE> <NUMBER>

Arguments:
  <NUMBER>

Options:
  -m, --message <MESSAGE>
      --server <SERVER>    The repository, SERVER/OWNER/NAME; defaults to the workspace's
      --token <TOKEN>      Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
      --path <PATH>
      --line <LINE>
  -h, --help               Print help
  -V, --version            Print version

pando cr review

Approve, request changes, or comment

Usage: pando cr review [OPTIONS] <NUMBER> <VERDICT>

Arguments:
  <NUMBER>


  <VERDICT>
          Possible values:
          - approve
          - changes: Request changes
          - comment

Options:
  -m, --message <MESSAGE>
          [default: ""]

      --server <SERVER>
          The repository, SERVER/OWNER/NAME; defaults to the workspace's

      --token <TOKEN>
          Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved

          [env: PANDO_TOKEN]

  -h, --help
          Print help (see a summary with '-h')

  -V, --version
          Print version

pando cr check

Report a CI check on the current version: `pando cr check 12 build passed --url …`

Usage: pando cr check [OPTIONS] <NUMBER> <NAME> <STATE>

Arguments:
  <NUMBER>
  <NAME>
  <STATE>   [possible values: pending, passed, failed, skipped]

Options:
      --server <SERVER>    The repository, SERVER/OWNER/NAME; defaults to the workspace's
      --token <TOKEN>      Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
      --url <URL>          [default: ""]
      --summary <SUMMARY>  [default: ""]
  -h, --help               Print help
  -V, --version            Print version

pando cr land

Land it, once nothing blocks it

Usage: pando cr land [OPTIONS] <NUMBER>

Arguments:
  <NUMBER>

Options:
      --server <SERVER>  The repository, SERVER/OWNER/NAME; defaults to the workspace's
      --token <TOKEN>    Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
  -h, --help             Print help
  -V, --version          Print version

pando cr rebase

Replay it on the target's tip

Usage: pando cr rebase [OPTIONS] <NUMBER>

Arguments:
  <NUMBER>

Options:
      --server <SERVER>  The repository, SERVER/OWNER/NAME; defaults to the workspace's
      --token <TOKEN>    Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
  -h, --help             Print help
  -V, --version          Print version

pando cr ready

Mark a draft ready for review

Usage: pando cr ready [OPTIONS] <NUMBER>

Arguments:
  <NUMBER>

Options:
      --server <SERVER>  The repository, SERVER/OWNER/NAME; defaults to the workspace's
      --token <TOKEN>    Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
  -h, --help             Print help
  -V, --version          Print version

pando cr close

Usage: pando cr close [OPTIONS] <NUMBER>

Arguments:
  <NUMBER>

Options:
      --server <SERVER>  The repository, SERVER/OWNER/NAME; defaults to the workspace's
      --token <TOKEN>    Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
  -h, --help             Print help
  -V, --version          Print version

pando cr reopen

Usage: pando cr reopen [OPTIONS] <NUMBER>

Arguments:
  <NUMBER>

Options:
      --server <SERVER>  The repository, SERVER/OWNER/NAME; defaults to the workspace's
      --token <TOKEN>    Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
  -h, --help             Print help
  -V, --version          Print version

pando issue

Issues: work to do, assigned to people or agents; "fixes #N" in a change request closes one when it lands

Usage: pando issue [OPTIONS] <COMMAND>

Commands:
  create   Open an issue
  list
  view
  comment
  assign   Give an issue to people or agents (replaces the assignees)
  close    Close an issue as done (or `--not-planned`)
  reopen
  help     Print this message or the help of the given subcommand(s)

Options:
      --server <SERVER>  The repository, SERVER/OWNER/NAME; defaults to the workspace's
      --token <TOKEN>    Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
  -h, --help             Print help
  -V, --version          Print version

pando issue create

Open an issue

Usage: pando issue create [OPTIONS] --title <TITLE>

Options:
      --server <SERVER>       The repository, SERVER/OWNER/NAME; defaults to the workspace's
      --title <TITLE>
      --token <TOKEN>         Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
      --body <BODY>           [default: ""]
      --assignee <ASSIGNEES>  Who should work on it, a person or an agent (repeatable)
      --label <LABELS>
  -h, --help                  Print help
  -V, --version               Print version

pando issue list

Usage: pando issue list [OPTIONS]

Options:
      --server <SERVER>      The repository, SERVER/OWNER/NAME; defaults to the workspace's
      --state <STATE>        [default: open] [possible values: open, closed, all]
      --token <TOKEN>        Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
      --assignee <ASSIGNEE>  [default: ""]
      --label <LABEL>        [default: ""]
  -h, --help                 Print help
  -V, --version              Print version

pando issue view

Usage: pando issue view [OPTIONS] <NUMBER>

Arguments:
  <NUMBER>

Options:
      --server <SERVER>  The repository, SERVER/OWNER/NAME; defaults to the workspace's
      --token <TOKEN>    Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
  -h, --help             Print help
  -V, --version          Print version

pando issue comment

Usage: pando issue comment [OPTIONS] --message <MESSAGE> <NUMBER>

Arguments:
  <NUMBER>

Options:
  -m, --message <MESSAGE>
      --server <SERVER>    The repository, SERVER/OWNER/NAME; defaults to the workspace's
      --token <TOKEN>      Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
  -h, --help               Print help
  -V, --version            Print version

pando issue assign

Give an issue to people or agents (replaces the assignees)

Usage: pando issue assign [OPTIONS] <NUMBER> [ASSIGNEES]...

Arguments:
  <NUMBER>
  [ASSIGNEES]...

Options:
      --server <SERVER>  The repository, SERVER/OWNER/NAME; defaults to the workspace's
      --token <TOKEN>    Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
  -h, --help             Print help
  -V, --version          Print version

pando issue close

Close an issue as done (or `--not-planned`)

Usage: pando issue close [OPTIONS] <NUMBER>

Arguments:
  <NUMBER>

Options:
      --not-planned
      --server <SERVER>  The repository, SERVER/OWNER/NAME; defaults to the workspace's
      --token <TOKEN>    Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
  -h, --help             Print help
  -V, --version          Print version

pando issue reopen

Usage: pando issue reopen [OPTIONS] <NUMBER>

Arguments:
  <NUMBER>

Options:
      --server <SERVER>  The repository, SERVER/OWNER/NAME; defaults to the workspace's
      --token <TOKEN>    Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
  -h, --help             Print help
  -V, --version          Print version

pando protect

Protected branches: changes land on them only through a reviewed change request

Usage: pando protect [OPTIONS] <COMMAND>

Commands:
  list    List protected branches
  set     Protect a branch or prefix (`main`, `release/*`)
  remove  Stop protecting a branch or prefix
  help    Print this message or the help of the given subcommand(s)

Options:
      --server <SERVER>
      --token <TOKEN>    Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
  -h, --help             Print help
  -V, --version          Print version

pando protect list

List protected branches

Usage: pando protect list [OPTIONS]

Options:
      --server <SERVER>
      --token <TOKEN>    Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
  -h, --help             Print help
  -V, --version          Print version

pando protect set

Protect a branch or prefix (`main`, `release/*`)

Usage: pando protect set [OPTIONS] <PATTERN>

Arguments:
  <PATTERN>

Options:
      --approvals <APPROVALS>  Approvals needed before a change request lands [default: 1]
      --server <SERVER>
      --token <TOKEN>          Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
      --check <CHECKS>         A check that must pass (repeatable)
      --keep-stale             Keep approvals when the branch changes after them
      --agents-direct          Let agents land on it directly, without review
      --bypass <BYPASS>        Who may land without review: a login, a team `org/team`, or `admins` (repeatable)
  -h, --help                   Print help
  -V, --version                Print version

pando protect remove

Stop protecting a branch or prefix

Usage: pando protect remove [OPTIONS] <PATTERN>

Arguments:
  <PATTERN>

Options:
      --server <SERVER>
      --token <TOKEN>    Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
  -h, --help             Print help
  -V, --version          Print version

pando webhook

Webhooks: call a URL (CI, chat, trackers) when something happens in a repository

Usage: pando webhook [OPTIONS] <COMMAND>

Commands:
  list    A repository's webhooks: `pando webhook list lumen/fleet`
  add     Call URL when something happens in the repository. Prints the signing secret once
  ping    Send a ping now and show how it went
  log     Recent deliveries and their answers
  remove
  help    Print this message or the help of the given subcommand(s)

Options:
      --server <SERVER>
      --token <TOKEN>    Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
  -h, --help             Print help
  -V, --version          Print version

pando webhook list

A repository's webhooks: `pando webhook list lumen/fleet`

Usage: pando webhook list [OPTIONS] <REPO>

Arguments:
  <REPO>

Options:
      --server <SERVER>
      --token <TOKEN>    Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
  -h, --help             Print help
  -V, --version          Print version

pando webhook add

Call URL when something happens in the repository. Prints the signing secret once

Usage: pando webhook add [OPTIONS] <REPO> <URL>

Arguments:
  <REPO>
  <URL>

Options:
      --event <EVENTS>   Only these events (repeatable): push, change_request, review, comment, check, issue
      --server <SERVER>
      --token <TOKEN>    Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
  -h, --help             Print help
  -V, --version          Print version

pando webhook ping

Send a ping now and show how it went

Usage: pando webhook ping [OPTIONS] <ID>

Arguments:
  <ID>

Options:
      --server <SERVER>
      --token <TOKEN>    Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
  -h, --help             Print help
  -V, --version          Print version

pando webhook log

Recent deliveries and their answers

Usage: pando webhook log [OPTIONS] <ID>

Arguments:
  <ID>

Options:
      --server <SERVER>
      --token <TOKEN>    Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
  -h, --help             Print help
  -V, --version          Print version

pando webhook remove

Usage: pando webhook remove [OPTIONS] <ID>

Arguments:
  <ID>

Options:
      --server <SERVER>
      --token <TOKEN>    Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
  -h, --help             Print help
  -V, --version          Print version

pando token

Access tokens for you and for agents you answer for

Usage: pando token [OPTIONS] <COMMAND>

Commands:
  list    Your tokens
  create  Create a token. Prints it once: store it as PANDO_TOKEN
  revoke  Revoke a token by its id
  help    Print this message or the help of the given subcommand(s)

Options:
      --server <SERVER>
      --token <TOKEN>    Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
  -h, --help             Print help
  -V, --version          Print version

pando token list

Your tokens

Usage: pando token list [OPTIONS]

Options:
      --server <SERVER>
      --token <TOKEN>    Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
  -h, --help             Print help
  -V, --version          Print version

pando token create

Create a token. Prints it once: store it as PANDO_TOKEN

Usage: pando token create [OPTIONS] <NAME>

Arguments:
  <NAME>


Options:
      --as <ACTS_AS>
          Act as an agent you answer for, e.g. `agent/refactor-07` (created if new)

      --server <SERVER>


      --token <TOKEN>
          Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved

          [env: PANDO_TOKEN]

      --repo <REPOSITORIES>
          Only these repositories, OWNER/NAME (repeatable); default: all you can reach

      --role <ROLE>
          Highest role the token may use

          Possible values:
          - read
          - triage
          - write
          - maintain
          - admin
          - none:     Removes the member or grant

          [default: write]

      --days <DAYS>
          Days until it expires (1–366)

          [default: 90]

  -h, --help
          Print help (see a summary with '-h')

  -V, --version
          Print version

pando token revoke

Revoke a token by its id

Usage: pando token revoke [OPTIONS] <ID>

Arguments:
  <ID>

Options:
      --server <SERVER>
      --token <TOKEN>    Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
  -h, --help             Print help
  -V, --version          Print version

pando admin

Server administration (site administrators only)

Usage: pando admin [OPTIONS] <COMMAND>

Commands:
  signup    Who may create an account by signing in (`pandod server --signup waitlist`)
  identity  Ways people sign in from a browser
  limits    Organisations' plans and limits (enforced with `pandod server --multi-tenant`)
  help      Print this message or the help of the given subcommand(s)

Options:
      --server <SERVER>
      --token <TOKEN>    Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
  -h, --help             Print help
  -V, --version          Print version

pando admin signup

Who may create an account by signing in (`pandod server --signup waitlist`)

Usage: pando admin signup [OPTIONS] <COMMAND>

Commands:
  allow   Let an email address, or everyone at `@domain`, create an account
  remove  Take an entry off the list (accounts already created stay)
  list
  help    Print this message or the help of the given subcommand(s)

Options:
      --server <SERVER>
      --token <TOKEN>    Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
  -h, --help             Print help
  -V, --version          Print version

pando admin signup allow

Let an email address, or everyone at `@domain`, create an account

Usage: pando admin signup allow [OPTIONS] <ENTRY>

Arguments:
  <ENTRY>

Options:
      --server <SERVER>
      --token <TOKEN>    Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
  -h, --help             Print help
  -V, --version          Print version

pando admin signup remove

Take an entry off the list (accounts already created stay)

Usage: pando admin signup remove [OPTIONS] <ENTRY>

Arguments:
  <ENTRY>

Options:
      --server <SERVER>
      --token <TOKEN>    Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
  -h, --help             Print help
  -V, --version          Print version

pando admin signup list

Usage: pando admin signup list [OPTIONS]

Options:
      --server <SERVER>
      --token <TOKEN>    Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
  -h, --help             Print help
  -V, --version          Print version

pando admin identity

Ways people sign in from a browser

Usage: pando admin identity [OPTIONS] <COMMAND>

Commands:
  link  Let an identity sign in as an existing account, e.g. the first administrator: `pando admin identity link kabir --provider email --subject kabir@example.com`
  help  Print this message or the help of the given subcommand(s)

Options:
      --server <SERVER>
      --token <TOKEN>    Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
  -h, --help             Print help
  -V, --version          Print version
Let an identity sign in as an existing account, e.g. the first administrator: `pando admin identity link kabir --provider email --subject kabir@example.com`

Usage: pando admin identity link [OPTIONS] --provider <PROVIDER> --subject <SUBJECT> <LOGIN>

Arguments:
  <LOGIN>

Options:
      --provider <PROVIDER>  `email`, or a sign-in provider's id such as `google`
      --server <SERVER>
      --token <TOKEN>        Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
      --subject <SUBJECT>    The email address, or the provider's `sub` for the person
  -h, --help                 Print help
  -V, --version              Print version

pando admin limits

Organisations' plans and limits (enforced with `pandod server --multi-tenant`)

Usage: pando admin limits [OPTIONS] <COMMAND>

Commands:
  show     The limits that apply to an organisation and what it uses
  set      Change some limits; the others stay. A number, or `unlimited`
  suspend  Stop an organisation writing (it can still read)
  restore  Let a suspended organisation write again
  help     Print this message or the help of the given subcommand(s)

Options:
      --server <SERVER>
      --token <TOKEN>    Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
  -h, --help             Print help
  -V, --version          Print version

pando admin limits show

The limits that apply to an organisation and what it uses

Usage: pando admin limits show [OPTIONS] <ORG>

Arguments:
  <ORG>

Options:
      --server <SERVER>
      --token <TOKEN>    Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
  -h, --help             Print help
  -V, --version          Print version

pando admin limits set

Change some limits; the others stay. A number, or `unlimited`

Usage: pando admin limits set [OPTIONS] <ORG>

Arguments:
  <ORG>

Options:
      --seats <SEATS>
      --server <SERVER>
      --token <TOKEN>                  Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
      --agents <AGENTS>                Agents that land a change in a calendar month
      --storage <STORAGE>              e.g. 10GiB
      --private-repos <PRIVATE_REPOS>
      --plan <PLAN>                    The plan's name as people see it
  -h, --help                           Print help
  -V, --version                        Print version

pando admin limits suspend

Stop an organisation writing (it can still read)

Usage: pando admin limits suspend [OPTIONS] <ORG>

Arguments:
  <ORG>

Options:
      --server <SERVER>
      --token <TOKEN>    Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
  -h, --help             Print help
  -V, --version          Print version

pando admin limits restore

Let a suspended organisation write again

Usage: pando admin limits restore [OPTIONS] <ORG>

Arguments:
  <ORG>

Options:
      --server <SERVER>
      --token <TOKEN>    Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
  -h, --help             Print help
  -V, --version          Print version

pando migrate

Move a repository from any git host to Grovedock: full history, and from GitHub, open pull requests (as change requests) and issues with their comments

Usage: pando migrate [OPTIONS] --to <TO> <FROM>

Arguments:
  <FROM>  Where it is now: any URL git can clone

Options:
      --to <TO>
          Where it goes: SERVER/OWNER/NAME
      --token <TOKEN>
          Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
      --github-token <GITHUB_TOKEN>
          For private GitHub repositories, and to avoid GitHub's rate limit. Prefer the PANDO_GITHUB_TOKEN (or GITHUB_TOKEN) environment variable or --github-token-file: a value given here is visible to other users of the machine (`ps`) [env: PANDO_GITHUB_TOKEN]
      --github-token-file <GITHUB_TOKEN_FILE>
          Read the GitHub token from this file
      --no-issues
          Skip issues
      --no-pull-requests
          Skip pull requests
      --github-api <GITHUB_API>
          GitHub Enterprise Server's API, e.g. https://github.example.com/api/v3
  -h, --help
          Print help
  -V, --version
          Print version

pando mirror

Keep a git repository (GitHub, GitLab, Bitbucket, any git remote) up to date with a branch: every landed change becomes the same git commit every time, pushed without force

Usage: pando mirror [OPTIONS] --to <TO>

Options:
      --to <TO>            Where to push: any URL git accepts. Credentials come from git (SSH keys or a credential helper)
      --token <TOKEN>      Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
      --server <SERVER>
      --rev <REV>          [default: main]
      --git-ref <GIT_REF>  [default: main]
      --dir <DIR>          The mirror's local copy (a bare git repository); defaults to the user cache
      --every <SECONDS>    Mirror again every SECONDS until interrupted; without it, mirror once and exit
  -h, --help               Print help
  -V, --version            Print version

pando export-git

Export first-parent history to a git repository

Usage: pando export-git [OPTIONS] <GIT_DIR>

Arguments:
  <GIT_DIR>

Options:
      --server <SERVER>
      --token <TOKEN>      Bearer token for the server ($PANDO_TOKEN); without one, the token `pando login` saved [env: PANDO_TOKEN]
      --rev <REV>          [default: main]
      --git-ref <GIT_REF>  [default: main]
  -h, --help               Print help
  -V, --version            Print version

Server reference

Every pandod command and option, as printed by --help. Options listed under pandod itself apply to every command and go before it (pandod --metrics 0.0.0.0:9420 server ...). Each option’s environment variable is shown in brackets. The most used settings are summarised in Configuration.

pandod

Grovedock: source control server, regional cache and node daemon

Usage: pandod [OPTIONS] <COMMAND>

Commands:
  server   Authoritative server: all features, durable storage
  license  Support licences: `pandod license verify FILE`
  repo     Operator actions on one hosted repository, straight on the registry (site administration; works while the server runs, which notices within seconds)
  token    Create an access token directly in a platform registry, without a running server: how the first administrator gets in. Prints the token once
  backup   Back up a platform server while it runs: a consistent snapshot of the registry and of every hosted repository's metadata database, with a manifest of where each belongs and where objects live. Restore by copying back while stopped (docs/backup.md)
  compact  Delta-compress stored blob versions (single writer; safe to run while serving)
  copy     Copy a repository to another store: a backup, a restore, or a move to another backend (e.g. SQLite to Postgres). Safe while the source serves; repeat it to copy only what is new
  cache    Regional cache: stateless, memory-bounded, read-through to an upstream tier
  daemon   Per-node daemon: memory + local disk cache shared by every job on the machine
  help     Print this message or the help of the given subcommand(s)

Options:
      --metrics <METRICS>
          Address for `/metrics` (Prometheus), `/healthz` and `/readyz` (no token needed), e.g. 0.0.0.0:9420 [env: PANDO_METRICS=]
      --log-json
          Emit JSON logs [env: PANDO_LOG_JSON=]
      --audit-log <AUDIT_LOG>
          Append an audit trail (JSON lines: who read or changed what, from where, with what result) to this file. Off by default; it grows with every request [env: PANDO_AUDIT_LOG=]
      --audit-retention-days <AUDIT_RETENTION_DAYS>
          Days of audit files to keep (one file per UTC day); 0 keeps all. 365 suits hosted services (docs/deploy.md) [env: PANDO_AUDIT_RETENTION_DAYS=] [default: 365]
      --cors-origin <WEB_ORIGINS>
          Origins allowed to call this server from a browser on another site (repeatable or comma-separated), e.g. your own console's address. Default: none (same origin only); `*` allows any (development only) [env: PANDO_WEB_ORIGINS=] [alias: --web-origins]
      --trust-proxy
          Take the client address (audit log, sign-in rate limits) from `X-Forwarded-For` when the connection comes from a reverse proxy on this host (loopback) [env: PANDO_TRUST_PROXY=]
      --max-request-mb <MAX_REQUEST_MB>
          Largest request message in MiB (an uploaded object batch, a changeset). Default: 64 with --platform, 260 otherwise. Files of 8 MiB and more are uploaded in chunks [env: PANDO_MAX_REQUEST_MB=]
      --max-calls-per-principal <MAX_CALLS_PER_PRINCIPAL>
          Calls one account may have in progress at once (0: no limit). Default: 128 with --platform, no limit otherwise [env: PANDO_MAX_CALLS_PER_PRINCIPAL=]
      --sqlite-synchronous <SQLITE_SYNCHRONOUS>
          SQLite durability: `full` (default) syncs every commit, so an acknowledged land survives power loss; `normal` survives process crashes only, for more throughput on a slow disk [env: PANDO_SQLITE_SYNCHRONOUS=] [default: full] [possible values: full, normal]
      --allow-anonymous
          Accept unauthenticated calls on a non-loopback address. Without tokens or OIDC, pandod otherwise listens only on localhost, so a server is never open to a network by accident [env: PANDO_ALLOW_ANONYMOUS=]
      --request-timeout <REQUEST_TIMEOUT>
          Longest any one call may run, in seconds [env: PANDO_REQUEST_TIMEOUT=] [default: 600]
      --max-in-flight <MAX_IN_FLIGHT>
          Calls in progress at once across all clients; beyond this, calls are refused with Unavailable (clients retry) instead of queueing [env: PANDO_MAX_IN_FLIGHT=] [default: 4096]
      --tokens <TOKENS>
          Comma-separated bearer tokens accepted by this process. Unset = no auth (dev only) [env: PANDO_TOKENS]
      --oidc-issuer <OIDC_ISSUER>
          OpenID Connect issuer URL; enables SSO bearer tokens (JWTs) alongside static tokens [env: PANDO_OIDC_ISSUER=]
      --oidc-audience <OIDC_AUDIENCE>
          Expected `aud` claim of OIDC tokens [env: PANDO_OIDC_AUDIENCE=]
      --oidc-claim <OIDC_CLAIM>
          Claim used as the principal for access rules (sub, email, preferred_username...) [env: PANDO_OIDC_CLAIM=] [default: sub]
      --oidc-require <OIDC_REQUIRE>
          CLAIM=VALUE every OIDC token must carry (repeatable; all must match), e.g. `repository_owner=acme`, `hd=corp.example`, `email_verified=true`. Required for issuers that serve anyone (GitHub Actions, Google) [env: PANDO_OIDC_REQUIRE=]
      --oidc-admin <OIDC_ADMINS>
          Platform administrators who may sign in through OIDC (repeatable). Any other `--platform-admins` login is refused from an OIDC token, so an identity provider that lets users pick their name can't make someone an administrator [env: PANDO_OIDC_ADMINS=]
      --tls-cert <TLS_CERT>
          PEM certificate and key for TLS [env: PANDO_TLS_CERT=]
      --tls-key <TLS_KEY>
          [env: PANDO_TLS_KEY=]
  -h, --help
          Print help
  -V, --version
          Print version

pandod server

Authoritative server: all features, durable storage

Usage: pandod server [OPTIONS]

Options:
      --listen <LISTEN>
          [default: 127.0.0.1:7420]

      --metadata <METADATA>
          memory | sqlite://PATH | postgres://... Default: sqlite://./pando-data/meta.db, or memory with --platform (whose clients can't reach this repository)

          [env: PANDO_METADATA]

      --objects <OBJECTS>
          metadata | file:///PATH | s3://BUCKET/PREFIX | gs://BUCKET | az://CONTAINER | memory:///

          [env: PANDO_OBJECTS]
          [default: metadata]

      --acl <ACL>
          JSON access rules (per-directory write and per-ref permissions). Unset = allow all

          [env: PANDO_ACL=]

      --land-hook <LAND_HOOK>
          Pre-land check: every land is POSTed here and proceeds only on a 2xx answer

          [env: PANDO_LAND_HOOK]

      --land-hook-token <LAND_HOOK_TOKEN>
          Bearer token sent to the land hook

          [env: PANDO_LAND_HOOK_TOKEN]

      --land-hook-timeout <LAND_HOOK_TIMEOUT>
          Seconds to wait for the land hook before refusing the land

          [default: 10]

      --platform <PLATFORM>
          Host many repositories: the registry of which exist (sqlite://PATH or memory). Without it the server hosts one repository, `--metadata` / `--objects`

          [env: PANDO_PLATFORM]

      --repos-metadata <REPOS_METADATA>
          Storage for each hosted repository; `{repo}` becomes OWNER/NAME and `{repo_id}` OWNER--NAME, e.g. sqlite:///var/lib/pando/repos/{repo}.db

          [env: PANDO_REPOS_METADATA]

      --metrics <METRICS>
          Address for `/metrics` (Prometheus), `/healthz` and `/readyz` (no token needed), e.g. 0.0.0.0:9420

          [env: PANDO_METRICS=]

      --repos-objects <REPOS_OBJECTS>
          Objects for each hosted repository (same placeholders), or `metadata`

          [env: PANDO_REPOS_OBJECTS]
          [default: metadata]

      --log-json
          Emit JSON logs

          [env: PANDO_LOG_JSON=]

      --platform-admins <PLATFORM_ADMINS>
          Logins that administer the whole server: create organisations, and act as an administrator of every organisation and repository

          [env: PANDO_PLATFORM_ADMINS=]

      --audit-log <AUDIT_LOG>
          Append an audit trail (JSON lines: who read or changed what, from where, with what result) to this file. Off by default; it grows with every request

          [env: PANDO_AUDIT_LOG=]

      --multi-tenant
          Strangers share this server (a hosted service): internal repositories are for the owning organisation only, organisations add existing accounts only, and any person may create a few organisations

          [env: PANDO_MULTI_TENANT=]

      --audit-retention-days <AUDIT_RETENTION_DAYS>
          Days of audit files to keep (one file per UTC day); 0 keeps all. 365 suits hosted services (docs/deploy.md)

          [env: PANDO_AUDIT_RETENTION_DAYS=]
          [default: 365]

      --max-orgs-per-account <MAX_ORGS_PER_ACCOUNT>
          With --multi-tenant: how many organisations a person may administer

          [env: PANDO_MAX_ORGS_PER_ACCOUNT=]
          [default: 3]

      --cors-origin <WEB_ORIGINS>
          Origins allowed to call this server from a browser on another site (repeatable or comma-separated), e.g. your own console's address. Default: none (same origin only); `*` allows any (development only)

          [env: PANDO_WEB_ORIGINS=]
          [alias: --web-origins]

      --webhook-allow-private
          Let webhooks call loopback and private networks (self-hosted CI on an internal network). Cloud metadata addresses stay refused

          [env: PANDO_WEBHOOK_ALLOW_PRIVATE=]

      --login-listen <LOGIN_LISTEN>
          Serve the sign-in pages (`/login/*`: OpenID Connect providers such as Google, and email links) on this address, behind the same host name as the console

          [env: PANDO_LOGIN_LISTEN=]

      --trust-proxy
          Take the client address (audit log, sign-in rate limits) from `X-Forwarded-For` when the connection comes from a reverse proxy on this host (loopback)

          [env: PANDO_TRUST_PROXY=]

      --max-request-mb <MAX_REQUEST_MB>
          Largest request message in MiB (an uploaded object batch, a changeset). Default: 64 with --platform, 260 otherwise. Files of 8 MiB and more are uploaded in chunks

          [env: PANDO_MAX_REQUEST_MB=]

      --public-url <PUBLIC_URL>
          Where people reach this server, e.g. https://pando.example.com: the base of sign-in redirect addresses and email links

          [env: PANDO_PUBLIC_URL=]

      --max-calls-per-principal <MAX_CALLS_PER_PRINCIPAL>
          Calls one account may have in progress at once (0: no limit). Default: 128 with --platform, no limit otherwise

          [env: PANDO_MAX_CALLS_PER_PRINCIPAL=]

      --oidc <ID=ISSUER_URL>
          A sign-in provider, ID=ISSUER_URL (repeatable), e.g. google=https://accounts.google.com. Its client comes from PANDO_OIDC_<ID>_CLIENT_ID and PANDO_OIDC_<ID>_CLIENT_SECRET (or _FILE variants)

      --smtp-url <SMTP_URL>
          Mail server for sign-in links: smtps://HOST:465 or smtp://HOST:587?tls=required. Credentials from PANDO_SMTP_USERNAME and PANDO_SMTP_PASSWORD (or _FILE variants)

          [env: PANDO_SMTP_URL]

      --sqlite-synchronous <SQLITE_SYNCHRONOUS>
          SQLite durability: `full` (default) syncs every commit, so an acknowledged land survives power loss; `normal` survives process crashes only, for more throughput on a slow disk

          [env: PANDO_SQLITE_SYNCHRONOUS=]
          [default: full]
          [possible values: full, normal]

      --allow-anonymous
          Accept unauthenticated calls on a non-loopback address. Without tokens or OIDC, pandod otherwise listens only on localhost, so a server is never open to a network by accident

          [env: PANDO_ALLOW_ANONYMOUS=]

      --mail-from <MAIL_FROM>
          Sender of sign-in links, e.g. "Grovedock <login@example.com>"

          [env: PANDO_MAIL_FROM=]

      --request-timeout <REQUEST_TIMEOUT>
          Longest any one call may run, in seconds

          [env: PANDO_REQUEST_TIMEOUT=]
          [default: 600]

      --turnstile-site-key <TURNSTILE_SITE_KEY>
          Cloudflare Turnstile site key for the email form; its secret comes from PANDO_TURNSTILE_SECRET (or _FILE). Unset: no challenge

          [env: PANDO_TURNSTILE_SITE_KEY=]

      --max-in-flight <MAX_IN_FLIGHT>
          Calls in progress at once across all clients; beyond this, calls are refused with Unavailable (clients retry) instead of queueing

          [env: PANDO_MAX_IN_FLIGHT=]
          [default: 4096]

      --signup <SIGNUP>
          Who may create an account by signing in: waitlist (`pando admin signup allow`), open, or closed. Existing accounts can always sign in

          Possible values:
          - waitlist: Only addresses and `@domains` on the list (`pando admin signup allow`)
          - open
          - closed:   No new accounts

          [env: PANDO_SIGNUP=]
          [default: waitlist]

      --terms-url <TERMS_URL>
          Your terms of service, linked from the box people tick (18 or older, accept the terms) before an account is made

          [env: PANDO_TERMS_URL=]

      --tokens <TOKENS>
          Comma-separated bearer tokens accepted by this process. Unset = no auth (dev only)

          [env: PANDO_TOKENS]

      --oidc-issuer <OIDC_ISSUER>
          OpenID Connect issuer URL; enables SSO bearer tokens (JWTs) alongside static tokens

          [env: PANDO_OIDC_ISSUER=]

      --privacy-url <PRIVACY_URL>
          Your privacy policy, linked next to the terms

          [env: PANDO_PRIVACY_URL=]

      --max-open-repos <MAX_OPEN_REPOS>
          Most hosted repositories open at once; the least recently used idle ones close

          [env: PANDO_MAX_OPEN_REPOS=]
          [default: 256]

      --oidc-audience <OIDC_AUDIENCE>
          Expected `aud` claim of OIDC tokens

          [env: PANDO_OIDC_AUDIENCE=]

      --oidc-claim <OIDC_CLAIM>
          Claim used as the principal for access rules (sub, email, preferred_username...)

          [env: PANDO_OIDC_CLAIM=]
          [default: sub]

      --repo-idle-minutes <REPO_IDLE_MINUTES>
          Minutes an unused hosted repository stays open

          [env: PANDO_REPO_IDLE_MINUTES=]
          [default: 10]

      --default-org-limits <DEFAULT_ORG_LIMITS>
          With --multi-tenant: limits of organisations that have none of their own, e.g. `seats=5,agents=20,storage=1GiB,private_repos=unlimited,plan=Free` (ADR-035). Without it, organisations are unlimited until given limits (`pando admin limits set`)

          [env: PANDO_DEFAULT_ORG_LIMITS=]

      --oidc-require <OIDC_REQUIRE>
          CLAIM=VALUE every OIDC token must carry (repeatable; all must match), e.g. `repository_owner=acme`, `hd=corp.example`, `email_verified=true`. Required for issuers that serve anyone (GitHub Actions, Google)

          [env: PANDO_OIDC_REQUIRE=]

      --billing-url <BILLING_URL>
          Where organisation administrators change their plan; the console's Upgrade button and limit messages link here

          [env: PANDO_BILLING_URL=]
          [default: ""]

      --oidc-admin <OIDC_ADMINS>
          Platform administrators who may sign in through OIDC (repeatable). Any other `--platform-admins` login is refused from an OIDC token, so an identity provider that lets users pick their name can't make someone an administrator

          [env: PANDO_OIDC_ADMINS=]

      --support-url <SUPPORT_URL>
          Where people reach support, e.g. a suspended organisation (mailto: or https:); the console links to it

          [env: PANDO_SUPPORT_URL=]
          [default: ""]

      --tls-cert <TLS_CERT>
          PEM certificate and key for TLS

          [env: PANDO_TLS_CERT=]

      --license <LICENSE>
          A support licence file to check at startup. Nothing is turned off without one

          [env: PANDO_LICENSE=]

      --tls-key <TLS_KEY>
          [env: PANDO_TLS_KEY=]

      --console-dir <CONSOLE_DIR>
          Serve the web console from this directory (the release's `pando-console-VERSION` archive, unpacked) at `/` on the server's own address. Needs the `web` flag

          [env: PANDO_CONSOLE_DIR=]

  -h, --help
          Print help (see a summary with '-h')

pandod license

Support licences: `pandod license verify FILE`

Usage: pandod license [OPTIONS] <COMMAND>

Commands:
  verify  Check a licence file's signature and dates
  help    Print this message or the help of the given subcommand(s)

Options:
      --metrics <METRICS>
          Address for `/metrics` (Prometheus), `/healthz` and `/readyz` (no token needed), e.g. 0.0.0.0:9420 [env: PANDO_METRICS=]
      --log-json
          Emit JSON logs [env: PANDO_LOG_JSON=]
      --audit-log <AUDIT_LOG>
          Append an audit trail (JSON lines: who read or changed what, from where, with what result) to this file. Off by default; it grows with every request [env: PANDO_AUDIT_LOG=]
      --audit-retention-days <AUDIT_RETENTION_DAYS>
          Days of audit files to keep (one file per UTC day); 0 keeps all. 365 suits hosted services (docs/deploy.md) [env: PANDO_AUDIT_RETENTION_DAYS=] [default: 365]
      --cors-origin <WEB_ORIGINS>
          Origins allowed to call this server from a browser on another site (repeatable or comma-separated), e.g. your own console's address. Default: none (same origin only); `*` allows any (development only) [env: PANDO_WEB_ORIGINS=] [alias: --web-origins]
      --trust-proxy
          Take the client address (audit log, sign-in rate limits) from `X-Forwarded-For` when the connection comes from a reverse proxy on this host (loopback) [env: PANDO_TRUST_PROXY=]
      --max-request-mb <MAX_REQUEST_MB>
          Largest request message in MiB (an uploaded object batch, a changeset). Default: 64 with --platform, 260 otherwise. Files of 8 MiB and more are uploaded in chunks [env: PANDO_MAX_REQUEST_MB=]
      --max-calls-per-principal <MAX_CALLS_PER_PRINCIPAL>
          Calls one account may have in progress at once (0: no limit). Default: 128 with --platform, no limit otherwise [env: PANDO_MAX_CALLS_PER_PRINCIPAL=]
      --sqlite-synchronous <SQLITE_SYNCHRONOUS>
          SQLite durability: `full` (default) syncs every commit, so an acknowledged land survives power loss; `normal` survives process crashes only, for more throughput on a slow disk [env: PANDO_SQLITE_SYNCHRONOUS=] [default: full] [possible values: full, normal]
      --allow-anonymous
          Accept unauthenticated calls on a non-loopback address. Without tokens or OIDC, pandod otherwise listens only on localhost, so a server is never open to a network by accident [env: PANDO_ALLOW_ANONYMOUS=]
      --request-timeout <REQUEST_TIMEOUT>
          Longest any one call may run, in seconds [env: PANDO_REQUEST_TIMEOUT=] [default: 600]
      --max-in-flight <MAX_IN_FLIGHT>
          Calls in progress at once across all clients; beyond this, calls are refused with Unavailable (clients retry) instead of queueing [env: PANDO_MAX_IN_FLIGHT=] [default: 4096]
      --tokens <TOKENS>
          Comma-separated bearer tokens accepted by this process. Unset = no auth (dev only) [env: PANDO_TOKENS]
      --oidc-issuer <OIDC_ISSUER>
          OpenID Connect issuer URL; enables SSO bearer tokens (JWTs) alongside static tokens [env: PANDO_OIDC_ISSUER=]
      --oidc-audience <OIDC_AUDIENCE>
          Expected `aud` claim of OIDC tokens [env: PANDO_OIDC_AUDIENCE=]
      --oidc-claim <OIDC_CLAIM>
          Claim used as the principal for access rules (sub, email, preferred_username...) [env: PANDO_OIDC_CLAIM=] [default: sub]
      --oidc-require <OIDC_REQUIRE>
          CLAIM=VALUE every OIDC token must carry (repeatable; all must match), e.g. `repository_owner=acme`, `hd=corp.example`, `email_verified=true`. Required for issuers that serve anyone (GitHub Actions, Google) [env: PANDO_OIDC_REQUIRE=]
      --oidc-admin <OIDC_ADMINS>
          Platform administrators who may sign in through OIDC (repeatable). Any other `--platform-admins` login is refused from an OIDC token, so an identity provider that lets users pick their name can't make someone an administrator [env: PANDO_OIDC_ADMINS=]
      --tls-cert <TLS_CERT>
          PEM certificate and key for TLS [env: PANDO_TLS_CERT=]
      --tls-key <TLS_KEY>
          [env: PANDO_TLS_KEY=]
  -h, --help
          Print help

pandod license verify

Check a licence file's signature and dates

Usage: pandod license verify [OPTIONS] <FILE>

Arguments:
  <FILE>

Options:
      --metrics <METRICS>
          Address for `/metrics` (Prometheus), `/healthz` and `/readyz` (no token needed), e.g. 0.0.0.0:9420 [env: PANDO_METRICS=]
      --log-json
          Emit JSON logs [env: PANDO_LOG_JSON=]
      --audit-log <AUDIT_LOG>
          Append an audit trail (JSON lines: who read or changed what, from where, with what result) to this file. Off by default; it grows with every request [env: PANDO_AUDIT_LOG=]
      --audit-retention-days <AUDIT_RETENTION_DAYS>
          Days of audit files to keep (one file per UTC day); 0 keeps all. 365 suits hosted services (docs/deploy.md) [env: PANDO_AUDIT_RETENTION_DAYS=] [default: 365]
      --cors-origin <WEB_ORIGINS>
          Origins allowed to call this server from a browser on another site (repeatable or comma-separated), e.g. your own console's address. Default: none (same origin only); `*` allows any (development only) [env: PANDO_WEB_ORIGINS=] [alias: --web-origins]
      --trust-proxy
          Take the client address (audit log, sign-in rate limits) from `X-Forwarded-For` when the connection comes from a reverse proxy on this host (loopback) [env: PANDO_TRUST_PROXY=]
      --max-request-mb <MAX_REQUEST_MB>
          Largest request message in MiB (an uploaded object batch, a changeset). Default: 64 with --platform, 260 otherwise. Files of 8 MiB and more are uploaded in chunks [env: PANDO_MAX_REQUEST_MB=]
      --max-calls-per-principal <MAX_CALLS_PER_PRINCIPAL>
          Calls one account may have in progress at once (0: no limit). Default: 128 with --platform, no limit otherwise [env: PANDO_MAX_CALLS_PER_PRINCIPAL=]
      --sqlite-synchronous <SQLITE_SYNCHRONOUS>
          SQLite durability: `full` (default) syncs every commit, so an acknowledged land survives power loss; `normal` survives process crashes only, for more throughput on a slow disk [env: PANDO_SQLITE_SYNCHRONOUS=] [default: full] [possible values: full, normal]
      --allow-anonymous
          Accept unauthenticated calls on a non-loopback address. Without tokens or OIDC, pandod otherwise listens only on localhost, so a server is never open to a network by accident [env: PANDO_ALLOW_ANONYMOUS=]
      --request-timeout <REQUEST_TIMEOUT>
          Longest any one call may run, in seconds [env: PANDO_REQUEST_TIMEOUT=] [default: 600]
      --max-in-flight <MAX_IN_FLIGHT>
          Calls in progress at once across all clients; beyond this, calls are refused with Unavailable (clients retry) instead of queueing [env: PANDO_MAX_IN_FLIGHT=] [default: 4096]
      --tokens <TOKENS>
          Comma-separated bearer tokens accepted by this process. Unset = no auth (dev only) [env: PANDO_TOKENS]
      --oidc-issuer <OIDC_ISSUER>
          OpenID Connect issuer URL; enables SSO bearer tokens (JWTs) alongside static tokens [env: PANDO_OIDC_ISSUER=]
      --oidc-audience <OIDC_AUDIENCE>
          Expected `aud` claim of OIDC tokens [env: PANDO_OIDC_AUDIENCE=]
      --oidc-claim <OIDC_CLAIM>
          Claim used as the principal for access rules (sub, email, preferred_username...) [env: PANDO_OIDC_CLAIM=] [default: sub]
      --oidc-require <OIDC_REQUIRE>
          CLAIM=VALUE every OIDC token must carry (repeatable; all must match), e.g. `repository_owner=acme`, `hd=corp.example`, `email_verified=true`. Required for issuers that serve anyone (GitHub Actions, Google) [env: PANDO_OIDC_REQUIRE=]
      --oidc-admin <OIDC_ADMINS>
          Platform administrators who may sign in through OIDC (repeatable). Any other `--platform-admins` login is refused from an OIDC token, so an identity provider that lets users pick their name can't make someone an administrator [env: PANDO_OIDC_ADMINS=]
      --tls-cert <TLS_CERT>
          PEM certificate and key for TLS [env: PANDO_TLS_CERT=]
      --tls-key <TLS_KEY>
          [env: PANDO_TLS_KEY=]
  -h, --help
          Print help

pandod repo

Operator actions on one hosted repository, straight on the registry (site administration; works while the server runs, which notices within seconds)

Usage: pandod repo [OPTIONS] --platform <PLATFORM> <COMMAND>

Commands:
  disable  Take a repository down at once: every read and write is refused ("disabled by the operator"); nothing is deleted. Undo with `enable`
  enable   Serve a disabled repository again
  delete   Delete a repository: its registry rows (reviews, issues, webhooks, grants) and its storage files. Cannot be undone; take a backup first
  help     Print this message or the help of the given subcommand(s)

Options:
      --platform <PLATFORM>
          [env: PANDO_PLATFORM]
      --metrics <METRICS>
          Address for `/metrics` (Prometheus), `/healthz` and `/readyz` (no token needed), e.g. 0.0.0.0:9420 [env: PANDO_METRICS=]
      --log-json
          Emit JSON logs [env: PANDO_LOG_JSON=]
      --audit-log <AUDIT_LOG>
          Append an audit trail (JSON lines: who read or changed what, from where, with what result) to this file. Off by default; it grows with every request [env: PANDO_AUDIT_LOG=]
      --audit-retention-days <AUDIT_RETENTION_DAYS>
          Days of audit files to keep (one file per UTC day); 0 keeps all. 365 suits hosted services (docs/deploy.md) [env: PANDO_AUDIT_RETENTION_DAYS=] [default: 365]
      --cors-origin <WEB_ORIGINS>
          Origins allowed to call this server from a browser on another site (repeatable or comma-separated), e.g. your own console's address. Default: none (same origin only); `*` allows any (development only) [env: PANDO_WEB_ORIGINS=] [alias: --web-origins]
      --trust-proxy
          Take the client address (audit log, sign-in rate limits) from `X-Forwarded-For` when the connection comes from a reverse proxy on this host (loopback) [env: PANDO_TRUST_PROXY=]
      --max-request-mb <MAX_REQUEST_MB>
          Largest request message in MiB (an uploaded object batch, a changeset). Default: 64 with --platform, 260 otherwise. Files of 8 MiB and more are uploaded in chunks [env: PANDO_MAX_REQUEST_MB=]
      --max-calls-per-principal <MAX_CALLS_PER_PRINCIPAL>
          Calls one account may have in progress at once (0: no limit). Default: 128 with --platform, no limit otherwise [env: PANDO_MAX_CALLS_PER_PRINCIPAL=]
      --sqlite-synchronous <SQLITE_SYNCHRONOUS>
          SQLite durability: `full` (default) syncs every commit, so an acknowledged land survives power loss; `normal` survives process crashes only, for more throughput on a slow disk [env: PANDO_SQLITE_SYNCHRONOUS=] [default: full] [possible values: full, normal]
      --allow-anonymous
          Accept unauthenticated calls on a non-loopback address. Without tokens or OIDC, pandod otherwise listens only on localhost, so a server is never open to a network by accident [env: PANDO_ALLOW_ANONYMOUS=]
      --request-timeout <REQUEST_TIMEOUT>
          Longest any one call may run, in seconds [env: PANDO_REQUEST_TIMEOUT=] [default: 600]
      --max-in-flight <MAX_IN_FLIGHT>
          Calls in progress at once across all clients; beyond this, calls are refused with Unavailable (clients retry) instead of queueing [env: PANDO_MAX_IN_FLIGHT=] [default: 4096]
      --tokens <TOKENS>
          Comma-separated bearer tokens accepted by this process. Unset = no auth (dev only) [env: PANDO_TOKENS]
      --oidc-issuer <OIDC_ISSUER>
          OpenID Connect issuer URL; enables SSO bearer tokens (JWTs) alongside static tokens [env: PANDO_OIDC_ISSUER=]
      --oidc-audience <OIDC_AUDIENCE>
          Expected `aud` claim of OIDC tokens [env: PANDO_OIDC_AUDIENCE=]
      --oidc-claim <OIDC_CLAIM>
          Claim used as the principal for access rules (sub, email, preferred_username...) [env: PANDO_OIDC_CLAIM=] [default: sub]
      --oidc-require <OIDC_REQUIRE>
          CLAIM=VALUE every OIDC token must carry (repeatable; all must match), e.g. `repository_owner=acme`, `hd=corp.example`, `email_verified=true`. Required for issuers that serve anyone (GitHub Actions, Google) [env: PANDO_OIDC_REQUIRE=]
      --oidc-admin <OIDC_ADMINS>
          Platform administrators who may sign in through OIDC (repeatable). Any other `--platform-admins` login is refused from an OIDC token, so an identity provider that lets users pick their name can't make someone an administrator [env: PANDO_OIDC_ADMINS=]
      --tls-cert <TLS_CERT>
          PEM certificate and key for TLS [env: PANDO_TLS_CERT=]
      --tls-key <TLS_KEY>
          [env: PANDO_TLS_KEY=]
  -h, --help
          Print help

pandod repo disable

Take a repository down at once: every read and write is refused ("disabled by the operator"); nothing is deleted. Undo with `enable`

Usage: pandod repo --platform <PLATFORM> disable [OPTIONS] --reason <REASON> <REPO>

Arguments:
  <REPO>

Options:
      --reason <REASON>

      --metrics <METRICS>
          Address for `/metrics` (Prometheus), `/healthz` and `/readyz` (no token needed), e.g. 0.0.0.0:9420 [env: PANDO_METRICS=]
      --log-json
          Emit JSON logs [env: PANDO_LOG_JSON=]
      --audit-log <AUDIT_LOG>
          Append an audit trail (JSON lines: who read or changed what, from where, with what result) to this file. Off by default; it grows with every request [env: PANDO_AUDIT_LOG=]
      --audit-retention-days <AUDIT_RETENTION_DAYS>
          Days of audit files to keep (one file per UTC day); 0 keeps all. 365 suits hosted services (docs/deploy.md) [env: PANDO_AUDIT_RETENTION_DAYS=] [default: 365]
      --cors-origin <WEB_ORIGINS>
          Origins allowed to call this server from a browser on another site (repeatable or comma-separated), e.g. your own console's address. Default: none (same origin only); `*` allows any (development only) [env: PANDO_WEB_ORIGINS=] [alias: --web-origins]
      --trust-proxy
          Take the client address (audit log, sign-in rate limits) from `X-Forwarded-For` when the connection comes from a reverse proxy on this host (loopback) [env: PANDO_TRUST_PROXY=]
      --max-request-mb <MAX_REQUEST_MB>
          Largest request message in MiB (an uploaded object batch, a changeset). Default: 64 with --platform, 260 otherwise. Files of 8 MiB and more are uploaded in chunks [env: PANDO_MAX_REQUEST_MB=]
      --max-calls-per-principal <MAX_CALLS_PER_PRINCIPAL>
          Calls one account may have in progress at once (0: no limit). Default: 128 with --platform, no limit otherwise [env: PANDO_MAX_CALLS_PER_PRINCIPAL=]
      --sqlite-synchronous <SQLITE_SYNCHRONOUS>
          SQLite durability: `full` (default) syncs every commit, so an acknowledged land survives power loss; `normal` survives process crashes only, for more throughput on a slow disk [env: PANDO_SQLITE_SYNCHRONOUS=] [default: full] [possible values: full, normal]
      --allow-anonymous
          Accept unauthenticated calls on a non-loopback address. Without tokens or OIDC, pandod otherwise listens only on localhost, so a server is never open to a network by accident [env: PANDO_ALLOW_ANONYMOUS=]
      --request-timeout <REQUEST_TIMEOUT>
          Longest any one call may run, in seconds [env: PANDO_REQUEST_TIMEOUT=] [default: 600]
      --max-in-flight <MAX_IN_FLIGHT>
          Calls in progress at once across all clients; beyond this, calls are refused with Unavailable (clients retry) instead of queueing [env: PANDO_MAX_IN_FLIGHT=] [default: 4096]
      --tokens <TOKENS>
          Comma-separated bearer tokens accepted by this process. Unset = no auth (dev only) [env: PANDO_TOKENS]
      --oidc-issuer <OIDC_ISSUER>
          OpenID Connect issuer URL; enables SSO bearer tokens (JWTs) alongside static tokens [env: PANDO_OIDC_ISSUER=]
      --oidc-audience <OIDC_AUDIENCE>
          Expected `aud` claim of OIDC tokens [env: PANDO_OIDC_AUDIENCE=]
      --oidc-claim <OIDC_CLAIM>
          Claim used as the principal for access rules (sub, email, preferred_username...) [env: PANDO_OIDC_CLAIM=] [default: sub]
      --oidc-require <OIDC_REQUIRE>
          CLAIM=VALUE every OIDC token must carry (repeatable; all must match), e.g. `repository_owner=acme`, `hd=corp.example`, `email_verified=true`. Required for issuers that serve anyone (GitHub Actions, Google) [env: PANDO_OIDC_REQUIRE=]
      --oidc-admin <OIDC_ADMINS>
          Platform administrators who may sign in through OIDC (repeatable). Any other `--platform-admins` login is refused from an OIDC token, so an identity provider that lets users pick their name can't make someone an administrator [env: PANDO_OIDC_ADMINS=]
      --tls-cert <TLS_CERT>
          PEM certificate and key for TLS [env: PANDO_TLS_CERT=]
      --tls-key <TLS_KEY>
          [env: PANDO_TLS_KEY=]
  -h, --help
          Print help

pandod repo enable

Serve a disabled repository again

Usage: pandod repo --platform <PLATFORM> enable [OPTIONS] <REPO>

Arguments:
  <REPO>

Options:
      --metrics <METRICS>
          Address for `/metrics` (Prometheus), `/healthz` and `/readyz` (no token needed), e.g. 0.0.0.0:9420 [env: PANDO_METRICS=]
      --log-json
          Emit JSON logs [env: PANDO_LOG_JSON=]
      --audit-log <AUDIT_LOG>
          Append an audit trail (JSON lines: who read or changed what, from where, with what result) to this file. Off by default; it grows with every request [env: PANDO_AUDIT_LOG=]
      --audit-retention-days <AUDIT_RETENTION_DAYS>
          Days of audit files to keep (one file per UTC day); 0 keeps all. 365 suits hosted services (docs/deploy.md) [env: PANDO_AUDIT_RETENTION_DAYS=] [default: 365]
      --cors-origin <WEB_ORIGINS>
          Origins allowed to call this server from a browser on another site (repeatable or comma-separated), e.g. your own console's address. Default: none (same origin only); `*` allows any (development only) [env: PANDO_WEB_ORIGINS=] [alias: --web-origins]
      --trust-proxy
          Take the client address (audit log, sign-in rate limits) from `X-Forwarded-For` when the connection comes from a reverse proxy on this host (loopback) [env: PANDO_TRUST_PROXY=]
      --max-request-mb <MAX_REQUEST_MB>
          Largest request message in MiB (an uploaded object batch, a changeset). Default: 64 with --platform, 260 otherwise. Files of 8 MiB and more are uploaded in chunks [env: PANDO_MAX_REQUEST_MB=]
      --max-calls-per-principal <MAX_CALLS_PER_PRINCIPAL>
          Calls one account may have in progress at once (0: no limit). Default: 128 with --platform, no limit otherwise [env: PANDO_MAX_CALLS_PER_PRINCIPAL=]
      --sqlite-synchronous <SQLITE_SYNCHRONOUS>
          SQLite durability: `full` (default) syncs every commit, so an acknowledged land survives power loss; `normal` survives process crashes only, for more throughput on a slow disk [env: PANDO_SQLITE_SYNCHRONOUS=] [default: full] [possible values: full, normal]
      --allow-anonymous
          Accept unauthenticated calls on a non-loopback address. Without tokens or OIDC, pandod otherwise listens only on localhost, so a server is never open to a network by accident [env: PANDO_ALLOW_ANONYMOUS=]
      --request-timeout <REQUEST_TIMEOUT>
          Longest any one call may run, in seconds [env: PANDO_REQUEST_TIMEOUT=] [default: 600]
      --max-in-flight <MAX_IN_FLIGHT>
          Calls in progress at once across all clients; beyond this, calls are refused with Unavailable (clients retry) instead of queueing [env: PANDO_MAX_IN_FLIGHT=] [default: 4096]
      --tokens <TOKENS>
          Comma-separated bearer tokens accepted by this process. Unset = no auth (dev only) [env: PANDO_TOKENS]
      --oidc-issuer <OIDC_ISSUER>
          OpenID Connect issuer URL; enables SSO bearer tokens (JWTs) alongside static tokens [env: PANDO_OIDC_ISSUER=]
      --oidc-audience <OIDC_AUDIENCE>
          Expected `aud` claim of OIDC tokens [env: PANDO_OIDC_AUDIENCE=]
      --oidc-claim <OIDC_CLAIM>
          Claim used as the principal for access rules (sub, email, preferred_username...) [env: PANDO_OIDC_CLAIM=] [default: sub]
      --oidc-require <OIDC_REQUIRE>
          CLAIM=VALUE every OIDC token must carry (repeatable; all must match), e.g. `repository_owner=acme`, `hd=corp.example`, `email_verified=true`. Required for issuers that serve anyone (GitHub Actions, Google) [env: PANDO_OIDC_REQUIRE=]
      --oidc-admin <OIDC_ADMINS>
          Platform administrators who may sign in through OIDC (repeatable). Any other `--platform-admins` login is refused from an OIDC token, so an identity provider that lets users pick their name can't make someone an administrator [env: PANDO_OIDC_ADMINS=]
      --tls-cert <TLS_CERT>
          PEM certificate and key for TLS [env: PANDO_TLS_CERT=]
      --tls-key <TLS_KEY>
          [env: PANDO_TLS_KEY=]
  -h, --help
          Print help

pandod repo delete

Delete a repository: its registry rows (reviews, issues, webhooks, grants) and its storage files. Cannot be undone; take a backup first

Usage: pandod repo --platform <PLATFORM> delete [OPTIONS] --repos-metadata <REPOS_METADATA> --confirm <CONFIRM> <REPO>

Arguments:
  <REPO>

Options:
      --repos-metadata <REPOS_METADATA>
          [env: PANDO_REPOS_METADATA]
      --repos-objects <REPOS_OBJECTS>
          [env: PANDO_REPOS_OBJECTS] [default: metadata]
      --confirm <CONFIRM>
          Confirm by repeating OWNER/NAME
      --metrics <METRICS>
          Address for `/metrics` (Prometheus), `/healthz` and `/readyz` (no token needed), e.g. 0.0.0.0:9420 [env: PANDO_METRICS=]
      --log-json
          Emit JSON logs [env: PANDO_LOG_JSON=]
      --audit-log <AUDIT_LOG>
          Append an audit trail (JSON lines: who read or changed what, from where, with what result) to this file. Off by default; it grows with every request [env: PANDO_AUDIT_LOG=]
      --audit-retention-days <AUDIT_RETENTION_DAYS>
          Days of audit files to keep (one file per UTC day); 0 keeps all. 365 suits hosted services (docs/deploy.md) [env: PANDO_AUDIT_RETENTION_DAYS=] [default: 365]
      --cors-origin <WEB_ORIGINS>
          Origins allowed to call this server from a browser on another site (repeatable or comma-separated), e.g. your own console's address. Default: none (same origin only); `*` allows any (development only) [env: PANDO_WEB_ORIGINS=] [alias: --web-origins]
      --trust-proxy
          Take the client address (audit log, sign-in rate limits) from `X-Forwarded-For` when the connection comes from a reverse proxy on this host (loopback) [env: PANDO_TRUST_PROXY=]
      --max-request-mb <MAX_REQUEST_MB>
          Largest request message in MiB (an uploaded object batch, a changeset). Default: 64 with --platform, 260 otherwise. Files of 8 MiB and more are uploaded in chunks [env: PANDO_MAX_REQUEST_MB=]
      --max-calls-per-principal <MAX_CALLS_PER_PRINCIPAL>
          Calls one account may have in progress at once (0: no limit). Default: 128 with --platform, no limit otherwise [env: PANDO_MAX_CALLS_PER_PRINCIPAL=]
      --sqlite-synchronous <SQLITE_SYNCHRONOUS>
          SQLite durability: `full` (default) syncs every commit, so an acknowledged land survives power loss; `normal` survives process crashes only, for more throughput on a slow disk [env: PANDO_SQLITE_SYNCHRONOUS=] [default: full] [possible values: full, normal]
      --allow-anonymous
          Accept unauthenticated calls on a non-loopback address. Without tokens or OIDC, pandod otherwise listens only on localhost, so a server is never open to a network by accident [env: PANDO_ALLOW_ANONYMOUS=]
      --request-timeout <REQUEST_TIMEOUT>
          Longest any one call may run, in seconds [env: PANDO_REQUEST_TIMEOUT=] [default: 600]
      --max-in-flight <MAX_IN_FLIGHT>
          Calls in progress at once across all clients; beyond this, calls are refused with Unavailable (clients retry) instead of queueing [env: PANDO_MAX_IN_FLIGHT=] [default: 4096]
      --tokens <TOKENS>
          Comma-separated bearer tokens accepted by this process. Unset = no auth (dev only) [env: PANDO_TOKENS]
      --oidc-issuer <OIDC_ISSUER>
          OpenID Connect issuer URL; enables SSO bearer tokens (JWTs) alongside static tokens [env: PANDO_OIDC_ISSUER=]
      --oidc-audience <OIDC_AUDIENCE>
          Expected `aud` claim of OIDC tokens [env: PANDO_OIDC_AUDIENCE=]
      --oidc-claim <OIDC_CLAIM>
          Claim used as the principal for access rules (sub, email, preferred_username...) [env: PANDO_OIDC_CLAIM=] [default: sub]
      --oidc-require <OIDC_REQUIRE>
          CLAIM=VALUE every OIDC token must carry (repeatable; all must match), e.g. `repository_owner=acme`, `hd=corp.example`, `email_verified=true`. Required for issuers that serve anyone (GitHub Actions, Google) [env: PANDO_OIDC_REQUIRE=]
      --oidc-admin <OIDC_ADMINS>
          Platform administrators who may sign in through OIDC (repeatable). Any other `--platform-admins` login is refused from an OIDC token, so an identity provider that lets users pick their name can't make someone an administrator [env: PANDO_OIDC_ADMINS=]
      --tls-cert <TLS_CERT>
          PEM certificate and key for TLS [env: PANDO_TLS_CERT=]
      --tls-key <TLS_KEY>
          [env: PANDO_TLS_KEY=]
  -h, --help
          Print help

pandod token

Create an access token directly in a platform registry, without a running server: how the first administrator gets in. Prints the token once

Usage: pandod token [OPTIONS] --platform <PLATFORM> --account <ACCOUNT>

Options:
      --platform <PLATFORM>
          [env: PANDO_PLATFORM]
      --account <ACCOUNT>
          The account the token belongs to and acts as (list it in --platform-admins to make it an administrator)
      --name <NAME>
          [default: bootstrap]
      --expires-days <EXPIRES_DAYS>
          Days until it expires (1–366) [default: 30]
      --metrics <METRICS>
          Address for `/metrics` (Prometheus), `/healthz` and `/readyz` (no token needed), e.g. 0.0.0.0:9420 [env: PANDO_METRICS=]
      --log-json
          Emit JSON logs [env: PANDO_LOG_JSON=]
      --audit-log <AUDIT_LOG>
          Append an audit trail (JSON lines: who read or changed what, from where, with what result) to this file. Off by default; it grows with every request [env: PANDO_AUDIT_LOG=]
      --audit-retention-days <AUDIT_RETENTION_DAYS>
          Days of audit files to keep (one file per UTC day); 0 keeps all. 365 suits hosted services (docs/deploy.md) [env: PANDO_AUDIT_RETENTION_DAYS=] [default: 365]
      --cors-origin <WEB_ORIGINS>
          Origins allowed to call this server from a browser on another site (repeatable or comma-separated), e.g. your own console's address. Default: none (same origin only); `*` allows any (development only) [env: PANDO_WEB_ORIGINS=] [alias: --web-origins]
      --trust-proxy
          Take the client address (audit log, sign-in rate limits) from `X-Forwarded-For` when the connection comes from a reverse proxy on this host (loopback) [env: PANDO_TRUST_PROXY=]
      --max-request-mb <MAX_REQUEST_MB>
          Largest request message in MiB (an uploaded object batch, a changeset). Default: 64 with --platform, 260 otherwise. Files of 8 MiB and more are uploaded in chunks [env: PANDO_MAX_REQUEST_MB=]
      --max-calls-per-principal <MAX_CALLS_PER_PRINCIPAL>
          Calls one account may have in progress at once (0: no limit). Default: 128 with --platform, no limit otherwise [env: PANDO_MAX_CALLS_PER_PRINCIPAL=]
      --sqlite-synchronous <SQLITE_SYNCHRONOUS>
          SQLite durability: `full` (default) syncs every commit, so an acknowledged land survives power loss; `normal` survives process crashes only, for more throughput on a slow disk [env: PANDO_SQLITE_SYNCHRONOUS=] [default: full] [possible values: full, normal]
      --allow-anonymous
          Accept unauthenticated calls on a non-loopback address. Without tokens or OIDC, pandod otherwise listens only on localhost, so a server is never open to a network by accident [env: PANDO_ALLOW_ANONYMOUS=]
      --request-timeout <REQUEST_TIMEOUT>
          Longest any one call may run, in seconds [env: PANDO_REQUEST_TIMEOUT=] [default: 600]
      --max-in-flight <MAX_IN_FLIGHT>
          Calls in progress at once across all clients; beyond this, calls are refused with Unavailable (clients retry) instead of queueing [env: PANDO_MAX_IN_FLIGHT=] [default: 4096]
      --tokens <TOKENS>
          Comma-separated bearer tokens accepted by this process. Unset = no auth (dev only) [env: PANDO_TOKENS]
      --oidc-issuer <OIDC_ISSUER>
          OpenID Connect issuer URL; enables SSO bearer tokens (JWTs) alongside static tokens [env: PANDO_OIDC_ISSUER=]
      --oidc-audience <OIDC_AUDIENCE>
          Expected `aud` claim of OIDC tokens [env: PANDO_OIDC_AUDIENCE=]
      --oidc-claim <OIDC_CLAIM>
          Claim used as the principal for access rules (sub, email, preferred_username...) [env: PANDO_OIDC_CLAIM=] [default: sub]
      --oidc-require <OIDC_REQUIRE>
          CLAIM=VALUE every OIDC token must carry (repeatable; all must match), e.g. `repository_owner=acme`, `hd=corp.example`, `email_verified=true`. Required for issuers that serve anyone (GitHub Actions, Google) [env: PANDO_OIDC_REQUIRE=]
      --oidc-admin <OIDC_ADMINS>
          Platform administrators who may sign in through OIDC (repeatable). Any other `--platform-admins` login is refused from an OIDC token, so an identity provider that lets users pick their name can't make someone an administrator [env: PANDO_OIDC_ADMINS=]
      --tls-cert <TLS_CERT>
          PEM certificate and key for TLS [env: PANDO_TLS_CERT=]
      --tls-key <TLS_KEY>
          [env: PANDO_TLS_KEY=]
  -h, --help
          Print help

pandod backup

Back up a platform server while it runs: a consistent snapshot of the registry and of every hosted repository's metadata database, with a manifest of where each belongs and where objects live. Restore by copying back while stopped (docs/backup.md)

Usage: pandod backup [OPTIONS] --platform <PLATFORM> --repos-metadata <REPOS_METADATA> --to <TO>

Options:
      --platform <PLATFORM>
          [env: PANDO_PLATFORM]
      --repos-metadata <REPOS_METADATA>
          [env: PANDO_REPOS_METADATA]
      --repos-objects <REPOS_OBJECTS>
          [env: PANDO_REPOS_OBJECTS] [default: metadata]
      --metadata <METADATA>
          The server's default repository, backed up too when it is SQLite [env: PANDO_METADATA]
      --to <TO>
          A new or empty directory, or an object store URL (s3://BUCKET/PREFIX, gs://, az://)
      --metrics <METRICS>
          Address for `/metrics` (Prometheus), `/healthz` and `/readyz` (no token needed), e.g. 0.0.0.0:9420 [env: PANDO_METRICS=]
      --log-json
          Emit JSON logs [env: PANDO_LOG_JSON=]
      --audit-log <AUDIT_LOG>
          Append an audit trail (JSON lines: who read or changed what, from where, with what result) to this file. Off by default; it grows with every request [env: PANDO_AUDIT_LOG=]
      --audit-retention-days <AUDIT_RETENTION_DAYS>
          Days of audit files to keep (one file per UTC day); 0 keeps all. 365 suits hosted services (docs/deploy.md) [env: PANDO_AUDIT_RETENTION_DAYS=] [default: 365]
      --cors-origin <WEB_ORIGINS>
          Origins allowed to call this server from a browser on another site (repeatable or comma-separated), e.g. your own console's address. Default: none (same origin only); `*` allows any (development only) [env: PANDO_WEB_ORIGINS=] [alias: --web-origins]
      --trust-proxy
          Take the client address (audit log, sign-in rate limits) from `X-Forwarded-For` when the connection comes from a reverse proxy on this host (loopback) [env: PANDO_TRUST_PROXY=]
      --max-request-mb <MAX_REQUEST_MB>
          Largest request message in MiB (an uploaded object batch, a changeset). Default: 64 with --platform, 260 otherwise. Files of 8 MiB and more are uploaded in chunks [env: PANDO_MAX_REQUEST_MB=]
      --max-calls-per-principal <MAX_CALLS_PER_PRINCIPAL>
          Calls one account may have in progress at once (0: no limit). Default: 128 with --platform, no limit otherwise [env: PANDO_MAX_CALLS_PER_PRINCIPAL=]
      --sqlite-synchronous <SQLITE_SYNCHRONOUS>
          SQLite durability: `full` (default) syncs every commit, so an acknowledged land survives power loss; `normal` survives process crashes only, for more throughput on a slow disk [env: PANDO_SQLITE_SYNCHRONOUS=] [default: full] [possible values: full, normal]
      --allow-anonymous
          Accept unauthenticated calls on a non-loopback address. Without tokens or OIDC, pandod otherwise listens only on localhost, so a server is never open to a network by accident [env: PANDO_ALLOW_ANONYMOUS=]
      --request-timeout <REQUEST_TIMEOUT>
          Longest any one call may run, in seconds [env: PANDO_REQUEST_TIMEOUT=] [default: 600]
      --max-in-flight <MAX_IN_FLIGHT>
          Calls in progress at once across all clients; beyond this, calls are refused with Unavailable (clients retry) instead of queueing [env: PANDO_MAX_IN_FLIGHT=] [default: 4096]
      --tokens <TOKENS>
          Comma-separated bearer tokens accepted by this process. Unset = no auth (dev only) [env: PANDO_TOKENS]
      --oidc-issuer <OIDC_ISSUER>
          OpenID Connect issuer URL; enables SSO bearer tokens (JWTs) alongside static tokens [env: PANDO_OIDC_ISSUER=]
      --oidc-audience <OIDC_AUDIENCE>
          Expected `aud` claim of OIDC tokens [env: PANDO_OIDC_AUDIENCE=]
      --oidc-claim <OIDC_CLAIM>
          Claim used as the principal for access rules (sub, email, preferred_username...) [env: PANDO_OIDC_CLAIM=] [default: sub]
      --oidc-require <OIDC_REQUIRE>
          CLAIM=VALUE every OIDC token must carry (repeatable; all must match), e.g. `repository_owner=acme`, `hd=corp.example`, `email_verified=true`. Required for issuers that serve anyone (GitHub Actions, Google) [env: PANDO_OIDC_REQUIRE=]
      --oidc-admin <OIDC_ADMINS>
          Platform administrators who may sign in through OIDC (repeatable). Any other `--platform-admins` login is refused from an OIDC token, so an identity provider that lets users pick their name can't make someone an administrator [env: PANDO_OIDC_ADMINS=]
      --tls-cert <TLS_CERT>
          PEM certificate and key for TLS [env: PANDO_TLS_CERT=]
      --tls-key <TLS_KEY>
          [env: PANDO_TLS_KEY=]
  -h, --help
          Print help

pandod compact

Delta-compress stored blob versions (single writer; safe to run while serving)

Usage: pandod compact [OPTIONS]

Options:
      --metadata <METADATA>
          [env: PANDO_METADATA] [default: sqlite://./pando-data/meta.db]
      --objects <OBJECTS>
          [env: PANDO_OBJECTS] [default: metadata]
      --force-unlock
          Take over the lease of a compaction that crashed
      --metrics <METRICS>
          Address for `/metrics` (Prometheus), `/healthz` and `/readyz` (no token needed), e.g. 0.0.0.0:9420 [env: PANDO_METRICS=]
      --log-json
          Emit JSON logs [env: PANDO_LOG_JSON=]
      --audit-log <AUDIT_LOG>
          Append an audit trail (JSON lines: who read or changed what, from where, with what result) to this file. Off by default; it grows with every request [env: PANDO_AUDIT_LOG=]
      --audit-retention-days <AUDIT_RETENTION_DAYS>
          Days of audit files to keep (one file per UTC day); 0 keeps all. 365 suits hosted services (docs/deploy.md) [env: PANDO_AUDIT_RETENTION_DAYS=] [default: 365]
      --cors-origin <WEB_ORIGINS>
          Origins allowed to call this server from a browser on another site (repeatable or comma-separated), e.g. your own console's address. Default: none (same origin only); `*` allows any (development only) [env: PANDO_WEB_ORIGINS=] [alias: --web-origins]
      --trust-proxy
          Take the client address (audit log, sign-in rate limits) from `X-Forwarded-For` when the connection comes from a reverse proxy on this host (loopback) [env: PANDO_TRUST_PROXY=]
      --max-request-mb <MAX_REQUEST_MB>
          Largest request message in MiB (an uploaded object batch, a changeset). Default: 64 with --platform, 260 otherwise. Files of 8 MiB and more are uploaded in chunks [env: PANDO_MAX_REQUEST_MB=]
      --max-calls-per-principal <MAX_CALLS_PER_PRINCIPAL>
          Calls one account may have in progress at once (0: no limit). Default: 128 with --platform, no limit otherwise [env: PANDO_MAX_CALLS_PER_PRINCIPAL=]
      --sqlite-synchronous <SQLITE_SYNCHRONOUS>
          SQLite durability: `full` (default) syncs every commit, so an acknowledged land survives power loss; `normal` survives process crashes only, for more throughput on a slow disk [env: PANDO_SQLITE_SYNCHRONOUS=] [default: full] [possible values: full, normal]
      --allow-anonymous
          Accept unauthenticated calls on a non-loopback address. Without tokens or OIDC, pandod otherwise listens only on localhost, so a server is never open to a network by accident [env: PANDO_ALLOW_ANONYMOUS=]
      --request-timeout <REQUEST_TIMEOUT>
          Longest any one call may run, in seconds [env: PANDO_REQUEST_TIMEOUT=] [default: 600]
      --max-in-flight <MAX_IN_FLIGHT>
          Calls in progress at once across all clients; beyond this, calls are refused with Unavailable (clients retry) instead of queueing [env: PANDO_MAX_IN_FLIGHT=] [default: 4096]
      --tokens <TOKENS>
          Comma-separated bearer tokens accepted by this process. Unset = no auth (dev only) [env: PANDO_TOKENS]
      --oidc-issuer <OIDC_ISSUER>
          OpenID Connect issuer URL; enables SSO bearer tokens (JWTs) alongside static tokens [env: PANDO_OIDC_ISSUER=]
      --oidc-audience <OIDC_AUDIENCE>
          Expected `aud` claim of OIDC tokens [env: PANDO_OIDC_AUDIENCE=]
      --oidc-claim <OIDC_CLAIM>
          Claim used as the principal for access rules (sub, email, preferred_username...) [env: PANDO_OIDC_CLAIM=] [default: sub]
      --oidc-require <OIDC_REQUIRE>
          CLAIM=VALUE every OIDC token must carry (repeatable; all must match), e.g. `repository_owner=acme`, `hd=corp.example`, `email_verified=true`. Required for issuers that serve anyone (GitHub Actions, Google) [env: PANDO_OIDC_REQUIRE=]
      --oidc-admin <OIDC_ADMINS>
          Platform administrators who may sign in through OIDC (repeatable). Any other `--platform-admins` login is refused from an OIDC token, so an identity provider that lets users pick their name can't make someone an administrator [env: PANDO_OIDC_ADMINS=]
      --tls-cert <TLS_CERT>
          PEM certificate and key for TLS [env: PANDO_TLS_CERT=]
      --tls-key <TLS_KEY>
          [env: PANDO_TLS_KEY=]
  -h, --help
          Print help

pandod copy

Copy a repository to another store: a backup, a restore, or a move to another backend (e.g. SQLite to Postgres). Safe while the source serves; repeat it to copy only what is new

Usage: pandod copy [OPTIONS] --from-metadata <FROM_METADATA> --to-metadata <TO_METADATA>

Options:
      --from-metadata <FROM_METADATA>

      --from-objects <FROM_OBJECTS>
          [default: metadata]
      --to-metadata <TO_METADATA>

      --to-objects <TO_OBJECTS>
          [default: metadata]
      --metrics <METRICS>
          Address for `/metrics` (Prometheus), `/healthz` and `/readyz` (no token needed), e.g. 0.0.0.0:9420 [env: PANDO_METRICS=]
      --log-json
          Emit JSON logs [env: PANDO_LOG_JSON=]
      --audit-log <AUDIT_LOG>
          Append an audit trail (JSON lines: who read or changed what, from where, with what result) to this file. Off by default; it grows with every request [env: PANDO_AUDIT_LOG=]
      --audit-retention-days <AUDIT_RETENTION_DAYS>
          Days of audit files to keep (one file per UTC day); 0 keeps all. 365 suits hosted services (docs/deploy.md) [env: PANDO_AUDIT_RETENTION_DAYS=] [default: 365]
      --cors-origin <WEB_ORIGINS>
          Origins allowed to call this server from a browser on another site (repeatable or comma-separated), e.g. your own console's address. Default: none (same origin only); `*` allows any (development only) [env: PANDO_WEB_ORIGINS=] [alias: --web-origins]
      --trust-proxy
          Take the client address (audit log, sign-in rate limits) from `X-Forwarded-For` when the connection comes from a reverse proxy on this host (loopback) [env: PANDO_TRUST_PROXY=]
      --max-request-mb <MAX_REQUEST_MB>
          Largest request message in MiB (an uploaded object batch, a changeset). Default: 64 with --platform, 260 otherwise. Files of 8 MiB and more are uploaded in chunks [env: PANDO_MAX_REQUEST_MB=]
      --max-calls-per-principal <MAX_CALLS_PER_PRINCIPAL>
          Calls one account may have in progress at once (0: no limit). Default: 128 with --platform, no limit otherwise [env: PANDO_MAX_CALLS_PER_PRINCIPAL=]
      --sqlite-synchronous <SQLITE_SYNCHRONOUS>
          SQLite durability: `full` (default) syncs every commit, so an acknowledged land survives power loss; `normal` survives process crashes only, for more throughput on a slow disk [env: PANDO_SQLITE_SYNCHRONOUS=] [default: full] [possible values: full, normal]
      --allow-anonymous
          Accept unauthenticated calls on a non-loopback address. Without tokens or OIDC, pandod otherwise listens only on localhost, so a server is never open to a network by accident [env: PANDO_ALLOW_ANONYMOUS=]
      --request-timeout <REQUEST_TIMEOUT>
          Longest any one call may run, in seconds [env: PANDO_REQUEST_TIMEOUT=] [default: 600]
      --max-in-flight <MAX_IN_FLIGHT>
          Calls in progress at once across all clients; beyond this, calls are refused with Unavailable (clients retry) instead of queueing [env: PANDO_MAX_IN_FLIGHT=] [default: 4096]
      --tokens <TOKENS>
          Comma-separated bearer tokens accepted by this process. Unset = no auth (dev only) [env: PANDO_TOKENS]
      --oidc-issuer <OIDC_ISSUER>
          OpenID Connect issuer URL; enables SSO bearer tokens (JWTs) alongside static tokens [env: PANDO_OIDC_ISSUER=]
      --oidc-audience <OIDC_AUDIENCE>
          Expected `aud` claim of OIDC tokens [env: PANDO_OIDC_AUDIENCE=]
      --oidc-claim <OIDC_CLAIM>
          Claim used as the principal for access rules (sub, email, preferred_username...) [env: PANDO_OIDC_CLAIM=] [default: sub]
      --oidc-require <OIDC_REQUIRE>
          CLAIM=VALUE every OIDC token must carry (repeatable; all must match), e.g. `repository_owner=acme`, `hd=corp.example`, `email_verified=true`. Required for issuers that serve anyone (GitHub Actions, Google) [env: PANDO_OIDC_REQUIRE=]
      --oidc-admin <OIDC_ADMINS>
          Platform administrators who may sign in through OIDC (repeatable). Any other `--platform-admins` login is refused from an OIDC token, so an identity provider that lets users pick their name can't make someone an administrator [env: PANDO_OIDC_ADMINS=]
      --tls-cert <TLS_CERT>
          PEM certificate and key for TLS [env: PANDO_TLS_CERT=]
      --tls-key <TLS_KEY>
          [env: PANDO_TLS_KEY=]
  -h, --help
          Print help

pandod cache

Regional cache: stateless, memory-bounded, read-through to an upstream tier

Usage: pandod cache [OPTIONS] --upstream <UPSTREAM>

Options:
      --listen <LISTEN>
          [default: 127.0.0.1:7421]
      --upstream <UPSTREAM>
          [env: PANDO_UPSTREAM]
      --capacity-mb <CAPACITY_MB>
          [default: 4096]
      --metrics <METRICS>
          Address for `/metrics` (Prometheus), `/healthz` and `/readyz` (no token needed), e.g. 0.0.0.0:9420 [env: PANDO_METRICS=]
      --log-json
          Emit JSON logs [env: PANDO_LOG_JSON=]
      --audit-log <AUDIT_LOG>
          Append an audit trail (JSON lines: who read or changed what, from where, with what result) to this file. Off by default; it grows with every request [env: PANDO_AUDIT_LOG=]
      --audit-retention-days <AUDIT_RETENTION_DAYS>
          Days of audit files to keep (one file per UTC day); 0 keeps all. 365 suits hosted services (docs/deploy.md) [env: PANDO_AUDIT_RETENTION_DAYS=] [default: 365]
      --cors-origin <WEB_ORIGINS>
          Origins allowed to call this server from a browser on another site (repeatable or comma-separated), e.g. your own console's address. Default: none (same origin only); `*` allows any (development only) [env: PANDO_WEB_ORIGINS=] [alias: --web-origins]
      --trust-proxy
          Take the client address (audit log, sign-in rate limits) from `X-Forwarded-For` when the connection comes from a reverse proxy on this host (loopback) [env: PANDO_TRUST_PROXY=]
      --max-request-mb <MAX_REQUEST_MB>
          Largest request message in MiB (an uploaded object batch, a changeset). Default: 64 with --platform, 260 otherwise. Files of 8 MiB and more are uploaded in chunks [env: PANDO_MAX_REQUEST_MB=]
      --max-calls-per-principal <MAX_CALLS_PER_PRINCIPAL>
          Calls one account may have in progress at once (0: no limit). Default: 128 with --platform, no limit otherwise [env: PANDO_MAX_CALLS_PER_PRINCIPAL=]
      --sqlite-synchronous <SQLITE_SYNCHRONOUS>
          SQLite durability: `full` (default) syncs every commit, so an acknowledged land survives power loss; `normal` survives process crashes only, for more throughput on a slow disk [env: PANDO_SQLITE_SYNCHRONOUS=] [default: full] [possible values: full, normal]
      --allow-anonymous
          Accept unauthenticated calls on a non-loopback address. Without tokens or OIDC, pandod otherwise listens only on localhost, so a server is never open to a network by accident [env: PANDO_ALLOW_ANONYMOUS=]
      --request-timeout <REQUEST_TIMEOUT>
          Longest any one call may run, in seconds [env: PANDO_REQUEST_TIMEOUT=] [default: 600]
      --max-in-flight <MAX_IN_FLIGHT>
          Calls in progress at once across all clients; beyond this, calls are refused with Unavailable (clients retry) instead of queueing [env: PANDO_MAX_IN_FLIGHT=] [default: 4096]
      --tokens <TOKENS>
          Comma-separated bearer tokens accepted by this process. Unset = no auth (dev only) [env: PANDO_TOKENS]
      --oidc-issuer <OIDC_ISSUER>
          OpenID Connect issuer URL; enables SSO bearer tokens (JWTs) alongside static tokens [env: PANDO_OIDC_ISSUER=]
      --oidc-audience <OIDC_AUDIENCE>
          Expected `aud` claim of OIDC tokens [env: PANDO_OIDC_AUDIENCE=]
      --oidc-claim <OIDC_CLAIM>
          Claim used as the principal for access rules (sub, email, preferred_username...) [env: PANDO_OIDC_CLAIM=] [default: sub]
      --oidc-require <OIDC_REQUIRE>
          CLAIM=VALUE every OIDC token must carry (repeatable; all must match), e.g. `repository_owner=acme`, `hd=corp.example`, `email_verified=true`. Required for issuers that serve anyone (GitHub Actions, Google) [env: PANDO_OIDC_REQUIRE=]
      --oidc-admin <OIDC_ADMINS>
          Platform administrators who may sign in through OIDC (repeatable). Any other `--platform-admins` login is refused from an OIDC token, so an identity provider that lets users pick their name can't make someone an administrator [env: PANDO_OIDC_ADMINS=]
      --tls-cert <TLS_CERT>
          PEM certificate and key for TLS [env: PANDO_TLS_CERT=]
      --tls-key <TLS_KEY>
          [env: PANDO_TLS_KEY=]
  -h, --help
          Print help

pandod daemon

Per-node daemon: memory + local disk cache shared by every job on the machine

Usage: pandod daemon [OPTIONS] --upstream <UPSTREAM>

Options:
      --listen <LISTEN>
          [default: 127.0.0.1:7422]
      --upstream <UPSTREAM>
          [env: PANDO_UPSTREAM]
      --dir <DIR>
          [default: ./pando-daemon]
      --memory-mb <MEMORY_MB>
          [default: 512]
      --server <SERVER>
          Authoritative server, used to resolve `--prefetch` revisions [env: PANDO_SERVER]
      --prefetch <PREFETCH>
          Warm the cache with this revision (e.g. `main`) at startup
      --prefetch-path <PREFETCH_PATHS>
          Limit prefetch to these paths (repeatable)
      --prefetch-interval <PREFETCH_INTERVAL>
          Re-prefetch periodically (seconds) to follow a moving branch
      --metrics <METRICS>
          Address for `/metrics` (Prometheus), `/healthz` and `/readyz` (no token needed), e.g. 0.0.0.0:9420 [env: PANDO_METRICS=]
      --log-json
          Emit JSON logs [env: PANDO_LOG_JSON=]
      --audit-log <AUDIT_LOG>
          Append an audit trail (JSON lines: who read or changed what, from where, with what result) to this file. Off by default; it grows with every request [env: PANDO_AUDIT_LOG=]
      --audit-retention-days <AUDIT_RETENTION_DAYS>
          Days of audit files to keep (one file per UTC day); 0 keeps all. 365 suits hosted services (docs/deploy.md) [env: PANDO_AUDIT_RETENTION_DAYS=] [default: 365]
      --cors-origin <WEB_ORIGINS>
          Origins allowed to call this server from a browser on another site (repeatable or comma-separated), e.g. your own console's address. Default: none (same origin only); `*` allows any (development only) [env: PANDO_WEB_ORIGINS=] [alias: --web-origins]
      --trust-proxy
          Take the client address (audit log, sign-in rate limits) from `X-Forwarded-For` when the connection comes from a reverse proxy on this host (loopback) [env: PANDO_TRUST_PROXY=]
      --max-request-mb <MAX_REQUEST_MB>
          Largest request message in MiB (an uploaded object batch, a changeset). Default: 64 with --platform, 260 otherwise. Files of 8 MiB and more are uploaded in chunks [env: PANDO_MAX_REQUEST_MB=]
      --max-calls-per-principal <MAX_CALLS_PER_PRINCIPAL>
          Calls one account may have in progress at once (0: no limit). Default: 128 with --platform, no limit otherwise [env: PANDO_MAX_CALLS_PER_PRINCIPAL=]
      --sqlite-synchronous <SQLITE_SYNCHRONOUS>
          SQLite durability: `full` (default) syncs every commit, so an acknowledged land survives power loss; `normal` survives process crashes only, for more throughput on a slow disk [env: PANDO_SQLITE_SYNCHRONOUS=] [default: full] [possible values: full, normal]
      --allow-anonymous
          Accept unauthenticated calls on a non-loopback address. Without tokens or OIDC, pandod otherwise listens only on localhost, so a server is never open to a network by accident [env: PANDO_ALLOW_ANONYMOUS=]
      --request-timeout <REQUEST_TIMEOUT>
          Longest any one call may run, in seconds [env: PANDO_REQUEST_TIMEOUT=] [default: 600]
      --max-in-flight <MAX_IN_FLIGHT>
          Calls in progress at once across all clients; beyond this, calls are refused with Unavailable (clients retry) instead of queueing [env: PANDO_MAX_IN_FLIGHT=] [default: 4096]
      --tokens <TOKENS>
          Comma-separated bearer tokens accepted by this process. Unset = no auth (dev only) [env: PANDO_TOKENS]
      --oidc-issuer <OIDC_ISSUER>
          OpenID Connect issuer URL; enables SSO bearer tokens (JWTs) alongside static tokens [env: PANDO_OIDC_ISSUER=]
      --oidc-audience <OIDC_AUDIENCE>
          Expected `aud` claim of OIDC tokens [env: PANDO_OIDC_AUDIENCE=]
      --oidc-claim <OIDC_CLAIM>
          Claim used as the principal for access rules (sub, email, preferred_username...) [env: PANDO_OIDC_CLAIM=] [default: sub]
      --oidc-require <OIDC_REQUIRE>
          CLAIM=VALUE every OIDC token must carry (repeatable; all must match), e.g. `repository_owner=acme`, `hd=corp.example`, `email_verified=true`. Required for issuers that serve anyone (GitHub Actions, Google) [env: PANDO_OIDC_REQUIRE=]
      --oidc-admin <OIDC_ADMINS>
          Platform administrators who may sign in through OIDC (repeatable). Any other `--platform-admins` login is refused from an OIDC token, so an identity provider that lets users pick their name can't make someone an administrator [env: PANDO_OIDC_ADMINS=]
      --tls-cert <TLS_CERT>
          PEM certificate and key for TLS [env: PANDO_TLS_CERT=]
      --tls-key <TLS_KEY>
          [env: PANDO_TLS_KEY=]
  -h, --help
          Print help

Configuration

The settings most deployments touch, with their environment variables and defaults. Every option of every command is in the server reference, generated from --help.

SettingFlag / environmentDefault
Metadata store--metadata / PANDO_METADATAsqlite://./pando-data/meta.db; also memory, postgres://…
Object store--objects / PANDO_OBJECTSmetadata (in the metadata store); also file:///…, s3://…, gs://…, az://…
Bearer tokens--tokens / PANDO_TOKENSnone; e.g. alice:$(openssl rand -hex 24). Tokens shorter than 16 characters or left as a placeholder (CHANGE-ME) are refused at startup. Without tokens or OIDC, pandod listens only on a loopback address unless --allow-anonymous / PANDO_ALLOW_ANONYMOUS=1 says otherwise
Request limits--request-timeout / PANDO_REQUEST_TIMEOUT, --max-in-flight / PANDO_MAX_IN_FLIGHT600 s per call; 4,096 calls in progress, beyond which calls are refused with Unavailable so clients back off; 256 per connection
Per-account limits--max-calls-per-principal / PANDO_MAX_CALLS_PER_PRINCIPAL, --max-request-mb / PANDO_MAX_REQUEST_MBwith --platform: 128 calls in progress per account (more are refused with ResourceExhausted) and 64 MiB per request message; otherwise no per-account limit and 260 MiB. Files of 8 MiB and more are uploaded in chunks, so 64 MiB never limits file size
Browser access--cors-origin / PANDO_WEB_ORIGINS (repeatable or comma-separated; --web-origins still works)none: same-origin only. List only origins you control, such as your own copy of the web console
Client addresses--trust-proxy / PANDO_TRUST_PROXYoff; on, a connection from loopback (a reverse proxy on the same host) gives the client address in X-Forwarded-For, used in the audit log
Durability--sqlite-synchronous / PANDO_SQLITE_SYNCHRONOUSfull: every SQLite commit is on disk before it is acknowledged, so a land survives power loss. normal survives process crashes only. file:// object stores always sync each object and its directory
Open repositories (platform)--max-open-repos / PANDO_MAX_OPEN_REPOS, --repo-idle-minutes / PANDO_REPO_IDLE_MINUTES256 open at once; one unused for 10 minutes closes. A repository in use is never closed
Audit retention--audit-retention-days / PANDO_AUDIT_RETENTION_DAYS365 days of daily files; 0 keeps all
Single sign-on--oidc-issuer, --oidc-audience, --oidc-claim, --oidc-require CLAIM=VALUE, --oidc-admin LOGINoff; works with Okta, Entra ID, Google, Auth0, Keycloak, GitHub Actions
Browser sign-in (platform)--login-listen, --public-url, --oidc ID=ISSUER, --smtp-url, --mail-from, --turnstile-site-key, --signupoff; see Signing in from a browser
Access rules--acl / PANDO_ACLallow everything
Pre-land check--land-hook URL, --land-hook-token, --land-hook-timeoutoff
TLS--tls-cert, --tls-keyoff
Metrics--metrics / PANDO_METRICSoff; e.g. 0.0.0.0:9420
Feature flagsPANDO_FLAGSall on; e.g. land=off,git-import=off

Run pandod server --help for every option.

Protocol reference

Grovedock speaks gRPC (HTTP/2, and gRPC-Web for browsers). The pando.v1 package is stable: it changes only by adding. Generate a client for any language from the files below; the SDKs wrap the read calls for Python, TypeScript and Go.

Calls to a server that hosts many repositories name one with the x-pando-repo: OWNER/NAME header. Every call takes a bearer token in authorization when the server requires one.

pando.proto

Source

Objects

Tier-agnostic object service. Served identically by server, cache and daemon.

CallRequestResponseWhat it does
GetIdListstream ObjectPayload
Putstream ObjectPayloadPutObjectsResponse
MissingIdListIdListReturns the subset of ids the tier cannot serve.

Changesets

CallRequestResponseWhat it does
CreateCreateChangesetRequestChangesetInfo
GetResolveRequestChangesetInfo

Refs

CallRequestResponseWhat it does
GetRefNameRefValue
CompareAndSwapCasRequestCasResponse
ListEmptyRefList

Land

Server-side rebase + lock-free compare-and-swap of a ref.

CallRequestResponseWhat it does
LandLandRequestLandResponse

History

CallRequestResponseWhat it does
LogLogRequeststream ChangesetInfo
BlameBlameRequestBlameResponse

Diff

CallRequestResponseWhat it does
DiffDiffRequestDiffResponse

MergeBase

CallRequestResponseWhat it does
MergeBaseMergeBaseRequestMergeBaseResponse

Browse

API-first file access: CI and tools read source without any checkout.

CallRequestResponseWhat it does
ListDirBrowseRequestListDirResponse
ReadFileBrowseRequestReadFileResponse
FindFindRequestFindResponse
CheckoutCheckoutRequeststream CheckoutPiecePath-scoped checkout: only files the caller may read. Used by principals with restricted read access, who may not read objects by id.

Access

CallRequestResponseWhat it does
WhoAmIEmptyWhoAmIResponse

Locks

CallRequestResponseWhat it does
LockLockRequestLockInfo
UnlockUnlockRequestEmpty
ListListLocksRequestLockList

pando_platform.proto

Source

Platform

CallRequestResponseWhat it does
ViewerEmptyViewerResponse
GetAccountOrgNameAccount
ListOrganizationsEmptyListOrganizationsResponse
GetOrganizationOrgNameOrganization
CreateOrganizationCreateOrganizationRequestOrganization
ListMembersOrgNameListMembersResponse
SetMemberSetMemberRequestMember
ListTeamsOrgNameListTeamsResponse
SetTeamTeamTeamCreates the team, or replaces its name, description and members.
DeleteTeamTeamRefEmpty
ListRepositoriesListRepositoriesRequestListRepositoriesResponse
GetRepositoryRepositoryRefRepository
CreateRepositoryCreateRepositoryRequestRepository
UpdateRepositoryUpdateRepositoryRequestRepository
ListGrantsRepositoryRefListGrantsResponse
SetGrantSetGrantRequestGrant
ListTokensEmptyListTokensResponse
CreateTokenCreateTokenRequestCreateTokenResponse
RevokeTokenRevokeTokenRequestEmpty
ListWebhooksRepositoryRefListWebhooksResponse
CreateWebhookCreateWebhookRequestCreateWebhookResponse
DeleteWebhookWebhookRefEmpty
ListWebhookDeliveriesWebhookRefListWebhookDeliveriesResponse
PingWebhookWebhookRefWebhookDeliverySends a ping event now.
ListSignupAllowEmptyListSignupAllowResponseWho may create an account by signing in when --signup waitlist: email addresses and @domain entries. Site administrators only.
SetSignupAllowSetSignupAllowRequestEmpty
LinkIdentityLinkIdentityRequestEmptyLets an identity sign in as an existing account, e.g. the first administrator’s email address. Site administrators only.
CreateInvitationCreateInvitationRequestCreateInvitationResponseInvitations to an organisation: a single-use link, valid 7 days, emailed when the server sends mail. Whoever opens it and signs in joins with the role. Organisation administrators.
ListInvitationsOrgNameListInvitationsResponseInvitations not yet accepted, including expired ones.
RevokeInvitationInvitationRefEmpty
GetUsageGetUsageRequestUsagePlans and usage (ADR-035). An organisation’s active agents, people, storage and limits: its administrators and site administrators.
SetLimitsSetLimitsRequestOrgLimitsReplaces an organisation’s limits (not its suspension). Site administrators only.
SetSuspendedSetSuspendedRequestOrgLimitsA suspended organisation can read but not write. Site administrators only.
GetCapabilitiesEmptyCapabilitiesWhat this server offers: anyone may ask.
GetLicenseEmptyLicenseThe support licence this server was started with. Site administrators only.

Issues

CallRequestResponseWhat it does
ListIssuesListIssuesRequestListIssuesResponse
GetIssueIssueRefIssue
CreateIssueCreateIssueRequestIssue
UpdateIssueUpdateIssueRequestIssue
ListIssueCommentsIssueRefListCommentsResponse
AddIssueCommentAddIssueCommentRequestComment

Reviews

CallRequestResponseWhat it does
ListChangeRequestsListChangeRequestsRequestListChangeRequestsResponse
GetChangeRequestChangeRequestRefChangeRequest
CreateChangeRequestCreateChangeRequestRequestChangeRequest
UpdateChangeRequestUpdateChangeRequestRequestChangeRequest
ListCommentsChangeRequestRefListCommentsResponse
AddCommentAddCommentRequestComment
ResolveThreadResolveThreadRequestComment
SubmitReviewSubmitReviewRequestChangeRequest
LandChangeRequestRefLandChangeRequestResponse
RebaseChangeRequestRefChangeRequestRe-applies the change on the target’s tip. Edits that do not overlap what landed since are carried over; otherwise it fails with FAILED_PRECONDITION naming the files that conflict. Approvals made on the old version stop counting when the protection says so.
ListProtectionsRepositoryRefListProtectionsResponse
SetProtectionProtectionProtection
DeleteProtectionProtectionRefEmpty
ReportCheckReportCheckRequestChangeRequest
InboxInboxRequestInboxResponse
ListEventsListEventsRequestListEventsResponse

How landing works

Landing is how a change reaches a branch. In git you push, and if someone pushed first you fetch, rebase and try again. In Grovedock the server does that part: you send a changeset, and the server either lands it or tells you exactly why not.

What happens when you land

pando land (or git push through git-remote-pando, or pando_write over MCP) asks the server to move a branch to include your changeset. There are three outcomes:

OutcomeWhenWhat you see
Landed as isthe branch has not moved since your baselanded c9bdddcbb14c on main
Landed, rebasedthe branch moved, but nothing that landed since your base touched your pathsthe server creates the rebased changeset and lands that; nothing to pull or retry
Refusedsomeone changed one of your paths since your base, a file you changed is locked, a pre-land check said no, or the branch is protectedthe reason and the paths; the branch is untouched

A refusal is never partial: either your whole changeset lands or none of it does.

Why there is no merge queue

Lands to the same branch join a queue inside the server. Whichever land takes the branch’s turn lands everything queued as one batch: each changeset is checked against the tip only along its own paths, rebased in memory if it needs to be, and the branch moves once, by a single compare-and-swap, for the whole batch. A conflicting change fails alone and the rest of its batch lands.

Because the conflict check walks only the changeset’s own paths, its cost doesn’t grow with the number of changes that landed since your base. Measured on one laptop with every agent landing on main (method and limits):

Agentsgit lands/sGrovedock lands/sGrovedock p99Grovedock errors
162.075855 ms0
2000.51,012302 ms0
1,0000.25042.66 s0

Between server replicas the compare-and-swap is still the only coordination, so correctness doesn’t depend on the queue. Landing is tested by deterministic simulation and chaos runs against four invariants, the first being that no acknowledged land is ever lost.

What can stop a land

  • A real conflict. Someone changed one of your paths since your base. Sync, resolve, land again. For change requests, pando cr rebase replays the change on the target’s tip when the two touch different files.
  • A lock. pando lock reserves files that can’t be merged; a land that changes someone else’s locked file is refused with who holds it.
  • A pre-land check. With --land-hook, the server asks your URL before every land and refuses on anything but a 2xx answer (Architecture).
  • Branch protection. A protected branch accepts changes only through a reviewed change request; an agent’s direct write becomes a change request automatically (Organisations, people and access).
  • Access. Access rules can limit who writes which directories and branches.

The full algorithm is in Architecture, and the reasoning in ADR-006 and ADR-017.

Architecture

Grovedock is a client/server source control system designed around one observation: when humans and AI agents merge tens of thousands of changes a day into one monorepo, the expensive operations of git (full clones, client-side history, pushing whole packfiles, merge queues behind a global lock) become the bottleneck. Grovedock moves history, commits, merges and rebases to the server, keeps clients thin, and makes every read an API call.

Topology

Clients fetch file contents through a node daemon and a regional cache from the pandod server; commits, lands and history queries go straight to the server, which stores metadata in SQLite, Postgres or CockroachDB and objects in S3, GCS, Azure or disk.
  • Server: every feature. Stateless apart from its storage adapters, so several replicas can share one Postgres/CockroachDB + object store. A server that hosts many repositories (--platform) keeps its registry in one process and runs as a single node; see Scaling and operations.
  • Cache and daemon: serve only the Objects service. Objects are immutable and content-addressed, so a cached object is correct forever: no invalidation, no coherence protocol. Tiers speak the identical RPC, so a client cannot tell which tier answered.
  • Client: holds no history. A workspace is the materialised tree of one changeset plus an index of file stats.

Data model

ConceptRepresentation
Object idBLAKE3-256 of kind byte ‖ content (domain-separated)
Blobfile bytes; stored as kind ‖ zstd(content)
Treesorted (name, mode, id) entries (prost-encoded, canonical)
Changesetparents, changes (path → blob or delete) relative to first parent, author, message, timestamp. Id = BLAKE3 of the canonical encoding
Refname → changeset id; moved only by compare-and-swap
Derived dataany value defined recursively from parents: root tree, generation number + skew-binary jump pointer

A changeset stores what changed, not a tree. The root tree is derived data, computed once by applying the changes to the parent’s tree. Because trees are Merkle trees, applying k changes touches O(k · depth) tree objects regardless of repository size, and diffing two trees only descends into subtrees whose ids differ.

Feature slices (vertical slicing)

Each feature owns its use case, its transport adapter and its feature flag, and depends only on the domain kernel (pando_core::Repo), never on another slice.

Slice (server)RPCsFlag
objectsGet / Put / Missingobjects
changesetsCreate / Getchangesets
refsGet / CompareAndSwap / Listrefs
landLand (server-side rebase + CAS)land
historyLog, Blamehistory, blame
diffDiffdiff
merge_baseMergeBasemerge-base
browseListDir, ReadFile, Findbrowse, search
(cross-cutting)per-directory write ACLs, per-ref ACLspermissions

Client slices: clone, commit, land, sync/checkout, diff, browse, git (import/export, flags git-import / git-export), mcp; sparse checkout is flagged by sparse-checkout.

Flags are read from PANDO_FLAGS, e.g. PANDO_FLAGS="land=off,git-import=on". All flags default to on. The flag source is a port (FeatureFlags), so a flag service can replace the env adapter.

Ports and adapters (clean architecture)

pando-core      domain + ports (no I/O, no transport)       <- depends on nothing
pando-adapters  memory, SQLite, Postgres, object_store, caches, gRPC upstream, flags, ACL rules
pando-proto     wire protocol (gRPC)
pando-server    slices + composition root (config.rs) + binary
pando               client slices + Remote port + CLI + MCP
PortPurposeAdapters
ObjectStorecontent-addressed bytesmemory, SQLite, Postgres, S3/GCS/Azure/local (object_store), BoundedCache, Tiered, GrpcObjects
ChangesetLogappend-only changeset logmemory, SQLite, Postgres
RefStoreCAS refsmemory, SQLite, Postgres
DerivedStorederived data cachememory, SQLite, Postgres
FeatureFlagsflagsSpecFlags (env)
AccessPolicywrite/ref permissionsRulesPolicy (JSON), AllowAllPolicy
AuthenticatoridentityStaticTokens, OidcAuthenticator (JWT/JWKS), FirstMatch, AllowAll
Remote (client)server accessGrpcRemote

Every storage adapter passes the same contract test suite (crates/pando-adapters/tests/contract.rs; GCS and Azure are not run in CI), including a 16-writer CAS linearizability test, which is what makes adapters substitutable rather than merely type-compatible.

Landing without a global lock

Lands to the same ref queue up inside a server process. Whoever takes the ref’s turn lands everything queued as one batch:

head = refs.get(ref)
for each queued changeset, concurrently:          (reads only; depends on head alone)
  already in head's history?                      -> lands as is
  require parent is an ancestor of head           (O(log n) jump pointers)
  record the entries along each changed path in root(parent)
tip = head
for each queued changeset, in order:              (reads staged, in-memory data)
  if parent == tip: candidate = changeset
  else:
    conflict if, along any of our paths, root(parent) and root(tip) differ
      (unless tip already holds exactly our change)
    candidate = create_changeset(parents=[tip], same changes)   (staged in memory)
  tip = candidate                                 (a conflicting change fails alone)
write everything staged; refs.compare_and_swap(ref, head, tip)
  (lost only to another server replica: redo the batch on its head)

There is no merge queue to wait in and no client retry loop: concurrent lands that touch disjoint paths all succeed. The conflict check walks only the changeset’s own paths, so its cost does not grow with the number of changes that landed since the changeset’s base. One compare-and-swap moves the ref for the whole batch; between server replicas that swap is still the only coordination. On SQLite, one writer thread commits whatever writes have queued as a single transaction (group commit).

Integrity and safety

  • Every object is verified (decompress + hash) when a server accepts it, when a tier receives it from upstream, and when a client materialises it. A tier never caches a corrupt object.
  • Decompression is capped (256 MiB per object) against zip bombs.
  • All paths are validated at every trust boundary (.., .pando, separators, NUL).
  • The client never writes through a symlink: every parent directory is checked with lstat, so a hostile tree cannot escape the working copy (the class of git CVE-2024-32002).
  • Auth tokens are compared in constant time.

Land checks

--land-hook URL makes the server POST every land to a hook before moving the ref, so CI results and required reviews gate main without a merge queue:

{"ref": "main", "changeset": "<hex id>", "parent": "<hex id or null>",
 "principal": "alice", "author": "Alice <alice@example.com>", "message": "...",
 "paths": ["src/lib.rs"], "paths_truncated": false}

A 2xx answer lands the change. Any other answer refuses it, with the response body (first 500 characters) shown to the author; a timeout or unreachable hook also refuses (fail closed). --land-hook-token is sent as Authorization: Bearer …. Approve by changeset id: the id covers the parents and every changed path, so an approval cannot be reused for different content. The hook runs once per land, before any server-side rebase, and a rebase only happens when no path conflicts, so what was approved is what lands. Other integrations implement the LandCheck port.

Virtual checkouts

pando mount DIR exposes one revision as a read-only file system on Linux, macOS and Windows. The platform-independent VirtualTree maps inodes to tree entries and loads each directory on first access; one thin adapter per platform translates operating-system requests:

PlatformMechanismNeeds
LinuxFUSEfuse3
macOSa read-only NFSv3 server on localhost, mounted with the system mount_nfsnothing to install, no kernel extension, no admin rights
Windowsthe Projected File System (ProjFS)the built-in optional feature Client-ProjFS

On any Unix, --serve-only runs the NFS server and prints the mount command instead, for machines where only an administrator may mount. On Windows, ProjFS shows every file at once as a placeholder and fills it in on first read; files already read stay on disk after the mount stops. A file’s size comes from its zstd frame header or large-file manifest, so ls -l downloads no content, and a read of a chunked file fetches only the chunks it overlaps. Pointing --objects at a node daemon makes first reads local.

Warm build nodes

A node daemon can prefetch a revision at startup and on an interval (--prefetch main --prefetch-interval 300), walking the tree through its own tiered store so every tree, file and chunk lands on local disk. Objects already cached are not refetched, so following a moving branch costs only what changed. The prefetched content keeps serving even if the upstream cache is unreachable.

Delta storage

pandod compact re-stores each blob version as a zstd delta against the previous version of the same path (single writer, lease-protected, safe to run while serving). Reads go through DeltaObjects, which reconstructs chains on the blocking pool and verifies the result’s BLAKE3 id, so nothing above the storage layer ever sees a delta. See ADR-014 for the safety argument (base pinning, verification before replace, bounded chains).

SQLite returns freed pages to the filesystem with VACUUM, which compact runs at the end and which temporarily needs free disk space roughly equal to the database size.

Large files

Files of 8 MiB or more are split with FastCDC (content-defined, ~1 MiB average chunks) into ordinary blob objects and stored under the file’s own id as a manifest (0x03 | size | [(chunk id, length)]). The file id is still BLAKE3 of the whole content, so trees, diffs and the git bridge are unaware of chunking. Editing part of a large file re-uploads only the chunks it touches, and identical chunks dedupe across files and versions.

Nothing holds a whole large file in memory: pando status hashes files as streams, pando commit chunks on a worker thread and uploads through a bounded channel, and checkout reassembles into a temporary file (verifying every chunk and the whole-file id) before an atomic rename. The server verifies an uploaded manifest by streaming its chunks against the id; caches can only check structure, and clients verify end to end. Measured on a 1 GiB file (v3.5.0, macOS arm64, maximum resident set size): pando commit peaks at 163 MB, pando clone at 139 MB.

Performance notes

  • BLAKE3 (SIMD) for ids, zstd level 3 for storage, mimalloc in both binaries.
  • Object fetches are batched (512-1024 ids per round trip) and pipelined (8 batches in flight); servers stream responses with bounded buffers (backpressure).
  • pando status uses size+mtime against the index and hashes only candidates, in parallel on all cores; the walker is ripgrep’s (ignore) and honours .gitignore and .pandoignore (and the deprecated .sapignore).
  • SQLite runs in WAL mode with one writer and a pool of readers; Postgres batches use UNNEST/ANY for single-round-trip multi-row operations.

Architecture Decision Records

Each record states the decision, the alternatives considered and why they lost.

ADR-001: Rust for every component

Decision. Server, tiers and client are written in Rust (edition 2024). Alternatives. Go (fast to write, GC pauses on large heaps, larger memory per cached object); C++ (performance parity, memory-safety risk in a network-facing parser-heavy system); mixed Go server + Rust client (two toolchains, duplicated domain code). Why. Predictable latency with no GC on hot cache paths, memory safety for code that parses untrusted trees and streams, one domain crate shared by server and client.

ADR-002: BLAKE3 content addressing

Decision. Object ids are BLAKE3-256 over kind ‖ content. Alternatives. SHA-1 (git; broken collision resistance, SHAttered 2017); SHA-256 (git’s migration target; 3-6x slower than BLAKE3 on modern CPUs without SHA extensions). Why. Cryptographic strength with SIMD throughput of several GB/s per core; domain separation byte prevents blob/tree/changeset id confusion.

ADR-003: zstd level 3 for stored objects, hash over uncompressed bytes

Decision. Store kind ‖ zstd(content); hash the uncompressed content. Alternatives. zlib (git; slower and worse ratio); LZ4 (faster, ~30% worse ratio); higher zstd levels (marginal ratio, much slower writes). Why. Best ratio/speed balance; hashing raw content keeps ids stable if compression changes; every tier ships stored bytes as-is with no recompression.

ADR-004: Changesets list changes; trees are derived data

Decision. A changeset records file changes relative to its first parent. Root trees are computed by the generic derived-data mechanism. Alternatives. git-style commits pointing at a full tree. Why. Changeset creation cost scales with change size; derived data can be computed eagerly or lazily and re-defined without touching clients.

ADR-005: gRPC (tonic) over HTTP/2

Decision. One protobuf schema, gRPC transport, identical Objects service on every tier. Alternatives. git smart-HTTP protocol (negotiates on what the client has, packfiles are monolithic and not resumable); REST/JSON (no streaming, larger payloads). Why. Streaming with backpressure, multiplexing, codegen for every language (so CI tools in Python/Go/TS can call the API), reflection and health checking out of the box.

ADR-006: Lock-free landing via CAS + server-side rebase

Decision. Refs move only by compare-and-swap; the server rebases on conflict-free races. Alternatives. Merge queue with a global lock (serialises all merges; large monorepo operators report removing exactly this); client-side rebase + push retry loop (git; each retry is a fetch). Why. Throughput scales with disjointness of changes, not with a lock.

ADR-007: Ports and adapters with contract tests

Decision. Every infrastructure dependency is a trait in pando-core::ports; each adapter must pass a shared contract suite. Why. Infrastructure is replaceable (SQLite → Postgres → CockroachDB; disk → S3) without touching features; the contract suite turns “implements the trait” into “behaves identically”.

ADR-008: SQLite default, Postgres for HA, object_store for blobs

Decision. Default deployment is a single binary with SQLite (WAL). Postgres (and Postgres-wire databases such as CockroachDB) for multi-replica deployments. Objects may live in the metadata DB or in any object_store backend (S3, GCS, Azure, local). Alternatives. Spanner only (not available to most users); FoundationDB (excellent, but operationally rare); RocksDB (single-node only, no SQL ops tooling). Why. Zero-setup start, standard managed services for scale, one adapter for every cloud.

ADR-009: quick_cache for tier memory

Decision. Byte-weighted, sharded cache with CLOCK-Pro-style eviction. Alternatives. lru (global lock, scan-sensitive); moka (excellent, heavier, async eviction listeners not needed here). Why. Low per-op overhead under high concurrency; resists one-off scans (CI checkouts) evicting the hot set.

ADR-010: git fast-export for import, fast-import for export

Decision. Stream git history through git fast-export/fast-import. Alternatives. libgit2 / gitoxide object walking (requires re-diffing every commit). Why. fast-export already emits changes relative to the first parent, which is the changeset model. Single process, streaming, battle-tested by git itself. Import is deterministic, so re-imports are idempotent and incremental.

ADR-011: Feature flags on every slice

Decision. Each slice checks a flag at its entry; flags come from a port. Why. Operators can disable a feature (e.g. land during an incident) without a deploy; new features can ship dark.

ADR-012: MCP server via the official Rust SDK

Decision. pando mcp exposes read/commit/land tools over stdio using rmcp. Why. Agent-agnostic integration: any MCP client can drive Grovedock without bespoke glue. The official SDK tracks the protocol specification.

ADR-015: Path-scoped reads without object-level ACLs

Decision. A principal with restricted read access may not read objects by id at all. The server serves them path-filtered views: a streaming Checkout, filtered ListDir/Find, diffs limited to readable paths, and redacted changesets (marked redacted, so clients know they cannot hash-verify them). pando detects the scope via WhoAmI and switches to these APIs automatically. Alternatives. Per-object ACLs (an object can be reachable from many paths and revisions, so every read would need a reverse index); per-principal projected trees (new tree ids per principal break content addressing and caching). Why. Nothing a restricted reader receives names content they cannot read (unreadable directory ids are blanked in listings), the content-addressed core stays untouched, and full-read principals keep the fast object paths.

ADR-013: v1 permissions scope

Decision. v1 enforces per-directory write permissions and per-ref permissions. Read access is repository-wide. Why. Content-addressed objects are reachable through parent trees, so path-scoped reads require object-level authorization (tracked on the roadmap). Shipping write protection now covers the OWNERS-style use case without a leaky read model.

Update (2026-10-18, v3.5.0). Superseded for reads by ADR-015: read access can be scoped to paths.

ADR-014: Delta storage via an offline, single-writer compaction job

Decision. Blob versions are delta-encoded in storage by pandod compact: zstd “patch-from” frames (the previous version of the same path as a raw-content prefix, with long-distance matching), chain depth ≤ 50 (git’s default pack.depth). Trees are delta-encoded the same way; they dominate storage because every changeset writes a new root. Live servers only read deltas: the DeltaObjects decorator reconstructs and hash-verifies every object before it leaves the storage layer, so the wire protocol, tiers and clients are unchanged.

Alternatives.

  • Delta-encode at write time on every server: concurrent replicas can create delta cycles (A→B and B→A) that make both objects unreadable. Rejected: data loss is unacceptable.
  • git-style packfiles: repacking requires global maintenance windows and breaks the “every request is one immutable object” property that makes caching trivial.
  • zstd dictionaries trained per repository: ~2x, versus 20x+ for per-path deltas.

Why this is safe.

  • One compactor at a time (lease = CAS on a reserved system ref).
  • A base is pinned before anything is stored against it, and pinned objects are never converted; chains never change after something depends on them (depth headers exact, no cycles). A chain check guards against cycles as a second line of defence.
  • Each delta is decoded and compared with the original in memory before the atomic replace; readers re-verify the BLAKE3 id on every reconstruction.
  • Readers accept chains up to 256 frames (5x what compaction creates), so an unexpected chain can only slow a read, never make data unreadable.

Measured. Level 3 + long-distance matching: a one-line edit to a 22 KB source file deltas to 45 bytes (vs 6.9 KB for standalone zstd) at the same speed as plain level 3; higher levels cost 50-500x more time for <10% gain. On git/git (82,443 commits) compaction converts 312k object versions (159k of them trees) in 15 minutes: storage 4.6 GB -> 369 MB (git’s own pack: 333 MB), and all 24,380 mainline commits read back with identical content.

ADR-016: Pre-land checks as a fail-closed webhook, run before rebase

Decision. Landing calls a LandCheck port once, after authorization and before the rebase/compare-and-swap loop. The shipped adapter POSTs the proposal to a URL; only a 2xx answer lands, and timeouts or errors refuse the land. Why. A webhook lets any CI system or review tool gate landing without Grovedock owning a review database. Checking before rebase keeps the hook off the contended retry path, and is sound because a server-side rebase only happens when no changed path conflicts, so the approved file changes are exactly what lands. Failing closed means an outage of the gate cannot let unreviewed code onto a protected branch.

ADR-017: Batch concurrent lands per ref instead of retrying compare-and-swap

Decision. Within one server process, lands to the same ref join a queue. The land that takes the ref’s turn rebases everything queued as one chain over an in-memory overlay, writes the chain and advances the ref with a single compare-and-swap. Checks that depend only on the starting head run concurrently; the conflict check compares base and tip only along the changeset’s own paths. Why. The original loop (read head, rebase, compare-and-swap, retry on loss) is lock-free but does quadratic work under contention: with N landers, N-1 lose each swap and rebase again. Measured on one machine it fell from 105 lands/s at 16 agents to 10 lands/s with 237 “too contended” errors at 200 agents. Batched, the same machine sustains 758-1,012 lands/s at 16-200 agents and 504 at 1,000, with no errors. The compare-and-swap stays as the arbiter between replicas, so correctness does not depend on the queue, and a conflicting change fails without affecting the rest of its batch. A randomized test checks the path-scoped conflict check against the previous full-diff rule.

ADR-018: Test landing by deterministic simulation; write in dependency order

Decision. The landing protocol is tested by a seeded simulation: several replicas share a store that fails, delays and loses replies, replicas crash and restart, and clients land changes, some conflicting, on random replicas. One seed drives a single-threaded runtime with a paused clock, so any failure replays exactly. After each run four invariants are checked: no change lands twice, every acknowledged land is in history, the tree equals replaying history, and no change lands over a conflicting change it never saw. It runs 200 seeds in every CI build and 50,000 new seeds nightly. Why. Landing is where a bug loses work, and its failure cases (a reply lost after a write was applied, a crash halfway through a batch) are hard to reach with example tests. The first runs found three real bugs: a batch failed entirely on one transient storage error; a flush that failed halfway could leave a record naming a tree or changeset that was never written, which a later land recreating the same changeset would trust; and a change rebased by a land whose reply was lost was applied a second time when retried. The fixes: batches retry storage errors from the current head; staged writes go out in dependency order (objects, then changesets, then derived records); and each rebased change records the id it landed as, so a retry reports that landing instead of repeating it. Scope: this simulates the landing protocol over in-memory stores, not the whole system (network, disk and SQLite are real code paths it does not exercise).

Addendum (2.1.3). A black-box chaos test against real replicas on Postgres (bench/chaos.sh) found that the landed-as record is not enough: two replicas landing the same retried change both write it, and the replica that loses the race can overwrite the winner’s record, so a later retry lands the change again. The fix that holds without bookkeeping: a change rebased onto a tip that already holds all of its changes is reported as landed at that tip instead of becoming an empty commit, which is also what git rebase does with commits that become empty. The landed-as record stays as a fast path. The simulation’s clients now give up waiting and retry on other replicas, and it catches this race at the second seed when the fix is removed.

ADR-019: File locks are system refs moved by compare-and-swap

Decision. A lock on path is the system ref _lock/<hash of path>, pointing at a small record object (path, owner, time). Taking a lock is a compare-and-swap from absent (or released) to the record; releasing swaps it to a fixed “unlocked” id, since refs cannot be deleted. Landing lists the lock refs once and refuses changes to paths locked by someone else. Locking and forced unlocking need write access to the path. Why. Teams with binary assets (games, film, hardware) need exclusive checkout, which is why many stay on Perforce; Git LFS added a locking API for the same reason. Refs already give an atomic, replicated compare-and-swap in every backend, so locks need no new storage port, and two people can never hold one lock, across server replicas too. User ref names cannot contain _, so clients cannot forge or move a lock through the refs API. The check runs when a land arrives, as Git LFS checks at push: a lock taken while a land is already being applied does not stop that land.

ADR-020: Virtual checkouts use NFS on macOS and ProjFS on Windows

Decision. One platform-independent VirtualTree serves every platform. Linux mounts it with FUSE. macOS mounts a read-only NFSv3 server that pando runs on localhost (the nfsserve crate) with the system’s own mount_nfs. Windows projects it with the Projected File System, whose library is loaded at run time. Why. On macOS, FUSE needs macFUSE’s kernel extension, which users must approve in recovery settings; its newer FSKit backend is opt-in and lacks features. A localhost NFS server needs nothing installed, and it is where Meta’s EdenFS moved as Apple deprecated kernel extensions; XetHub mounts multi-terabyte repositories the same way. NFS never says when the client drops a file handle, so every inode stays valid for the life of the mount, which VirtualTree already guarantees. On Windows, ProjFS is built in (the mechanism behind VFS for Git and EdenFS there) and needs no driver. Loading its library at run time keeps pando starting on machines where the optional feature is off. Both mounts are tested in CI against the real kernels: a stock macOS runner mounts without admin rights, and a Windows runner projects with ProjFS, each compared file by file with pando clone. On managed Macs whose policy forbids user mounts, --serve-only prints the command for an administrator.

ADR-021: The web console and the platform contract

Decision. The console is a static React 19 app (React Compiler, React Aria, TanStack Router and Query, Connect-ES over gRPC-Web, Shiki in a worker) that talks to pandod directly. Hosting features (organisations, repositories, accounts, change requests, reviews, protected branches) are specified first as pando_platform.proto, implemented by an in-browser demo, then by the server. Repository-scoped calls name their repository in the x-pando-repo header; a call without it reaches the default repository. Why. Solid, Svelte and Leptos were compared. Rendering cost is bounded by what is on screen, not by the framework, so the deciding factors were accessible components (React Aria has the most complete keyboard and screen-reader behaviour) and contributors. A static app with no backend of its own has nothing to operate and can be hosted anywhere, including for free. Contract first lets the whole interface be built, tested and demonstrated before the server work lands, and the header keeps every existing client working when a server starts hosting several repositories. Servers without the platform service answer Unimplemented, and the console shows their one repository. The cost is about 107 KB of gzipped JavaScript for React, the router and the gRPC-Web runtime before any interface code; the build enforces a budget for the first screen.

ADR-022: Hosting many repositories by dispatching to one set of services per repository

Decision. A server started with --platform keeps a small registry of repositories and opens each one’s storage from templates ({repo}, {repo_id}) the first time it is used. A layer inside authentication reads the x-pando-repo header and hands the call to that repository’s own services, built by the same authority_routes as a single-repository server. Calls without the header reach the default repository. Why. Nothing in the features had to change: every repository gets the code that is already tested, simulated and chaos-tested, including its own landing queues, so a busy repository cannot slow another. Separate storage per repository is the isolation a hosted, multi-tenant service needs (backup, move, delete or place one repository on its own), and the content-addressed object store can still be shared by pointing every template at one bucket. Putting the dispatch inside authentication means the principal is known before any repository is opened, and the header keeps every existing client working. The cost is one open database per active repository, which is why repositories open lazily.

ADR-023: Accounts, roles and tokens live in the platform registry

Decision. A platform server keeps accounts, organisations, members, teams, grants and access tokens in the registry database next to the list of repositories. Every call that names a repository is checked in the hosting layer, after authentication and before the repository opens: methods that only read need the Read role, everything else needs Write. The read list is explicit, so a method added later needs Write until someone decides otherwise. A caller who may not read a repository gets NotFound, the same answer as for a repository that doesn’t exist.

A role is the highest of organisation membership, direct grants and team grants; internal and public repositories add Read for every account. Agents act with their owner’s role unless given their own. Tokens are 32 random bytes, prefixed pando_ for secret scanners, stored as a BLAKE3 hash, scoped to repositories and a highest role, and expire within a year. A token made with a token can’t reach further than the one used to make it.

Platform tokens are checked before the server’s other authentication; anyone who signs in another way gets an account, with no role until an administrator gives one. pandod token writes the first token straight into the registry, so a server can run on platform tokens alone and nobody has to configure a shared secret.

Why. GitHub and Bitbucket model access the same way (organisation base role, repository collaborators, teams), so administrators already know it. Keeping it in the registry means one small database to back up per server, and the check needs no network call. Treating unknown methods as writes, and unreadable repositories as missing, fails safe.

Trade-offs. The check reads SQLite on every repository call: a few indexed lookups under one mutex, measured well below the cost of a land; a cache comes if profiles show it. Servers with no authentication at all stay open, for local development, as before. Repositories created in phase 1 become private to their organisation’s members: administrators add members after upgrading.

Update (2026-10-18, v3.5.0). The cache arrived with ADR-029: access answers are cached for up to five seconds and dropped by every write.

ADR-024: Change requests land as one rebased changeset; protection is an access policy

Decision. A change request proposes a branch (the source) for a target. It records the source’s merge base with the target, follows the source branch as it moves, and lands as one changeset: everything the source changed since its base, landed through the same server-side, per-file rebase as any other land. Reviews, comments, checks, protections and an activity log live in the platform registry beside accounts and grants.

Branch protection is an access policy wrapped around each repository’s own: updating a protected branch is allowed only to those it lets bypass review (and to agents when it lets agents land directly). Change requests land through a separate land path without that wrapper, so their review is the gate. Checks are recorded per version of the source, so a new push starts with none; approvals made on an older version stop counting when the protection says so.

Why. Landing a single rebased changeset needs no new merge machinery and inherits the landing guarantees already tested by simulation and chaos runs. Putting protection in the access policy covers every way to move a branch (land and compare-and-swap) with one check, and the refusal says how to proceed.

Trade-offs. The landed changeset is a squash: the branch’s own changesets are not replayed one by one. Conflicts are computed from two tree diffs on each read of an open change request, proportional to what changed; a cache comes if listings of hundreds of open requests get slow. An agent’s land on a protected branch is refused rather than turned into a change request automatically; pando cr create is one command away.

Update (2026-10-18, v3.5.0). pando land still refuses; writes through the remote MCP tool pando_write open a change request on a review/ branch instead (ADR-026).

ADR-025: Webhooks are a signed, leased delivery queue in the registry

Decision. Events are written to a deliveries table in the platform registry, one row per interested webhook, and a worker in every server process claims due rows with a single UPDATE … RETURNING that pushes their next attempt a minute ahead (a lease), posts them, and records the outcome; failures retry with exponential backoff, eight attempts in all. Bodies are signed with HMAC-SHA256 in GitHub’s sha256= format. Push events come from a wrapper around each hosted repository’s ref store, the one place every branch move passes through.

Why. A queue in the database survives restarts and needs no broker to run or pay for. The lease makes any number of replicas safe without coordination. GitHub’s signature format means receivers reuse the code they already have. Wrapping the ref store catches lands, pushes and rebases alike, including ways to move a branch added later.

Trade-offs. Addresses are checked when resolved, not pinned for the connection, so a DNS answer that changes in between could reach an address that was refused; webhooks are set up by repository administrators, and link-local addresses are refused at both points that matter in practice. Secrets are stored as given, because signing needs them.

ADR-026: Remote MCP is a stateless gateway that passes the caller’s token through

Decision. pando mcp --http serves Model Context Protocol tools for a whole server over streamable HTTP, statelessly (MCP 2026-07-28; no sessions). Each request’s bearer token is forwarded to the server for every call the tool makes, so the gateway holds no credentials and an agent can do exactly what its token allows. Writing files lands them as one changeset on a branch; on a protected branch the change goes to a review/ branch and a change request opens. An agent’s change request asks the person the agent answers to for review.

Why. Agents shouldn’t need a clone, a workspace or a git client to change code: a URL and a token is the whole setup, which is what makes Grovedock usable from hosted agents. Passing the token through avoids a confused deputy and reuses every permission check, token limit and audit record the server already has. Statelessness matches the server: gateways scale horizontally behind any load balancer and need nothing to coordinate.

Trade-offs. A gateway is a second process to run (it can share the server’s host or container). Changesets with no author are attributed to the authenticated caller; a client can still claim another author name, as in git, which a later change will restrict on platform servers.

ADR-027: Issues share numbering with change requests and close from “fixes #N”

Decision. Issues live in the platform registry beside change requests and take numbers from the same per-repository sequence, so #12 names one thing. Anyone who can read a repository may open an issue and comment; assigning, labelling and closing someone else’s issue take triage, as on GitHub. A change request whose title or description says “fixes”, “closes” or “resolves” #N closes that issue as done when it lands. Issues assigned to you are in your inbox, and issue webhook events (opened, assigned, closed, reopened) let an agent runner start work the moment an issue is assigned to an agent.

Why. The agent loop (assign, work, propose, review, land, done) needs no glue: an agent reads its inbox over MCP, opens a change request that fixes the issue, and landing it closes the loop. Shared numbers and GitHub’s keywords keep habits and links working for people moving from GitHub.

Trade-offs. No milestones, projects or issue templates yet; labels are free text. Which change requests fix an issue is worked out when the issue is read, by scanning the repository’s change requests, which a link table will replace if repositories reach many thousands of them.

ADR-028: Migration imports history with git and the rest through GitHub’s API

Decision. pando migrate FROM --to SERVER/OWNER/NAME mirror-clones the source with git, imports the default branch first and then every other branch with the existing incremental importer, and, when the source is on GitHub (or --github-api names a GitHub Enterprise Server), brings open pull requests across as change requests from github/pr-N branches and issues with their comments, labels and state. It finishes by printing the pando mirror command that keeps the old host current.

Why. Moving must be one command and reversible, or nobody tries it: git already knows how to fetch everything, the importer is already deterministic and incremental, and the mirror turns “migrate” into “try it side by side”. Pull requests and issues are what teams lose most often in moves, so they come too.

Trade-offs. Commit ids change. Imported issues and change requests are posted by the person running the migration, with the original author named and linked, because Grovedock accounts don’t map to GitHub logins. Only open pull requests move; review comments on them don’t yet. Each branch is imported separately, so a repository with very many branches takes longer.

ADR-029: Access answers are cached per process and dropped by every write

Decision. The reads every repository call makes against the platform registry (the repository’s record, a caller’s role, an agent’s owner, team membership, branch protection, and whether any webhook wants pushes) are cached in memory. Every write that could change an answer (membership, grants, teams, repositories, protections, webhooks, new accounts) clears the cache at once; entries also expire after five seconds, which bounds how long a write on another replica can go unseen. Push events are prepared off the landing path, and only when a webhook wants them.

Why. Measured with 200 agents landing on one branch (release build, one laptop): through a hosted repository with access checks, throughput fell from about 1,140 to 770–960 changes a second and p99 latency rose from 0.3 s to 2.8–3.8 s. With the cache and the push change it is 1,010–1,150 changes a second with p99 0.22–0.25 s, the same as a server without the platform (also with a non-administrator token, zero errors).

Trade-offs. A revoked grant is enforced at once on the replica that made the change and within five seconds on others. Webhook lists in the console are read uncached, so delivery status stays current.

ADR-030: Reviews and checks come from someone else; platform servers are single-node

Decision. A change request’s author, the person an agent author answers to (followed up any chain of agents), and every agent that person answers for count as one person. That person can’t approve or request changes on the change, their approvals don’t count towards a protected branch, and they can’t report the change’s checks. Checks are otherwise reported by anyone with Write, so CI runs as its own account or agent. --oidc-require CLAIM=VALUE binds single sign-on to a tenant, and pandod refuses to start with a multi-tenant issuer (GitHub Actions, Google) without such a rule; single sign-on can’t claim a platform administrator’s login (unless listed with --oidc-admin) or an agent/ or bot/ login. Paths named .git or .pando in any case or Windows spelling are invalid everywhere. A platform server is one process over one SQLite registry, backed up with pandod backup and restored by copying back while stopped.

Why. A production review showed a single developer token could ship to a protected branch: mint an agent, approve as the agent, report build: passed. Agents already record their owner, so the rule needs no new state. OIDC issuers such as GitHub Actions sign tokens for anyone with a workflow, so validating iss/aud alone admits the internet. A tracked .git/hooks file runs code on the next git command in a colocated checkout (git’s own CVE-2014-9390).

Trade-offs. An agent’s work needs a human other than its owner to approve it, and a team of one can’t use required approvals. Checks aren’t bound to a named reporter yet; naming one per required check (build@bot/ci) is the next step if CI identities need pinning. Existing history with .git or .pando paths can’t be read or checked out. Scaling a platform server out needs a shared registry (Postgres), which doesn’t exist yet.

ADR-031: Sign-in issues platform tokens

Decision. Browser sign-in is a separate listener inside pandod (--login-listen) that turns a verified identity into an ordinary platform token (“browser session”, 30 days, Admin ceiling, every repository), handed to the console in the URL fragment. Providers are generic OpenID Connect (authorization code, PKCE S256, state, nonce), with Google first; email links are the provider-free alternative. Identities are keyed by provider and subject in identities, never by email, and a new identity is never attached to an existing account by matching email; a site administrator links one explicitly (LinkIdentity). New accounts need a verified email and the signup policy’s consent (--signup waitlist by default, signup_allow by address or @domain). Sign-in state, nonces and email links are stored only as hashes, for one use.

Why. Every RPC already authorises platform tokens (ADR-023), so sign-in adds no second session mechanism: revoking the token signs the browser out, and the token list shows browser sessions next to the rest. Generic OIDC covers Google, Okta, Entra and Keycloak with one reviewed path (ID tokens are checked by the same code as SSO bearer tokens); not depending on GitHub matters because Grovedock competes with it. A fragment never reaches servers or logs.

Trade-offs. The console holds a bearer token in browser storage rather than an HttpOnly cookie, so cross-site scripting in the console could read it; a strict CSP on the console, the 30-day lifetime and revocation limit that. Providers that omit email_verified (some Entra setups) can’t create accounts. Request limits are per process.

ADR-032: Multi-tenant mode

Decision. One open-source switch, pandod server --multi-tenant, makes a platform server safe for strangers to share. Internal visibility means “members of the owning organisation” instead of “everyone with an account”; members and grants must name existing accounts; any person may create a few organisations (--max-orgs-per-account, default 3) in a namespace shared with logins, with service names reserved. It is one flag on the shared access rules (Access), read where each decision is made; no new types. Two fixes found while preparing it apply to every platform server, with or without the flag: a repository call that names no repository is refused (it used to reach the server’s own repository with no access check), and webhooks refuse loopback, private networks and cloud metadata addresses unless --webhook-allow-private (refused with --multi-tenant), checking the address at save and delivery and connecting only to the checked address.

Why. On a company’s own server everyone is a colleague: “internal” meaning “anyone here”, adding a login before its owner signs in, and webhooks to the internal network are features. On a shared server they leak one tenant’s code to another, let a login be claimed in advance, and turn webhooks into a probe of the host’s network. Keeping both behaviours behind one flag leaves self-hosted servers unchanged and keeps the hosted service on the open-source code.

Trade-offs. Without invitations (next milestone), someone must sign in before they can be added. The organisation cap is counted before the insert, so two creations at the same moment can exceed it by one. A webhook host that doesn’t resolve is refused when saved. Servers that relied on the header-less default repository, or on webhooks to private networks, must move the repository into the registry or start with --webhook-allow-private.

Update (2026-10-18, v3.5.0). Invitations shipped (ADR-033), so a person can be invited before they first sign in.

ADR-033: CLI sign-in, invitations and the terms confirmation

Decision. The CLI signs in with the OAuth device flow (RFC 8628) on the sign-in listener: pando login gets a device code (32 random bytes, stored as a BLAKE3 hash, 10 minutes) and a user code from 20 consonants (BCDF-GHJK); the person signs in with any provider and approves on a server-rendered page; the polling CLI receives a 90-day platform token (“cli on HOST”, Admin ceiling) once, and the request is deleted. Polling sooner than the interval gets slow_down. The token is kept in ~/.config/pando/credentials.json (mode 0600, per server origin) and used after --token and PANDO_TOKEN; pando logout revokes it with RevokeToken, using the token id that Viewer now returns. Invitations are rows in invitations holding a hashed single-use token, valid 7 days, created by organisation administrators and accepted at /login/invite/TOKEN after signing in; acceptance goes through directory::add_member, the same path as SetMember, so checks on joining (seat limits) apply to both. An invitation lets its holder create an account even when the waitlist would not. Finally, a first sign-in no longer creates an account at once: the person must confirm they are 18 or older and accept the terms (linked from --terms-url and --privacy-url), and the time is stored on the account.

The three steps that follow a sign-in (accept the terms, approve the CLI, join) are pages with one form. The listener keeps no browser session, so the form carries a random secret whose hash keys a short-lived, single-use server-side record (who signed in, what comes next), and the secret must also arrive in an HttpOnly, SameSite=Strict cookie set with the page: another site can neither read the secret nor make the browser send the cookie.

Why. The device flow needs no local web server or pasted tokens, works over SSH, and reuses every sign-in provider. Invitations are what a shared server needs to add someone who has never signed in. The link, not the address, is the authority (as in most SaaS): people are often invited at one address and sign in with another (a personal Google account, an alias), and a link that only works for the invited address breaks that while adding little, since whoever reads the mail could sign up with that address anyway. The page shows the invited address so the person accepting sees who it was meant for. Legal review asked for an explicit age and terms confirmation before any account exists.

Trade-offs. A device code can be phished (an attacker sends someone a link to approve the attacker’s code); the approve page names the client, host and code and says to approve only your own. The pending page records live in memory, so a restart loses pages already shown (people sign in again). Every sign-in for the CLI or an invitation goes through the provider again, even if the browser has a console session. A forwarded invitation lets the forwardee join; administrators can withdraw invitations and remove members.

ADR-034: Operating a hosted service: health, durability, limits, takedown

Decision. A platform server is run as a service for outside customers, so it is operable without reading its code. --metrics ADDR also serves /healthz and /readyz (no token); gRPC health needs no token and follows readiness, which checks the registry and a synced write to each local data directory every 5 seconds and turns off when shutdown starts. SIGTERM drains calls, then the webhook worker. SQLite runs with synchronous=FULL (registry and repository databases; --sqlite-synchronous normal opts out) and file:// object stores fsync each object and its directory; an object whose stored bytes fail their hash is replaced by a put of the right bytes. The registry has a schema_version table and an append-only migration list, and its lock recovers from a panic. Platform defaults bound one account: 128 calls in progress and 64 MiB per request message; listings scan at most a fixed number of rows and page; at most 256 repositories stay open, idle ones close, and one in use (a call, a review, a streamed response holds a reference) never closes. The operator takes a repository down with pandod repo disable (all calls refused, nothing deleted) and removes one with pandod repo delete; both write the registry directly, which the server reads through its 5-second cache. Webhooks are at least once, two in flight per hook, 30 s each, pruned after 30 days. Metric labels are only methods the protocol defines. Browser origins come only from --cors-origin; internal errors reach clients as an id. Audit files rotate daily and are kept --audit-retention-days (365).

Why. Each default above is what a load balancer, an uptime checker, a regulator or a power cut assumes. Doing them in the open-source server keeps the hosted service on the same code as self-hosted ones, and every change is a default with a flag, so small self-hosted servers can loosen it.

Trade-offs. FULL costs one more sync per group commit; measured with the land_load example (64 agents, debug build, macOS APFS SSD) it was within run-to-run noise (FULL 269 and 235 lands/s, NORMAL 219 and 231). Readiness checks only local directories and the registry, not Postgres or object stores. repo disable from another process takes up to 5 seconds. Listing figures for closed repositories come from the registry as last computed. The registry still runs its SQL on async workers; moving it to a blocking thread is left for when it shows in profiles. There is no object garbage collection, so a purged secret is removed by deleting the repository.

ADR-035: Plans are limits in the open-source server; billing stays outside it

Decision. The registry keeps each organisation’s limits (org_limits: seats, active agents, storage bytes, private repositories, a suspension flag and a plan label; NULL is unlimited) and its usage (usage_agents per organisation, month and agent; repo_sizes; each month’s storage peak in usage_storage). Organisations without a row take --default-org-limits. Limits are enforced only with --multi-tenant, in HostingLayer before a write (suspension, storage, and a new agent’s first land beyond the active-agent limit) and in the SetMember and CreateRepository/UpdateRepository calls (seats, private repositories). An agent becomes active after HostingLayer sees its Land succeed (status OK and a landed id) or its CompareAndSwap swap (status OK and swapped): a refused land is a gRPC error and a lost swap is status OK with swapped: false, so neither counts. Landing a change request (Reviews/Land) makes active both the agent who lands it and the agent who wrote it, under the same limit. An in-process set remembers the agents already recorded this month, so only an agent’s first land in a month writes. GetUsage, SetLimits (absolute values; site administrators), SetSuspended and GetCapabilities (--billing-url) are the only interface a billing service needs; prices, plan values and the payment provider live outside this repository. Support licences are Ed25519-signed JSON checked offline against a key compiled into pandod, and only reported, never enforced.

Why. A hosted service needs plans to mean something, and the open-source server is where writes happen; doing it with plain rows and absolute SetLimits calls keeps the billing service stateless toward pandod (replayed or reordered payment events set the same values) and lets any operator run their own billing, or none. Counting only lands that succeeded matches what is sold (“an agent that landed a change this month”). Enforcing only with --multi-tenant keeps self-hosted servers unlimited without configuration. A support licence that switched features off would punish customers for a missed renewal.

Trade-offs. The active-agent limit is checked before a land and recorded after it, so new agents landing at the same moment can pass it by a few; storage is measured hourly, so it can be passed by an hour of pushes. Postgres repository metadata is not measured. The seen-set is per process: another replica at most records an agent once more, which the primary key absorbs.

ADR-036: pandod serves the console’s files on its own address

Decision. Releases attach the console’s build as pando-console-vVERSION.tar.gz. pandod server --console-dir DIR serves it on the gRPC address through an outermost layer that takes every GET and HEAD (gRPC and gRPC-Web calls are always POSTs) to tower_http’s ServeDir, with index.html for paths that aren’t files. The layer sits outside authentication, metering and the audit log: the files hold no data. assets/ (Vite’s content-hashed names) is cached immutably for a year, everything else (index.html above all) is no-cache; every file gets a CSP header adding frame-ancestors 'none' and connect-src 'self' to the build’s <meta> policy, plus nosniff, X-Frame-Options, Referrer-Policy and Cross-Origin-Opener-Policy.

Why. The console only talks to pandod, and pandod already speaks HTTP/1.1 for gRPC-Web, so one process and one origin serve both: no CORS, no second web server, and connect-src 'self' keeps a token in the page from being sent elsewhere.

Trade-offs. No compression and no HSTS: a proxy in front (Caddy in the hosted service) adds them, and serving the same tarball from a proxy instead stays documented.

git vs Grovedock

Both tools are excellent at different things. This page says where each one wins, with measurements you can reproduce (bench/run.sh). The table below was recorded with pando 1.0.0 and git 2.50.1 (bench/results/RESULTS.md).

Measured results

ScenariogitGrovedockGrovedock vs git
full-clone19,130 ms463 ms41.3x faster
sparse-clone (Documentation/)9,513 ms106 ms89.7x faster
read one file, no prior clone (CI)149 ms9 ms16.0x faster
status (clean, 4857 files)91 ms48 ms1.9x faster
log -n 50 – builtin/add.c100 ms19 ms5.4x faster
blame Makefile528 ms352 ms1.5x faster
commit + publish 1 file130 ms77 ms1.7x faster

status measured at parity (36 ms vs 36 ms) in an earlier run on the same machine; treat it as parity to ~2x faster. Other rows were stable across runs.

Footprint after full clone

gitGrovedock
Client metadata334M (.git, full history)380K (.pando, index only)
Server storage333M (delta-compressed pack)4.6G per-object zstd; 369M after pandod compact

Full-history import: 226 s for 82,443 commits (first run). Re-import of unchanged history uploads nothing.

Concurrent publishing: 16 agents x 5 changes = 80 merges to one branch

git (push / fetch+rebase retry)Grovedock (land)
Wall time42.8 s3.5 s
Throughput1.9 merges/s22.7 merges/s
Client retries4560 (server rebased 79)

Landing at scale: 16 to 5,000 concurrent agents

Every agent holds its own connection and loops: read the branch head, upload one new file in its own directory, create a changeset on that head, land it. All changes are non-conflicting, so every lost race is resolved by the server (Grovedock) or by the client’s fetch-rebase-push loop (git). Lands that finish after the 30 s window are not counted.

AgentsLayoutgit lands/sgit p99Grovedock lands/sGrovedock p50Grovedock p99Grovedock errors
16one directory2.022.7 s75818 ms55 ms0
200one directory0.525.2 s1,012203 ms302 ms0
1,000one directory0.2 (5 lands)52.0 s5042.10 s2.66 s0
1,00064 directories9071.17 s1.30 s0
2,000one directorynot run3336.27 s6.80 s0
5,000one directorynot run1723.36 s15.8 s0
5,00064 directories5319.26 s10.3 s0

“One directory” puts every agent’s directory side by side in one parent, which every land rewrites (5,000 entries at the largest; the Linux kernel’s largest directory, include/linux, has about 1,600). “64 directories” spreads them over 64 parents, closer to agents working across a monorepo.

  • Setup: Apple M4 (10 cores), macOS 26.6, release build, one pandod on SQLite, a fresh store per run. git pushes to a local bare repository over file://, its best case: no network and no server-side hooks. Agents and server share the machine. Measured with the machine otherwise idle (an earlier run with a busy background VM gave about 30% less).
  • Grovedock: cargo run --release --example land_load -- --server http://127.0.0.1:7420 --agents N --fanout F --secs 30 against pandod server. git: python3 bench/git_push_load.py --agents N --secs 30 --work DIR.
  • Before batching (ADR-017), the same Grovedock build managed 105 lands/s at 16 agents and 10 lands/s with 237 “too contended” errors at 200.
  • Limits: past 1,000 agents throughput falls because the same process also serves every agent’s uploads and changeset creation, and each agent’s directory adds an entry to one shared directory that every land rewrites. At 5,000 agents in one directory almost every agent landed once in the window, so that row mostly measures the first round.
  • Context: Anthropic reported over 30,000 merges per day by September 2026 (about 0.35/s), Google about 40,000 commits per workday in 2015. Landing throughput of one server is not the constraint at those volumes; real lands also carry CI, review and larger changes, which this benchmark leaves out. A multi-replica deployment on Postgres has not been measured yet.
  • git at 2,000 and 5,000 agents was not run: at 1,000 it landed 5 changes in 30 s.

Methodology

  • Corpus: git/git (82,443 commits, ~4,900 files): real, public, and with 20 years of history.
  • git is served over file:// (the real pack protocol, no network latency) with uploadpack.allowFilter enabled so partial clone is available to git.
  • Grovedock runs as a release-build server on SQLite, over loopback gRPC.
  • For each scenario git gets its best practice: partial clone + sparse checkout for sparse clones, --depth 1 --filter=blob:none for single-file CI reads, --first-parent for log/blame (Grovedock’s history is first-parent, i.e. what landed).
  • hyperfine median of 5 runs after a warm-up run.

Where Grovedock wins, and why

  • Clones: Grovedock transfers one tree’s worth of objects, never history. The gap grows with history length; at monorepo scale git’s full clone is the dominant CI cost.
  • CI reads: pando cat main path is one RPC. git needs at least a shallow, filtered clone.
  • Publishing under contention: git’s push loses every race and must fetch + rebase + retry on the client. Grovedock rebases on the server and moves the ref by compare-and-swap, so concurrent non-conflicting lands all succeed without client retries.
  • Thin clients: a Grovedock workspace’s metadata is an index, not a copy of history.

Where git wins

  • Offline work: git has everything locally. Grovedock needs the server for history, commits and landing (by design).
  • Server storage (before compaction): uncompacted, Grovedock stores every object version whole (4.6 GB for git/git vs a 333 MB pack). After pandod compact (delta storage) the same store is 369 MB, 1.1x git’s pack.
  • Cold path-filtered history: Grovedock’s log -- path and blame are fast once the server’s immutable metadata cache is warm (measured above); on a cold server they walk first-parent history from storage. A derived per-path index is on the roadmap.
  • Ecosystem: every tool speaks git. Grovedock offers lossless import-git / export-git so you are never locked in.

Semantics differences

gitGrovedock
Commitlocal snapshotserver-side changeset (list of changed files vs first parent)
Staging areayesno; pando commit [PATHS] commits all or a subset
Branchescheap, everywheresupported (pando checkout -b), but main is the default lane; refs are global state
History shown by log/blamefull DAGfirst-parent (what landed)
Submodulesyesnot supported (skipped on import)
HashSHA-1 (SHA-256 opt-in)BLAKE3

CI checkouts on one machine

Each CI job starts from an empty directory, checks out one top-level directory of git/git (sparse, the usual CI case), checks the file count and deletes it. 16 jobs run at once on one machine for 30 s; jobs rotate over Documentation, builtin, t, contrib, compat and po (28 to 2,554 files). Every system checked out identical file counts.

SystemCheckouts/sp50p99
git: shallow, blobless, sparse clone from git daemon (standard CI)2.73.71 s10.3 s
git: --shared clone from a mirror on the same machine (no network)4.21.87 s5.6 s
Grovedock: pando clone --path from the server5.80.35 s5.4 s
Grovedock: through the machine’s node daemon6.10.43 s4.6 s
  • One checkout on an idle machine: 0.15-0.23 s for Grovedock against about 1.7 s for git, the largest directory included. With 16 at once, both systems are limited by writing files on the client machine, so the gap narrows to about 2x. Grovedock’s checkout of t/ (2,554 files, fetched, decoded and verified) takes as long as a plain loop that only writes the same files (0.20 s), so the client is at the filesystem’s limit; going further means not writing files at all, which pando mount does.
  • Server cost: 1.57 CPU-seconds for 175 checkouts, about 9 ms each.
  • The node daemon adds little here because server and jobs share one machine; it exists to take load off the server and the network between them.
  • At 64 and 256 jobs the laptop is saturated for every system (at 256, git daemon connections time out), so those runs measure the machine rather than the tools.
  • This benchmark found a bug: about 3% of Grovedock checkouts failed with File exists when two writers created the same directory at once. Fixed in 2.1.1.
  • Reproduce: bench/ci_checkout_load.py --system {git,git-mirror,pando,pando-node} --jobs 16 --paths Documentation,builtin,t,contrib,compat,po --work DIR plus --server, --node, --git-url or --mirror for the system under test.

Prior art

Grovedock builds on ideas that large engineering organisations have described in public. No code from any of these systems was used. This page credits them, records what each one proved, and sets the bar Grovedock measures itself against.

SystemOrganisationPublished scaleIdeas Grovedock sharesOpen source?
Piper and Clients in the Cloud (CitC)Google~1 billion files, ~35 million commits, 86 TB (2015) 1Server holds history; workspaces show the whole tree but store only what you changeNo
Sapling, Mononoke, EdenFSMetaRepositories with millions of files and very long histories 2Operations scale with the files you use, not the repository; lazy virtual checkoutsClient yes; Mononoke and EdenFS not supported for outside use 3
VFS for Git (GVFS) and ScalarMicrosoftWindows: ~3.5 million files, ~300 GB repository (2017) 4Fetch only what is used; virtualize the working directoryYes (MIT), git-based
Redesigning source controlAnthropicTens of thousands of merges a day from humans and agents 5Server-side commits and rebases, lock-free landing, tiered caches, API-first readsNo

What Grovedock adds

  • Open source and self-hosted under Apache-2.0: one binary on a laptop with SQLite, or stateless replicas on Postgres/CockroachDB and S3/GCS/Azure.
  • Agent-native interfaces: a built-in MCP server and SDKs in Python, Go and TypeScript.
  • Plain git keeps working through a two-way remote helper; import and export round-trip with identical trees, so there is no lock-in.
  • Pluggable everything: every infrastructure piece is a port with a shared contract suite, and every source-control feature has a runtime flag.
  • Measured in public: the benchmarks are reproducible from public data with the scripts in bench/ and the examples in crates/pando/examples/.

Where these systems are ahead

They run in production at a scale Grovedock has not yet been tested at, with years of operating experience and integrated code review, search and build tooling. Grovedock is new. The benchmarks in docs/comparisons/git-vs-pando.md and larger-scale runs still to come are how it earns comparison, one public dataset at a time.


  1. Potvin and Levenberg, Why Google Stores Billions of Lines of Code in a Single Repository, Communications of the ACM, July 2016. Figures from January 2015; the file count includes release branches and deleted files. ↩

  2. Meta Engineering, Sapling: Source control that’s user-friendly and scalable, November 2022. ↩

  3. facebook/sapling README. ↩

  4. Brian Harry, The largest Git repo on the planet, May 2017. ↩

  5. Anthropic Engineering, Redesigning source control at Anthropic. ↩

Roadmap

What comes next for the open-source server, roughly in order. Everything here stays Apache-2.0. Releases stay backward compatible with the pando.v1 protocol and storage formats.

Shipped in 3.x

Hosting many repositories; organisations, teams, roles and scoped tokens; change requests, reviews, protected branches and required checks; issues assignable to people and agents; webhooks; remote MCP for agents; pando migrate and pando mirror; the web console for all of it. In 3.4: browser sign-in (Google, any OpenID Connect provider, email links), multi-tenant servers with self-serve organisations, and pandod backup. In 3.5: pando login from the browser, invitations by email, and usage and limits per organisation.

Next

ItemWhy
Platform registry on Postgresseveral pandod replicas sharing accounts, reviews and webhooks, for high availability
Email notificationsreview requested, assigned, mentioned, with per-person settings
Per-person and per-token rate limitsfairness on shared servers
Review comments carried across pando migratepull requests arrive with their discussion
Path-history indexlog -- path walks first-parent history today
Writable virtual checkoutspando mount is read-only on every platform today

Later

Grovedock Cloud, a managed service with a free tier, and enterprise features (SAML, SCIM, audit-log streaming, high-availability deployment templates) are planned. The open-source server stays complete: everything shipped here stays open.

Changelog

All notable changes to this project are documented here. The format follows Keep a Changelog and the project adheres to Semantic Versioning.

[Unreleased]

[3.5.1] - 2026-10-19

Added

  • Website: a new landing page and a pricing page with a Team cost calculator. The product name, links and list prices live in site/site.conf, which scripts/build-site.sh fills in; the pages self-host their fonts. URLs are unchanged: the landing page at the site root, docs under /docs/, the console demo under /console/, and the new /pricing.html.
  • Releases attach the web console, pando-console-vVERSION.tar.gz with its .sha256, covered by the release’s signed provenance and SBOM attestations.
  • pandod server --console-dir DIR (PANDO_CONSOLE_DIR) serves the console at / on the server’s own address: links into the app fall back to index.html, hashed assets are cached for a year and everything else revalidated, with CSP, nosniff, framing and referrer headers (ADR-036).
  • A cross-tenant isolation suite, a release gate: every method in the protocol is called with another organisation’s tokens (a person, an agent, tokens limited to a repository) at an organisation’s repositories, change requests, issues, webhooks, protections, locks, teams and tokens, and must be refused or show nothing of theirs; a method missing from its table fails it.

Changed

  • The product is now called Grovedock: the website, documentation, web console and sign-in pages use the new name and wordmark. Commands, packages, environment variables (PANDO_*), webhook headers and the protocol keep the pando name until 3.6.
  • The website moves to https://grovedock.com/ (landing at /, /pricing.html, /docs/, /console/); old kabirnarang39.github.io/pando/ links redirect. A server that lists the hosted console in --web-origins should list https://grovedock.com.
  • Website and docs home redesigned: a night-grove hero (aspen trunks on one root, animated once and still under reduced motion), a benchmark panel, a reworked pricing page and footer.

Fixed

  • docs/platform.md said git manages 2 lands/s at 200 agents; the benchmark measured 0.5.
  • docker compose up failed: every service listens on the network without a token, which pandod refuses. The compose file now requires PANDO_TOKEN in .env (with a clear error when it is missing) and passes it to the server, the cache and the daemon.
  • A bare PANDO_SERVER (no /OWNER/NAME) naming a clone’s own server no longer hides the clone’s repository, so pando protect and pando cr work inside a clone with it set.
  • The remote MCP endpoint answers clients on protocol versions before 2026-07-28 (their initialize) without a session, instead of “Session management is not supported”.
  • --repos-objects file:///…/{repo_id} no longer makes a directory literally named {repo_id} (readiness checks the template’s base directory), and a --platform server no longer creates an unused ./pando-data: its default --metadata is now memory.
  • pando admin limits suspend says when the server doesn’t enforce limits (not --multi-tenant); pando org usage prints dates instead of epoch milliseconds.
  • pando migrate prints the pando mirror command for any git source, not only GitHub.
  • The working copy reads .pandoignore; .sapignore still works, with a deprecation warning.
  • pando webhook add --help lists the issue event; pandod backup --help points to docs/backup.md.
  • The console demo on the public site no longer asks for /login/providers (a 404).
  • docs/platform.md no longer claims per-path owners in repository settings.

Security

  • Creating a token limited to repositories told whether a private repository you can’t see exists; such a name is now “no repository”, like everywhere else.
  • Granting a repository to a team told whether another organisation has a team of that name; only the owning organisation’s teams are recognised.

[3.5.0] - 2026-10-18

Added

  • pando login [SERVER_URL] signs the CLI in from the browser (OAuth device flow) and saves a 90-day token in ~/.config/pando/credentials.json (mode 0600), used when neither --token nor PANDO_TOKEN is given; pando logout revokes it and pando whoami shows the account (ADR-033).
  • Invitations: organisation administrators invite by email (CreateInvitation, ListInvitations, RevokeInvitation); the single-use, 7-day link signs the person in (creating an account if needed) and adds them with the invited role (ADR-033).
  • First sign-ins ask the person to confirm they are 18 or older and accept the terms (--terms-url, --privacy-url) before an account is created; the time is stored on the account.
  • Viewer returns the calling token’s id, so clients can sign out by revoking it.
  • Console: “Sign in with …” buttons and an email form when the server has sign-in pages, sign out, a guided set-up for a new organisation (name it, invite teammates, connect an agent), and invitations on the organisation’s People tab.
  • pandod repo disable|enable OWNER/NAME takes a repository down at once without deleting it, and pandod repo delete OWNER/NAME removes it and its storage; documented with a secret purge procedure in docs/deploy.md.
  • --trust-proxy: audit-log client addresses from X-Forwarded-For sent by a local proxy.
  • Console: “New repository” on the organisation page; an empty repository shows how to clone it, make the first change or bring in a git repository; a new token shows ready-to-copy CLI and MCP setup.

Changed

  • SQLite commits are synced to disk (synchronous=FULL) so an acknowledged land survives power loss; --sqlite-synchronous normal restores the previous behaviour. file:// object stores fsync every object and its directory (ADR-034).
  • --metrics also serves /healthz and /readyz; gRPC health checks need no token and report NOT_SERVING when the registry or a data directory fails, and from the start of shutdown.
  • SIGTERM drains calls in progress and stops the webhook worker before exiting.
  • Platform servers limit each account to 128 calls in progress (--max-calls-per-principal) and request messages to 64 MiB (--max-request-mb), keep at most 256 repositories open (--max-open-repos, --repo-idle-minutes), and page repository listings.
  • Webhooks: deliveries run concurrently, two at a time per webhook, 30 s each; delivery is at least once (dedupe by X-Pando-Delivery); finished deliveries are deleted after 30 days.
  • The audit log is written to one file per UTC day and kept --audit-retention-days (365).
  • --web-origins is now --cors-origin (the old name still works); pando mcp --http takes --allowed-origin and stops on SIGTERM.
  • The registry records its schema version and applies ordered migrations.
  • Console UX pass: a first visit to a server with sign-in pages opens sign-in instead of the demo; set-up asks for the organisation’s name and derives its short name; the full repository header shows on Overview only; gold marks landings only (dark-theme buttons, selection and unread marks use Fishlake blue); form errors read as sentences.

Fixed

  • Console: the clone and mount commands name the repository (SERVER/OWNER/NAME) on servers that host many; Home shows an error instead of loading forever when the server is unreachable or refuses the token; Issues breadcrumbs; Escape closes the phone navigation drawer; wide tables no longer widen the page on phones; imported git authors show their name without the email; the Connect page’s “remember” checkbox is visible.

Security

  • Production hardening (ADR-034): metric labels only for methods the protocol defines (no unbounded series from made-up paths); internal errors return an id, not SQL or paths; the registry survives a panic while locked; inbox, events, issue and repository listings scan a bounded number of rows and never open every repository; archived repositories refuse writes; Locks/List shows only paths the caller may read; the MCP gateway refuses browser origins unless allowed; the free-tier cloud-init no longer trusts a demo console origin; a stored object that fails its hash check is repaired by pushing it again.
  • Plans and limits (ADR-035): platform servers record each organisation’s active agents (agents that landed a change in the calendar month), people, storage (measured hourly, with the month’s peak) and private repositories. With --multi-tenant, organisations’ limits are enforced: suspended organisations and those over storage can read but not write, new agents beyond the active-agent limit can’t land while active ones keep landing, and seats and private repositories are checked when added. Defaults come from --default-org-limits. New RPCs GetUsage, SetLimits, SetSuspended, GetCapabilities and GetLicense; commands pando org usage and pando admin limits show|set|suspend|restore; a Usage & plan tab in the console with a Change plan button linking to --billing-url, and a banner on every page of a suspended organisation linking to --support-url.
  • Support licences: pandod server --license FILE and pandod license verify FILE check an Ed25519-signed licence offline and report it; nothing is turned off without one.

Changed

  • The sign-in pages (/login/*: sign in, check your email, create your account, CLI approval, invitations, errors) carry the console’s brand: the Pando mark, its palette in light and dark, labelled fields, a clear primary action and plain error messages, still self-contained under default-src 'none'.

[3.4.0] - 2026-10-18

Added

  • pandod server --multi-tenant (PANDO_MULTI_TENANT) for servers that strangers share: internal repositories are for the owning organisation’s members, members and grants must name existing accounts, and any person may create up to --max-orgs-per-account (default 3) organisations, in a namespace shared with logins and with service names reserved (ADR-032).
  • Browser sign-in for platform servers (pandod server --login-listen): Google and any OpenID Connect provider (--oidc ID=ISSUER), and single-use email links (--smtp-url, --mail-from, optional Cloudflare Turnstile). A sign-in creates a 30-day platform token for the console; first sign-ins create an account if --signup allows (default: a waitlist, managed with pando admin signup allow|remove|list). pando admin identity link lets an existing account, such as the first administrator, sign in from a browser (ADR-031).

Changed

  • Looking up one organisation reads only that organisation.

Security

  • Protected branches: a change’s author can no longer approve it through an agent they mint, approve an agent’s change they answer for, or report its required checks; such approvals don’t count (ADR-030).

  • Single sign-on: --oidc-require CLAIM=VALUE binds tokens to your tenant, and pandod refuses to start with GitHub Actions or Google as issuer without one. Tokens can’t sign in as a platform administrator (unless listed with --oidc-admin) or as an agent/ or bot/ account. Unknown signing keys refetch the key set at most once a minute.

  • Paths named .git or .pando in any case, with trailing dots or spaces, as an NTFS stream or as an 8.3 short name (GIT~1) are refused by the server, by checkouts and by imports, so a writer can’t plant git hooks or config in other people’s checkouts. Existing history holding such paths can no longer be read.

  • Static tokens that are empty, shorter than 16 characters or a placeholder (CHANGE-ME) are refused at startup.

  • Access answers are no longer re-cached from a read that raced a write, so a removed member or grant loses access at once.

  • pando migrate: the GitHub token comes from PANDO_GITHUB_TOKEN (or GITHUB_TOKEN) or --github-token-file; --github-token warns that it is visible to other users. Source URLs are printed without credentials, and the mirror clone goes to a private, randomly named directory.

  • Platform servers refuse repository calls that name no repository. Before, such a call reached the server’s own --metadata repository with no access check, so any signed-in caller (or any token, however limited) could read and write it. A non-UTF-8 x-pando-repo header is refused.

  • Webhooks no longer call loopback, private networks (RFC 1918, 100.64.0.0/10, fc00::/7) or cloud metadata (including 100.100.100.200, fd00:ec2::254 and IPv4 written as IPv6: mapped, NAT64, 6to4). Start with --webhook-allow-private to let a self-hosted server call its internal network (metadata stays refused). The address is checked when a webhook is saved and at every delivery, deliveries connect only to the checked address (no DNS rebinding), DNS lookups time out after 5 s, proxies from the environment are ignored, and a failed delivery or ping reports only the HTTP status or “unreachable”, never why.

Added

  • pandod backup snapshots a platform server’s registry and every repository’s database to a directory or object store while it runs; docs/deploy.md documents the restore.

Changed

  • Docs: a platform server (--platform) is single-node; only servers without it scale out.
  • Go SDK: requires Go 1.26.9 and uses golang.org/x/net 0.60.0 and golang.org/x/text 0.42.0, fixing six known HTTP/2 and text-parsing vulnerabilities (GO-2026-6603, 6610, 6611, 6612, 6617, 6629).
  • Releases attach their signed SLSA provenance (*.intoto.jsonl) next to the archives.
  • CI hardening: least-privilege workflow tokens, every action pinned to a commit, CodeQL on every change, and Dependabot for Go, npm and Python as well as Rust and Actions.

[3.3.0] - 2026-10-17

Security

  • On platform servers a change is attributed to whoever made it: a member can no longer record someone else as the author. Repository administrators may still record other authors, so imports and pando migrate keep the original history. Single-repository servers keep git’s free-form authors.

Fixed

  • Platform servers no longer slow landing down: access answers are cached per process (cleared by every write that changes them) and push events are prepared off the landing path only when a webhook wants them. With 200 agents on one branch, p99 latency through a hosted repository went from 2.8–3.8 s to 0.22–0.25 s, matching a server without the platform (ADR-029).

Added

  • pando migrate FROM --to SERVER/OWNER/NAME (ADR-028, docs/migrate.md): moves a repository from GitHub, GitLab, Bitbucket or any git host with its full history on every branch; from GitHub (and GitHub Enterprise Server with --github-api), open pull requests become change requests and issues come with comments, labels and state. It prints the pando mirror command that keeps the old host current while you move.
  • Issues (ADR-027): open, discuss, label and assign issues to people or agents, in the console (Issues tab), the CLI (pando issue) and over MCP (pando_issues, pando_issue, pando_open_issue, pando_comment_on_issue, pando_update_issue). Issues and change requests share numbers; a change request saying “fixes #N” closes the issue when it lands. Issues assigned to you are in your inbox; issue webhook events let an agent runner start work as soon as an issue is assigned.
  • Remote MCP (ADR-026): pando mcp --http ADDR --server URL serves a whole server to agents anywhere over the stateless MCP 2026-07-28 protocol. With a URL and a token an agent reads, writes and lands files, opens, reviews and lands change requests, reports checks and reads its inbox, without a checkout; each request’s token is passed through, so the server’s permissions, token limits and audit log apply.
  • An agent’s change request asks the person the agent answers to for review; change requests nobody was asked to review show in the inbox of everyone who may approve them.
  • Changesets created without an author are attributed to the authenticated caller.
  • Webhooks (ADR-025, docs/webhooks.md): push, change_request, review, comment and check events posted to URLs and signed like GitHub’s (X-Pando-Signature-256), retried with backoff from a queue that is safe across replicas. pando webhook add|list|ping|log|remove and repository Settings → Webhooks in the console.
  • pando land on a protected branch opens a change request from a review/ branch instead of failing. The console can open a change request from any branch.

Added

  • Change requests and review on platform servers (ADR-024): open a change request from a branch, comment on lines with suggestions, approve or request changes, report CI checks, and land it as one changeset once nothing blocks it; pando cr rebase replays it on its target. Protected branches (pando protect, or repository Settings in the console) require approvals and passing checks, make approvals stale on a new push, and refuse direct lands except for those allowed to bypass review. An inbox (review requested, changes requested, agents’ changes ready) and an activity feed. CLI: pando cr create|list|view|comment|review|check|land|rebase|ready|close|reopen.
  • The web console’s review screens work against a platform server, and admins protect branches from repository Settings.

[3.2.0] - 2026-10-17

Added

  • Accounts, organisations, members, teams, per-repository roles and access tokens on platform servers (ADR-023): pando org, pando team, pando repo grant and pando token, and the same in the web console (Tokens, organisation People, repository Settings → Access). Tokens start with pando_, are stored as a hash, can act as an agent, be limited to repositories and a highest role, and expire within a year.
  • pandod token --platform URL --account LOGIN creates the first administrator’s token without a running server.

Changed

  • Platform servers now check every repository call: reading needs the Read role and anything else Write; repositories are private to their organisation unless made internal or public. After upgrading from 3.1, add people with pando org member ORG LOGIN ROLE. Servers without any authentication stay open, for local development.

[3.1.0] - 2026-10-17

Added

  • Console review comments on code: a “+” on every line, shift-click for a range of lines, a Markdown editor with preview, suggested changes shown as a small diff of current and proposed lines, ⌘/Ctrl+Enter to send, Escape to cancel, and resolved conversations collapsed.
  • pando mirror --to URL [--every SECONDS]: keeps GitHub, GitLab, Bitbucket or any git remote up to date with a branch. Incremental, deterministic commits, never force-pushes, refuses to overwrite commits it did not make, and redacts credentials in messages.
  • One server, many repositories: pandod server --platform REGISTRY --repos-metadata TEMPLATE [--repos-objects TEMPLATE] --platform-admins NAMES. Each repository has its own storage and landing queues; clients address SERVER/OWNER/NAME; calls without a repository reach the default one (ADR-022).
  • pando repo create OWNER/NAME and pando repo list.
  • The web console lists a server’s repositories and shows only what the server supports.

Changed

  • pandod server and pandod cache listen on 127.0.0.1 by default; listening on a network is a deliberate --listen 0.0.0.0:7420.

Fixed

  • Console: the phone code view cut off line numbers and the first characters of each line; long titles no longer truncate to one line on phones; keys typed in a form never trigger page shortcuts.
  • The web console kept one data cache across connections, so data from the demo or a previous server could appear after switching. Each connection now has its own cache.

[3.0.0] - 2026-10-17

Added

  • The web console (console/): browse code with blame and history, follow changes as they land with word-level diffs, see who and which agent landed what on a live fleet timeline, find files from a command menu, and see locks. Works with any pandod; a demo of the full platform (several repositories, change requests, reviews, a land gate and rebase) runs in the browser. See docs/console.md.
  • pandod accepts gRPC-Web from browsers (web feature flag, on by default) and allows the origins in --web-origins / PANDO_WEB_ORIGINS to call it.
  • The platform contract, pando_platform.proto: organisations, repositories, accounts and agents, tokens, change requests, reviews, comments, protected branches (ADR-021).

Changed

  • Security (breaking): without PANDO_TOKENS or an OIDC issuer, pandod refuses to listen on a non-loopback address unless --allow-anonymous / PANDO_ALLOW_ANONYMOUS=1 is set, so a server is never open to a network by accident. The Docker image therefore needs a token: docker run -e PANDO_TOKENS=me:SECRET ....
  • Request limits: each call may run at most --request-timeout (600 s); beyond --max-in-flight (4,096) calls in progress, new calls are refused with Unavailable instead of queueing; 256 concurrent calls per connection.
  • A new mark: the letter P drawn as a branch landing back on main.

[2.4.0] - 2026-10-17

Added

  • pando mount on macOS and Windows. macOS serves a read-only NFSv3 view on localhost and mounts it with the system mount_nfs: no kernel extension, nothing to install, no admin rights. Windows uses the built-in Projected File System, loaded at run time. Both are tested in CI against the real kernels, file by file against pando clone (ADR-020).
  • pando mount --serve-only on any Unix prints the mount command instead of mounting.
  • PANDO_LOG=debug turns on client-side logs.

2.3.0 - 2026-10-17

Added

  • File locking for files that cannot be merged: pando lock, pando unlock [--force], pando locks. A land that changes a file someone else has locked is refused. Locks are compare-and-swap system refs (ADR-019), need write access, are audited, and sit behind the locks feature flag.

Fixed

  • A land refused by a pre-land check or a lock showed as a garbled storage error on the client; it now reads rejected: <reason>.

2.2.0 - 2026-10-16

Added

  • pandod copy: back up, restore, or move a repository between backends (SQLite, Postgres, CockroachDB; disk, S3, GCS, Azure). Consistent while the source serves, incremental when repeated, keeps compacted objects compact and verifies whole objects. Storage ports gain paged listing, covered by the adapter contract tests.
  • --audit-log FILE: an audit trail as JSON lines. One record per authenticated request (principal, client address, method, status, duration), per rejected request (address and reason, never the token), and per land, branch move, content read and history query, with paths and results. Off by default.
  • Coverage-guided fuzzing (cargo-fuzz) of every decoder of untrusted bytes: objects, trees, changesets, chunk manifests, deltas and paths, with a seed corpus of valid inputs. Ten minutes per target nightly, one minute on pull requests.

2.1.3 - 2026-10-16

Fixed

  • Two replicas landing the same retried change could both apply it, leaving it in history twice. A change rebased onto a tip that already holds all of its changes is now reported as landed there instead of becoming an empty commit (as git rebase drops commits that become empty).

Added

  • bench/chaos.sh: three replicas on Postgres behind a connection-cutting proxy, killed and restarted while clients land; checks the landing invariants through the public API. Runs nightly with the simulation.
  • Documentation: how Pando is tested.

2.1.2 - 2026-10-16

Fixed

  • A land retried after a lost reply could apply a rebased change a second time. Each rebased change now records the id it landed as, and a retry reports that landing.
  • A batch flush that failed halfway could leave a derived record naming a tree or changeset that was never written; a later land of the same changeset then failed with “not found”. Staged writes now go out objects first, then changesets, then derived data.
  • One transient storage error failed every change in a land batch. Batches now retry storage errors (up to four times, with backoff) from the current head.

Added

  • A deterministic simulation of the landing protocol with fault injection, replica crashes and conflicting changes (crates/pando-server/tests/simulation.rs, ADR-018): 200 seeds in every CI run, 50,000 new seeds nightly. It found the three bugs above.

2.1.1 - 2026-10-16

Added

  • A documentation site (mdBook, with search) at kabirnarang39.github.io/pando/docs: getting started, coming from git, deploying, and AI agents and SDKs, alongside the architecture, design decisions and benchmarks.
  • New logo, README, landing page and social preview. Charts and diagrams are generated as light and dark SVGs by scripts/charts.py.
  • bench/ci_checkout_load.py: concurrent CI checkouts on one machine, for git and Pando.

Fixed

  • Checkouts that write many files in parallel could fail with File exists (os error 17) when two writers created the same new directory at once (seen in about 3% of concurrent CI checkouts of git/git’s t/).
  • The MCP tools still used the pre-rename prefix: sap_cat, sap_land and the others are now pando_cat, pando_land and so on. MCP clients discover tool names, so agents pick up the new names on restart.
  • Commands that work without a workspace (cat, ls, log, …) suggested a --server flag they do not have; the error now points to PANDO_SERVER.

Security

  • Release archives get signed SLSA build provenance and an SPDX SBOM through GitHub artifact attestations (verify with gh attestation verify); container images carry provenance and SBOM attestations. See SECURITY.md.
  • OpenSSF Scorecard runs weekly and on every push to main.

2.1.0 - 2026-10-16

Added

  • examples/land_load.rs and bench/git_push_load.py: landing throughput with N concurrent agents, for Pando and git.

Fixed

  • bench/concurrent_land.py failed after the rename; README links to the benchmark page pointed at a file that still had the old name.
  • pandod compact on SQLite no longer leaves a write-ahead log as large as the database behind after VACUUM; on git/git that was 392 MB next to a 388 MB database.

Performance

  • Concurrent lands to the same branch are batched: the server rebases everything queued as one chain and moves the branch with one compare-and-swap. With 200 agents landing at once on one machine, throughput went from 10 lands/s with 237 “too contended” errors to 1,012 lands/s with none; 1,000 agents sustain 504 lands/s (git: 0.2/s). A conflicting change fails without affecting the rest of its batch.
  • The rebase conflict check walks only the changeset’s own paths instead of diffing everything that landed since its base.
  • SQLite writes are group-committed by one writer thread instead of one transaction per write behind a mutex.
  • pandod compact is incremental: finished changesets are recorded and later runs process only what landed since, instead of rescanning all history.
  • Compaction looks up paths in a cache of parsed trees instead of re-reading and re-parsing every directory level from storage for each changed file.
  • Compaction caches delta chains instead of re-reading a base’s whole chain from storage for every pair.
  • SQLite runs without memory-mapped I/O and with a 64 MB page cache per connection: under concurrent writes SQLite remapped the file on every read transaction. On the Linux kernel’s history, compaction went from 34-47 to 68 changesets/s and import from 78 to 89 commits/s; clone and log times are unchanged.

2.0.0 - 2026-10-15

Changed (breaking)

  • Sapwood is now Pando. Named after the Pando aspen grove: some 47,000 trunks that share one root system. What changed, and what to do:
    • Binaries: sap → pando, sapwood → pandod, git-remote-sapwood → git-remote-pando (git URLs pando::https://…).
    • Environment variables: SAPWOOD_* → PANDO_*; SAP_SERVER, SAP_TOKEN, SAP_AUTHOR, SAP_OBJECTS → PANDO_SERVER, PANDO_TOKEN, PANDO_AUTHOR, PANDO_OBJECTS.
    • Workspaces live in .pando/ instead of .sap/: clone existing workspaces again.
    • Wire protocol package sapwood.v1 → pando.v1: upgrade clients and servers together.
    • Packages: crates pando-vcs (CLI), pando-core, pando-adapters, pando-proto, pando-server; PyPI pando-vcs (import pando); npm pando-vcs; Go module github.com/kabirnarang39/pando/sdk/go/v2; image ghcr.io/kabirnarang39/pando; Homebrew kabirnarang39/tap/pando.
  • Stored data carries over unchanged: object ids, changesets and SQLite databases are identical, and Postgres/CockroachDB tables named sapwood_* are renamed on first start.

Fixed

  • pando import-git accepts merges that list the same parent twice, which git allows and the Linux kernel’s history contains four of; the repeated parent is imported once.

1.9.0 - 2026-10-15

Added

  • Delta storage (sapwood compact, flag delta-storage): blob and tree versions are stored as zstd patch-from deltas against the previous version of the same path. On git/git (82k commits) server storage drops from 4.6 GB to 369 MB (git’s pack: 333 MB). Single-writer compaction behind a lease, base pinning (chains never change after use, no cycles), verification before every replace, hash-verified reconstruction on every read. Wire protocol unchanged.
  • ObjectStore::replace on memory, SQLite, Postgres and object_store backends (contract-tested).
  • Install with Homebrew: brew install kabirnarang39/tap/sapwood.
  • Release workflow publishes to crates.io (sapwood, sapwood-server and the library crates), PyPI (sapwood), npm (sapwood-client) and the Homebrew tap, each enabled by a repository variable. CI builds every crate from its packaged tarball.

Changed

  • sap refs / the Refs API hide system refs (_sapwood/...).
  • The client crate is now named sapwood (binaries sap and git-remote-sapwood are unchanged): cargo install sapwood. The protocol files moved to crates/sapwood-proto/proto/ so the crate can be published.

Compatibility

  • A store compacted by this version cannot be read by v1.8.x or older servers (they do not understand delta payloads). Upgrade every server before running sapwood compact.

Fixed

  • Linux release binaries are built on Ubuntu 22.04 and need glibc 2.35 or newer (Debian 12, Ubuntu 22.04, RHEL 10); they previously needed glibc 2.39.
  • The release container image is assembled from the release binaries instead of compiling arm64 under emulation, which took over an hour and failed for v1.6.0. Images for earlier tags can be rebuilt with the Release workflow’s manual trigger.

1.8.0 - 2026-10-15

Added

  • Pre-land checks (sapwood server --land-hook URL [--land-hook-token T] [--land-hook-timeout SECS], flag land-checks): every land is POSTed as JSON to a CI gate, code review service or policy bot, and proceeds only on a 2xx answer. The response body is shown to the author as the reason; timeouts and unreachable hooks refuse the land. The check runs once before any server-side rebase, which happens only without path conflicts, so the approved changes are exactly what lands. New LandCheck port for in-process checks.

1.7.0 - 2026-10-15

Added

  • Virtual checkouts on Linux (sap mount DIR [--rev main] [--objects URL], flag virtual-checkout): mounts a revision read-only through FUSE. Directories load when first listed, file sizes come from object headers without downloading content, and reads of a large file fetch only the chunks covering the requested range. Every object is hash-verified.

Changed

  • New objects record their uncompressed size in the zstd frame header, so a file’s size is known without decompressing it. Ids and the storage format are unchanged; older objects are still read (and sized by decompressing).

1.6.0 - 2026-10-15

Added

  • Daemon prefetch (sapwood daemon --server URL --prefetch main [--prefetch-path P] [--prefetch-interval SECS], flag daemon-prefetch): warms a node’s cache with a revision in the background and keeps it warm as the branch moves; already-cached objects cost nothing. On git/git, a new node’s first full checkout went from 4.2 s to 0.35 s.

1.5.0 - 2026-10-15

Added

  • SDKs for Python, Go and TypeScript (sdk/): read files, list directories, search, log, diff, blame, refs and merge-base without a clone, with BLAKE3 verification of changesets and file contents. Tested in CI against a live server.
  • proto/sapwood_objects.proto: the canonical object encodings, published for third-party tools; a test proves it is byte-identical to the core’s encoding.

1.4.0 - 2026-10-15

Added

  • Path-scoped read permissions: read grants in the access rules. Restricted principals are denied raw object reads and get path-filtered views instead: streaming Checkout, filtered ListDir/Find, diffs limited to readable paths, and redacted history. New Access.WhoAmI lets sap clone/sync switch to path-scoped checkout automatically. Rules without read keep full read access for everyone.

1.3.0 - 2026-10-15

Added

  • Large files: files of 8 MiB or more are stored as FastCDC chunks plus a manifest under the file’s own id, removing the 256 MiB file limit. Status hashing, commit upload, checkout and sync all stream; chunks dedupe across files and versions; the server verifies uploaded manifests against the file id and clients verify reassembled content end to end. Large git blobs are chunked on import-git and on git push through the bridge.
  • sap commit reports how many new objects were uploaded.

Performance

  • Tiered storage is now write-around and forwards batched writes upstream intact (one SQLite transaction per batch instead of per object): a 1 GiB commit went from 29 s to 14 s, and large uploads no longer evict the server’s hot read cache.

1.2.0 - 2026-10-15

Added

  • SSO via OpenID Connect (--oidc-issuer, --oidc-audience, --oidc-claim, flag oidc): JWT bearer tokens verified against the issuer’s JWKS with discovery and automatic key rotation. Asymmetric algorithms only, algorithm must match the key, iss/aud/exp required. The principal claim feeds per-directory and per-ref permissions. Static tokens and OIDC can be used together.

1.1.0 - 2026-10-15

Added

  • Two-way git bridge: git-remote-sapwood lets plain git clone, fetch, pull and push against a Sapwood server (git clone sapwood::http://host:7420). Fetches are incremental; pushes land server-side, so a push to a branch that moved is rebased instead of rejected (SAPWOOD_PUSH=strict restores git’s behaviour). Feature flag git-bridge. Verified by cloning git/git’s 24,466 mainline commits through plain git clone (136 s, identical tree).
  • Project website (GitHub Pages) and a checksum-verifying install.sh.

1.0.1 - 2026-10-15

Fixed

  • sap commit failed with “x is both a file and a directory” when a file was replaced by a directory of the same name (or the reverse) in one commit. The redundant delete is now folded into the upsert that replaces it. Found by the compaction test suite; covered by file_and_directory_swaps_commit_cleanly.

1.0.0 - 2026-10-15

First stable release. The sapwood.v1 wire protocol, storage formats and sap CLI are covered by SemVer from this release on.

Server (sapwood)

  • Content-addressed object store: BLAKE3-256 ids, zstd-compressed payloads, verified on every write and every tier hop.
  • Server-side changesets: clients send changed paths; the server builds Merkle trees (cost proportional to the change).
  • Generic derived-data framework: root trees, generation numbers, skew-binary jump pointers (O(log n) ancestry).
  • Lock-free landing: server-side rebase with path-level conflict detection plus compare-and-swap ref updates; no merge queue.
  • Server-side history: log (path-filtered), blame, diff, merge-base, file browsing and path search, all without a client checkout.
  • Tiered object serving: identical Objects RPC on server, regional cache (sapwood cache) and node daemon (sapwood daemon).
  • Storage adapters: memory, SQLite (WAL), PostgreSQL / CockroachDB, and S3 / GCS / Azure / local disk via object_store.
  • Security: bearer-token auth with principals, TLS, per-directory write ACLs and per-ref ACLs (JSON rules).
  • Operations: feature flags for every feature, Prometheus metrics, JSON logs, gRPC health and reflection, graceful shutdown.

Client (sap)

  • clone (lazy, sparse with --path), status, commit, land, sync, checkout [-b], log, diff, show, cat, ls, find, blame, refs, merge-base.
  • git-compatible aliases: push → land, pull → sync, st, ci, switch, branch.
  • import-git (deterministic, idempotent, incremental) and export-git (bit-identical trees; submodules are not imported).
  • sap mcp: Model Context Protocol server so AI agents can read, commit and land natively.
  • Parallel status hashing, batched and pipelined object fetches, symlink-safe working-copy writes.

How Grovedock is tested

Source control is trusted for not losing work. These are the checks that back that up, from the smallest to the most realistic. All of them run in CI.

Contract tests for every storage adapter

Memory, SQLite, Postgres and CockroachDB, and the local-disk, in-memory and S3 object stores (GCS and Azure use the same object_store code but are not run in CI), pass the same suite (crates/pando-adapters/tests/contract.rs), including a 16-writer compare-and-swap test. That is what makes adapters interchangeable rather than merely type-compatible.

Deterministic simulation of landing

crates/pando-server/tests/simulation.rs runs three server replicas over one shared store that fails operations, slows them down, and applies writes but loses the reply. Replicas crash mid-batch and restart. Clients land changes on random replicas, give up waiting and retry elsewhere, and a third of the changes conflict. One seed drives a single-threaded runtime with a paused clock, so any failure replays exactly:

PANDO_SIM_SEED=1234 cargo test -p pando-server --test simulation

After every run four invariants must hold:

  1. no change is in the branch’s history twice;
  2. every land the server acknowledged is in the branch’s history;
  3. the branch’s tree equals replaying its history;
  4. no change landed over a conflicting change it never saw.

Every CI build runs 200 seeds; a nightly job explores 50,000 new ones. Deliberately breaking the conflict check, or acknowledging a land whose ref update failed, is caught at the first seed.

Chaos against real binaries

bench/chaos.sh starts three pandod replicas on one Postgres and puts a proxy in front of them that cuts connections while replies are in flight. Clients land through the proxy and retry failures on other replicas, while the script kills replicas with SIGKILL, restarts them, and pauses the database. At the end the same four invariants are checked through the public API. A nightly job runs it for five minutes; a typical two-minute run kills replicas about 30 times and lands about 9,000 changes.

Fuzzing the decoders

Everything Grovedock reads from the network or disk goes through a handful of decoders: objects (zstd frames, chunk manifests, deltas), trees, changesets, and path validation. Each has a cargo-fuzz target in fuzz/, seeded with valid inputs from fuzz/examples/seed_corpus.rs so the fuzzer starts past checksums and headers. Any input must produce a value or an error, never a panic; the path target also checks that every accepted path stays inside the working copy. Each target runs for ten minutes nightly and one minute on pull requests that touch the decoders.

cargo +nightly fuzz run tree fuzz/seeds/tree

What these found

BugFound byFixed in
Parallel checkout writers creating the same directory failed with File existsCI checkout benchmark2.1.1
One transient storage error failed every change in a land batchsimulation2.1.2
A flush that failed halfway left a record pointing at data never writtensimulation2.1.2
A land retried after a lost reply applied the change twicesimulation2.1.2
Two replicas landing the same retried change could still apply it twicechaos test2.1.3

What is not covered yet

The simulation covers the landing protocol over in-memory stores, not the whole server. The gRPC message decoders are generated code (prost) and are not fuzzed separately. There has been no independent (Jepsen) analysis.

Security Policy

Supported versions

VersionSupported
3.x (latest release)✅
< 3.0❌ (upgrade to the latest release)

Reporting a vulnerability

Please do not open a public issue. Use GitHub’s private vulnerability reporting. You will receive an acknowledgement within 72 hours (and in any case within 14 days) and a fix timeline after triage.

Please include the affected version (pando --version), the component (client, server, SDK, console), steps to reproduce and the impact you expect. We fix confirmed medium or higher severity issues within 60 days, coordinate the disclosure date with the reporter, publish a GitHub security advisory, and list the fix, with its CVE or advisory id, under Security in CHANGELOG.md and the release notes.

Security model (v1)

  • Authentication: bearer tokens (PANDO_TOKENS, principal:token pairs), compared in constant time. Run with TLS (--tls-cert/--tls-key) whenever traffic leaves the host. Without PANDO_TOKENS, OIDC or platform tokens the server allows anonymous access, logs a warning, and refuses to listen beyond loopback unless --allow-anonymous is set.
  • SSO: OpenID Connect bearer tokens (--oidc-issuer/--oidc-audience). Keys come from the issuer’s JWKS and rotate automatically; only asymmetric algorithms matching the key are accepted (no alg: none, no HMAC key confusion); iss, aud and exp are required. The principal is a configurable claim and works with the access rules below. Static tokens and OIDC can be enabled together.
  • Authorization: per-directory read and write permissions and per-ref permissions (--acl rules.json). Principals with path-scoped read access never read objects by id: the server denies Objects.Get/Missing to them and serves filtered views instead (path- scoped checkout, browse, find, diff, and redacted history). Regional caches and node daemons serve objects by id without a policy, so expose them only to full-read principals.
  • Integrity: every object is hash-verified when accepted by a server, when received by a tier from upstream, and when materialised by a client.
  • Client filesystem safety: paths are validated (.., .pando, separators, NUL) and no write follows a symlink, so a malicious tree cannot write outside the working copy.
  • Resource limits: per-object size cap (256 MiB; larger files are chunked) with bounded decompression; bounded ids per request, history scan, find and blame work per request.

Audit trail

Run the server with --audit-log FILE to record who read or changed what, from where, with what result, including rejected requests. The format is described in the security guide.

Verifying a release

From 2.1.1 on, every release archive carries a signed SLSA build provenance attestation and an SPDX software bill of materials, both signed through Sigstore by the release workflow. Check an archive before installing it:

gh attestation verify pando-v2.1.1-x86_64-unknown-linux-gnu.tar.gz --repo kabirnarang39/pando

Container images on ghcr.io/kabirnarang39/pando carry build provenance and an SBOM as OCI attestations (docker buildx imagetools inspect --format '{{ json .Provenance }}' IMAGE). Each archive also ships a .sha256 checksum, which install.sh verifies.

Contributing to Pando

Thanks for helping. Pando aims to stay small, fast and correct, so contributions are held to a few concrete rules.

Setup

# Rust stable (see rust-version in Cargo.toml) and protoc
brew install protobuf        # or: apt-get install protobuf-compiler
cargo test --workspace       # unit, property, contract and end-to-end tests

Optional, for the database adapters:

docker run -d -p 55432:5432 -e POSTGRES_PASSWORD=pando -e POSTGRES_DB=pando postgres:17-alpine
PANDO_TEST_POSTGRES_URL=postgres://postgres:pando@localhost:55432/pando \
  cargo test -p pando-adapters --test contract

Where code goes

  • Domain logic → crates/pando-core. No I/O, no transport.
  • Infrastructure → a new adapter in crates/pando-adapters implementing a port. It must pass tests/contract.rs; add your adapter to that file.
  • A server feature → a new slice in crates/pando-server/src/features/<name>/ owning its use case, gRPC handler and feature flag (pando_core::flags::Flag).
  • A client feature → crates/pando/src/features/<name>.rs, returning data; printing belongs in main.rs.
  • Protocol changes → crates/pando-proto/proto/ (pando.proto, pando_platform.proto). v1 is stable: add fields and RPCs, never renumber or change the meaning of existing ones.

Before opening a PR

cargo fmt --all
cargo clippy --workspace --all-targets -- -D warnings
cargo test --workspace
  • Every behaviour change comes with a test that fails without it.
  • Performance-sensitive changes include before/after numbers (cargo bench -p pando-core or bench/run.sh).
  • Keep commits focused; use Conventional Commits (feat:, fix:, perf:, docs:…).

Tests for new functionality

Every new feature and every behaviour change lands with automated tests in the same pull request: unit tests next to the code, and an end-to-end test in crates/*/tests/ for anything a user can run. A bug fix includes a test that fails without the fix. Changes are not merged without them. See How Pando is tested.

Questions and discussion

Ask questions and discuss ideas in GitHub Discussions; track bugs and feature requests as issues. Both are public, searchable and open to anyone with a GitHub account. Issues, pull requests and code comments are in English.

Reporting bugs

Open an issue with the pando --version and pandod --version output, the command, what you expected and what happened. Security issues: see SECURITY.md.