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
},
})
}
|