worklyn / zukapublic
Agent-first git hosting. One Rust binary: git over HTTP and SSH, a REST API, MCP, CI, and multi-tenant isolation.
Get a copy:
git clone https://zuka.worklyn.com/worklyn/zuka.git
| 1 | # AGENTS.md — zuka |
| 2 | |
| 3 | Read [`../AGENTS.md`](../AGENTS.md) first; conventions cascade. This file records the |
| 4 | exceptions and the rules that only apply here. |
| 5 | |
| 6 | Before working on a topic, read the doc that owns it: [`SPEC.md`](SPEC.md) for |
| 7 | architecture, API, security and data model; [`ROADMAP.md`](ROADMAP.md) for sequencing, |
| 8 | risk and open decisions; [`README.md`](README.md) for what the product is and is not. |
| 9 | |
| 10 | ## Exception: the runtime is Rust, not Deno |
| 11 | |
| 12 | `product/AGENTS.md` states "Runtime is Deno. TypeScript throughout." **zuka is |
| 13 | Rust**, and this is a stated override rather than a silent one. |
| 14 | |
| 15 | Why: the workload is git object manipulation, subprocess supervision and long-lived |
| 16 | byte streams, and it ships as a single self-hostable binary a stranger can drop on a |
| 17 | VM. A Deno service would need a runtime installed and would still shell out for every |
| 18 | git operation. The two existing Rust services in `yangu/` (`atlas`, `kazi`) set the |
| 19 | in-house precedent this follows. |
| 20 | |
| 21 | Deno still applies to **tests** — the live suite in `test/live/` is TypeScript, |
| 22 | spawning the compiled binary and driving it with a real `git` CLI and real `fetch`. |
| 23 | |
| 24 | The service now has one UI: the read-only browser viewer in `src/web/` (SPEC §4.9). |
| 25 | It is server-rendered from the binary with no bundler and no CDN, because zuka ships |
| 26 | as a single self-hostable file — so Motheo tokens and Worklyn Sans, which arrive |
| 27 | through `@ori/motheo` and a font pipeline, do **not** apply to it. It uses a system |
| 28 | font stack and its own small stylesheet. The marketing page in `yangu/website/` |
| 29 | follows the design system there, where a build step is available. |
| 30 | |
| 31 | If that ever stops being true — if the viewer grows enough to justify a build — this |
| 32 | exception is the thing to revisit first. |
| 33 | |
| 34 | ## Rust conventions |
| 35 | |
| 36 | Follow `yangu/atlas` and `yangu/kazi`, whose patterns are already load-bearing in |
| 37 | production: |
| 38 | |
| 39 | - `hyper` 1.x + `tokio`, no web framework. `anyhow` internally, one typed `Error` at |
| 40 | the HTTP boundary (SPEC §8.3). `serde` on the wire. |
| 41 | - `edition = "2021"`, standalone crate, committed `Cargo.lock`, `.gitignore` = `target/`, |
| 42 | `[profile.release] opt-level = 3`, `lto = "thin"`. |
| 43 | - Config is `std::env::var` with defaults gathered into `Config::from_env() -> |
| 44 | Result<Config>` that fails boot loudly on an invalid value. Prefix `ZUKA_`. |
| 45 | - Tests are inline `#[cfg(test)] mod tests` with sentence-named cases |
| 46 | (`rejects_ref_write_without_if_match`), not `test_`-prefixed. |
| 47 | - CI gates on `cargo test` → `cargo clippy --all-targets -- -D warnings` → |
| 48 | `cargo build --release`, following `yangu/.github/workflows/build-kazi.yml`. |
| 49 | |
| 50 | **The live suite must run in CI.** It self-skips without `ZUKA_LIVE_TEST`, and a |
| 51 | proof that never executes is not a proof — SPEC calls the live tests the place the |
| 52 | wire protocol and REST contract are actually proven, so the CI job sets the flag and |
| 53 | installs `git`. |
| 54 | |
| 55 | ### Deviation: nested `src/`, not flat modules |
| 56 | |
| 57 | `yangu/atlas` and `yangu/kazi` keep everything in one `main.rs` (1,158 and 3,050 |
| 58 | lines). zuka nests by concern (SPEC §8) because it carries four protocol surfaces |
| 59 | against atlas's one. This is a judgment call, recorded as such — do not restate it as |
| 60 | a measured failure of the flat pattern. |
| 61 | |
| 62 | ## Rules specific to this service |
| 63 | |
| 64 | - **`git::exec::Git` is the only way to invoke git.** Not `Command::new("git")`. It |
| 65 | clears the environment and sets `GIT_CONFIG_NOSYSTEM`, so host configuration cannot |
| 66 | change what our git calls do. Five wrappers with three different policies existed |
| 67 | before it, and `/etc/gitconfig` applied to the ref rule and the quota accounting but |
| 68 | not to the wire. |
| 69 | - **Exit 1 is an answer; anything higher is a fault.** `Git::query` encodes this. |
| 70 | Collapsing every non-zero exit into `404` once meant a corrupt repository reported |
| 71 | as empty and nothing paged anyone. |
| 72 | - **Git work never runs on a runtime worker.** Use `git::exec::blocking`. A fork+exec |
| 73 | on a tokio worker stalls unrelated requests including `/healthz`, and the pool is |
| 74 | only as wide as the core count. |
| 75 | - **Shared operations live in `core/`.** REST and MCP each had their own repository |
| 76 | create and delete once; the copies drifted in four ways before anyone noticed. If a |
| 77 | facade needs a rule, the rule goes in `core/` or `git/` and the facade calls it. |
| 78 | - **`AppState::open_repo` is how a repository is resolved.** It carries the access |
| 79 | check, the ready check and — for `Access::Write` — the disk reserve and the storage |
| 80 | quota, so no write path can forget them. |
| 81 | |
| 82 | - **Validation in SPEC §2.5 is normative.** Ref names, repo names, tree paths and file |
| 83 | modes are security controls, not input hygiene. A ref name reaching the filesystem |
| 84 | unvalidated is remote code execution. Never hand-roll ref validation; use |
| 85 | `gix-validate`. |
| 86 | - **Every ref mutation goes through a `gix-ref` transaction** with the precondition |
| 87 | enforced *inside* it (SPEC §1.3). Check-then-write is a race against `receive-pack`. |
| 88 | - **One rule, one implementation.** The `update` hook and the REST ref endpoint call |
| 89 | the same core function. If a rule appears in both a hook and a handler, it is in the |
| 90 | wrong layer. |
| 91 | - **git is the only writer.** No endpoint and no MCP tool modifies a repository's |
| 92 | contents or moves a ref; that is `git push`. Adding a write path means a second |
| 93 | home for the fast-forward rule, a concurrency model and a merge story, all of |
| 94 | which git already has (SPEC §1.3). If a task seems to need one, the answer is |
| 95 | almost certainly that the caller should run git. |
| 96 | - **Facades translate; they do not decide.** `api/` and `mcp/` may fill defaults and |
| 97 | reshape payloads. They may not carry authorization logic — that lives on |
| 98 | `Identity`, so REST, MCP and SSH cannot drift. A facade must never call into |
| 99 | another facade. |
| 100 | - **The git paths are not REST.** `/{account}/{repo}.git/*` has its own error contract: |
| 101 | `WWW-Authenticate` on `401`, `application/x-git-*` content types, never |
| 102 | `problem+json` (SPEC §4.7). |
| 103 | - **Scheduled work lives in `jobs/`** and its own systemd unit, never the web |
| 104 | entrypoint. |
| 105 | - **CI isolation claims must name a mechanism.** "No network" means a network |
| 106 | namespace; "isolated" means a uid, a cgroup and a scrubbed environment. An intention |
| 107 | is not a control (SPEC §6.3). |
| 108 | |
| 109 | ## The gate |
| 110 | |
| 111 | Run all four before pushing; CI runs the same set and gates on every one. |
| 112 | |
| 113 | ```sh |
| 114 | cargo fmt --check |
| 115 | cargo clippy --all-targets -- -D warnings |
| 116 | cargo test |
| 117 | deno task test:live |
| 118 | ``` |
| 119 | |
| 120 | ## Status |
| 121 | |
| 122 | Standalone is complete: M0–M5 and M7. M6 (multi-tenant control plane) is deferred and |
| 123 | unbuilt — see `ROADMAP.md` D9. |
| 124 | |
| 125 | `SPEC.md` describes how the service works; `ROADMAP.md` says what exists and why. When |
| 126 | they disagree about what is built, the roadmap is right. |