Run byre and the app on their own embedded Tailscale nodes

This commit is contained in:
Ian Gulliver
2026-10-20 13:32:56 -07:00
parent a7b2333535
commit 2dfcc5de47
29 changed files with 1602 additions and 602 deletions
+5 -13
View File
@@ -1,14 +1,11 @@
package main
import (
"context"
"encoding/json"
"fmt"
"os"
"os/exec"
"strings"
"g.fc.run/ai/byre/internal/tailscale"
)
const (
@@ -30,7 +27,6 @@ type check struct {
func checks() []check {
return []check{
gitCheck(),
tailscaleCheck(),
command("claude", needAgent, "Claude models", "curl -fsSL https://claude.ai/install.sh | bash", "Then run claude once to sign in."),
command("codex", needAgent, "GPT models", "brew install --cask codex", "Then run codex once to sign in."),
command("agy", needAgent, "Gemini models", "brew install --cask antigravity-cli", "Then run agy once to sign in."),
@@ -47,11 +43,6 @@ func gitCheck() check {
return c
}
func tailscaleCheck() check {
problem := tailscale.Problem(context.Background())
return check{Name: "Tailscale", Need: needRequired, For: "the server's HTTPS address and identity", OK: problem == "", Fix: problem}
}
func command(name, need, purpose, install, fix string) check {
_, err := exec.LookPath(name)
return check{Name: name, Need: need, For: purpose, OK: err == nil, Command: install, Fix: fix}
@@ -79,9 +70,10 @@ func checkCmd() {
cs := checks()
ready := markBlocking(cs)
out, err := json.Marshal(struct {
Ready bool `json:"ready"`
Checks []check `json:"checks"`
}{ready, cs})
Ready bool `json:"ready"`
Loopback string `json:"loopback"`
Checks []check `json:"checks"`
}{ready, loopbackURL, cs})
if err != nil {
fatal("encode checks", "err", err)
}
@@ -93,7 +85,7 @@ func requireChecks() {
if markBlocking(cs) {
return
}
fmt.Println("byre can't run on this Mac yet. It needs git, Tailscale, and at least one of claude, codex and agy:")
fmt.Println("byre can't run on this Mac yet. It needs git and at least one of claude, codex and agy:")
for _, c := range cs {
if c.Blocking {
fmt.Printf(" %s: %s\n", c.Name, strings.TrimSpace(c.Command+" "+c.Fix))
+1 -8
View File
@@ -1,7 +1,6 @@
package main
import (
"context"
"encoding/xml"
"fmt"
"os"
@@ -9,8 +8,6 @@ import (
"path/filepath"
"strings"
"syscall"
"g.fc.run/ai/byre/internal/tailscale"
)
const serviceLabel = "run.fc.byre"
@@ -71,11 +68,7 @@ func installCmd() {
}
fmt.Println("wrote", plistPath)
url, err := tailscale.ServeRoot(context.Background(), "http://"+defaultListen)
if err != nil {
fatal("tailscale serve", "err", err)
}
fmt.Println("byre is at", url)
fmt.Println("byre's loopback address is", loopbackURL)
domain := fmt.Sprintf("gui/%d", os.Getuid())
service := domain + "/" + serviceLabel
+90 -11
View File
@@ -2,8 +2,10 @@ package main
import (
"context"
"crypto/tls"
"errors"
"flag"
"fmt"
"log/slog"
"net"
"net/http"
@@ -17,9 +19,13 @@ import (
"g.fc.run/ai/byre/internal/server"
"g.fc.run/ai/byre/internal/store"
"tailscale.com/tsnet"
)
const defaultListen = "127.0.0.1:8270"
const (
defaultListen = "127.0.0.1:8270"
loopbackURL = "http://" + defaultListen
)
func serveCmd(args []string) {
configDir, err := os.UserConfigDir()
@@ -29,11 +35,34 @@ func serveCmd(args []string) {
flags := flag.NewFlagSet("serve", flag.ExitOnError)
dataDir := flags.String("data", filepath.Join(configDir, "byre"), "directory holding byre's database, worktrees, leads and blobs")
listenAddr := flags.String("listen", defaultListen, "address to listen on")
listenAddr := flags.String("listen", defaultListen, "loopback address serving GET /api/tailnet")
ports := flags.Int("ports", store.DefaultPortFirst, "first port of the tasks' port blocks")
hostname := flags.String("ts-hostname", "byre-"+macSlug(), "hostname of byre's tailnet node")
flags.Parse(args)
srv, err := server.New(*dataDir, *ports)
host, _, err := net.SplitHostPort(*listenAddr)
if err != nil {
fatal("parse --listen", "err", err)
}
ip := net.ParseIP(host)
if ip == nil || !ip.IsLoopback() {
fatal("--listen must be a loopback address", "addr", *listenAddr)
}
ts := &tsnet.Server{
Dir: filepath.Join(*dataDir, "tsnet"),
Hostname: *hostname,
UserLogf: func(format string, args ...any) {
slog.Info("tsnet", "msg", fmt.Sprintf(format, args...))
},
}
err = ts.Start()
if err != nil {
fatal("start tsnet", "err", err)
}
defer ts.Close()
srv, err := server.New(*dataDir, *ports, ts)
if err != nil {
fatal("start server", "err", err)
}
@@ -46,26 +75,76 @@ func serveCmd(args []string) {
go watchExecutable(ctx, replaced)
go updateDaily(ctx)
hs := &http.Server{
Addr: *listenAddr,
Handler: srv.Handler(),
BaseContext: func(net.Listener) context.Context { return ctx },
}
base := func(net.Listener) context.Context { return ctx }
loopback := &http.Server{Addr: *listenAddr, Handler: srv.LoopbackHandler(), BaseContext: base}
tailnet := &http.Server{Handler: srv.Handler(), BaseContext: base}
go func() {
<-ctx.Done()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
hs.Shutdown(shutdownCtx)
loopback.Shutdown(shutdownCtx)
tailnet.Shutdown(shutdownCtx)
}()
slog.Info("listening", "addr", *listenAddr, "data", *dataDir, "ports", *ports)
err = hs.ListenAndServe()
go serveTailnet(ctx, ts, tailnet)
slog.Info("listening", "addr", *listenAddr, "hostname", *hostname, "data", *dataDir, "ports", *ports)
err = loopback.ListenAndServe()
if !errors.Is(err, http.ErrServerClosed) {
fatal("serve", "err", err)
}
}
func serveTailnet(ctx context.Context, ts *tsnet.Server, hs *http.Server) {
st, err := ts.Up(ctx)
if ctx.Err() != nil {
return
}
if err != nil {
fatal("tsnet up", "err", err)
}
if len(st.CertDomains) == 0 {
slog.Error("tailnet has no https certificates; turn on HTTPS on the DNS page of the tailscale admin console, then restart byre")
return
}
lc, err := ts.LocalClient()
if err != nil {
fatal("tsnet local client", "err", err)
}
ln, err := ts.Listen("tcp", ":443")
if err != nil {
fatal("tsnet listen", "err", err)
}
tlsLn := tls.NewListener(ln, &tls.Config{GetCertificate: lc.GetCertificate, NextProtos: []string{"h2", "http/1.1"}})
slog.Info("listening on tailnet", "origin", "https://"+st.CertDomains[0])
err = hs.Serve(tlsLn)
if !errors.Is(err, http.ErrServerClosed) {
fatal("serve tailnet", "err", err)
}
}
func macSlug() string {
out, err := exec.Command("scutil", "--get", "ComputerName").Output()
if err != nil {
fatal("computer name", "err", err)
}
slug := strings.Builder{}
dash := false
for _, r := range strings.ToLower(strings.TrimSpace(string(out))) {
if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') {
if dash && slug.Len() > 0 {
slug.WriteByte('-')
}
slug.WriteRune(r)
dash = false
continue
}
dash = true
}
return slug.String()
}
func watchExecutable(ctx context.Context, replaced context.CancelFunc) {
exe, err := os.Executable()
if err != nil {