diff --git a/bench/render.py b/bench/render.py index 29ff3830..331c8202 100644 --- a/bench/render.py +++ b/bench/render.py @@ -176,8 +176,8 @@ def clean_count(case: dict, tool: str) -> int: "", f'graft vs git, p50 ms (lower is faster)', 'teal graft, grey git. ' - "1k: vogt-prod, seed 2294, 5 iterations. " - "10k: graft-dev, seed 2294, 5 iterations. " + "1k host A: 24 CPUs, 62.6 GiB, seed 2294, 5 iterations. " + "10k host B: 4 vCPUs, 7.8 GiB, seed 2294, 5 iterations. " "Fresh CLI process per step.", f'', ] diff --git a/changes/WI-2542.md b/changes/WI-2542.md new file mode 100644 index 00000000..b96607fd --- /dev/null +++ b/changes/WI-2542.md @@ -0,0 +1 @@ +Added a user glossary and git-to-graft concept table to the guide. diff --git a/changes/WI-2544.md b/changes/WI-2544.md new file mode 100644 index 00000000..e766d434 --- /dev/null +++ b/changes/WI-2544.md @@ -0,0 +1 @@ +Added a complete MCP registration and tool reference, enforced by a test that every registered tool is documented. diff --git a/changes/WI-2545.md b/changes/WI-2545.md new file mode 100644 index 00000000..01bd70cc --- /dev/null +++ b/changes/WI-2545.md @@ -0,0 +1 @@ +Document server configuration, authorization, administration, health, and schema upgrades. diff --git a/changes/WI-2546.md b/changes/WI-2546.md new file mode 100644 index 00000000..4f1651a0 --- /dev/null +++ b/changes/WI-2546.md @@ -0,0 +1 @@ +- Add a Git migration guide covering lossless import, GitHub follow, batch pull-request delivery, full export, and the advanced authority switch. diff --git a/changes/WI-2547.md b/changes/WI-2547.md new file mode 100644 index 00000000..42037fc9 --- /dev/null +++ b/changes/WI-2547.md @@ -0,0 +1,2 @@ +Added a concise guide FAQ covering interoperability, offline use, lines, +conflicts, CI, git export, performance, licensing, and supported platforms. diff --git a/changes/WI-2548.md b/changes/WI-2548.md new file mode 100644 index 00000000..c8f1cb97 --- /dev/null +++ b/changes/WI-2548.md @@ -0,0 +1 @@ +- Benchmark documentation describes the measured hosts by CPU and memory instead of estate-specific names, and the agent guide now documents `graft serve --dev` as available. diff --git a/crates/graft-mcp/tests/guide.rs b/crates/graft-mcp/tests/guide.rs new file mode 100644 index 00000000..b6c3db36 --- /dev/null +++ b/crates/graft-mcp/tests/guide.rs @@ -0,0 +1,23 @@ +//! Documentation coverage for the public MCP tool registry. + +use std::fs; + +#[test] +fn mcp_guide_lists_every_registered_tool() { + let guide = fs::read_to_string( + std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../docs/guide/mcp.md"), + ) + .expect("read docs/guide/mcp.md"); + + let missing: Vec<&str> = graft_mcp::TOOLS + .iter() + .map(|tool| tool.name) + .filter(|name| !guide.contains(&format!("### `{name}`"))) + .collect(); + + assert!( + missing.is_empty(), + "docs/guide/mcp.md is missing registered MCP tools: {}", + missing.join(", ") + ); +} diff --git a/docs/agents.md b/docs/agents.md index 1593c235..57b1096c 100644 --- a/docs/agents.md +++ b/docs/agents.md @@ -13,7 +13,7 @@ export GRAFT_AUTHOR_NAME="Name" GRAFT_AUTHOR_EMAIL="name@example" The token is a bearer credential. Never print it, never pass it as an argument, never copy it into the checkout or a prompt. `graft login` copies it into a private token file and stores only that file's path in the user config. For command overrides, endpoint and token resolve independently in this order: `--endpoint` / `--token-file`, `GRAFT_ENDPOINT` / `GRAFT_TOKEN`, the working copy's named server, then the user's `default_server`. -`graft serve --dev` (a local server with a dev token) is also coming in v0.1.0. +`graft serve --dev` starts a local server with a dev token. ## The loop diff --git a/docs/guide/bench.svg b/docs/guide/bench.svg index 20c0cbe6..12eedf3b 100644 --- a/docs/guide/bench.svg +++ b/docs/guide/bench.svg @@ -14,7 +14,7 @@ } graft vs git, p50 ms (lower is faster) -teal graft, grey git. 1k: 24-CPU host, seed 2294, 5 iterations. 10k: graft-dev, seed 2294, 5 iterations. Fresh CLI process per step. +teal graft, grey git. 1k host A: 24 CPUs, 62.6 GiB, seed 2294, 5 iterations. 10k host B: 4 vCPUs, 7.8 GiB, seed 2294, 5 iterations. Fresh CLI process per step. 1,000 commits diff --git a/docs/guide/benchmarks.md b/docs/guide/benchmarks.md index 87602b63..eb55d5cf 100644 --- a/docs/guide/benchmarks.md +++ b/docs/guide/benchmarks.md @@ -11,8 +11,8 @@ CLI process for every step (graft and git alike): | Scale | File | Host | graft commit | | --- | --- | --- | --- | -| 1,000 commits | `bench/results/2026-10-23-scales-10-1000/results.json` | 24 CPUs, 62.6 GiB | `35c308759e8f` | -| 10,000 commits | `bench/results/2026-10-23-scale-10000/results.json` | graft-dev, 4 vCPU, 7.8 GiB | `2c32be2d39a3` | +| 1,000 commits | `bench/results/2026-10-23-scales-10-1000/results.json` | host A: 24 CPUs, 62.6 GiB | `35c308759e8f` | +| 10,000 commits | `bench/results/2026-10-23-scale-10000/results.json` | host B: 4 vCPUs, 7.8 GiB | `2c32be2d39a3` | Bars are p50 milliseconds. Lower is faster. Teal is graft, grey is git. Operations: full clone, sparse clone, `log -50`, `log --path`, blame, diff --git a/docs/guide/concepts.md b/docs/guide/concepts.md new file mode 100644 index 00000000..f5e80ab0 --- /dev/null +++ b/docs/guide/concepts.md @@ -0,0 +1,119 @@ +# Graft concepts for git users + +Graft is a server-centric source control system. A working copy contains the +files you edit, while the server owns history, workspaces, drafts, lines, and +the operations that move work between them. + +## Glossary + +### Repository + +A repository is a named namespace on a server. It contains the repository's +objects, changesets, refs, workspaces, and journal. + +### Object + +An object is an immutable, content-addressed value. A blob holds file bytes; +a tree describes one directory. + +### Changeset + +A changeset is the unit of work. It names its parents, records file changes +against its first parent, and identifies the resulting root tree. + +### Draft + +A draft is a changeset that no line has reached yet. `graft commit` creates a +draft from inline content or the working copy, and a workspace holds it while +it is active. + +### Landed + +A changeset is landed when it is on a line's first-parent history. Landed +changesets remain part of that line's history. + +### Line + +A line is a protected ref, such as `main` or `release/1.4`, that moves forward +through landed changesets. `graft line create` forks a new line at a changeset +and never overwrites an existing line. + +### Ref + +A ref is a named pointer to a changeset. Lines are movable refs; tags are +immutable refs. + +### Tag + +A tag is an immutable name for a changeset. `graft tag create` creates it once +and never moves it. + +### Workspace + +A workspace is a named, owned, leased server record with a base and a head. It +is the server-side twin of a working copy; `graft workspace` can show, list, +open, renew, or close workspaces. + +### Stack + +A stack is the drafts between a workspace's base and head, oldest first. +`graft stack` lists, squashes, reorders, splits, or drops those drafts. + +### Journal + +The journal is the repository's append-only, sequence-numbered record of +writes. Watchers and mirrors follow it without scanning repository history. + +### Base + +The base is the landed changeset on a line that a workspace starts from. +`graft sync` moves the workspace base to the line tip and rebases its stack. + +### Head + +The head is the newest draft in a workspace, or the base when the stack is +empty. Working-copy status is measured against the workspace head. + +### Land + +To land is to put the workspace stack on a line. `graft land` rebases the +stack on the server and moves the line only if its expected version still +matches. + +### Sync + +To sync is to move the workspace base to the line tip. The server rebases the +stack, and the client updates the working-copy files. + +### Line merge + +A line merge brings one line into another and records a merge point between +them. `graft line merge` uses a three-way merge and writes unresolved +conflicts into the working copy for resolution. + +### Conflict + +A conflict is a path that cannot be merged automatically. Text conflicts use +markers in the file; other conflicts use a `.graft-conflict` sidecar until the +workspace records a resolution. + +## Git to graft + +Graft keeps familiar command names where their meaning is close, but its +server-owned workspaces and drafts replace git's local-history workflow. + +| Git term | Graft term | What differs | +| --- | --- | --- | +| Repository | Repository | A remote repository is a named namespace on one server, not a collection of peer clones and remotes. | +| Commit | Changeset; `graft commit` creates a draft | The changeset is created on the server and is visible immediately; there is no later push of a private local commit. | +| Branch | Line for shared history; workspace stack for work in progress | Lines are few protected refs. Ongoing work stays as leased drafts rather than short-lived branches. | +| Tag | Tag | A graft tag is an immutable ref to a changeset and never moves. | +| Working tree | Working copy | The files are the client-side view of a server workspace, often limited to selected path prefixes. | +| Index / staging area | None | `graft commit` records the working copy's changes, or only the paths named by the caller. | +| Push | `graft land` | Land rebases the workspace stack on the server and advances the line with a compare-and-swap. | +| Pull / fetch | `graft sync`; object fetch | Sync moves the workspace base to the line tip and updates files. Object fetch retrieves named content rather than negotiating histories. | +| Merge | Land, or line merge | Ordinary work lands by server-side rebase. A line merge integrates one line into another and records a merge point. | +| Rebase | Server-side rebase during sync or land | The server rebases the workspace stack; the client resolves only paths that cannot merge automatically. | +| Cherry-pick | `graft pick` | Pick creates a draft that re-applies a landed changeset; conflicts are written into the working-copy files. | +| Revert | `graft revert` | Revert creates a draft that inverts a changeset, ready to land like any other draft. | +| Pull request | Review of a workspace stack before land | Graft itself has no pull-request object or branch-per-change requirement; review can use changesets, diffs, and workspace metadata directly. | diff --git a/docs/guide/faq.md b/docs/guide/faq.md new file mode 100644 index 00000000..5999cd5a --- /dev/null +++ b/docs/guide/faq.md @@ -0,0 +1,67 @@ +# Frequently asked questions + +## Why is graft not compatible with git on the wire? + +Graft is server-centric: the server owns workspaces, drafts, rebases, history, +and landing. It therefore does not speak the git wire protocol or store git +objects; familiar command names are used only where their meanings remain +close. + +## Can I use graft offline? + +A working copy of a remote repository has a bounded cache, so reads of cached +objects continue to work while disconnected. Remote writes wait for the server; +for fully local, single-user work, `graft init` runs the engine in the client +process and needs no network connection. + +## Are lines the same as branches? + +Lines are a small set of shared, protected histories rather than a feature +branch for every change. Work in progress lives in a leased workspace as a +stack of drafts, and landing rebases that stack on the server before moving the +line with compare-and-swap. + +## How do changes move between lines? + +`graft line merge` brings one line into another with a three-way merge and +records a merge point between them. The source line is read, not moved, and an +unresolved merge is returned to the working copy for resolution. + +## How do conflicts reach me? + +Graft reports conflicting paths and leaves the shared line unchanged. After +`graft sync` or `graft land --sync`, text conflicts appear as markers in the +files and other conflicts use `.graft-conflict` sidecars, so they can be +resolved and committed from the working copy. + +## Where does CI run? + +In phase one, graft runs no build, test, check, or checker anywhere, including +the land path. For git-primary repositories, CI stays on the forge and runs on +the batch pull request that carries landed changes back to git. + +## Can I go back to git? + +Yes. `graft mirror export` writes the full history back to git, and import keeps +raw tag objects so migration does not discard them; deterministic full export +is the exit path, not an afterthought. + +## Is graft faster than git? + +It depends on the operation and scale. Recorded results show graft ahead on +full and sparse clone, path log, blame, and some concurrent lands, but behind +on recent log, status, and sync; see the [benchmark results](benchmarks.md) for +the measurements and limitations. + +## What does AGPL-3.0-only mean for an internal server? + +The licence is AGPL-3.0-only; if users interact over a network with your +modified version, those users must be offered its corresponding source. See +the [FSF licence FAQ](https://www.gnu.org/licenses/gpl-faq.html#AGPLv3InteractingRemotely) +for the rule and consult your own counsel for legal advice. + +## Which platforms are supported? + +The documented release archives target Linux on x86_64 and aarch64, plus macOS +on Apple Silicon. There is no Intel macOS archive, and no Windows archive is +currently documented. diff --git a/docs/guide/git-migration.md b/docs/guide/git-migration.md new file mode 100644 index 00000000..673ae0cb --- /dev/null +++ b/docs/guide/git-migration.md @@ -0,0 +1,147 @@ +# Migrate a Git repository gradually + +Graft can sit beside an existing GitHub repository without changing where the +repository is authoritative. In the recommended setup for this release, Git +remains the authority: maintainers continue to protect and merge the default +branch in GitHub, while `graft-mirror` follows that branch into a graft line. +Work may land on a separate graft integration line and leave graft as a batch +pull request. + +The examples below use these placeholders: + +- `/srv/git/project.git` is a local bare clone of the GitHub repository. +- `project` is the graft repository name. +- `main` is both the GitHub default branch and its followed graft line. +- `integration` is the graft line that accepts native lands. +- `/etc/graft/mirror.toml` is the mirror configuration file. +- `/run/secrets/graft-mirror-token` is a file containing the graft token. + +## Import once + +Start with a bare Git repository that has the branches and tags you want to +preserve. Import its default branch into the corresponding graft line: + +```sh +graft-mirror import /srv/git/project.git \ + --repo project \ + --server https://graft.example.com \ + --db /var/lib/graft-mirror/project.sqlite \ + --token-file /run/secrets/graft-mirror-token \ + --line refs/heads/main=main +``` + +Import preserves the reachable commit graph, tags, commit headers, modes, and +the mapping between Git commit SHAs and graft changesets. It performs a +fidelity check for every commit: exporting an imported commit must reproduce +the original SHA. A commit with a path that graft cannot represent, such as a +non-UTF-8 or non-portable path, stops the import and reports the offenders. If +an operator deliberately accepts a known exception, repeat `--allow-lossy +` for those commits; otherwise do not bypass the fidelity check. + +## Follow the GitHub default branch + +Create a mirror configuration. Keep credentials in token files, not in this +file or on the command line: + +```toml +server = "https://graft.example.com" +repo = "project" +upstream = "https://github.com/example/project.git" +mirror_dir = "/var/lib/graft-mirror/project.git" +db = "/var/lib/graft-mirror/project.sqlite" +token_file = "/run/secrets/graft-mirror-token" +poll_interval_secs = 15 + +[github] +owner = "example" +repo = "project" +token_file = "/run/secrets/github-token" + +[export] +default_author = "Graft Export " + +[[lines]] +branch = "main" +line = "main" + +[[lines]] +branch = "graft/integration" +line = "integration" +``` + +Run the follower as a long-lived service. It fetches GitHub and advances the +configured graft lines when their Git branches move: + +```sh +graft-mirror follow --config /etc/graft/mirror.toml +``` + +The followed `main` line remains Git-primary. Do not land directly on it; +GitHub remains the place where its protected default branch moves. + +## Deliver lands as batch pull requests + +Land work on the graft `integration` line. To publish its pending lands, sync +them onto the followed GitHub base and create or update one batch pull request: + +```sh +graft-mirror pr-batch integration \ + --base main \ + --config /etc/graft/mirror.toml +``` + +Run the command again when more lands are ready. Each land is exported as one +commit in the batch. `graft-mirror` creates or updates the pull request but +never merges it. Review and merge that pull request with your normal process; +afterward, `follow` imports the resulting GitHub history and settles the batch. + +Graft runs no build, test, check, checker, or CI job when work lands. Required +checks run in the user's forge on the batch pull request, and the user's merge +process decides when the pull request may enter the protected branch. + +## Export the complete repository + +A full export is the exit path. It writes every line, tag, and landed +changeset that graft can reach into a bare Git repository: + +```sh +graft-mirror export \ + --to /srv/git/project-export.git \ + --repo project \ + --db /var/lib/graft-mirror/project.sqlite \ + --server https://graft.example.com \ + --token-file /run/secrets/graft-mirror-token +``` + +Keep the mirror database with the graft backup while migration is in progress: +it contains the imported Git headers and SHA mapping used for exact round trips. +The exported bare repository can be checked with ordinary Git tools and pushed +to a forge if you decide to leave graft. + +## Advanced: switch authority + +Switching a line from Git-primary to graft-primary is an advanced operation and +is not recommended in this release. It changes which system may accept writes, +so schedule it as an operator-controlled migration rather than a routine part +of onboarding. + +Inspect the current state, freeze writes, verify recent history, flip the +authority, and unfreeze only after the new authority is ready: + +```sh +graft-mirror authority status --config /etc/graft/mirror.toml --line main +graft-mirror authority freeze --config /etc/graft/mirror.toml --line main +graft-mirror authority verify --config /etc/graft/mirror.toml --line main --last 100 +graft-mirror authority flip --config /etc/graft/mirror.toml --line main +graft-mirror authority unfreeze --config /etc/graft/mirror.toml --line main +``` + +After a successful switch, the continuous exporter is the graft-primary bridge +back to GitHub: + +```sh +graft-mirror export --config /etc/graft/mirror.toml +``` + +Do not skip or reorder the authority steps. If verification finds divergence, +leave the line frozen and resolve it explicitly before flipping authority. diff --git a/docs/guide/mcp.md b/docs/guide/mcp.md new file mode 100644 index 00000000..c2648c76 --- /dev/null +++ b/docs/guide/mcp.md @@ -0,0 +1,255 @@ +# MCP guide + +Graft's MCP server exposes bounded source-control reads and writes to agents. +Every result is capped at 64 KiB. List and patch tools may return less than the +requested limit to stay under that cap. + +## Register the server + +From a Graft working copy, print the registration for your agent client: + +```sh +graft mcp --print-config claude +graft mcp --print-config codex +graft mcp --print-config cursor +``` + +The generated registration starts `graft mcp` over stdio. It never prints a +token. The token is read from Graft's configured server, a configured token +file, or `GRAFT_TOKEN`; it is never a tool argument. + +An agent without a checkout can connect by streamable HTTP to the Graft +server's `/mcp` endpoint. Configure that URL and its bearer token in the MCP +client rather than passing a token to any tool. HTTP provides repository and +workspace operations, while working-copy-only behavior is available only from +`graft mcp` over stdio inside a working copy. + +## Results, paging, and errors + +- Results are structured JSON. Tool arguments reject unknown fields. +- When a result has `next`, call the same tool again with that value as + `cursor`. Continue until `next` is `null`; a short page is not the last page. +- `graft_read` uses `next_offset`: pass it back as `offset` until it is `null`. +- Patches are capped. Each text patch is at most 4 KiB and reports + `truncated: true` when clipped; binary changes have a null patch. +- Errors are structured and include `kind` and `message`. API errors also + report `retryable`. Common kinds include `invalid_argument`, `not_found`, + `unauthenticated`, `forbidden`, `stale_base`, `conflict`, `contended`, + `unavailable`, and `needs_working_copy`. A `conflict` result identifies the + target and paths to resolve; `stale_base` means the line moved and the + workspace must be synchronized before retrying. + +## Read tools + +### `graft_log` + +Reads first-parent history, optionally restricted to paths. + +- Arguments: `repo` and `from` are required; `until`, `paths`, `limit` (1-100, + default 50), and `cursor` are optional. +- Result: `changesets` contains bounded changeset summaries and `next` is the + resume token. Keep paging with `cursor` while `next` is set. +- Errors: invalid refs or paths use `invalid_argument` or `not_found`. + +### `graft_show` + +Reads one changeset and its first-parent patch. + +- Arguments: required `repo` and `changeset`; optional `limit` (1-100, default + 20) and `cursor`. +- Result: `changeset`, `files`, and `next`. File entries include `path`, `op`, + `kind`/`id` for puts, `patch`, and `truncated`. Keep paging with `cursor`. +- Errors: an unknown changeset is `not_found`. + +### `graft_diff` + +Diffs two snapshots, optionally for selected paths. + +- Arguments: required `repo`, `from`, and `to`; optional `paths`, `patch` + (default true), `limit` (1-100, default 20), and `cursor`. +- Result: `files` and `next`; file entries use the same capped patch shape as + `graft_show`. Keep paging with `cursor`. +- Errors: invalid snapshots or paths are typed errors. + +### `graft_blame` + +Reads line origins for a bounded window of one file. + +- Arguments: required `repo`, `at`, and `path`; optional zero-based + `start_line` and exclusive `end_line` (at most 1,000 lines by default). +- Result: file identity and a `lines` array with line ranges and originating + changesets. Request another range to continue. +- Errors: missing or non-text files return a typed error. + +### `graft_read` + +Reads a bounded byte range from one file. + +- Arguments: required `repo`, `at`, and `path`; optional `offset` (default 0) + and `limit` (1-65,536 bytes, default 16,384). +- Result: `path`, `kind`, `encoding`, `data`, `offset`, and `next_offset`. + Continue by passing `next_offset` as `offset` until it is null. +- Errors: missing paths, invalid offsets, and directories are typed errors. + +### `graft_ls` + +Lists a directory at a snapshot. + +- Arguments: required `repo` and `at`; optional `path` (root by default), + `recursive` (default false), `limit` (1-500, default 100), and `cursor`. +- Result: `entries` with path, kind, and object id, plus `next`. Keep paging + with `cursor`. +- Errors: a missing directory or invalid path is typed. + +### `graft_status` + +Reads the current working copy's repository, workspace, and changed paths. + +- Arguments: required `repo`. +- Result: working-copy repository/workspace identity and a capped changed-file + summary. +- Errors: `needs_working_copy` outside stdio `graft mcp`, or when the requested + repository is not the current working copy. + +### `graft_upstream` + +Shows how far a workspace is behind its line and predicts whether its work +merges cleanly onto the tip. + +- Arguments: required `repo`; optional `workspace` (defaults to the current + working copy), `limit` (1-100, default 50), and `cursor`. +- Result: `line`, `workspace`, `base`, `head`, `tip`, `up_to_date`, `behind`, + `changed`, `overlaps`, `stack_conflict`, `clean`, and `next`. Keep paging the + `changed` list with `cursor`. +- Errors: `workspace` is required outside a working copy; missing workspaces + and refs are typed errors. + +### `graft_draft_overlap` + +Finds other live workspaces whose open drafts touch files or directory +prefixes you plan to change. It never returns file contents. + +- Arguments: required `repo` and `paths` (1-64); optional `line` (default + `main`), `include_own` (default false), `limit` (1-200, default 50), and + `cursor`. +- Result: `line`, grouped `overlaps` with workspace, owner, session, work item, + draft age, lease and paths, plus `next`. Keep paging with `cursor`. +- Errors: requires the repository drafts scope; otherwise returns `forbidden`. + +## Workspace and draft tools + +### `graft_workspace` + +Opens, reads, renews, or closes a server-side workspace. + +- Arguments: required `repo` and `action` (`open`, `get`, `renew`, `close`). + `name` is required except when opening; `line`, `at`, and `lease_ms` apply to + open/renew, and `keep_drafts` applies to close. +- Result: the workspace view (`name`, `line`, `base`, `head`, owner, + attribution, and lease), or close confirmation. +- Errors: invalid action-specific arguments, missing workspaces, or contention + are typed errors. + +### `graft_commit` + +Creates a draft on a workspace from inline file changes or current +working-copy changes. + +- Arguments: required `repo`, `workspace`, and `message`; `files` contains up + to 256 `{path, op, content, encoding, kind}` entries and at most 1 MiB of + content. `from_working_copy` selects current local changes. Optional `parent` + and `authored_at_ms` make retries deterministic. +- Result: draft changeset id, `parent`, and `authored_at_ms`. +- Errors: invalid content/path data, `needs_working_copy`, `stale_base`, or + workspace errors are typed. + +### `graft_sync` + +Rebases a workspace stack onto its line tip or a named target. + +- Arguments: required `repo` and `workspace`; optional `onto`. +- Result: updated workspace/base/head and any rebased draft ids. +- Errors: `conflict` reports the target and conflicting paths; contention and + missing refs are typed. + +### `graft_land` + +Lands a workspace stack by compare-and-swap onto one line. + +- Arguments: required `repo` and `workspace`; optional `head`, `line`, + `squash`, merge mode (`line` default or `file`), `check` (default true), and + `merged_from` for a resolved line merge. +- Result: landed changeset id, line, previous tip, and pre-land summary. +- Errors: `stale_base` means synchronize and retry; `conflict` supplies paths; + `contended` is retryable. The pre-land check never runs a build or test. + +### `graft_conflicts` + +Previews a workspace stack's conflicts against a target without moving refs. + +- Arguments: required `repo` and `workspace`; optional `onto`, `limit` (1-64, + default 16), and `cursor`. +- Result: target, expected workspace head, `conflicts` with bounded markers, + and `next`. Keep paging with `cursor`. +- Errors: invalid targets and workspace state are typed. + +### `graft_resolve` + +Rebases a conflicted stack using caller-provided resolutions. + +- Arguments: required `repo`, `workspace`, `onto`, and `resolutions` using the + same file-change shape as `graft_commit`; optional `expected_head` protects + the update with compare-and-swap. +- Result: updated workspace and rebased draft ids. +- Errors: incomplete or stale resolutions return `conflict` or `stale_base`. + +### `graft_revert` + +Creates a draft that inverses a changeset. + +- Arguments: required `repo` and `changeset`; optional `onto` and `workspace`. +- Result: new draft changeset id and workspace information. +- Errors: overlapping inverse changes return `conflict`; unknown changesets + and invalid workspace state are typed. + +### `graft_pick` + +Creates a draft that reapplies a landed changeset. + +- Arguments: required `repo` and `changeset`; optional `onto`, `workspace`, + and merge mode (`line` default or `file`). +- Result: new draft changeset id and workspace information. +- Errors: overlapping changes return `conflict`; unknown changesets and + invalid workspace state are typed. + +## Ref tools + +### `graft_line_create` + +Creates a new line at a changeset without overwriting an existing line. + +- Arguments: required `repo`, `name`, and `from`. +- Result: line name and target changeset. +- Errors: an existing line is `already_exists`; invalid names or refs are + typed. + +### `graft_merge` + +Merges one line into another while recording a merge point. + +- Arguments: required `repo` and source line `src`; optional target `into` + (default `main`), source `at`, `message`, and merge mode (`line` default or + `file`). +- Result: landed changeset, source and target line information, and merge + point. +- Errors: conflicts return `conflict`; stale or contended target updates are + typed and never move two refs at once. + +### `graft_tag` + +Creates an immutable tag at a changeset. + +- Arguments: required `repo`, `name`, and `at`; `tags/` is added when absent. +- Result: tag name and target changeset. +- Errors: an existing tag is `already_exists`; invalid names or refs are + typed. Tags never move. diff --git a/docs/guide/server.md b/docs/guide/server.md new file mode 100644 index 00000000..bfce01b7 --- /dev/null +++ b/docs/guide/server.md @@ -0,0 +1,233 @@ +# Server guide + +This page covers a Graft server's local-development start, production +configuration, authentication, repository and line administration, operational +endpoints, and database upgrades. + +## Quick start + +For a loopback-only development server, run: + +```sh +graft serve --dev +``` + +The command creates a `dev` repository, chooses a free loopback port, stores the +generated administrator token in the user configuration, and prints a `graft +clone` command. Its data defaults to `./.graft-serve` and its signing-key state +to `./.graft-serve-state`. Use `--repo`, `--listen`, `--data`, `--state`, or +`--server` to override those defaults. A non-loopback `--listen` also requires +`--insecure-bind`. + +For a configured server, `graft serve` runs the production server role. It reads +`--config `, then `GRAFT_SERVER_CONFIG`, then +`/etc/graft/server.toml`. + +## Configuration reference + +The configuration is TOML. Unknown keys and invalid combinations are rejected +at startup. Optional values shown as `none` are absent when omitted; required +values have no default and must be present when their table is configured. + +A minimal production configuration is: + +```toml +listen = "127.0.0.1:7878" +metrics = "127.0.0.1:9464" +objects_dir = "/var/lib/graft/objects" +meta_db = "/var/lib/graft/meta.db" +key_file = "/var/lib/graft/signing.key" +json_logs = true + +[[lines]] +name = "main" +policy = "direct" +``` + +### Server keys + +| Key | Type | Default | Meaning | +| --- | --- | --- | --- | +| `listen` | string | `"127.0.0.1:7878"` | Listener for gRPC, the JSON facade, MCP, metrics, and health endpoints. | +| `metrics` | string or none | none | Optional second listener serving only metrics and health endpoints. | +| `objects_dir` | path | `"/var/lib/graft/objects"` | Filesystem object-store root. | +| `meta_db` | path | `"/var/lib/graft/meta.db"` | SQLite metadata database. | +| `key_file` | path or none | none | Ed25519 signing-key seed. When omitted, `signing.key` beside `meta_db`; created on first use with mode `0600`. | +| `min_free_fraction` | number | `0.05` | Writes are refused when either storage volume's free fraction is lower. | +| `min_free_bytes` | integer | `0` | Writes are refused when either storage volume has fewer free bytes. | +| `json_logs` | boolean | `true` | Emit JSON logs instead of plain text. | +| `otlp_endpoint` | string or none | none | OTLP/HTTP trace endpoint. | +| `max_token_ttl_s` | integer | `43200` | Maximum lifetime, in seconds, for tokens minted through the API. | +| `shutdown_grace_s` | integer | `10` | Graceful-shutdown wait for in-flight calls, in seconds. | +| `max_watch_wait_ms` | integer | `60000` | Maximum idle wait requested by one watch call, in milliseconds. | +| `max_watch_lifetime_s` | integer | `3600` | Maximum watch-stream lifetime, also bounded by token expiry. | +| `max_watches_per_principal` | integer | `16` | Concurrent watch streams allowed per principal. | +| `max_watches` | integer | `1024` | Concurrent watch streams allowed server-wide. | +| `max_put_call_bytes` | integer | `67108864` | Maximum frame bytes buffered by one `PutObjects` call; cannot exceed 80 MiB. | +| `put_buffer_bytes` | integer | `536870912` | Shared frame-byte budget across in-flight `PutObjects` calls. | +| `put_wait_ms` | integer | `10000` | Maximum wait for shared put-buffer capacity; at most one hour. | +| `put_idle_ms` | integer | `30000` | Maximum gap between messages in one `PutObjects` call; at most one hour. | +| `ci` | table | defaults below | CI identity-exchange configuration. | +| `lines` | array of tables | `[]` | Per-line land-policy overrides. With no override, lines use `direct`. | +| `default_line` | table or none | none | Policy for lines not named by `lines`; it has no `name` key. | +| `gc` | table | defaults below | Scheduled garbage-collection configuration. | + +### `[gc]` + +| Key | Type | Default | Meaning | +| --- | --- | --- | --- | +| `gc.enabled` | boolean | `false` | Run the sweep scheduler. Deletion must also be enabled per repository. | +| `gc.interval_s` | integer | `3600` | Seconds between scheduled passes. | +| `gc.repos` | array of strings | `[]` | Repositories swept by the scheduler. | +| `gc.grace_s` | integer | `900` | Grace between marking an object doomed and deleting it; minimum 60 seconds. | +| `gc.retention_s` | integer | `604800` | Retention for abandoned and superseded drafts, in seconds. | +| `gc.verify` | boolean | `true` | Verify the plan against the ancestry walk before each pass. | + +### `[ci]`, `[[ci.issuers]]`, and `[[ci.bindings]]` + +With no issuer configured, CI token exchange is disabled. + +| Key | Type | Default | Meaning | +| --- | --- | --- | --- | +| `ci.max_token_ttl_s` | integer | `900` | Maximum exchanged-token lifetime; also capped by the source token. | +| `ci.issuers` | array of tables | `[]` | Trusted CI issuers. | +| `ci.bindings` | array of tables | `[]` | Mappings from CI repositories to Graft repositories. | +| `ci.issuers[].provider` | `github` or `woodpecker` | required | CI provider. | +| `ci.issuers[].issuer` | string | required | Exact expected issuer claim. | +| `ci.issuers[].audience` | string | required | Deployment-specific expected audience. | +| `ci.issuers[].jwks_file` | path or none | none | JWKS file read at startup; mutually exclusive with `jwks_url`. | +| `ci.issuers[].jwks_url` | string or none | none | HTTPS JWKS URL, or HTTP on loopback; mutually exclusive with `jwks_file`. | +| `ci.issuers[].jwks_refresh_s` | integer | `3600` | Refresh interval for URL-backed keys. | +| `ci.issuers[].leeway_s` | integer | `60` | Clock-skew allowance for `nbf` and `iat`; maximum 300 seconds. | +| `ci.issuers[].max_lifetime_s` | integer | `3600` | Maximum accepted `exp - iat`. | +| `ci.bindings[].provider` | `github` or `woodpecker` | required | Provider whose identities the binding accepts. | +| `ci.bindings[].ci_repo` | string | required | CI repository in `owner/name` form. | +| `ci.bindings[].repo_id` | string or none | none | Numeric CI repository ID that must match when set. | +| `ci.bindings[].graft_repo` | string | required | Graft repository available to the job. | +| `ci.bindings[].refs` | array of strings | `[]` | Allowed refs; an empty list allows any ref. | +| `ci.bindings[].workflows` | array of strings | `[]` | Allowed workflows; GitHub bindings require a workflow restriction. | +| `ci.bindings[].scopes` | array of strings | required | One or more `read` or `attest` scopes on `graft_repo`. | + +### `[[lines]]`, `[default_line]`, and path rules + +`[[lines]]` entries require `name`. `[default_line]` accepts the same keys +except `name`. + +| Key | Type | Default | Meaning | +| --- | --- | --- | --- | +| `lines[].name` | string | required | Line receiving this policy. Not valid in `[default_line]`. | +| `lines[].policy` | `direct`, `attested`, or `queued` | `direct` | Land policy. Only `direct` is enabled in this release; the others are parked. | +| `lines[].checks` | array of strings | `[]` | Required check names for non-direct policies. A direct line takes no checks. | +| `lines[].attest_merged` | boolean | `false` | Require attestations over merged changes; valid only with `attested`. | +| `lines[].authority` | `graft-primary` or `git-primary` | `graft-primary` | Which side of a mirrored line accepts writes. | +| `lines[].path_rules` | array of tables | `[]` | Prefix-specific merge and parked check/review rules. | +| `lines[].post_land` | table or none | none | Parked post-land policy. | +| `lines[].path_rules[].prefix` | string | required | Repository path prefix matched by the rule. | +| `lines[].path_rules[].checks` | array of strings | `[]` | Parked path-specific checks. | +| `lines[].path_rules[].human_review` | boolean | `false` | Parked path-specific human-review requirement. | +| `lines[].path_rules[].merge` | `line`, `none`, or omitted | omitted | `line` performs three-way line merge; `none` makes every concurrent change conflict. Omission uses the engine's normal merge behavior. | +| `lines[].post_land.checks` | array of strings | `[]` | Parked checks run by a post-land policy. | +| `lines[].post_land.auto_revert` | boolean | `false` | Parked automatic revert after a failed post-land check. | + +Policies other than `direct`, all check fields, human-review requirements, and +post-land behavior are not enabled in this release. A direct land never runs a +build, test, check, or checker. Path merge rules are available independently: +use `merge = "line"` for bounded text merge or `merge = "none"` for a conflict +whenever both sides changed a matching path. + +## Tokens and scopes + +Tokens are signed bearer tokens. Clients read them from a token file or from +`GRAFT_TOKEN`; command-line token values are not accepted. Each scope is one of: + +| Scope | Grants | +| --- | --- | +| `repo::read` | Read the repository. | +| `repo::write` | Write any repository path and read the repository. | +| `repo::write:` | Write only the prefix and its descendants, and read the repository. | +| `repo::land:` | Land on one line and read the repository. | +| `repo::attest` | Post attestations only; it does not grant read. | +| `repo::drafts` | Read other sessions' open drafts by path only. | +| `repo::admin` | Perform all repository operations, including attestations and draft-overlap reads. | +| `objects:read` | Read objects for repositories the caller can read. | + +Mint an operator token offline with the server administration command, piping +stdout directly to a mode-`0600` file or secret store: + +```sh +graft serve --config /etc/graft/server.toml admin token mint \ + --subject operator --kind human \ + --scope repo:example:admin --ttl 3600 > token.tmp +chmod 600 token.tmp +``` + +The default administration-command TTL is 30 days. API minting defaults to the +server's `max_token_ttl_s`, refuses a requested lifetime above that limit, and +never creates a token that outlives its caller. A non-service caller can mint +only its own identity and no broader scopes; a service principal may delegate a +different subject within its own scopes. + +Revoke a live token by passing it on standard input, never as an argument: + +```sh +graft serve --config /etc/graft/server.toml admin token revoke < token.tmp +``` + +Revocation stores only the token ID and expiry. It takes effect for writes; +read-only requests continue to accept an otherwise valid revoked token until it +expires. + +## Repositories and lines + +Create a repository with the offline administration command: + +```sh +graft serve --config /etc/graft/server.toml admin repo create example +``` + +The command is idempotent. With a server running, create a line from an +existing changeset or ref: + +```sh +graft --repo example line create release --from main +``` + +Line creation is compare-and-swap from absence: it never overwrites an existing +line. Land policy is server-wide by line name, not per repository. In this +release, use the default `direct` policy; configured `attested` and `queued` +policies are parked and not enabled. + +## Metrics and health + +Every main listener serves: + +- `GET /healthz`: liveness; returns `ok` while the HTTP service is running. +- `GET /readyz`: readiness; returns `ready` only after startup is ready to serve. +- `GET /metrics`: Prometheus text exposition, including a fresh free-space + admission sample. + +When `metrics` is configured, the second listener serves only those three +endpoints. Keep it on a trusted network; these endpoints do not require bearer +authentication. + +## Upgrade and schema versions + +The current SQLite schema version is **7**, stored in SQLite's `user_version`. +Opening the metadata database applies each missing migration in order, one +migration per transaction, before the server begins serving. Existing data is +preserved across supported forward upgrades. Released migrations are append-only +and are never edited; a crash leaves the database at either the old version or +the next version, never between them. A database whose schema version is newer +than the running binary is refused rather than downgraded. + +Back up the metadata database and object store before replacing a server binary. +Keep the signing key with the deployment: replacing it invalidates existing +tokens. + +## Running with compose + +Compose deployment instructions will be added below. + +## Backup and restore + +Backup and restore instructions will be added below.