The OpenEng Git app is a browser-based git workbench backed by a local, sealed engine. The UI lives at git.openeng.app; every git operation actually runs in a small gRPC server bound to 127.0.0.1:50053 on your own machine, against your own working trees and credentials. Nothing about your repositories or the credentials that authenticate a push goes to OpenEng: git work runs on your machine, push, pull and fetch go to your remotes, and the CI & CD view calls your forge from the browser. Apart from CI & CD, the browser is a thin renderer; the engine does all the git work with libgit2 and your own git binary.
The Git engine is not published yet
The web app at git.openeng.app is live, but no openeng-git engine binary has been published for any platform yet. The installer at https:// exists, but it stops with a message that no Git engine binary is published for your platform, so the app cannot be paired until the engine ships. Git is free, like every engineering app. This page describes the app and engine as built, so it is ready when the engine is released.
Overview & architecture
The Git app follows the same "one engine, many surfaces" split as the rest of the OpenEng suite. The web UI issues repo-scoped operations; a locally-running engine executes them and returns structured results. The single boundary between the two is a per-frame-encrypted gRPC channel over loopback, guarded by a handshake token (see the origin caveat below).
Web UI
Served from git.openeng.app. Renders the workspace overview, repo workbench (Changes / History / Branches), diff and commit-graph panes. Navigation is URL-driven, so refresh and deep-links restore the exact repo, tab, file, or commit.
Local engine
The openeng-git binary running serve on 127.0.0.1:50053. Implements every RPC in git.proto. One engine per machine, coexisting with the other app engines, such as Terminal (50052). The OpenEng AI engine binds no local port, so it never competes for one.
Sealed transport
gRPC-Web over TLS, reached at https://. That host resolves to 127.0.0.1 everywhere but carries a publicly-trusted certificate, so the browser connects with no manual trust step while the socket stays pure loopback. The exact-origin lockdown of the Linux Terminal, Kubernetes and Data engines is still pending here: in its current source the Git engine answers any web origin and hands its handshake token to any caller.
Note
The engine contacts OpenEng only to pair and keep its sign-in fresh (api.openeng.app: the one-time-key exchange at boot, token renewal and the engine certificate) and to check for updates (releases., at start and every 30 minutes), and, when an update exists, downloads it from openeng.app. Every git operation is local, except the network operations you ask for (push, pull and fetch go to your remotes). Your repositories and credentials never go to OpenEng.
Two backends, chosen per operation
The engine deliberately uses two mechanisms and routes each operation to whichever is stronger:
libgit2(thegit2crate) for read-heavy, structured work: status, diff, log, single-commit detail, branch listing (with ahead/behind), blame, stash listing, tags, remotes, branch create/checkout/delete, and plain (unsigned) commits.- Your own
gitbinary (shelled out) for what libgit2 does poorly or not at all, and — crucially — anything touching the network or your signing keys: push, pull, fetch (streamed), merge, stash save/apply/pop/drop, GPG/signoff commits, and cross-history search.
Shelling out to git is what lets remote operations transparently reuse your local credential helper and SSH agent: the child process inherits your environment, so authentication "just works" without a single credential ever crossing the gRPC wire.
Getting connected
Opening git.openeng.app lands on the pairing gate (/pair) until a local engine is reachable and a session is established. If no engine is running, the screen shows a copy-paste command; the app connects automatically the moment the engine comes up (via Retry or the background heartbeat) and returns you to the originally requested URL.
Booting the engine
The pairing screen mints a single-use, short-lived OAuth key and shows a command of the form:
openeng-git serve --oauth=<one-time key>
OAuth is the only supported boot path — there is no offline or static-key mode. The engine consumes the key, authenticates you, binds loopback, and advertises its endpoint for the browser to discover.
The handshake & per-frame seal
CreateSession is the only unsealed RPC — it bootstraps the seal. The client offers a protocol version and an X25519 public key; the engine replies with its own public key, a session id (ses_<uuid>), its protocol version, and a compatibility verdict. From then on every frame is sealed with a fresh ephemeral X25519 key, HKDF-SHA256 and ChaCha20-Poly1305, so even the loopback payload is encrypted. The key exchange isn't authenticated, a session opened without a client key isn't sealed, and a request frame that fails to unseal is passed on as it arrived.
The handshake response also carries three display-only fields, read from your global git config and home directory:
| Field | Source | Purpose |
|---|---|---|
user | git config user. | Shown in the header; never a credential. |
email | git config user. | Display only. |
default_root | Your home directory | Placeholder for the manual "add repo by path" field. |
Protocol compatibility
The wire contract is versioned. The engine currently speaks openeng.git.v1 (version 1.0). Compatibility is major-only: a client and engine with the same major interoperate (minor bumps are additive and backward-compatible), while a major mismatch is fatal and the session response carries human-readable "update your client/engine" guidance in mismatch_reason.
Invariant
The schema has no field for an OpenEng secret or product prompt, and none for a credential, API key, password, or private key: remote auth uses your local git credential helper or SSH agent. A test in the proto crate checks the field names. The only legitimate payload is your own repo data: paths, diffs, commit messages, and refs.
Using the app
This section walks through the workbench at git.openeng.app as built. The sections after it are the reference for what the engine does underneath.
The workspace
- Left menu: Overview, Repos, CI & CD, Requests, and Insights, with Settings at the bottom. Every view has its own URL:
/overview,/cicd,/requests,/insights,/settings, and, for a repository,/,repos/ <repo-id>/ changes /,repos/ <repo-id>/ history /, andrepos/ <repo-id>/ branches /for one commit (the selected file rides along asrepos/ <repo-id>/ history/ <commit> ?file=, plus&staged=1for its staged side). A refresh, a shared link, or back and forward restores the exact view. The old/connectpath opens/settings, and any other path opens/overview. - Header: a button that hides or shows the side panel, the git server switcher, a status dot for the engine, a search button for the command palette, and your account menu. The server switcher lists All servers and each git server (forge host) with its repository count. Picking one limits the repository lists, including the one in CI & CD, to repositories whose remote is on that host. The list ends with Add server… and Settings.
- Command palette: ⌘+K (Ctrl+K on Windows and Linux) jumps to any section or opens any repository's workbench.
Overview
Overview (/overview) is a dashboard across all your repositories, built only from what your engine reports:
- Stat cards: Repositories (how many, across how many workspace folders, with a sparkline of commit activity), Uncommitted (how many repositories have changes), and Open requests and Pipelines today. The last two need forge data the engine does not have, so they show
0with the hint "connect a forge". - Repository health table: each repository's branch (or
detached), how far it is ahead of or behind its upstream (or in sync), its working tree (Clean, Modified, or Conflicts), and a CI column that shows—. Click a repository's name to open it. - Needs attention: repositories with merge conflicts first, then repositories behind their upstream, then repositories with uncommitted changes. When nothing qualifies, it says "Everything is in sync".
- Recent activity: the newest commits across your repositories (up to 8 from each, 16 in total), with author, message, repository, branch, and age.
- Add repository takes you to the Repos section.
Repos and the workbench
- The Repos side panel lists every repository with its branch, an uncommitted-changes marker, and ahead/behind counts. The engine caches the list, so it appears instantly and shows "refreshing…" while it updates. The panel has refresh, remove, and collapse buttons.
- Opening a repository opens its workbench: a header with the repository's name, current branch, and a Push button (it pushes the current branch and sets its upstream, with a live progress log); a navigator with Changes, History, and Branches tabs; and a detail pane on the right.
- Changes: staged and unstaged files, each with an M/A/D badge and line counts. Tick a file's checkbox to stage or unstage it, or use stage all and unstage all. Click a file to see its diff (working tree or staged) with line numbers, renames marked, and "Binary file — no textual diff." for binaries. Write a commit message (Enter commits, Shift+Enter adds a line) and click the commit button, which reads like "Commit 2 files to main" and stays disabled until something is staged and the message is not empty. See Staging & committing.
- History: the commit list, with summary, short hash, and age, while the right pane shows the Commit graph: branch lanes, merge joins, a ring on
HEAD, and branch and tag labels. Click a commit in either one to see its full message, author, time, and the diff of every file it changed; the × closes it and brings the graph back. - Branches: the right pane shows the same commit graph. The navigator lists local branches with a tracking label (
↑n,↓n,synced, orlocal); click a branch to check it out, and use + to create a branch (it is checked out right away). The current branch's card has Push and Pull buttons with a live progress log, and Remotes lists the configured remotes.
CI & CD
CI & CD (/cicd) shows a repository's latest pipeline or workflow runs (up to 25), read from its forge: GitHub Actions, GitLab pipelines, Bitbucket Pipelines, and Gitea Actions (best effort). Pick a repository in the side panel; the app works out the forge from the repository's remote URL. Each run shows its status, name, branch, commit, start time, duration, and a link to the run on the forge. Azure DevOps runs are not wired up yet, and hosts set to Other are not supported. A repository with no remote shows a message saying so.
The first time, the section asks for an API token, with steps for creating one and the scope it needs:
| Forge | Token scope |
|---|---|
| GitHub | Actions: Read-only (fine-grained token), or repo and workflow (classic token) |
| GitLab | read_api |
| Bitbucket | Pipelines: Read (a repository access token, not an app password) |
| Gitea / Forgejo | read:repository |
These requests go straight from your browser to the forge's API. The token is saved only in that browser (never in the engine or its cache), and it is only ever sent to the API address it was saved for; if a server's address changes, the app asks you to confirm the token again. If the forge rejects the token (HTTP 401 or 403), the token form comes back.
Settings
Settings (/settings) has two categories:
- Repositories: your repositories. Add a repository opens your system's folder picker on your machine (through the engine), or choose enter a path manually to type a path relative to your home directory. Detected repositories appear on their own; see Managing repositories. Removing a repository only forgets it; the files on disk are not touched.
- Connect: your git servers. Hosts found in your repositories' remotes appear automatically (tagged "auto"). Add server asks for the platform (GitHub, GitLab, Bitbucket, Gitea / Forgejo, Azure DevOps, or Other), a name, the host or, for a self-hosted forge, its API base URL, an API token, and an SSH key label. The server list is shared through the engine with every browser on the machine; the token stays in the browser you entered it in, and Clear saved token removes it. The SSH key label is only a label: push, pull, and fetch keep using your ssh-agent and git credential helper, exactly as on the command line.
Requests and Insights
Requests (pull and merge requests) and Insights (activity trends) are placeholders marked "Not connected"; they do not show any data yet.
Managing repositories
Every operation except the handshake is repo-scoped by a stable repo_id. The engine keeps a registry of your working trees, populated two ways and persisted so it survives restarts and is shared across browser tabs.
How repositories enter the registry
- Detection — a bounded scan (depth 3) of your home directory plus conventional workspace roots (
Projects,projects,src,Code,code,dev,Developer,workspace,git,repos,work), with$HOMEitself scanned shallowly. The scan skips heavy/noisy directories (node_modules,target,vendor, and top-level dotfolders) and stops descending once it finds a.git, so it stays fast on a large home. - Explicit —
AddRepo(path)for a directory you point at. The path is validated (it must be a git working tree — have a.gitdirectory/file, or be openable by libgit2) and canonicalized to an absolute path before it is stored.
Each repo's id is the first 16 bytes of the SHA-256 of its absolute path, rendered as 32 hex characters — so the same path always maps to the same id, across restarts and detection runs. The registry (a list of paths) persists to ~/.
The repository RPCs
| RPC | Returns | Notes |
|---|---|---|
ListRepos | Every registered repo with live facts | Each entry carries id, path, name, current branch, dirty flag, and ahead/behind. A facts failure degrades to an identity-only entry rather than failing the whole list. |
AddRepo | The new (or existing) repo | Idempotent — re-adding a path returns the same entry. Rejects a non-repo with an invalid-argument error. |
RemoveRepo | removed: bool | Forgets the repo from the registry only. It never touches the working tree on disk. |
GetRepoInfo | Facts + remotes + upstream | Adds head_oid, head_summary, the configured remotes, and the upstream ref (e.g. origin/main). |
Repo facts
For any repo the engine computes, on demand: the current branch (or the short OID / (detached) for a detached HEAD), a dirty flag (any staged, unstaged, or untracked change), the HEAD OID and summary, the configured upstream, and how many commits the branch is ahead/behind that upstream (via libgit2's graph_).
Native folder picker
Because a browser cannot read your filesystem, the "Add repository" flow can pop the OS-native directory chooser on your machine via the PickFolder RPC, and hand the chosen absolute path to AddRepo. It is fail-soft: a canceled dialog, a headless host with no display, or any dialog error all return an empty path with cancelled: true — the RPC never errors. On Linux it uses the GTK chooser directly; on macOS it drives osascript as a separate process, because an in-process Cocoa panel needs a main-thread run loop the headless engine doesn't have.
Tip
In the app, adding repositories lives under Settings → Repositories: use Add a repository to browse, or enter a path manually to type one relative to your home directory. Detected repos appear automatically.
Working tree: status & diff
Status
GetStatus returns a porcelain-style view grouped into three lists: staged (index differs from HEAD), unstaged (working tree differs from index), and untracked. A single modified-and-staged file legitimately appears in both the staged and unstaged groups, each with its own two-character XY code. Rename detection is enabled on both the head→index and index→worktree sides, so renames surface with an orig_path.
Each entry carries per-file line counts (additions/deletions), computed best-effort — binary files and any counting error degrade to 0/0 rather than failing. Untracked files are reported with the ?? code and no counts.
StatusEntry {
path // repo-relative path
status // two-char XY code: "M ", " M", "??", "A ", "D ", "R ", "UU", …
staged // belongs in the staged group
untracked
additions deletions
orig_path // set for renames
}
Diff
GetDiff is a single RPC with three modes, selected by which fields you set:
| Fields set | Diff produced |
|---|---|
commit_oid non-empty | That commit against its first parent. |
staged: true | The index against HEAD (what a commit would record). |
| neither | The working tree against the index (unstaged edits). |
An empty path diffs the whole tree; a non-empty path scopes it to one file. Diffs use 3 lines of context and include untracked content. The response is a list of FileDiff → DiffHunk → DiffLine, where each line has an origin (' ' context, '+' add, '-' delete) and old/new line numbers (0 where not applicable). Binary files are flagged and carry no hunks.
Staging & committing
Stage, unstage, discard
StageFile
Adds a path to the index. A deleted file is handled correctly — it is removed from the index rather than added.
UnstageFile
Resets the index entry back to HEAD. On an unborn branch (no HEAD yet) it simply drops the path from the index.
DiscardFile
Restores the working-tree file from the index/HEAD version (a forced, path-scoped checkout). This is destructive — the working-tree change is gone.
In the workbench Changes tab, each file row has a stage/unstage checkbox, and "stage all" / "unstage all" shortcuts fan the operation across every file in a group. Selecting a file loads its diff in the shared right pane.
Commit
Commit writes a commit from the current index. The engine routes it by whether signing is requested:
- Plain commits go through libgit2. They require a committer identity — if
user.name/user.emailare unset the RPC returns a clear invalid-argument error. - Signed (
gpg_sign) orsignoffcommits go through thegitbinary, because libgit2's signing support is patchy. The engine passes--gpg-sign/--signoffand resolves the resulting HEAD OID afterward.
amend rewrites HEAD; an empty message on an amend keeps the previous message (--no-edit). The response carries the new commit's OID.
Not yet in the UI
The workbench's commit composer currently sends plain commits only. The amend, signoff, and gpg_sign options exist in the engine and client but are not yet exposed as UI toggles — they are implemented in the engine but not exposed in the app yet (Install).
History: log & commit detail
GetLog walks history from a given rev (or HEAD), time-sorted, with limit (default 100) and skip paging, and an optional path filter that keeps only commits which touched that path. Each Commit carries its OID and short OID, summary and body, author name/email, an RFC3339 commit time, parent OIDs, and the branch/tag refs that point at it (used to draw the graph).
GetCommit returns a single commit's metadata plus the full per-file diff of that commit against its first parent — the same FileDiff structure used everywhere else.
The workbench renders history two ways from one dataset: a linear commit list in the left navigator and a commit-graph DAG in the right pane, with a commit-detail pane (message + changed files + diffs) when one is selected. The History tab loads up to 120 commits.
Branches
ListBranches returns local and remote branches together. Each Branch reports whether it is remote, whether it is the current branch, its upstream, its tip OID, and — for local branches — how far ahead/behind its upstream it is.
| RPC | Behavior |
|---|---|
CreateBranch | Creates a branch at a start_point (or HEAD), optionally checking it out. |
CheckoutBranch | Updates HEAD and the working tree (a safe checkout). Resolves a local branch ref, else any revparse-able ref, in which case HEAD goes detached. |
DeleteBranch | Deletes a local branch. The force flag is currently accepted but ignored — deletion behaves as an ordinary delete. |
MergeBranch | Shells out to git merge (optionally --no-ff). Returns ok, a conflicts flag (detected from the git output), and the combined message. |
In the app, the Branches tab lists local branches with a tracking label (↑n / ↓n / synced / local), and clicking a non-current branch checks it out. The "+" opens a create-branch prompt (which checks out the new branch). Branch deletion and merge are implemented in the engine and client but are not yet surfaced in the workbench UI.
Remotes, push, pull & fetch
ListRemotes returns each configured remote's name, fetch URL, and push URL. The three network operations — Push, Pull, Fetch — are server-streaming: the engine shells out to git … --progress and streams its output line by line as ProgressLine events (splitting on both \n and \r so progress repaints arrive as distinct lines), terminated by a single ProgressDone carrying ok, the exit code, and a message.
// The stream shape (mirrors the terminal engine's Run):
ProgressLine* → ProgressDone { ok, exit_code, message }
Options
| Op | Options honored |
|---|---|
Push | set_upstream (--set-upstream), force_ (--force-with-lease), tags (--tags), plus optional remote and ref. |
Pull | strategy: merge (--no-rebase), rebase (--rebase), or ff-only (--ff-only). |
Fetch | all_remotes (--all) and tags (--tags). |
Remote authentication
No credential is ever accepted from the caller. The git child inherits your local environment, so your credential helper and SSH agent provide authentication exactly as they would on the command line. The engine sets GIT_ and a null stdin, so a missing credential fails fast rather than hanging the engine on a prompt that can never be answered.
Heads up
Because prompts are disabled, a repository that would normally ask for a username/password interactively will fail. Configure a credential helper or SSH key so pushes and pulls authenticate non-interactively.
In the app, the workbench header has a one-click Push (with set_upstream), and the Branches tab's active-branch card offers Push and Pull with a live progress log. Fetch exists in the engine and its typed client but is not yet a dedicated button in the workbench.
Stash
Stash listing uses libgit2 (index, message, OID, and a best-effort created-at timestamp), while save/apply/pop/drop shell out to git stash:
ListStash— enumerate stash entries.StashSave—git stash push, optionally--include-untrackedand with a message.StashApply— apply or pop by index (thepopflag selectsgit stash pop stash@vs{N} apply).StashDrop—git stash drop stash@.{N}
Not yet in the UI
Stash is fully implemented in the engine and typed client, but the workbench does not yet render a stash panel: it is implemented in the engine but not exposed in the app yet.
Tags
ListTags returns both lightweight and annotated tags, distinguishing them via the annotated flag and surfacing the target OID and (for annotated tags) the message. CreateTag creates an annotated tag when a message is given (which requires a tagger identity) or a lightweight tag otherwise, at an explicit target or HEAD. DeleteTag removes a tag. As with stash, tags are engine- and client-complete but not yet surfaced as a workbench panel.
Blame
GetBlame(path) returns per-line attribution for a file: the line number and content (read from the working-tree file), the commit OID that last touched it, the author, the commit time, and that commit's summary. A path is required. Lines with no blame hunk (e.g. brand-new content) come back with empty attribution but their text intact. Blame is implemented in the engine and client; a blame view is not yet part of the workbench.
Search across history
Search(query, runs a cross-history search by shelling out to git log with a machine-parseable format, returning matching commits. Four modes:
| Mode | git mechanism | Finds |
|---|---|---|
message | --grep | Commits whose message matches. |
author | --author | Commits by an author. |
file | -- <path> | Commits that touched a path. |
content | -S (pickaxe) | Commits that added/removed an occurrence of a string. |
The result commits carry OID, summary, author, time, and parents (body and refs are omitted for search results). A failed pattern with no output returns an empty list rather than an error.
Not yet in the UI
The ⌘K command palette searches sections and repositories to jump between them; it does not yet drive the history Search RPC. Cross-history search exists in the engine and its typed client.
Configuration, scopes & data locations
Request scoping
Every RPC except CreateSession carries a session_id and, for repo operations, a repo_id. The engine resolves the id to an absolute working-tree path; an unknown id yields a NotFound status. There are no server-side "scopes" or permission tiers — the engine acts entirely on your local machine with your own filesystem permissions, and the sealed session is what gates access.
Identity & config
The committer/tagger identity comes from your global git config (user.name / user.email); the engine reads it for display and requires it for commits and annotated tags. The engine does not manage git config, credentials, or SSH keys — those remain the domain of your local git installation.
Stale-while-revalidate cache
An engine-owned, file-based key/value cache (CacheGet / CacheSet / CacheDelete) stores the last result of costly reads, each stamped with the wall-clock millisecond it was refreshed. On open the UI renders the cached value instantly with a small "refreshing…" hint, fetches fresh in the background, and writes it back. Because it is file-based, every browser on the machine shares one last-known state. The stored value is opaque UI-encoded JSON — never a secret. The repo list uses this under the key repos:list.
Runtime & state layout
The engine roots its files under ~/.openeng/git/ (override the whole OpenEng home with the OPENENG_HOME env var), keeping it isolated from the AI and terminal engines:
| Path | Contents |
|---|---|
~/.openeng/git/runtime/ | The spawn lock (one engine per machine, via an O_EXCL PID file), the endpoint descriptor, the 0600 handshake token, and the TLS certs/keys. |
~/ | The persisted list of registered repository paths. |
| Setting | Value |
|---|---|
| Loopback bind | 127.0.0.1:50053 (fixed, zero-config) |
| Browser endpoint | https:// |
| UI origin | git.openeng.app |
| Protocol version | openeng.git.v1 (1.0) |
| Sibling engines | Terminal 50052 · Kubernetes 50054 · Chat 50056 · Data 50057 (the AI engine binds no local port) |
What is not yet exposed
In the interest of an honest reference, here is what the Git app does not do today:
- No file-content or tree-read RPC. There is no way to browse a repository's file tree or read a blob at an arbitrary ref. Content is only ever surfaced through diffs (which read the working tree, index, or a commit-vs-parent) and blame (which reads the working-tree file). You cannot open an arbitrary file at HEAD or an old commit.
- No pull/merge requests or insights. The Requests and Insights sections are honest "Not connected" placeholders — the local engine has no pull/merge requests or analytics to report. CI & CD is no longer a placeholder: it reads pipeline runs from GitHub, GitLab, Bitbucket, and Gitea straight from your browser, and the Connect settings category now manages your git servers (see CI & CD and Settings). Azure DevOps runs are not wired up yet, and the Overview's forge figures (Open requests, Pipelines today, and the health table's CI column) still show
0or—. - No conflict-resolution tooling.
MergeBranchreports aconflictsflag, but there is no UI to resolve conflicts. - UI gaps over an already-capable engine. The engine and typed client fully implement stash, tags, blame, cross-history search, branch delete/merge, fetch, and commit amend/signoff/GPG — but the workbench does not yet render panels or controls for them. They are implemented in the engine but not exposed in the app yet.
- No in-app credential management for git itself. Remote authentication relies entirely on your local git credential helper and SSH agent; the engine neither stores nor transmits tokens or keys. The only credential the app keeps is a forge API token you add for CI & CD, and it stays in your browser.
Design intent
These are surfacing gaps, not architectural ones. The engine's job is to be a complete, local, sealed git backend; the UI exposes that surface progressively. Because the transport carries no secrets and every operation is local, new panels can be added without changing the security model.