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
- Try it in 5 minutesInstall, run a server on your laptop, import a git repository and land your first change.
- Run agents on itGive each agent a URL and a token over MCP. It reads, writes and lands without a clone.
- Beside GitHubMirror every landed change to GitHub, GitLab or Bitbucket, or move history, pull requests and issues across in one command.
- Self-hostOne binary: SQLite on one machine, or Postgres and S3 behind caches. Sign-in, backups, monitoring.
- Protocol referenceEvery gRPC call in
pando.v1, for clients in any language. - Grovedock CloudA hosted Grovedock is coming. Until then, the same server runs on your own machines, with no feature gates.
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
| git | Grovedock | |
|---|---|---|
| Clone | downloads all history | downloads one tree, lazily, only the paths you ask for |
| Commit | built locally, pushed as packs | the server builds it from the list of changed files |
Concurrent pushes to main | one wins, the rest fetch, rebase and retry | the server rebases changes that touch different files and lands them all (how) |
| Reading a file in CI | clone first | one call: pando cat main path |
log, blame, diff | need local history | server APIs; ancestry in O(log n) |
| Integrity | SHA-1 | BLAKE3, 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
- Point agents at it: Agents and MCP.
- Keep using git against the same server: Coming from git.
- Keep GitHub current while you try it: Mirror to a git host.
- Run it for a team: Deploy.
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
| git | pando | Notes |
|---|---|---|
git clone URL | pando clone URL DIR | add --path services/api to check out part of the repository |
git status | pando status | alias st |
git add + git commit -m | pando commit -m MSG [PATHS] | no staging area; pass paths to commit a subset |
git push | pando land | alias push; the server rebases if main moved |
git pull | pando sync | alias pull |
git switch -c x / git switch x | pando checkout -b x / pando checkout x | |
git log -- path | pando log path | answered by the server, no local history |
git diff / git diff A B | pando diff / pando diff A B | |
git show REV | pando show REV | |
git blame f | pando blame f | |
git show REV:path | pando cat REV path | works without a clone |
git ls-tree REV dir | pando ls REV [dir] | works without a clone |
git branch | pando refs | alias branch |
git merge-base A B | pando merge-base A B | |
| (none) | pando find PATTERN | path search on the server |
git lfs lock f / git lfs unlock f | pando lock f / pando unlock f | exclusive locks for files that cannot be merged |
git lfs locks | pando locks [dir] | |
| (none) | pando mount DIR --rev main | a 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 committakes paths instead ofgit add. - No local history.
log,blameanddiffbetween revisions ask the server. You need the server to be reachable; this is the main trade-off against git. - Landing replaces push. If
mainmoved, 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.
| Tool | What it does |
|---|---|
pando_repositories | repositories you can read, with today’s activity |
pando_read_file, pando_list_directory, pando_find_files | read code at any branch or change |
pando_history, pando_blame, pando_diff | who changed what, and when |
pando_write | write or delete files and land them as one change (or open a change request on a protected branch) |
pando_change_requests, pando_change_request | change requests and what blocks each |
pando_open_change_request, pando_comment, pando_review | propose, discuss, approve or request changes |
pando_land_change_request | land once nothing blocks it |
pando_report_check | report a CI result on a change request |
pando_issues, pando_issue, pando_open_issue, pando_comment_on_issue, pando_update_issue | work to do: read, open, discuss, close, assign |
pando_inbox | what needs you: assigned issues, requested reviews, changes requested on yours |
The agent loop
- Someone assigns an issue to an agent (
pando issue assign 12 agent/fix-01, or in the console). Anissuewebhook can wake the agent’s runner at once. - The agent calls
pando_inbox, reads the issue withpando_issue, and writes the fix on a branch withpando_write. - It opens a change request saying “Fixes #12” with
pando_open_change_request; the person it answers to is asked to review. - 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:
| Tool | What it does |
|---|---|
pando_cat | read a file at any revision |
pando_ls | list a directory at any revision |
pando_find | find files whose path contains a string |
pando_log | history, optionally for one path |
pando_blame | which change last touched each line |
pando_diff | diff between two revisions |
pando_status | uncommitted changes in the workspace |
pando_commit | create a changeset from workspace changes |
pando_land | land committed work on the branch |
pando_sync | update 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.
| Language | Package | Example |
|---|---|---|
| Python | sdk/python | Client(url).read_file("src/lib.rs") |
| TypeScript | sdk/typescript | await new Client(url).readFile("src/lib.rs") |
| Go | sdk/go | c.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
| History | every 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 repository | created 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(orGITHUB_TOKEN), or in a file with--github-token-file.--github-tokenworks 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-issuesand--no-pull-requestsskip 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 (
--dirto choose).
Options
| Option | Default | Meaning |
|---|---|---|
--to URL | (required) | where to push: any URL git accepts |
--server URL | $PANDO_SERVER, or the workspace’s server | the Grovedock server, with /OWNER/NAME on a server that hosts many repositories |
--rev REV | main | the Grovedock branch to mirror |
--git-ref REF | main | the branch to update on the git host |
--every SECONDS | once | mirror again every SECONDS until interrupted |
--dir DIR | your cache directory | the 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
| Role | Read | Write (land, push objects, lock) | Administer (access, settings) |
|---|---|---|---|
| Read, Triage | yes | ||
| Write, Maintain | yes | yes | |
| Admin | yes | yes | yes |
- 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-adminsadminister 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, oradmins), and by agents when it lets agents land directly. Everyone else lands through a change request: a direct land opens one for them (from areview/…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 rebasereplays 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 theReportCheckcall) 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)
| Page | What is on it |
|---|---|
| Pull request list | query 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 request | split into Conversation, Commits, Checks and Files changed tabs; sidebar forms for reviewers, assignees, labels, projects, milestone and linked issues; participants |
| Files changed | commit range picker (“Changes from all commits”), file filter, conversations, jump to file, diff settings, collapse and expand all |
| Commits | branch, author and date filters; grouped by day; copy SHA; expand description |
| Commit | file tree with filter; one collapsible region per file with copy-path and options; commit comments |
| Blame and file | branch picker, path breadcrumbs, raw/blame/history actions, symbols |
| Issues and issue | the same query-and-filters list; timeline with sidebar metadata |
| Compare, releases, actions, branches, organisation | base/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 today | Grovedock |
|---|---|
| A pull request is four tabs; reviewing means switching between the conversation, the diff and checks | One 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 slow | Diffs 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 reload | Everything 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 list | Agents 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 feature | Grovedock 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 code | Protection 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 slow | Clone 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 everything | A 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.
- 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. - 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. - 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.
- 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.
- Inbox, checks and integrations (done in 3.3, ADR-025). Review inbox and notifications, check runs reported by CI, webhooks, and an audit view.
- 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-privatecan’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/NAMEorbot/NAME) is active in an organisation for a calendar month (UTC) once it lands a change in one of its repositories: aLandthat succeeded, aCompareAndSwapthat 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:
| Limit | What happens |
|---|---|
| Suspended | Writes are refused with PermissionDenied (“organisation ORG is suspended; contact support”); reads work. |
| Active agents | A 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. |
| Storage | Over the limit, writes are refused with ResourceExhausted; reads work. |
| Seats | Adding a person beyond the limit is refused (changing a member’s role isn’t). |
| Private repositories | Creating 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:
- 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.
- Profiles. Clicking a person or agent should open their page: activity, repositories, owned agents.
- Create a repository and an organisation from the app.
- Mark files as viewed in a review, with progress, so large reviews can be resumed.
- Syntax highlighting inside diffs.
- Search inside files. The server’s
Findmatches paths only. - Edit settings (protection, access) once the server implements them; the demo shows them read-only.
- 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
| What | Limit | Change it with |
|---|---|---|
| One call’s running time | 600 s | --request-timeout |
| Calls in progress, whole server | 4,096 (more are refused with Unavailable, and clients back off) | --max-in-flight |
| Calls in progress, one connection | 256 | |
| Calls in progress, one account (platform servers) | 128 (more are refused with ResourceExhausted) | --max-calls-per-principal |
| One request message | 64 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 lifetime | 90 days by default, a year at most | pando token create --days |
| Browser session | 30 days | revoke the token to sign out |
| Email sign-in link | 15 minutes, once | |
| Webhooks per repository | 20, keeping each one’s last 200 deliveries | |
| Pre-land check | 10 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
| Event | Sent when | action |
|---|---|---|
push | a branch is created or moved (a land, a push, a rebase) | created, updated |
change_request | a change request is opened, closed or landed | opened, closed, landed |
review | someone reviews | approved, requested changes, commented |
comment | someone comments on a change request or an issue | created |
check | CI reports a check | reported |
issue | an issue is opened, closed, reopened or assigned | opened, closed, reopened, assigned |
ping | you ping the webhook | ping |
A webhook with no events listed receives every event.
Requests
POST with a JSON body and these headers:
| Header | Value |
|---|---|
X-Pando-Event | the event, e.g. push |
X-Pando-Delivery | a unique id for this delivery; retries keep it |
X-Pando-Signature-256 | sha256= 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-Deliveryid (also the payload’sdeliveryfield). - 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
-
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(orPANDO_WEB_ORIGINS) takes a comma-separated list. Browser access is thewebfeature flag, on by default; turn it off withPANDO_FLAGS=web=off. -
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:
- 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.
- 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.
- 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 (defaultSERVER/mcp; change it to wherever yourpando mcp --httpgateway answers, see Agents) and a ready-to-pasteclaude mcp addcommand.
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
| Feature | pandod | pandod --platform | Coming next |
|---|---|---|---|
| Code, blame, file history, search by path | yes | yes | |
| Changes as they land, word-level diffs | yes | yes | |
| Locks, branch switching, fleet timeline | yes | yes | |
| Many repositories, grouped by owner | one repository | yes | |
| People, agents, teams, roles and tokens | yes (3.2) | ||
| Sign in, new organisations, invitations | yes, with --login-listen | ||
| Change requests, reviews, line comments, land gate, rebase | yes (3.3) | ||
| Inbox, activity, protected branches | yes (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
| Keys | Does |
|---|---|
⌘K or Ctrl K, / | search files, repositories, reviews and actions |
t | go to a file in this repository |
g then h, i, c, l, r, k | home, inbox, code, changes, reviews, locks |
j, k | next and previous item in a list |
n, p | next and previous file in a review |
a | review the change request |
b, h, u | blame, 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
--platformis the registry: which repositories exist, and who may use them.--repos-metadataand--repos-objectsare templates:{repo}becomesOWNER/NAMEand{repo_id}becomesOWNER--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/NAMEto the server address; the web console lists them. A repository call that names none is refused: the server’s own--metadatarepository is not reachable in this mode. To keep a single-repository server’s history, create the repository and copy it in withpandod 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-privatelets 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
}
| Setting | Flag / 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 |
--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:
- Disable the repository (
pandod repo disable) so nobody fetches it meanwhile. - Rewrite the history without the secret:
pando export-gitto a git repository, remove the file or string withgit filter-repo, andpando import-gitinto a new repository. - 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. - 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
- Organisations, people and access: roles, teams, tokens, protected branches.
- Security and audit: single sign-on, access rules and the audit log.
- Backup and restore:
pandod copyandpandod backup, and how to restore a platform server. - Scaling and operations: caches, node daemons, compaction and monitoring.
- Configuration: every common setting in one table.
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"}
kind | Recorded for |
|---|---|
rpc | every authenticated request: principal, client address, method, gRPC status, duration |
auth_failed | every rejected request: client address, whether a token was presented, why (never the token) |
land | each land: branch, changeset, and the landed id or the refusal |
set_ref | branch moves through the refs API |
read_file, list_dir, checkout, find | content reads, with revision and paths |
log, blame | history queries, with revision and path |
lock, unlock | file 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):
- Stop
pandod. - Copy
platform.dbto the--platformpath. - For each entry in
manifest.json→repositories, copy itsfileto the path in itsmetadata(withoutsqlite://); fordefault, likewise. - Delete any
-waland-shmfiles left next to the files you replaced. - 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
| Process | Command | Runs where | State |
|---|---|---|---|
| Server | pandod server | behind a load balancer, any number; with --platform, exactly one (see below) | none; everything is in the stores |
| Regional cache | pandod cache --upstream URL | one or more per region | memory only |
| Node daemon | pandod daemon --upstream URL | one per CI machine | memory and local disk |
| Compaction | pandod compact --metadata URL | a scheduled job | none; 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):
| Path | Answer |
|---|---|
/metrics | Prometheus 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) |
/healthz | liveness: 200 ok while the process answers |
/readyz | readiness: 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
pando admin identity 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`
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.
| Setting | Flag / environment | Default |
|---|---|---|
| Metadata store | --metadata / PANDO_METADATA | sqlite://./pando-data/meta.db; also memory, postgres://… |
| Object store | --objects / PANDO_OBJECTS | metadata (in the metadata store); also file:///…, s3://…, gs://…, az://… |
| Bearer tokens | --tokens / PANDO_TOKENS | none; 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_FLIGHT | 600 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_MB | with --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_PROXY | off; 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_SYNCHRONOUS | full: 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_MINUTES | 256 open at once; one unused for 10 minutes closes. A repository in use is never closed |
| Audit retention | --audit-retention-days / PANDO_AUDIT_RETENTION_DAYS | 365 days of daily files; 0 keeps all |
| Single sign-on | --oidc-issuer, --oidc-audience, --oidc-claim, --oidc-require CLAIM=VALUE, --oidc-admin LOGIN | off; 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, --signup | off; see Signing in from a browser |
| Access rules | --acl / PANDO_ACL | allow everything |
| Pre-land check | --land-hook URL, --land-hook-token, --land-hook-timeout | off |
| TLS | --tls-cert, --tls-key | off |
| Metrics | --metrics / PANDO_METRICS | off; e.g. 0.0.0.0:9420 |
| Feature flags | PANDO_FLAGS | all 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
Objects
Tier-agnostic object service. Served identically by server, cache and daemon.
| Call | Request | Response | What it does |
|---|---|---|---|
Get | IdList | stream ObjectPayload | |
Put | stream ObjectPayload | PutObjectsResponse | |
Missing | IdList | IdList | Returns the subset of ids the tier cannot serve. |
Changesets
| Call | Request | Response | What it does |
|---|---|---|---|
Create | CreateChangesetRequest | ChangesetInfo | |
Get | ResolveRequest | ChangesetInfo |
Refs
| Call | Request | Response | What it does |
|---|---|---|---|
Get | RefName | RefValue | |
CompareAndSwap | CasRequest | CasResponse | |
List | Empty | RefList |
Land
Server-side rebase + lock-free compare-and-swap of a ref.
| Call | Request | Response | What it does |
|---|---|---|---|
Land | LandRequest | LandResponse |
History
| Call | Request | Response | What it does |
|---|---|---|---|
Log | LogRequest | stream ChangesetInfo | |
Blame | BlameRequest | BlameResponse |
Diff
| Call | Request | Response | What it does |
|---|---|---|---|
Diff | DiffRequest | DiffResponse |
MergeBase
| Call | Request | Response | What it does |
|---|---|---|---|
MergeBase | MergeBaseRequest | MergeBaseResponse |
Browse
API-first file access: CI and tools read source without any checkout.
| Call | Request | Response | What it does |
|---|---|---|---|
ListDir | BrowseRequest | ListDirResponse | |
ReadFile | BrowseRequest | ReadFileResponse | |
Find | FindRequest | FindResponse | |
Checkout | CheckoutRequest | stream CheckoutPiece | Path-scoped checkout: only files the caller may read. Used by principals with restricted read access, who may not read objects by id. |
Access
| Call | Request | Response | What it does |
|---|---|---|---|
WhoAmI | Empty | WhoAmIResponse |
Locks
| Call | Request | Response | What it does |
|---|---|---|---|
Lock | LockRequest | LockInfo | |
Unlock | UnlockRequest | Empty | |
List | ListLocksRequest | LockList |
pando_platform.proto
Platform
| Call | Request | Response | What it does |
|---|---|---|---|
Viewer | Empty | ViewerResponse | |
GetAccount | OrgName | Account | |
ListOrganizations | Empty | ListOrganizationsResponse | |
GetOrganization | OrgName | Organization | |
CreateOrganization | CreateOrganizationRequest | Organization | |
ListMembers | OrgName | ListMembersResponse | |
SetMember | SetMemberRequest | Member | |
ListTeams | OrgName | ListTeamsResponse | |
SetTeam | Team | Team | Creates the team, or replaces its name, description and members. |
DeleteTeam | TeamRef | Empty | |
ListRepositories | ListRepositoriesRequest | ListRepositoriesResponse | |
GetRepository | RepositoryRef | Repository | |
CreateRepository | CreateRepositoryRequest | Repository | |
UpdateRepository | UpdateRepositoryRequest | Repository | |
ListGrants | RepositoryRef | ListGrantsResponse | |
SetGrant | SetGrantRequest | Grant | |
ListTokens | Empty | ListTokensResponse | |
CreateToken | CreateTokenRequest | CreateTokenResponse | |
RevokeToken | RevokeTokenRequest | Empty | |
ListWebhooks | RepositoryRef | ListWebhooksResponse | |
CreateWebhook | CreateWebhookRequest | CreateWebhookResponse | |
DeleteWebhook | WebhookRef | Empty | |
ListWebhookDeliveries | WebhookRef | ListWebhookDeliveriesResponse | |
PingWebhook | WebhookRef | WebhookDelivery | Sends a ping event now. |
ListSignupAllow | Empty | ListSignupAllowResponse | Who may create an account by signing in when --signup waitlist: email addresses and @domain entries. Site administrators only. |
SetSignupAllow | SetSignupAllowRequest | Empty | |
LinkIdentity | LinkIdentityRequest | Empty | Lets an identity sign in as an existing account, e.g. the first administrator’s email address. Site administrators only. |
CreateInvitation | CreateInvitationRequest | CreateInvitationResponse | Invitations 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. |
ListInvitations | OrgName | ListInvitationsResponse | Invitations not yet accepted, including expired ones. |
RevokeInvitation | InvitationRef | Empty | |
GetUsage | GetUsageRequest | Usage | Plans and usage (ADR-035). An organisation’s active agents, people, storage and limits: its administrators and site administrators. |
SetLimits | SetLimitsRequest | OrgLimits | Replaces an organisation’s limits (not its suspension). Site administrators only. |
SetSuspended | SetSuspendedRequest | OrgLimits | A suspended organisation can read but not write. Site administrators only. |
GetCapabilities | Empty | Capabilities | What this server offers: anyone may ask. |
GetLicense | Empty | License | The support licence this server was started with. Site administrators only. |
Issues
| Call | Request | Response | What it does |
|---|---|---|---|
ListIssues | ListIssuesRequest | ListIssuesResponse | |
GetIssue | IssueRef | Issue | |
CreateIssue | CreateIssueRequest | Issue | |
UpdateIssue | UpdateIssueRequest | Issue | |
ListIssueComments | IssueRef | ListCommentsResponse | |
AddIssueComment | AddIssueCommentRequest | Comment |
Reviews
| Call | Request | Response | What it does |
|---|---|---|---|
ListChangeRequests | ListChangeRequestsRequest | ListChangeRequestsResponse | |
GetChangeRequest | ChangeRequestRef | ChangeRequest | |
CreateChangeRequest | CreateChangeRequestRequest | ChangeRequest | |
UpdateChangeRequest | UpdateChangeRequestRequest | ChangeRequest | |
ListComments | ChangeRequestRef | ListCommentsResponse | |
AddComment | AddCommentRequest | Comment | |
ResolveThread | ResolveThreadRequest | Comment | |
SubmitReview | SubmitReviewRequest | ChangeRequest | |
Land | ChangeRequestRef | LandChangeRequestResponse | |
Rebase | ChangeRequestRef | ChangeRequest | Re-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. |
ListProtections | RepositoryRef | ListProtectionsResponse | |
SetProtection | Protection | Protection | |
DeleteProtection | ProtectionRef | Empty | |
ReportCheck | ReportCheckRequest | ChangeRequest | |
Inbox | InboxRequest | InboxResponse | |
ListEvents | ListEventsRequest | ListEventsResponse |
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:
| Outcome | When | What you see |
|---|---|---|
| Landed as is | the branch has not moved since your base | landed c9bdddcbb14c on main |
| Landed, rebased | the branch moved, but nothing that landed since your base touched your paths | the server creates the rebased changeset and lands that; nothing to pull or retry |
| Refused | someone changed one of your paths since your base, a file you changed is locked, a pre-land check said no, or the branch is protected | the 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):
| Agents | git lands/s | Grovedock lands/s | Grovedock p99 | Grovedock errors |
|---|---|---|---|---|
| 16 | 2.0 | 758 | 55 ms | 0 |
| 200 | 0.5 | 1,012 | 302 ms | 0 |
| 1,000 | 0.2 | 504 | 2.66 s | 0 |
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 rebasereplays the change on the target’s tip when the two touch different files. - A lock.
pando lockreserves 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
- 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
Objectsservice. 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
| Concept | Representation |
|---|---|
| Object id | BLAKE3-256 of kind byte ‖ content (domain-separated) |
| Blob | file bytes; stored as kind ‖ zstd(content) |
| Tree | sorted (name, mode, id) entries (prost-encoded, canonical) |
| Changeset | parents, changes (path → blob or delete) relative to first parent, author, message, timestamp. Id = BLAKE3 of the canonical encoding |
| Ref | name → changeset id; moved only by compare-and-swap |
| Derived data | any 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) | RPCs | Flag |
|---|---|---|
objects | Get / Put / Missing | objects |
changesets | Create / Get | changesets |
refs | Get / CompareAndSwap / List | refs |
land | Land (server-side rebase + CAS) | land |
history | Log, Blame | history, blame |
diff | Diff | diff |
merge_base | MergeBase | merge-base |
browse | ListDir, ReadFile, Find | browse, search |
| (cross-cutting) | per-directory write ACLs, per-ref ACLs | permissions |
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
| Port | Purpose | Adapters |
|---|---|---|
ObjectStore | content-addressed bytes | memory, SQLite, Postgres, S3/GCS/Azure/local (object_store), BoundedCache, Tiered, GrpcObjects |
ChangesetLog | append-only changeset log | memory, SQLite, Postgres |
RefStore | CAS refs | memory, SQLite, Postgres |
DerivedStore | derived data cache | memory, SQLite, Postgres |
FeatureFlags | flags | SpecFlags (env) |
AccessPolicy | write/ref permissions | RulesPolicy (JSON), AllowAllPolicy |
Authenticator | identity | StaticTokens, OidcAuthenticator (JWT/JWKS), FirstMatch, AllowAll |
Remote (client) | server access | GrpcRemote |
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:
| Platform | Mechanism | Needs |
|---|---|---|
| Linux | FUSE | fuse3 |
| macOS | a read-only NFSv3 server on localhost, mounted with the system mount_nfs | nothing to install, no kernel extension, no admin rights |
| Windows | the 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 statususes size+mtime against the index and hashes only candidates, in parallel on all cores; the walker is ripgrep’s (ignore) and honours.gitignoreand.pandoignore(and the deprecated.sapignore).- SQLite runs in WAL mode with one writer and a pool of readers; Postgres batches use
UNNEST/ANYfor 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
| Scenario | git | Grovedock | Grovedock vs git |
|---|---|---|---|
| full-clone | 19,130 ms | 463 ms | 41.3x faster |
| sparse-clone (Documentation/) | 9,513 ms | 106 ms | 89.7x faster |
| read one file, no prior clone (CI) | 149 ms | 9 ms | 16.0x faster |
| status (clean, 4857 files) | 91 ms | 48 ms | 1.9x faster |
| log -n 50 – builtin/add.c | 100 ms | 19 ms | 5.4x faster |
| blame Makefile | 528 ms | 352 ms | 1.5x faster |
| commit + publish 1 file | 130 ms | 77 ms | 1.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
| git | Grovedock | |
|---|---|---|
| Client metadata | 334M (.git, full history) | 380K (.pando, index only) |
| Server storage | 333M (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 time | 42.8 s | 3.5 s |
| Throughput | 1.9 merges/s | 22.7 merges/s |
| Client retries | 456 | 0 (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.
| Agents | Layout | git lands/s | git p99 | Grovedock lands/s | Grovedock p50 | Grovedock p99 | Grovedock errors |
|---|---|---|---|---|---|---|---|
| 16 | one directory | 2.0 | 22.7 s | 758 | 18 ms | 55 ms | 0 |
| 200 | one directory | 0.5 | 25.2 s | 1,012 | 203 ms | 302 ms | 0 |
| 1,000 | one directory | 0.2 (5 lands) | 52.0 s | 504 | 2.10 s | 2.66 s | 0 |
| 1,000 | 64 directories | 907 | 1.17 s | 1.30 s | 0 | ||
| 2,000 | one directory | not run | 333 | 6.27 s | 6.80 s | 0 | |
| 5,000 | one directory | not run | 172 | 3.36 s | 15.8 s | 0 | |
| 5,000 | 64 directories | 531 | 9.26 s | 10.3 s | 0 |
“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
pandodon SQLite, a fresh store per run. git pushes to a local bare repository overfile://, 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 30againstpandod 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) withuploadpack.allowFilterenabled 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:nonefor single-file CI reads,--first-parentfor 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 pathis 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 -- pathandblameare 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-gitso you are never locked in.
Semantics differences
| git | Grovedock | |
|---|---|---|
| Commit | local snapshot | server-side changeset (list of changed files vs first parent) |
| Staging area | yes | no; pando commit [PATHS] commits all or a subset |
| Branches | cheap, everywhere | supported (pando checkout -b), but main is the default lane; refs are global state |
| History shown by log/blame | full DAG | first-parent (what landed) |
| Submodules | yes | not supported (skipped on import) |
| Hash | SHA-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.
| System | Checkouts/s | p50 | p99 |
|---|---|---|---|
git: shallow, blobless, sparse clone from git daemon (standard CI) | 2.7 | 3.71 s | 10.3 s |
git: --shared clone from a mirror on the same machine (no network) | 4.2 | 1.87 s | 5.6 s |
Grovedock: pando clone --path from the server | 5.8 | 0.35 s | 5.4 s |
| Grovedock: through the machine’s node daemon | 6.1 | 0.43 s | 4.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, whichpando mountdoes. - 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 daemonconnections 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 existswhen 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 DIRplus--server,--node,--git-urlor--mirrorfor 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.
| System | Organisation | Published scale | Ideas Grovedock shares | Open source? |
|---|---|---|---|---|
| Piper and Clients in the Cloud (CitC) | ~1 billion files, ~35 million commits, 86 TB (2015) 1 | Server holds history; workspaces show the whole tree but store only what you change | No | |
| Sapling, Mononoke, EdenFS | Meta | Repositories with millions of files and very long histories 2 | Operations scale with the files you use, not the repository; lazy virtual checkouts | Client yes; Mononoke and EdenFS not supported for outside use 3 |
| VFS for Git (GVFS) and Scalar | Microsoft | Windows: ~3.5 million files, ~300 GB repository (2017) 4 | Fetch only what is used; virtualize the working directory | Yes (MIT), git-based |
| Redesigning source control | Anthropic | Tens of thousands of merges a day from humans and agents 5 | Server-side commits and rebases, lock-free landing, tiered caches, API-first reads | No |
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 incrates/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.
-
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. ↩
-
Meta Engineering, Sapling: Source control that’s user-friendly and scalable, November 2022. ↩
-
Brian Harry, The largest Git repo on the planet, May 2017. ↩
-
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
| Item | Why |
|---|---|
| Platform registry on Postgres | several pandod replicas sharing accounts, reviews and webhooks, for high availability |
| Email notifications | review requested, assigned, mentioned, with per-person settings |
| Per-person and per-token rate limits | fairness on shared servers |
Review comments carried across pando migrate | pull requests arrive with their discussion |
| Path-history index | log -- path walks first-parent history today |
| Writable virtual checkouts | pando 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, whichscripts/build-site.shfills 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.gzwith 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 toindex.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 thepandoname until 3.6. - The website moves to https://grovedock.com/ (landing at
/,/pricing.html,/docs/,/console/); oldkabirnarang39.github.io/pando/links redirect. A server that lists the hosted console in--web-originsshould listhttps://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.mdsaid git manages 2 lands/s at 200 agents; the benchmark measured 0.5.docker compose upfailed: every service listens on the network without a token, which pandod refuses. The compose file now requiresPANDO_TOKENin.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, sopando protectandpando crwork 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--platformserver no longer creates an unused./pando-data: its default--metadatais nowmemory.pando admin limits suspendsays when the server doesn’t enforce limits (not--multi-tenant);pando org usageprints dates instead of epoch milliseconds.pando migrateprints thepando mirrorcommand for any git source, not only GitHub.- The working copy reads
.pandoignore;.sapignorestill works, with a deprecation warning. pando webhook add --helplists theissueevent;pandod backup --helppoints todocs/backup.md.- The console demo on the public site no longer asks for
/login/providers(a 404). docs/platform.mdno 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--tokennorPANDO_TOKENis given;pando logoutrevokes it andpando whoamishows 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. Viewerreturns 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/NAMEtakes a repository down at once without deleting it, andpandod repo delete OWNER/NAMEremoves it and its storage; documented with a secret purge procedure in docs/deploy.md.--trust-proxy: audit-log client addresses fromX-Forwarded-Forsent 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 normalrestores the previous behaviour.file://object stores fsync every object and its directory (ADR-034). --metricsalso serves/healthzand/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-originsis now--cors-origin(the old name still works);pando mcp --httptakes--allowed-originand 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/Listshows 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 RPCsGetUsage,SetLimits,SetSuspended,GetCapabilitiesandGetLicense; commandspando org usageandpando 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 FILEandpandod license verify FILEcheck 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 underdefault-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--signupallows (default: a waitlist, managed withpando admin signup allow|remove|list).pando admin identity linklets 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=VALUEbinds 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 anagent/orbot/account. Unknown signing keys refetch the key set at most once a minute. -
Paths named
.gitor.pandoin 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 fromPANDO_GITHUB_TOKEN(orGITHUB_TOKEN) or--github-token-file;--github-tokenwarns 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
--metadatarepository with no access check, so any signed-in caller (or any token, however limited) could read and write it. A non-UTF-8x-pando-repoheader 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-privateto 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 backupsnapshots a platform server’s registry and every repository’s database to a directory or object store while it runs;docs/deploy.mddocuments 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/net0.60.0 andgolang.org/x/text0.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 migratekeep 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 thepando mirrorcommand 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;issuewebhook events let an agent runner start work as soon as an issue is assigned. - Remote MCP (ADR-026):
pando mcp --http ADDR --server URLserves 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,commentandcheckevents 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|removeand repository Settings → Webhooks in the console. pando landon a protected branch opens a change request from areview/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 rebasereplays 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 grantandpando token, and the same in the web console (Tokens, organisation People, repository Settings → Access). Tokens start withpando_, 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 LOGINcreates 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 addressSERVER/OWNER/NAME; calls without a repository reach the default one (ADR-022). pando repo create OWNER/NAMEandpando repo list.- The web console lists a server’s repositories and shows only what the server supports.
Changed
pandod serverandpandod cachelisten 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 anypandod; a demo of the full platform (several repositories, change requests, reviews, a land gate and rebase) runs in the browser. See docs/console.md. pandodaccepts gRPC-Web from browsers (webfeature flag, on by default) and allows the origins in--web-origins/PANDO_WEB_ORIGINSto 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_TOKENSor an OIDC issuer,pandodrefuses to listen on a non-loopback address unless--allow-anonymous/PANDO_ALLOW_ANONYMOUS=1is 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 mounton macOS and Windows. macOS serves a read-only NFSv3 view on localhost and mounts it with the systemmount_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 againstpando clone(ADR-020).pando mount --serve-onlyon any Unix prints the mount command instead of mounting.PANDO_LOG=debugturns 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 thelocksfeature 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 rebasedrops 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’st/). - The MCP tools still used the pre-rename prefix:
sap_cat,sap_landand the others are nowpando_cat,pando_landand 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--serverflag they do not have; the error now points toPANDO_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.rsandbench/git_push_load.py: landing throughput with N concurrent agents, for Pando and git.
Fixed
bench/concurrent_land.pyfailed after the rename; README links to the benchmark page pointed at a file that still had the old name.pandod compacton 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 compactis 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 URLspando::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; PyPIpando-vcs(importpando); npmpando-vcs; Go modulegithub.com/kabirnarang39/pando/sdk/go/v2; imageghcr.io/kabirnarang39/pando; Homebrewkabirnarang39/tap/pando.
- Binaries:
- 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-gitaccepts 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, flagdelta-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::replaceon memory, SQLite, Postgres andobject_storebackends (contract-tested).- Install with Homebrew:
brew install kabirnarang39/tap/sapwood. - Release workflow publishes to crates.io (
sapwood,sapwood-serverand 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(binariessapandgit-remote-sapwoodare unchanged):cargo install sapwood. The protocol files moved tocrates/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], flagland-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. NewLandCheckport for in-process checks.
1.7.0 - 2026-10-15
Added
- Virtual checkouts on Linux (
sap mount DIR [--rev main] [--objects URL], flagvirtual-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], flagdaemon-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:
readgrants in the access rules. Restricted principals are denied raw object reads and get path-filtered views instead: streamingCheckout, filteredListDir/Find, diffs limited to readable paths, and redacted history. NewAccess.WhoAmIletssap clone/syncswitch to path-scoped checkout automatically. Rules withoutreadkeep 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-gitand ongit pushthrough the bridge. sap commitreports 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, flagoidc): 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/exprequired. 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-sapwoodlets 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=strictrestores git’s behaviour). Feature flaggit-bridge. Verified by cloning git/git’s 24,466 mainline commits through plaingit clone(136 s, identical tree). - Project website (GitHub Pages) and a checksum-verifying
install.sh.
1.0.1 - 2026-10-15
Fixed
sap commitfailed with “xis 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 byfile_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
ObjectsRPC 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) andexport-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:
- no change is in the branch’s history twice;
- every land the server acknowledged is in the branch’s history;
- the branch’s tree equals replaying its history;
- 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
| Bug | Found by | Fixed in |
|---|---|---|
Parallel checkout writers creating the same directory failed with File exists | CI checkout benchmark | 2.1.1 |
| One transient storage error failed every change in a land batch | simulation | 2.1.2 |
| A flush that failed halfway left a record pointing at data never written | simulation | 2.1.2 |
| A land retried after a lost reply applied the change twice | simulation | 2.1.2 |
| Two replicas landing the same retried change could still apply it twice | chaos test | 2.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
| Version | Supported |
|---|---|
| 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:tokenpairs), compared in constant time. Run with TLS (--tls-cert/--tls-key) whenever traffic leaves the host. WithoutPANDO_TOKENS, OIDC or platform tokens the server allows anonymous access, logs a warning, and refuses to listen beyond loopback unless--allow-anonymousis 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 (noalg: none, no HMAC key confusion);iss,audandexpare 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 deniesObjects.Get/Missingto 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-adaptersimplementing a port. It must passtests/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 inmain.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-coreorbench/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.