Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
221 commits
Select commit Hold shift + click to select a range
d1803eb
feat: add agents configuration
dvrd Mar 27, 2026
4eb161d
fix(ssh): validate access tokens in keyboard-interactive auth (#800)
dvrd Mar 27, 2026
6eaa578
Merge pull request #1 from dvrd/fix/ss-800
dvrd Mar 27, 2026
a4d3f65
fix(ssh): accept split key args in user create -k (#750)
dvrd Mar 27, 2026
faaa55b
feat(backend): set gitweb.owner on repo create (#753)
dvrd Mar 27, 2026
4a53343
feat(ui): add 'c' shortcut to copy clone command in repo view (#769)
dvrd Mar 27, 2026
e3ce7b0
Merge pull request #2 from dvrd/fix/ss-750
dvrd Mar 27, 2026
797ea51
Merge pull request #3 from dvrd/fix/ss-753
dvrd Mar 27, 2026
c2b638d
Merge pull request #4 from dvrd/feat/ss-769
dvrd Mar 27, 2026
1ec1950
fix(ssh): merge trailing args into key unconditionally
dvrd Mar 27, 2026
2753b71
fix(ui): prevent double-copy and empty clipboard on 'c' key
dvrd Mar 27, 2026
8cdd0a7
fix(ssh): improve token auth logging and remove redundant DB lookup
dvrd Mar 27, 2026
6562765
fix(ssh): reject extra args when -k is not provided
dvrd Mar 27, 2026
e1c7666
fix(ui): rename copy variable to avoid shadowing builtin
dvrd Mar 27, 2026
0a34735
fix(ui): allow token-auth users in TUI when AllowKeyless is false
dvrd Mar 27, 2026
eb6e99e
refactor(ssh): remove dead code and unused parameters
dvrd Mar 27, 2026
f98d1cc
fix(ssh): persist permissions in initializePermissions
dvrd Mar 28, 2026
f02d5fc
fix(ssh): store user ID instead of username in token auth extensions
dvrd Mar 28, 2026
c836512
test(ssh): tighten assertions and add extra-args rejection test
dvrd Mar 28, 2026
c44e2cb
Merge pull request #5 from dvrd/fix/ss-800
dvrd Mar 28, 2026
7322872
Merge pull request #6 from dvrd/fix/ss-750
dvrd Mar 28, 2026
4d09f44
Merge pull request #7 from dvrd/feat/ss-769
dvrd Mar 28, 2026
c9e2c5d
fix(web): return io.ErrShortWrite and handle partial Read in ReadFrom
dvrd Mar 28, 2026
5c430e6
fix(server): pre-bind listeners so port permission errors surface imm…
dvrd Mar 28, 2026
fdc7f1e
fix(ui): use path instead of filepath for git tree paths on Windows
dvrd Mar 28, 2026
d439099
feat(config): add anon_access and allow_keyless config fields
dvrd Mar 28, 2026
6c56b32
Merge pull request #11 from dvrd/feat/ss-758
dvrd Mar 28, 2026
a71174c
Merge pull request #8 from dvrd/fix/ss-616
dvrd Mar 28, 2026
d4795ea
Merge pull request #10 from dvrd/fix/ss-681
dvrd Mar 28, 2026
4f03544
Merge pull request #9 from dvrd/fix/ss-645
dvrd Mar 28, 2026
f8b73a1
fix(ui): stop loading spinner when readme tab opens on empty repo
dvrd Mar 28, 2026
843b6da
fix(ui): pressing copy key no longer duplicates items in filtered list
dvrd Mar 28, 2026
bdced3a
fix(git): treat signal-killed git processes as client disconnects
dvrd Mar 28, 2026
3275323
fix(test): use single-word repo name in txtar test
dvrd Mar 28, 2026
85d0b64
fix(backend): find README in docs/, .github/, .gitlab/ subdirectories
dvrd Mar 28, 2026
954ff50
fix(ssh): use errors.Is for ErrReferenceNotExist sentinel comparison
dvrd Mar 28, 2026
8d328b3
fix(git): only suppress signal-killed errors when context is cancelled
dvrd Mar 28, 2026
6df9001
fix(backend): return nil error from Readme when no README found
dvrd Mar 28, 2026
bcdf335
fix(ui): persist copied feedback for 1.5s using timer reset message
dvrd Mar 28, 2026
130eab5
fix(git): narrow signal-kill suppression to context.Canceled only
dvrd Mar 28, 2026
61c9e10
fix(ui): use generation counter to prevent stale copied-indicator reset
dvrd Mar 28, 2026
c536801
fix(ui): store GlobalIndex in copiedIdx so Render highlights correct …
dvrd Mar 28, 2026
3a7f8a2
fix(ui): track copied item by identity not index to fix pagination
dvrd Mar 28, 2026
fd90b7e
fix(ui): restore generation counter to prevent premature copied-state…
dvrd Mar 28, 2026
2b23308
fix(git): restore error format order — wrap ExitError, append stderr …
dvrd Mar 28, 2026
6809665
fix(backend): handle ErrReferenceNotExist in README search loop
dvrd Mar 28, 2026
02d28da
fix(git): log suppressed signal-kill at debug level instead of silent…
dvrd Mar 28, 2026
d67403c
fix(ui): remove stale SetItem call — render now uses item identity no…
dvrd Mar 28, 2026
0b5f3b7
fix(backend): tighten README glob to avoid matching READMEFILE and si…
dvrd Mar 28, 2026
37193a6
fix(git): verify and document WaitDelay/ExitError interaction on canc…
dvrd Mar 28, 2026
dbfe3ad
fix(git): narrow WaitDelay fallback to exec.ErrWaitDelay specifically
dvrd Mar 28, 2026
831ae91
fix(git): use forward slashes in LatestFile path matching for Windows…
dvrd Mar 28, 2026
79cbb01
fix(git): use path.Dir instead of filepath.Dir in LatestFile for Wind…
dvrd Mar 28, 2026
f3eb83f
fix(git): check erro not err in stderr copy goroutine
dvrd Mar 28, 2026
4736251
fix(ui): propagate backend.Readme error instead of silently discarding
dvrd Mar 28, 2026
5d8d087
fix(git): make context-cancel suppression portable across Windows/Unix
dvrd Mar 28, 2026
c99245e
chore(git): remove dead os.ErrNotExist branch after cmd.Wait()
dvrd Mar 28, 2026
7ee638c
Merge pull request #12 from dvrd/fix/ss-522
dvrd Mar 28, 2026
ad03df5
Merge pull request #13 from dvrd/fix/ss-510
dvrd Mar 28, 2026
8fe4da1
Merge pull request #14 from dvrd/fix/ss-560
dvrd Mar 28, 2026
51d6d84
Merge pull request #15 from dvrd/fix/ss-634
dvrd Mar 28, 2026
35defc7
fix(ssh): user create -k correctly handles space-separated public key…
dvrd Mar 28, 2026
f37f6e8
fix(git): prevent zombie processes on context-cancelled git commands …
dvrd Mar 28, 2026
03abe8e
fix(ssh): create host key directory before generating key (#18)
dvrd Mar 28, 2026
c3c6c32
fix(server): pre-bind listeners; idempotent Shutdown; TLS guard (#645…
dvrd Mar 28, 2026
8b876c1
feat(web): add GET /{repo}/raw/{ref}/{filepath} raw blob endpoint (#4…
dvrd Mar 28, 2026
011b073
fix(mirror): disable gc.auto and no-timeout to prevent CPU peg on lar…
dvrd Mar 28, 2026
186e193
fix(ssh): validate access tokens in keyboard-interactive auth (#22)
dvrd Mar 28, 2026
0a6948c
fix(lfs): encode expires_in as seconds, not nanoseconds (#23)
dvrd Mar 28, 2026
fb91ef4
fix(web): return io.ErrShortWrite and handle EOF-with-data in ReadFro…
dvrd Mar 28, 2026
cb463f8
fix(cmd): user create -k accepts multi-word key from unquoted SSH arg…
dvrd Mar 28, 2026
6c584fd
fix(ssh): downgrade spurious signal-killed log to debug on client dis…
dvrd Mar 28, 2026
409092c
chore: merge dev into main
dvrd Mar 28, 2026
2fa63f4
Merge pull request #29 from dvrd/merge-dev-to-main
dvrd Mar 28, 2026
7dd252a
fix(lfs): fix open file handle blocking rename and concurrent upload …
dvrd Mar 28, 2026
a0f8462
fix(ssh): implement keyboard-interactive token auth, resolve user ide…
dvrd Mar 28, 2026
dedec5b
fix(git): fix stderr error var, add WaitDelay, handle context cancell…
dvrd Mar 28, 2026
190a5bc
feat(backend): set gitweb.owner in git config on repo creation (#34)
dvrd Mar 28, 2026
8e89aac
fix(server): pre-bind listeners, idempotent Shutdown, TLS guard (#35)
dvrd Mar 28, 2026
59a5f1b
feat(config): support anon_access and allow_keyless in config file (#36)
dvrd Mar 28, 2026
49f35e5
fix(lfs): serialize expires_in as seconds not nanoseconds (#37)
dvrd Mar 28, 2026
96b10e5
fix(ui): use path.Join/Dir instead of filepath for git tree paths (#38)
dvrd Mar 28, 2026
3b80ec5
feat(backend): support README files in docs/, .github/, .gitlab/ (#39)
dvrd Mar 28, 2026
1fbe804
feat(ui): copy clone command from repo view, fix copy feedback reset …
dvrd Mar 28, 2026
841e3b2
fix(ssh): create host key directory before generating key (#46)
dvrd Mar 28, 2026
caa369f
fix(lfs): enable WAL mode for SQLite and idempotent rename to fix con…
dvrd Mar 28, 2026
9632aa4
fix(ssh): record only first offered public-key fingerprint (#48)
dvrd Mar 28, 2026
c2a26d0
fix(backend): remove orphaned repo directory when CreateRepository fa…
dvrd Mar 28, 2026
ff3cdae
feat(ssh): add allow_mouse_events config option to restore terminal t…
dvrd Mar 28, 2026
858c0c6
feat(ssh): allow admin to create access tokens for other users
dvrd Mar 28, 2026
67c8019
feat(jobs): add enabled flag to mirror_pull cron job config
dvrd Mar 28, 2026
706a35a
feat(ssh): add key_exchanges, ciphers, macs config for SSH algorithm …
dvrd Mar 28, 2026
3ac3537
feat(ui): enable fuzzy filtering in the Files tab
dvrd Mar 28, 2026
997d5ed
feat(web): add strip_git_suffix config option for .git-free HTTP clon…
dvrd Mar 28, 2026
21c1fad
feat(ui): add Diff tab to repository view for branch comparison
dvrd Mar 28, 2026
2eff2eb
feat(backend): add push mirrors — auto-mirror repos to remote URLs on…
dvrd Mar 28, 2026
016b8d7
fix(jobs): fall back to default schedule when mirror_pull schedule is…
dvrd Mar 28, 2026
e1e7157
fix(web): fix RawPath offset and tighten .git suffix guard in gitSuff…
dvrd Mar 28, 2026
97ab501
fix(ui): show error message in Diff tab when ref is invalid (e.g. sin…
dvrd Mar 28, 2026
c3cabea
style(ssh): use short var decl in token create --user flag handler
dvrd Mar 28, 2026
dd4af80
fix(backend): fix push mirror context leak, add semaphore and URL val…
dvrd Mar 28, 2026
24bfd03
fix(git): cap DiffRefs output at 512 KB to prevent unbounded memory use
dvrd Mar 28, 2026
060024a
Merge pull request #51 from dvrd/feat/ss-530
dvrd Mar 28, 2026
7dcc4cf
Merge pull request #52 from dvrd/feat/ss-208
dvrd Mar 28, 2026
6757a11
Merge pull request #53 from dvrd/feat/ss-664
dvrd Mar 28, 2026
881586a
Merge pull request #54 from dvrd/feat/ss-773
dvrd Mar 28, 2026
2a62211
Merge pull request #55 from dvrd/feat/ss-589
dvrd Mar 28, 2026
d5e0737
Merge pull request #57 from dvrd/feat/ss-485
dvrd Mar 28, 2026
49f5b76
Merge pull request #59 from dvrd/feat/ss-194
dvrd Mar 28, 2026
182c0a3
feat(web): add allow_public_go_get option for private repo go-import …
dvrd Mar 28, 2026
9e83824
feat(backend): sync repo metadata from .soft-serve.yaml on push
dvrd Mar 28, 2026
abecb08
fix(backend): restrict private/hidden sync to admins, cap yaml size, …
dvrd Mar 28, 2026
b8d942b
fix(backend): run .soft-serve.yaml metadata sync asynchronously
dvrd Mar 28, 2026
46d0bf4
Merge pull request #56 from dvrd/feat/ss-690
dvrd Mar 28, 2026
ee4a7b5
Merge pull request #58 from dvrd/feat/ss-504
dvrd Mar 28, 2026
8dac8ea
feat(ui): copy full patch (commit header + diff) when pressing c in c…
dvrd Mar 28, 2026
9ee9756
Merge pull request #60 from dvrd/feat/ss-556
dvrd Mar 28, 2026
95e8baf
harden(security): comprehensive security hardening pass
dvrd Mar 28, 2026
c7d7be1
Merge pull request #61 from dvrd/harden/ss-830
dvrd Mar 28, 2026
2b44528
harden(security): second-pass hardening — env isolation, timing oracl…
dvrd Mar 28, 2026
99cf8cb
Merge pull request #62 from dvrd/harden/ss-831
dvrd Mar 28, 2026
6df1913
harden(security): third-pass — LFS lock scope, Git-Protocol sanitizat…
dvrd Mar 28, 2026
84b7d81
Merge pull request #63 from dvrd/harden/ss-832
dvrd Mar 28, 2026
7f9affd
harden(security): fourth-pass — rate limiting, collab escalation fix,…
dvrd Mar 29, 2026
e451fe9
Merge pull request #64 from dvrd/harden/ss-834
dvrd Mar 29, 2026
1650b71
fix: review-loop round 1 — deadlocks, LFS batch drop, goroutine leak,…
dvrd Mar 29, 2026
d8c3e36
Merge pull request #65 from dvrd/fix/ss-835
dvrd Mar 29, 2026
b5510da
fix: review-loop round 2 — repoc ordering, LFS Content-Length, auth g…
dvrd Mar 29, 2026
59291e1
Merge pull request #66 from dvrd/fix/ss-836
dvrd Mar 29, 2026
e16ec21
fix: apply 8 security/quality fixes (ss-837)
dvrd Mar 29, 2026
dd185b8
Merge pull request #67 from dvrd/fix/ss-837
dvrd Mar 29, 2026
ca77a81
fix: review-loop round 4 fixes (ss-839)
dvrd Mar 29, 2026
335e085
Merge pull request #68 from dvrd/fix/ss-839
dvrd Mar 29, 2026
58c6ff0
fix: review-loop round 5 fixes (ss-840)
dvrd Mar 29, 2026
212638d
Merge pull request #69 from dvrd/fix/ss-840
dvrd Mar 29, 2026
47a3899
fix: review-loop round 6 fixes (ss-841)
dvrd Mar 29, 2026
c82b745
Merge pull request #70 from dvrd/fix/ss-841
dvrd Mar 29, 2026
1062061
fix: review-loop round 7 fixes (ss-842)
dvrd Mar 29, 2026
a08a640
Merge pull request #71 from dvrd/fix/ss-842
dvrd Mar 29, 2026
f5083b0
fix: review-loop round 8 fixes (ss-843)
dvrd Mar 29, 2026
9fda5a4
Merge pull request #72 from dvrd/fix/ss-843
dvrd Mar 29, 2026
8407abf
fix: review-loop round 9 fixes (ss-844)
dvrd Mar 29, 2026
4e4db7b
Merge pull request #73 from dvrd/fix/ss-844
dvrd Mar 29, 2026
e8d4a42
fix: review-loop round 10 fixes (ss-845)
dvrd Mar 29, 2026
d7070f4
Merge pull request #74 from dvrd/fix/ss-845
dvrd Mar 29, 2026
5028aa4
fix: review-loop round 11 fixes (ss-846)
dvrd Mar 29, 2026
09598fe
Merge pull request #75 from dvrd/fix/ss-846
dvrd Mar 29, 2026
101a2f8
fix: review-loop round 12 fixes (ss-847)
dvrd Mar 29, 2026
3e2bad6
Merge pull request #76 from dvrd/fix/ss-847
dvrd Mar 29, 2026
2a6c196
fix: review-loop round 13 fixes (ss-848)
dvrd Mar 29, 2026
b5c4751
Merge pull request #77 from dvrd/fix/ss-848
dvrd Mar 29, 2026
eabcbb4
fix: review-loop round 14 fixes (ss-849)
dvrd Mar 29, 2026
4b98614
Merge pull request #78 from dvrd/fix/ss-849
dvrd Mar 29, 2026
31063d0
fix: review-loop round 15 fixes (ss-850)
dvrd Mar 29, 2026
49e9ab2
Merge pull request #79 from dvrd/fix/ss-850
dvrd Mar 29, 2026
226771e
fix: review-loop round 16 fixes (ss-851)
dvrd Mar 29, 2026
e7f6a79
Merge pull request #80 from dvrd/fix/ss-851
dvrd Mar 29, 2026
c0f66ac
fix: review-loop round 17 fixes (ss-852)
dvrd Mar 29, 2026
464510e
Merge pull request #81 from dvrd/fix/ss-852
dvrd Mar 29, 2026
18c3f02
fix: review-loop round 18 fixes (ss-853)
dvrd Mar 29, 2026
d780f7c
Merge pull request #82 from dvrd/fix/ss-853
dvrd Mar 29, 2026
b7029bb
fix: review-loop round 19 fixes (ss-854)
dvrd Mar 29, 2026
1611b3d
Merge pull request #83 from dvrd/fix/ss-854
dvrd Mar 29, 2026
26aa33d
fix: review-loop round 20 fixes (ss-855)
dvrd Mar 29, 2026
665fab8
Merge pull request #85 from dvrd/fix/ss-855
dvrd Mar 29, 2026
a855ed0
fix: review-loop round 21 fixes (ss-856)
dvrd Mar 29, 2026
86be6bf
Merge pull request #87 from dvrd/fix/ss-856
dvrd Mar 29, 2026
ed1f4ae
fix: review-loop round 22 fixes (ss-857)
dvrd Mar 29, 2026
89c46d1
Merge pull request #89 from dvrd/fix/ss-857
dvrd Mar 29, 2026
d9cb6f8
fix: review-loop round 23 fixes (ss-858)
dvrd Mar 29, 2026
2143bf4
Merge pull request #92 from dvrd/fix/ss-858
dvrd Mar 29, 2026
66ae0e1
fix: review-loop round 25 fixes (ss-859)
dvrd Mar 29, 2026
e87e4a2
Merge pull request #94 from dvrd/fix/ss-859
dvrd Mar 29, 2026
902b3ea
fix: review-loop round 26 fixes (ss-860)
dvrd Mar 29, 2026
c3c47f1
Merge pull request #96 from dvrd/fix/ss-860
dvrd Mar 29, 2026
545d85f
fix: review-loop round 27 fixes (ss-861)
dvrd Mar 29, 2026
ac19efd
Merge pull request #98 from dvrd/fix/ss-861
dvrd Mar 29, 2026
d6c500e
fix: review-loop round 28 fixes (ss-862)
dvrd Mar 29, 2026
4b886f0
Merge pull request #100 from dvrd/fix/ss-862
dvrd Mar 29, 2026
f01b5b5
fix: remove redundant GIT_PROTOCOL prefix check in middleware (ss-863)
dvrd Mar 29, 2026
1399180
Merge pull request #102 from dvrd/fix/ss-863
dvrd Mar 29, 2026
0275a1b
fix: review-loop round 31 fixes (ss-864)
dvrd Mar 29, 2026
e621d6d
Merge pull request #104 from dvrd/fix/ss-864
dvrd Mar 29, 2026
3c9c211
fix: review-loop round 32 fixes (ss-865)
dvrd Mar 29, 2026
6a11578
Merge pull request #106 from dvrd/fix/ss-865
dvrd Mar 29, 2026
c8df7ad
fix: review-loop round 33 fixes (ss-866)
dvrd Mar 29, 2026
345e0ec
Merge pull request #108 from dvrd/fix/ss-866
dvrd Mar 29, 2026
6071ec1
fix: review-loop round 34 fixes (ss-867)
dvrd Mar 29, 2026
0603480
Merge pull request #110 from dvrd/fix/ss-867
dvrd Mar 29, 2026
3e83079
fix: suppress errInvalidPassword from error log in withAccess (ss-868)
dvrd Mar 29, 2026
5d8b1fe
Merge pull request #112 from dvrd/fix/ss-868
dvrd Mar 29, 2026
6205aff
fix: review-loop round 37 fixes (ss-869)
dvrd Mar 29, 2026
0388ecb
Merge pull request #114 from dvrd/fix/ss-869
dvrd Mar 29, 2026
787d44f
fix(daemon): return ErrNotAuthed for missing repos to prevent enumera…
dvrd Mar 29, 2026
1b11272
Merge pull request #116 from dvrd/fix/ss-870
dvrd Mar 29, 2026
c1799bc
fix(review-39): hooks echo injection, task nil return, ssrf ctx-aware…
dvrd Mar 29, 2026
4944d06
Merge pull request #118 from dvrd/fix/ss-871
dvrd Mar 29, 2026
425e4c4
fix(review-40): shell injection, task completed flag, ssrf ctx, dead …
dvrd Mar 29, 2026
dedb9cb
Merge pull request #120 from dvrd/fix/ss-872
dvrd Mar 29, 2026
31ee0cf
fix(review-41): ValidateWebhookURL ctx, daemon max-conn, bulk INSERT,…
dvrd Mar 29, 2026
d6926ea
Merge pull request #122 from dvrd/fix/ss-873
dvrd Mar 29, 2026
38c1117
fix(review-42): task completed ordering, IPv6 zone bypass, LFS 413, g…
dvrd Mar 29, 2026
91be687
Merge pull request #124 from dvrd/fix/ss-874
dvrd Mar 29, 2026
3f7f7ec
fix: round 43 — task delete ordering, serviceRpc WriteHeader, auth lo…
dvrd Mar 29, 2026
4ef074d
fix: round 44 — PushMirrors context, XFF IP validation, task waiter, …
dvrd Mar 29, 2026
6258353
fix: round 45 — update hook comment, getInfoRefs flush writer (#130)
dvrd Mar 29, 2026
9c96c96
fix: round 46 — task panic recovery, git config isolation, delivery l…
dvrd Mar 29, 2026
d01b883
fix: round 47 — LFS delete errors, webhook limits, file:// comment, s…
dvrd Mar 29, 2026
630096d
fix: round 48 — ImportRepository race, SSRF IP selection, SCP host he…
dvrd Mar 29, 2026
c8f2b6e
fix: round 49 — ImportRepository nil-nil edge case, comment cleanup (…
dvrd Mar 29, 2026
54c9216
fix: round 50 — sendFile TOCTOU, Repositories singleflight, token com…
dvrd Mar 29, 2026
137f661
fix(push-mirror): narrow DNS rebinding window for SSH/SCP mirrors (#142)
dvrd Mar 29, 2026
5d2a9f5
fix: round 52 — shell-quote known_hosts, ImportRepository log, style …
dvrd Mar 29, 2026
32dc97d
fix: round 53 — git.Init mirror clobber, GIT_SSH_COMMAND safety, atom…
dvrd Mar 29, 2026
08493ba
fix: round 54 — LFS batch N+1, mirror schemes, dead code, docs (#148)
dvrd Mar 29, 2026
fa3631d
fix: round 55 — LFS disk-before-DB, push mirror DNS warning, bulk OID…
dvrd Mar 29, 2026
2f235e8
fix: round 55 partial — LFS orphan cleanup error handling (#151)
dvrd Mar 29, 2026
ed1bc2e
fix(web): JWT claims validation - expiration, not-before, issuer, aud…
dvrd Mar 29, 2026
d32e40d
wip: LFS N+1 query fix attempt
dvrd Mar 29, 2026
68fd30b
Merge remote-tracking branch 'origin/main' into fix/ss-857
dvrd Mar 29, 2026
9252d97
fix(web): JWT token expiration error handling
dvrd Mar 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
235 changes: 235 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
# AGENTS.md — Soft Serve

> AI agent guide for [Soft Serve](https://github.com/charmbracelet/soft-serve), a self-hosted Git server with SSH, HTTP, and native git protocol support, plus a built-in TUI.

## Project Identity

- **Module:** `github.com/charmbracelet/soft-serve`
- **Language:** Go 1.25+
- **Binary:** `soft` (entry point: `cmd/soft/main.go`)
- **Database:** SQLite (default) or PostgreSQL, via `jmoiron/sqlx`
- **CLI framework:** `spf13/cobra` (used for both the main CLI and SSH subcommands)
- **SSH framework:** `charm.land/wish/v2`
- **TUI framework:** `charm.land/bubbletea/v2` + `lipgloss/v2` + `glamour/v2`
- **HTTP router:** `gorilla/mux`
- **Config:** YAML file (`$SOFT_SERVE_DATA_PATH/config.yaml`) + env vars (`SOFT_SERVE_*` prefix)

## Architecture Overview

Soft Serve runs **four concurrent servers** via `errgroup`:

| Server | Package | Protocol | Purpose |
|--------|---------|----------|---------|
| SSH | `pkg/ssh/` | SSH | Git push/pull, interactive TUI, admin CLI commands |
| HTTP | `pkg/web/` | HTTP/HTTPS | Git smart HTTP, LFS API, go-get meta, health check |
| Git Daemon | `pkg/daemon/` | Native git (TCP) | Read-only anonymous git access |
| Stats | `pkg/stats/` | HTTP | Prometheus `/metrics` endpoint |

### Layered Design

```
cmd/soft/ → CLI entry points (serve, browse, admin, hook)
pkg/ssh/ → SSH server + middleware + command dispatch
pkg/web/ → HTTP server + middleware + routes
pkg/daemon/ → Git daemon (raw TCP, pktline)
pkg/backend/ → Central business logic orchestrator
pkg/store/ → Data store interface (7 sub-interfaces)
pkg/store/database/ → SQL implementation of Store
pkg/db/ → Database layer (open, migrate, models)
pkg/proto/ → Core domain interfaces (User, Repository)
pkg/access/ → Access level enum (NoAccess → Admin)
pkg/git/ → Git service handlers (upload-pack, receive-pack)
pkg/config/ → Configuration parsing (YAML + env)
pkg/ui/ → TUI components (Bubble Tea models)
```

### Canonical Data Flow

```
Transport layer (SSH/HTTP/daemon)
→ Middleware (context injection, auth, logging)
→ Command/Route dispatch
→ Backend (business logic)
→ Store (data access)
→ Database (SQLite/PostgreSQL)
```

## Directory Structure

```
cmd/
soft/main.go — Binary entry point
soft/serve/ — `soft serve` command (starts all servers)
soft/admin/ — `soft admin` SSH admin commands
soft/browse/ — `soft browse` TUI browser
soft/hook/ — `soft hook` git hook handler
cmd.go — Shared InitBackendContext / CloseDBContext helpers
git/ — Low-level git operations (repo, commit, tree, tag, refs)
pkg/
access/ — AccessLevel enum (NoAccess, ReadOnly, ReadWrite, Admin)
backend/ — Central Backend struct: users, repos, auth, LFS, webhooks, cache
config/ — Config struct (YAML + env, SOFT_SERVE_* prefix)
cron/ — Cron scheduler wrapper (robfig/cron)
daemon/ — Git daemon server (TCP, native git protocol)
db/ — Database abstraction (open, migrations, models)
migrate/ — SQL migration files (SQLite + PostgreSQL variants)
models/ — DB model structs
git/ — Git service handlers (upload-pack, receive-pack, LFS transfer)
hooks/ — Git hook generation and interface
jobs/ — Cron job definitions (mirror pull)
jwk/ — JSON Web Key support
lfs/ — Git LFS protocol (client, pointers, scanner, transfers)
log/ — Logger setup
proto/ — Core interfaces (Repository, User, AccessToken, errors)
ssh/ — SSH server, middleware, session handler
cmd/ — 27 SSH subcommands (git, repo, user, token, webhook, settings)
sshutils/ — SSH utility functions
ssrf/ — SSRF protection for outbound requests
stats/ — Prometheus metrics server
storage/ — Storage abstraction (file storage for LFS)
store/ — Store interface (composite of 7 sub-interfaces)
database/ — SQL-backed Store implementation
sync/ — Sync utilities
task/ — Async task manager
ui/ — Bubble Tea TUI components
common/ — Shared TUI state/styles
components/ — Reusable widgets (code, footer, header, selector, statusbar, tabs, viewport)
pages/ — TUI pages (repo list, repo detail)
styles/ — TUI styling
utils/ — General utilities (SanitizeRepo, ValidateRepo)
version/ — Version info
web/ — HTTP server (git smart HTTP, LFS API, auth, health)
webhook/ — Webhook event system (push, branch/tag, repo, collaborator)
testscript/ — Integration tests (testscript framework)
migrations/ — Goose SQL migrations
```

## Key Patterns

### Context-Based Dependency Injection

All major dependencies are threaded through `context.Context` with typed keys:

```go
// Injecting
ctx = config.WithContext(ctx, cfg)
ctx = backend.WithContext(ctx, be)

// Extracting
cfg := config.FromContext(ctx)
be := backend.FromContext(ctx)
```

Shared bootstrap in `cmd/cmd.go:InitBackendContext` (used as Cobra `PersistentPreRunE`).

### Backend as Central Orchestrator

`pkg/backend/backend.go:Backend` is the single entry point for all business logic. It wraps the Store and provides methods for repos, users, auth, LFS, webhooks, settings, and caching. Handlers and commands call Backend methods — never the Store directly.

### Store Interface (Composite)

`pkg/store/store.go:Store` aggregates 7 sub-interfaces:

- `RepositoryStore` — repo CRUD
- `UserStore` — user CRUD
- `CollaboratorStore` — repo collaborators
- `SettingStore` — server settings
- `LFSStore` — LFS objects/locks
- `AccessTokenStore` — API tokens
- `WebhookStore` — webhook management

Single SQL implementation in `pkg/store/database/`.

### Authentication

**SSH:** Public key verification → `be.UserByPublicKey()`. Keyboard-interactive as keyless fallback.

**HTTP:** Three schemes via `Authorization` header:
- `Basic` — username/password (bcrypt) or username/access-token
- `Token` — access token directly (`ss_` prefixed, SHA256 hashed)
- `Bearer` — JWT (Ed25519 signed, scoped to repo)

### Authorization

Four levels in `pkg/access/access.go`:
- `NoAccess` (0) — denied
- `ReadOnlyAccess` (1) — clone/fetch
- `ReadWriteAccess` (2) — push, LFS locks
- `AdminAccess` (3) — server management

### SSH Command Dispatch

Non-PTY SSH sessions go through `CommandMiddleware` which builds a Cobra command tree from `pkg/ssh/cmd/`. The same `cobra.Command` pattern used for the main CLI is reused for SSH commands.

PTY sessions launch the Bubble Tea TUI via `SessionHandler`.

### Git Operations

Git push/pull is handled by shelling out to the `git` binary (not pure Go) via `pkg/git/service.go:gitServiceHandler`. Environment variables pass context (repo name, username, public key) to git hooks.

### Webhook System

Events defined in `pkg/webhook/`: push, branch create/delete, tag create/delete, repository create/delete, collaborator add/remove. Delivery via HTTP POST with HMAC-SHA256 signatures. SSRF protection in `pkg/ssrf/`.

## Build & Test

```bash
# Build
go build ./cmd/soft

# Run all tests
go test ./...

# Run integration tests
go test ./testscript/...

# Run specific test
go test -run TestName ./pkg/backend/...
```

### Test Framework

- Unit tests: standard Go `testing` + `matryer/is` for assertions
- Integration tests: `rogpeppe/go-internal/testscript` (script-driven, in `testscript/`)
- Test helpers in `pkg/test/`

## Database

- **Drivers:** SQLite (`modernc.org/sqlite`, pure Go) and PostgreSQL (`lib/pq`)
- **Access:** `jmoiron/sqlx` with raw SQL queries, `$1`/`$2` placeholders
- **Migrations:** Go-embedded SQL files in `pkg/db/migrate/`, separate `.up.sql`/`.down.sql` per driver
- **Models:** `pkg/db/models/`
- **Transactions:** `db.TransactionContext` helper

## Configuration

Loaded from `$SOFT_SERVE_DATA_PATH/config.yaml` with env var overrides (`SOFT_SERVE_*` prefix). Parsed via `caarlos0/env`. Key config sections:

- `SSH` — listen addr, max timeout, key path
- `HTTP` — listen addr, TLS cert/key, public URL
- `Git` — listen addr, max connections, max timeout
- `Stats` — listen addr
- `LFS` — enabled flag
- `DB` — driver (sqlite/postgres), data source

## Metrics

Prometheus metrics throughout the codebase via `promauto`. Exposed at Stats server `/metrics`. Covers SSH connections, HTTP requests, git operations, TUI sessions, LFS transfers.

## Known TODOs

- `pkg/backend/backend.go` — proper caching interface (currently basic in-memory)
- `pkg/backend/lfs.go` — S3 storage support for LFS
- `pkg/lfs/ssh_client.go` — Git LFS SSH client (placeholder)
- `pkg/backend/hooks.go` — async hook execution
- `pkg/backend/user.go` — user repository ownership

## Common Pitfalls

1. **Never edit `*_templ.go` files** — they are generated. Edit `.templ` sources only.
2. **Context is king** — all dependencies flow through `context.Context`. Use `FromContext` accessors.
3. **Backend, not Store** — handlers call `Backend` methods, never `Store` directly.
4. **Git binary, not go-git** — push/pull shells out to `git`. `go-git` is used for read operations (log, tree, diff).
5. **Dual DB support** — SQL must work on both SQLite and PostgreSQL. Test with both if modifying queries.
6. **SSH commands are Cobra commands** — same pattern as CLI, dispatched in `CommandMiddleware`.
7. **Access levels gate everything** — check `AccessLevel` before any repo operation.
2 changes: 2 additions & 0 deletions cmd/soft/hook/hook.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,5 +168,7 @@ func runCommand(ctx context.Context, in io.Reader, out io.Writer, err io.Writer,
cmd.Stdin = in
cmd.Stdout = out
cmd.Stderr = err
cfg := config.FromContext(ctx)
cmd.Env = cfg.Environ()
return cmd.Run()
}
22 changes: 22 additions & 0 deletions cmd/soft/serve/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ import (
"syscall"
"time"

"charm.land/log/v2"
"github.com/charmbracelet/soft-serve/cmd"
"github.com/charmbracelet/soft-serve/pkg/access"
"github.com/charmbracelet/soft-serve/pkg/backend"
"github.com/charmbracelet/soft-serve/pkg/config"
"github.com/charmbracelet/soft-serve/pkg/db"
Expand Down Expand Up @@ -70,6 +72,26 @@ var (
return fmt.Errorf("migration error: %w", err)
}

// Apply config-driven settings so operators can automate
// server setup without a post-start SSH session.
{
cfgCtx := config.FromContext(ctx)
be := backend.FromContext(ctx)
logger := log.FromContext(ctx).WithPrefix("server")
if cfgCtx.AnonAccess != "" {
if err := be.SetAnonAccess(ctx, access.ParseAccessLevel(cfgCtx.AnonAccess)); err != nil {
return fmt.Errorf("set anon_access: %w", err)
}
logger.Debug("applied anon_access from config", "level", cfgCtx.AnonAccess)
}
if cfgCtx.AllowKeyless != nil {
if err := be.SetAllowKeyless(ctx, *cfgCtx.AllowKeyless); err != nil {
return fmt.Errorf("set allow_keyless: %w", err)
}
logger.Debug("applied allow_keyless from config", "value", *cfgCtx.AllowKeyless)
}
}

s, err := NewServer(ctx)
if err != nil {
return fmt.Errorf("start server: %w", err)
Expand Down
Loading