Download
176 lines 4.1 KiBGoModified 2026-10-31 raw
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
// Command blinker-agent is a coding agent that works inside blinker's
// sandbox, under an access policy the user controls.
package main

import (
	"errors"
	"fmt"
	"log"
	"net/http"
	"os"
	"path/filepath"
	"time"

	"golang.org/x/term"

	"blinker/internal/config"
	"blinker/internal/llm"
	"blinker/internal/policy"
	"blinker/internal/sandbox"
	"blinker/internal/tui"
	"blinker/policies"
)

const usage = `Usage:
  blinker-agent          start the agent in the current directory
  blinker-agent setup    choose the LLM backend
  blinker-agent policy   show this directory's access policy and where it is

Settings are in %s and %s (JSON).
`

func main() {
	if code, ok := sandbox.RunHidden(os.Args); ok {
		os.Exit(code)
	}
	if err := run(os.Args[1:]); err != nil {
		fmt.Fprintln(os.Stderr, "blinker-agent:", err)
		os.Exit(1)
	}
}

func run(args []string) error {
	configDir, err := config.Dir()
	if err != nil {
		return err
	}
	stateDir, err := config.StateDir()
	if err != nil {
		return err
	}
	dataDirs, err := config.DataDirs()
	if err != nil {
		return err
	}

	sub := ""
	if len(args) > 0 {
		sub = args[0]
	}
	help := fmt.Sprintf(usage, filepath.Join(configDir, config.AgentFile), filepath.Join(configDir, config.SandboxFile))
	switch sub {
	case "", "setup", "policy":
	case "help", "-h", "-help", "--help":
		fmt.Print(help)
		return nil
	default:
		fmt.Fprint(os.Stderr, help)
		return fmt.Errorf("unknown command %q", sub)
	}
	if len(args) > 1 {
		return fmt.Errorf("%s takes no arguments", sub)
	}

	if sub == "policy" {
		wd, err := os.Getwd()
		if err != nil {
			return err
		}
		project, err := filepath.EvalSymlinks(wd)
		if err != nil {
			return err
		}
		store := policy.Store{
			ConfigDir: configDir,
			Dirs:      policy.NamedDirs(configDir, policy.OutsideProject(dataDirs, project)),
			Builtin:   policies.FS,
		}
		loaded, err := store.Load(project)
		if err != nil {
			return err
		}
		fmt.Println(loaded.Text())
		home, err := os.UserHomeDir()
		if err != nil {
			return err
		}
		pol, err := loaded.Compile(policy.Floor{}, policy.Env{Home: home, Project: project})
		if err != nil {
			return err
		}
		for _, w := range sandbox.Lint(pol) {
			fmt.Println("warning:", w.Text)
		}
		return nil
	}

	if !term.IsTerminal(int(os.Stdin.Fd())) || !term.IsTerminal(int(os.Stdout.Fd())) {
		return errors.New("blinker-agent needs a terminal")
	}
	cfg, err := config.Load(configDir)
	if err != nil {
		return err
	}

	var logger *log.Logger
	if cfg.LogFile != "" {
		f, err := os.OpenFile(cfg.LogFile, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0o600)
		if err != nil {
			return err
		}
		defer f.Close()
		logger = log.New(f, "", log.LstdFlags)
	}

	sb, err := sandbox.New(sandbox.Options{
		ConfigDir:    configDir,
		StateDir:     stateDir,
		DataDirs:     dataDirs,
		ScratchRoot:  cfg.Sandbox.ScratchRoot,
		ProxyPorts:   cfg.Sandbox.ProxyPorts,
		AllowPrivate: cfg.Sandbox.AllowPrivateEgress,
		ExtraFloor:   policy.Floor{Blocklist: cfg.Sandbox.ExtraBlocklist, NeverWrite: cfg.Sandbox.ExtraNeverWrite},
		Log: func(line string) {
			if logger != nil {
				logger.Print(line)
			}
		},
	})
	if err != nil {
		return err
	}
	defer sb.Close()

	return tui.Run(tui.Deps{
		Config:    cfg,
		ConfigDir: configDir,
		StateDir:  stateDir,
		SB:        sb,
		SetupOnly: sub == "setup",
		NewClient: func(c config.Config) (*llm.Client, error) {
			client := llm.New(c.BaseURL, c.Key(), c.Model)
			client.Thinking = c.Thinking
			client.NoSampling = !c.Sampling
			client.Temperature, client.TopP, client.TopK = c.Temperature, c.TopP, c.TopK
			client.MaxTokens = c.MaxTokens
			client.ContextWindow = c.ContextWindow
			client.MaxTokensParam = c.MaxTokensParam
			if c.ThinkingParam != llm.ThinkingTemplate {
				client.ThinkingParam = ""
			}
			client.HTTP.Timeout = time.Duration(c.RequestTimeout)
			if c.CACert != "" {
				tlsConf, err := llm.TLSConfig(c.CACert)
				if err != nil {
					return nil, err
				}
				// The default transport's timeouts and HTTP/2 still apply.
				tr := http.DefaultTransport.(*http.Transport).Clone()
				tr.TLSClientConfig = tlsConf
				client.HTTP.Transport = tr
			}
			client.Logger = logger
			return client, nil
		},
	})
}