Run byre and the app on their own embedded Tailscale nodes
This commit is contained in:
+5
-13
@@ -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
@@ -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
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user