@@ -1,8 +1,8 @@
|
||||
name: CI
|
||||
|
||||
# Go CI for sherlock. Runs `go vet`, `go test -race`, `go build` on every
|
||||
# push and PR. No deploy pipeline — sherlock is operator-installed via
|
||||
# `go install`, not host-deployed.
|
||||
# Go CI for sherlock. Runs gofmt, go vet, errcheck, staticcheck,
|
||||
# go test -race, and go build on every push and PR. No deploy pipeline
|
||||
# — sherlock is operator-installed via `go install`, not host-deployed.
|
||||
#
|
||||
# Why no actions/checkout + actions/setup-go:
|
||||
# act_runner re-clones every third-party JS action from github.com per
|
||||
@@ -29,6 +29,10 @@ env:
|
||||
# x/sync now declare `go 1.25.0`, which transitively forces our module
|
||||
# to `go 1.25+`. 1.26.3 is the current upstream stable.
|
||||
GO_VERSION: "1.26.3"
|
||||
# Pinned linter versions. Bump deliberately; the runner caches the
|
||||
# built binaries under ~/go/bin so re-runs are no-ops.
|
||||
ERRCHECK_VERSION: "v1.20.0"
|
||||
STATICCHECK_VERSION: "v0.7.0"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -88,9 +92,43 @@ jobs:
|
||||
fetch --depth=1 origin "${GITHUB_SHA}"
|
||||
git checkout -q FETCH_HEAD
|
||||
|
||||
- name: Install linters (errcheck, staticcheck)
|
||||
run: |
|
||||
set -eu
|
||||
# Both tools are cached under $HOME/go/bin across jobs since
|
||||
# the host-executor runner persists the home directory.
|
||||
if ! command -v errcheck >/dev/null 2>&1; then
|
||||
echo "installing errcheck ${ERRCHECK_VERSION}"
|
||||
go install "github.com/kisielk/errcheck@${ERRCHECK_VERSION}"
|
||||
else
|
||||
echo "errcheck already installed: $(errcheck -h 2>&1 | head -1 || true)"
|
||||
fi
|
||||
if ! command -v staticcheck >/dev/null 2>&1; then
|
||||
echo "installing staticcheck ${STATICCHECK_VERSION}"
|
||||
go install "honnef.co/go/tools/cmd/staticcheck@${STATICCHECK_VERSION}"
|
||||
else
|
||||
echo "staticcheck already installed: $(staticcheck -version)"
|
||||
fi
|
||||
|
||||
- name: gofmt
|
||||
run: |
|
||||
set -eu
|
||||
diff=$(gofmt -l $(find . -name '*.go' -not -path './vendor/*'))
|
||||
if [ -n "$diff" ]; then
|
||||
echo "gofmt would reformat:"
|
||||
echo "$diff"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: go vet
|
||||
run: go vet ./...
|
||||
|
||||
- name: errcheck
|
||||
run: errcheck ./...
|
||||
|
||||
- name: staticcheck
|
||||
run: staticcheck ./...
|
||||
|
||||
- name: go test -race
|
||||
run: go test ./... -race
|
||||
|
||||
|
||||
@@ -35,9 +35,9 @@ import (
|
||||
var Version = "0.0.0-dev"
|
||||
|
||||
const (
|
||||
envIssuer = "SHERLOCK_AUTHENTIK_ISSUER"
|
||||
envClientID = "SHERLOCK_AUTHENTIK_CLIENT_ID"
|
||||
envScopes = "SHERLOCK_AUTHENTIK_SCOPES"
|
||||
envIssuer = "SHERLOCK_AUTHENTIK_ISSUER"
|
||||
envClientID = "SHERLOCK_AUTHENTIK_CLIENT_ID"
|
||||
envScopes = "SHERLOCK_AUTHENTIK_SCOPES"
|
||||
envIdleTimeout = "SHERLOCK_BROKER_IDLE"
|
||||
|
||||
defaultScopes = "openid profile email offline_access"
|
||||
@@ -105,12 +105,20 @@ func runDaemon() {
|
||||
fmt.Fprintln(os.Stderr, "sherlock-broker:", err)
|
||||
os.Exit(4)
|
||||
}
|
||||
defer lock.Release()
|
||||
defer func() {
|
||||
if err := lock.Release(); err != nil {
|
||||
log.Printf("pidlock release: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
cfg := loadConfig()
|
||||
|
||||
flow := authn.NewFlow(cfg, nil)
|
||||
defer flow.Close()
|
||||
defer func() {
|
||||
if err := flow.Close(); err != nil {
|
||||
log.Printf("flow close: %v", err)
|
||||
}
|
||||
}()
|
||||
refresher := authn.NewRefresher(cfg, nil)
|
||||
|
||||
profiles, err := agent.Load(agent.LoadOptions{UserDir: mustAgentsDir()})
|
||||
|
||||
+230
-231
@@ -5,99 +5,98 @@
|
||||
//
|
||||
// Routing rules:
|
||||
//
|
||||
//sherlock version → print version, no broker
|
||||
//sherlock login → ensure broker, OAuth flow, persist tokens
|
||||
//sherlock logout [--shutdown]→ ensure broker, clear tokens, maybe stop it
|
||||
//sherlock status → ensure broker, print status table
|
||||
//sherlock run <agent> [...] → ensure broker, load profile, exec agent
|
||||
//sherlock <agent-name> [...] → alias for `run <agent-name>` when the
|
||||
// first arg matches a loaded profile name
|
||||
// sherlock version → print version, no broker
|
||||
// sherlock login → ensure broker, OAuth flow, persist tokens
|
||||
// sherlock logout [--shutdown]→ ensure broker, clear tokens, maybe stop it
|
||||
// sherlock status → ensure broker, print status table
|
||||
// sherlock run <agent> [...] → ensure broker, load profile, exec agent
|
||||
// sherlock <agent-name> [...] → alias for `run <agent-name>` when the
|
||||
//
|
||||
// first arg matches a loaded profile name
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"time"
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"gitea.alexandru.macocian.me/Charlie/sherlock/internal/agent"
|
||||
"gitea.alexandru.macocian.me/Charlie/sherlock/internal/broker"
|
||||
"gitea.alexandru.macocian.me/Charlie/sherlock/internal/keyring"
|
||||
"gitea.alexandru.macocian.me/Charlie/sherlock/internal/rpc"
|
||||
"gitea.alexandru.macocian.me/Charlie/sherlock/internal/socket"
|
||||
"gitea.alexandru.macocian.me/Charlie/sherlock/internal/xdg"
|
||||
"gitea.alexandru.macocian.me/Charlie/sherlock/internal/agent"
|
||||
"gitea.alexandru.macocian.me/Charlie/sherlock/internal/broker"
|
||||
"gitea.alexandru.macocian.me/Charlie/sherlock/internal/keyring"
|
||||
"gitea.alexandru.macocian.me/Charlie/sherlock/internal/rpc"
|
||||
"gitea.alexandru.macocian.me/Charlie/sherlock/internal/socket"
|
||||
"gitea.alexandru.macocian.me/Charlie/sherlock/internal/xdg"
|
||||
)
|
||||
|
||||
// Version is overwritten at build time via -ldflags "-X main.Version=...".
|
||||
var Version = "0.0.0-dev"
|
||||
|
||||
const (
|
||||
exitOK = 0
|
||||
exitGeneric = 1
|
||||
exitUsage = 2
|
||||
exitKeyring = 3
|
||||
exitBroker = 4
|
||||
exitAuth = 5
|
||||
exitLoginWait = 5 * time.Minute
|
||||
exitOK = 0
|
||||
exitGeneric = 1
|
||||
exitUsage = 2
|
||||
exitKeyring = 3
|
||||
exitBroker = 4
|
||||
exitAuth = 5
|
||||
exitLoginWait = 5 * time.Minute
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
usage(os.Stderr)
|
||||
os.Exit(exitUsage)
|
||||
}
|
||||
if len(os.Args) < 2 {
|
||||
usage(os.Stderr)
|
||||
os.Exit(exitUsage)
|
||||
}
|
||||
|
||||
sub := os.Args[1]
|
||||
rest := os.Args[2:]
|
||||
sub := os.Args[1]
|
||||
rest := os.Args[2:]
|
||||
|
||||
switch sub {
|
||||
case "version", "--version", "-v":
|
||||
fmt.Println(Version)
|
||||
return
|
||||
case "help", "--help", "-h":
|
||||
usage(os.Stdout)
|
||||
return
|
||||
}
|
||||
switch sub {
|
||||
case "version", "--version", "-v":
|
||||
fmt.Println(Version)
|
||||
return
|
||||
case "help", "--help", "-h":
|
||||
usage(os.Stdout)
|
||||
return
|
||||
}
|
||||
|
||||
// Every non-trivial subcommand needs the keyring up.
|
||||
if err := keyring.Probe(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "sherlock:", err)
|
||||
fmt.Fprintln(os.Stderr, "Hint:", keyring.RemediationHint())
|
||||
os.Exit(exitKeyring)
|
||||
}
|
||||
// Every non-trivial subcommand needs the keyring up.
|
||||
if err := keyring.Probe(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "sherlock:", err)
|
||||
fmt.Fprintln(os.Stderr, "Hint:", keyring.RemediationHint())
|
||||
os.Exit(exitKeyring)
|
||||
}
|
||||
|
||||
switch sub {
|
||||
case "login":
|
||||
runLogin(rest)
|
||||
case "logout":
|
||||
runLogout(rest)
|
||||
case "status":
|
||||
runStatus(rest)
|
||||
case "run":
|
||||
if len(rest) == 0 {
|
||||
fmt.Fprintln(os.Stderr, "sherlock: run requires an agent name")
|
||||
os.Exit(exitUsage)
|
||||
}
|
||||
runAgent(rest[0], rest[1:])
|
||||
default:
|
||||
// Agent alias: `sherlock copilot ...` -> `run copilot ...`
|
||||
if profileExists(sub) {
|
||||
runAgent(sub, rest)
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "sherlock: unknown subcommand %q\n", sub)
|
||||
usage(os.Stderr)
|
||||
os.Exit(exitUsage)
|
||||
}
|
||||
switch sub {
|
||||
case "login":
|
||||
runLogin(rest)
|
||||
case "logout":
|
||||
runLogout(rest)
|
||||
case "status":
|
||||
runStatus(rest)
|
||||
case "run":
|
||||
if len(rest) == 0 {
|
||||
fmt.Fprintln(os.Stderr, "sherlock: run requires an agent name")
|
||||
os.Exit(exitUsage)
|
||||
}
|
||||
runAgent(rest[0], rest[1:])
|
||||
default:
|
||||
// Agent alias: `sherlock copilot ...` -> `run copilot ...`
|
||||
if profileExists(sub) {
|
||||
runAgent(sub, rest)
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "sherlock: unknown subcommand %q\n", sub)
|
||||
usage(os.Stderr)
|
||||
os.Exit(exitUsage)
|
||||
}
|
||||
}
|
||||
|
||||
func usage(w *os.File) {
|
||||
fmt.Fprintf(w, `sherlock - Charlie credential broker + agent wrapper
|
||||
res, err := fmt.Fprintf(w, `sherlock - Charlie credential broker + agent wrapper
|
||||
|
||||
Usage:
|
||||
sherlock <subcommand> [args...]
|
||||
@@ -114,199 +113,199 @@ Environment (consumed by the broker, set before `+"`sherlock login`"+`):
|
||||
SHERLOCK_AUTHENTIK_ISSUER, SHERLOCK_AUTHENTIK_CLIENT_ID,
|
||||
SHERLOCK_AUTHENTIK_SCOPES (optional), SHERLOCK_BROKER_IDLE (optional).
|
||||
`)
|
||||
|
||||
if err != nil || res == 0 {
|
||||
fmt.Fprintln(os.Stderr, "sherlock: usage:", err)
|
||||
os.Exit(exitGeneric)
|
||||
}
|
||||
}
|
||||
|
||||
func dialBroker(ctx context.Context) *socket.Client {
|
||||
sockPath, err := xdg.SocketPath()
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "sherlock: socket path:", err)
|
||||
os.Exit(exitBroker)
|
||||
}
|
||||
cli, err := broker.EnsureRunning(ctx, broker.EnsureRunningOptions{SocketPath: sockPath})
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "sherlock: cannot reach broker:", err)
|
||||
os.Exit(exitBroker)
|
||||
}
|
||||
return cli
|
||||
sockPath, err := xdg.SocketPath()
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "sherlock: socket path:", err)
|
||||
os.Exit(exitBroker)
|
||||
}
|
||||
cli, err := broker.EnsureRunning(ctx, broker.EnsureRunningOptions{SocketPath: sockPath})
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "sherlock: cannot reach broker:", err)
|
||||
os.Exit(exitBroker)
|
||||
}
|
||||
return cli
|
||||
}
|
||||
|
||||
func runLogin(args []string) {
|
||||
fs := flag.NewFlagSet("login", flag.ExitOnError)
|
||||
timeout := fs.Duration("timeout", exitLoginWait, "how long to wait for browser callback")
|
||||
_ = fs.Parse(args)
|
||||
fs := flag.NewFlagSet("login", flag.ExitOnError)
|
||||
timeout := fs.Duration("timeout", exitLoginWait, "how long to wait for browser callback")
|
||||
_ = fs.Parse(args)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), *timeout+10*time.Second)
|
||||
defer cancel()
|
||||
cli := dialBroker(ctx)
|
||||
defer cli.Close()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), *timeout+10*time.Second)
|
||||
defer cancel()
|
||||
cli := dialBroker(ctx)
|
||||
defer func() { _ = cli.Close() }()
|
||||
|
||||
var start broker.LoginStartResult
|
||||
if err := cli.Call(ctx, rpc.MethodLoginStart, struct{}{}, &start); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "sherlock: login_start:", err)
|
||||
os.Exit(exitAuth)
|
||||
}
|
||||
fmt.Println("Opening browser for Authentik login...")
|
||||
fmt.Println("If it doesn't open, visit:")
|
||||
fmt.Println(" ", start.AuthURL)
|
||||
if err := openBrowser(start.AuthURL); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "sherlock: could not open browser:", err)
|
||||
}
|
||||
var start broker.LoginStartResult
|
||||
if err := cli.Call(ctx, rpc.MethodLoginStart, struct{}{}, &start); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "sherlock: login_start:", err)
|
||||
os.Exit(exitAuth)
|
||||
}
|
||||
fmt.Println("Opening browser for Authentik login...")
|
||||
fmt.Println("If it doesn't open, visit:")
|
||||
fmt.Println(" ", start.AuthURL)
|
||||
if err := openBrowser(start.AuthURL); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "sherlock: could not open browser:", err)
|
||||
}
|
||||
|
||||
wait := broker.LoginWaitParams{State: start.State, TimeoutSeconds: int(timeout.Seconds())}
|
||||
if err := cli.Call(ctx, rpc.MethodLoginWait, wait, nil); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "sherlock: login failed:", err)
|
||||
os.Exit(exitAuth)
|
||||
}
|
||||
fmt.Println("Logged in.")
|
||||
wait := broker.LoginWaitParams{State: start.State, TimeoutSeconds: int(timeout.Seconds())}
|
||||
if err := cli.Call(ctx, rpc.MethodLoginWait, wait, nil); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "sherlock: login failed:", err)
|
||||
os.Exit(exitAuth)
|
||||
}
|
||||
fmt.Println("Logged in.")
|
||||
}
|
||||
|
||||
func runLogout(args []string) {
|
||||
fs := flag.NewFlagSet("logout", flag.ExitOnError)
|
||||
shutdown := fs.Bool("shutdown", false, "also stop the broker daemon")
|
||||
_ = fs.Parse(args)
|
||||
fs := flag.NewFlagSet("logout", flag.ExitOnError)
|
||||
shutdown := fs.Bool("shutdown", false, "also stop the broker daemon")
|
||||
_ = fs.Parse(args)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
cli := dialBroker(ctx)
|
||||
defer cli.Close()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
cli := dialBroker(ctx)
|
||||
defer func() { _ = cli.Close() }()
|
||||
|
||||
if err := cli.Call(ctx, rpc.MethodLogout, struct{}{}, nil); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "sherlock: logout:", err)
|
||||
os.Exit(exitAuth)
|
||||
}
|
||||
fmt.Println("Logged out.")
|
||||
if *shutdown {
|
||||
// Best-effort: the broker tears down the socket as part of
|
||||
// shutdown so the response may race with EOF.
|
||||
_ = cli.Call(ctx, rpc.MethodShutdown, struct{}{}, nil)
|
||||
fmt.Println("Broker stopped.")
|
||||
}
|
||||
if err := cli.Call(ctx, rpc.MethodLogout, struct{}{}, nil); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "sherlock: logout:", err)
|
||||
os.Exit(exitAuth)
|
||||
}
|
||||
fmt.Println("Logged out.")
|
||||
if *shutdown {
|
||||
// Best-effort: the broker tears down the socket as part of
|
||||
// shutdown so the response may race with EOF.
|
||||
_ = cli.Call(ctx, rpc.MethodShutdown, struct{}{}, nil)
|
||||
fmt.Println("Broker stopped.")
|
||||
}
|
||||
}
|
||||
|
||||
func runStatus(_ []string) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
cli := dialBroker(ctx)
|
||||
defer cli.Close()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
cli := dialBroker(ctx)
|
||||
defer func() { _ = cli.Close() }()
|
||||
|
||||
var st broker.StatusResult
|
||||
if err := cli.Call(ctx, rpc.MethodStatus, struct{}{}, &st); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "sherlock: status:", err)
|
||||
os.Exit(exitGeneric)
|
||||
}
|
||||
if !st.LoggedIn {
|
||||
fmt.Println("Not logged in.")
|
||||
} else {
|
||||
fmt.Printf("Logged in as %s <%s>\n", st.Name, st.Email)
|
||||
fmt.Printf(" subject: %s\n", st.Subject)
|
||||
if st.IDExpiresAt != nil {
|
||||
fmt.Printf(" id token until: %s (%s)\n",
|
||||
st.IDExpiresAt.Format(time.RFC3339),
|
||||
humanizeUntil(*st.IDExpiresAt))
|
||||
}
|
||||
if st.RefreshExpAt != nil {
|
||||
fmt.Printf(" refresh until: %s (%s)\n",
|
||||
st.RefreshExpAt.Format(time.RFC3339),
|
||||
humanizeUntil(*st.RefreshExpAt))
|
||||
}
|
||||
}
|
||||
if len(st.Agents) > 0 {
|
||||
fmt.Println("Available agents:")
|
||||
for _, a := range st.Agents {
|
||||
fmt.Printf(" - %s\n", a)
|
||||
}
|
||||
}
|
||||
var st broker.StatusResult
|
||||
if err := cli.Call(ctx, rpc.MethodStatus, struct{}{}, &st); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "sherlock: status:", err)
|
||||
os.Exit(exitGeneric)
|
||||
}
|
||||
if !st.LoggedIn {
|
||||
fmt.Println("Not logged in.")
|
||||
} else {
|
||||
fmt.Printf("Logged in as %s <%s>\n", st.Name, st.Email)
|
||||
fmt.Printf(" subject: %s\n", st.Subject)
|
||||
if st.IDExpiresAt != nil {
|
||||
fmt.Printf(" id token until: %s (%s)\n",
|
||||
st.IDExpiresAt.Format(time.RFC3339),
|
||||
humanizeUntil(*st.IDExpiresAt))
|
||||
}
|
||||
if st.RefreshExpAt != nil {
|
||||
fmt.Printf(" refresh until: %s (%s)\n",
|
||||
st.RefreshExpAt.Format(time.RFC3339),
|
||||
humanizeUntil(*st.RefreshExpAt))
|
||||
}
|
||||
}
|
||||
if len(st.Agents) > 0 {
|
||||
fmt.Println("Available agents:")
|
||||
for _, a := range st.Agents {
|
||||
fmt.Printf(" - %s\n", a)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func runAgent(name string, userArgs []string) {
|
||||
profiles, err := loadProfiles()
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "sherlock: load agent profiles:", err)
|
||||
os.Exit(exitGeneric)
|
||||
}
|
||||
p, ok := profiles[name]
|
||||
if !ok {
|
||||
fmt.Fprintf(os.Stderr, "sherlock: unknown agent %q. Loaded: %v\n", name, agent.SortedNames(profiles))
|
||||
os.Exit(exitUsage)
|
||||
}
|
||||
profiles, err := loadProfiles()
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "sherlock: load agent profiles:", err)
|
||||
os.Exit(exitGeneric)
|
||||
}
|
||||
p, ok := profiles[name]
|
||||
if !ok {
|
||||
fmt.Fprintf(os.Stderr, "sherlock: unknown agent %q. Loaded: %v\n", name, agent.SortedNames(profiles))
|
||||
os.Exit(exitUsage)
|
||||
}
|
||||
|
||||
// Ensure broker is up so future MCPs (Phase 2+) can reach it.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
cli := dialBroker(ctx)
|
||||
_ = cli.Close()
|
||||
cancel()
|
||||
// Ensure broker is up so future MCPs (Phase 2+) can reach it.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
cli := dialBroker(ctx)
|
||||
_ = cli.Close()
|
||||
cancel()
|
||||
|
||||
// Phase 1: empty servers map. Phase 2 fills this from services.d.
|
||||
mcpPath, err := agent.RenderMCPConfig(p, agent.MCPConfig{Servers: map[string]agent.MCPServer{}})
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "sherlock: render mcp config:", err)
|
||||
os.Exit(exitGeneric)
|
||||
}
|
||||
// Phase 1: empty servers map. Phase 2 fills this from services.d.
|
||||
mcpPath, err := agent.RenderMCPConfig(p, agent.MCPConfig{Servers: map[string]agent.MCPServer{}})
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "sherlock: render mcp config:", err)
|
||||
os.Exit(exitGeneric)
|
||||
}
|
||||
|
||||
argv0, err := agent.ResolveExecutable(p)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "sherlock: resolve executable:", err)
|
||||
os.Exit(exitGeneric)
|
||||
}
|
||||
argv := agent.BuildArgv(p, mcpPath, userArgs)
|
||||
argv[0] = argv0
|
||||
sockPath, _ := xdg.SocketPath()
|
||||
env := agent.BuildEnv(p, mcpPath, map[string]string{"SHERLOCK_SOCKET": sockPath})
|
||||
argv0, err := agent.ResolveExecutable(p)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "sherlock: resolve executable:", err)
|
||||
os.Exit(exitGeneric)
|
||||
}
|
||||
argv := agent.BuildArgv(p, mcpPath, userArgs)
|
||||
argv[0] = argv0
|
||||
sockPath, _ := xdg.SocketPath()
|
||||
env := agent.BuildEnv(p, mcpPath, map[string]string{"SHERLOCK_SOCKET": sockPath})
|
||||
|
||||
if err := (agent.SyscallExecer{}).Exec(argv0, argv, env); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "sherlock: exec:", err)
|
||||
os.Exit(exitGeneric)
|
||||
}
|
||||
if err := (agent.SyscallExecer{}).Exec(argv0, argv, env); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "sherlock: exec:", err)
|
||||
os.Exit(exitGeneric)
|
||||
}
|
||||
}
|
||||
|
||||
func profileExists(name string) bool {
|
||||
profiles, err := loadProfiles()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
_, ok := profiles[name]
|
||||
return ok
|
||||
profiles, err := loadProfiles()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
_, ok := profiles[name]
|
||||
return ok
|
||||
}
|
||||
|
||||
func loadProfiles() (map[string]agent.Profile, error) {
|
||||
dir, err := xdg.AgentsConfigDir()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return agent.Load(agent.LoadOptions{UserDir: dir})
|
||||
dir, err := xdg.AgentsConfigDir()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return agent.Load(agent.LoadOptions{UserDir: dir})
|
||||
}
|
||||
|
||||
func openBrowser(url string) error {
|
||||
var bin string
|
||||
var args []string
|
||||
switch runtime.GOOS {
|
||||
case "darwin":
|
||||
bin = "open"
|
||||
args = []string{url}
|
||||
case "windows":
|
||||
bin = "rundll32"
|
||||
args = []string{"url.dll,FileProtocolHandler", url}
|
||||
default:
|
||||
bin = "xdg-open"
|
||||
args = []string{url}
|
||||
}
|
||||
cmd := exec.Command(bin, args...)
|
||||
if err := cmd.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
go func() { _ = cmd.Wait() }()
|
||||
return nil
|
||||
var bin string
|
||||
var args []string
|
||||
switch runtime.GOOS {
|
||||
case "darwin":
|
||||
bin = "open"
|
||||
args = []string{url}
|
||||
case "windows":
|
||||
bin = "rundll32"
|
||||
args = []string{"url.dll,FileProtocolHandler", url}
|
||||
default:
|
||||
bin = "xdg-open"
|
||||
args = []string{url}
|
||||
}
|
||||
cmd := exec.Command(bin, args...)
|
||||
if err := cmd.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
go func() { _ = cmd.Wait() }()
|
||||
return nil
|
||||
}
|
||||
|
||||
func humanizeUntil(t time.Time) string {
|
||||
d := time.Until(t).Round(time.Second)
|
||||
if d < 0 {
|
||||
return "expired"
|
||||
d := time.Until(t).Round(time.Second)
|
||||
if d < 0 {
|
||||
return "expired"
|
||||
}
|
||||
return "in " + d.String()
|
||||
}
|
||||
return "in " + d.String()
|
||||
}
|
||||
|
||||
// Silence unused-import noise if a stub is wired in later (keeps the
|
||||
// json/errors imports honest in case the CLI grows a custom parser).
|
||||
var _ = json.Marshal
|
||||
var _ = errors.New
|
||||
|
||||
+1
-1
@@ -55,7 +55,7 @@ No top-level `pkg/` until we have an external consumer.
|
||||
|
||||
## CI
|
||||
|
||||
- `.gitea/workflows/ci.yaml` runs on every push + PR. It runs `go vet`, `go test -race`, `go build`. No other gates.
|
||||
- `.gitea/workflows/ci.yaml` runs on every push + PR. It runs `gofmt`, `go vet`, `errcheck`, `staticcheck`, `go test -race`, and `go build`. No other gates.
|
||||
- No deploy pipeline — sherlock is operator-installed via `go install`. Release artifacts (binaries to the gitea registry / releases) land in Phase 5 if at all.
|
||||
|
||||
## Security
|
||||
|
||||
@@ -143,7 +143,7 @@ func TestFlow_EndToEnd(t *testing.T) {
|
||||
ClientID: "sherlock-client",
|
||||
Scopes: []string{"openid", "profile", "email"},
|
||||
}, nil)
|
||||
defer flow.Close()
|
||||
defer func() { _ = flow.Close() }()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
@@ -187,7 +187,7 @@ func TestFlow_EndToEnd(t *testing.T) {
|
||||
|
||||
func TestFlow_NotConfigured(t *testing.T) {
|
||||
flow := authn.NewFlow(authn.Config{}, nil)
|
||||
defer flow.Close()
|
||||
defer func() { _ = flow.Close() }()
|
||||
_, err := flow.StartFlow(context.Background())
|
||||
if err == nil || !strings.Contains(err.Error(), "not configured") {
|
||||
t.Fatalf("expected not-configured error, got %v", err)
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"gitea.alexandru.macocian.me/Charlie/sherlock/internal/authn"
|
||||
@@ -29,14 +28,6 @@ type Methods struct {
|
||||
// AgentNames feeds the status response so the CLI can show which
|
||||
// profiles are available without re-loading them.
|
||||
AgentNames func() []string
|
||||
|
||||
// Clock is injected for tests.
|
||||
Clock func() time.Time
|
||||
|
||||
mu sync.Mutex
|
||||
currentSub string
|
||||
currentSt keyring.TokenSet
|
||||
currentInit bool
|
||||
}
|
||||
|
||||
// MethodTable returns the map suitable for NewService.
|
||||
@@ -52,13 +43,6 @@ func (m *Methods) MethodTable() map[string]MethodHandler {
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Methods) now() time.Time {
|
||||
if m.Clock != nil {
|
||||
return m.Clock()
|
||||
}
|
||||
return time.Now()
|
||||
}
|
||||
|
||||
// loadTokens reads the keyring once and caches the result in memory.
|
||||
// We re-read on every status to keep things simple — the keyring is
|
||||
// the source of truth.
|
||||
|
||||
@@ -7,16 +7,16 @@ import (
|
||||
"time"
|
||||
|
||||
"gitea.alexandru.macocian.me/Charlie/sherlock/internal/authn"
|
||||
fakekeyring "gitea.alexandru.macocian.me/Charlie/sherlock/internal/keyring/fake"
|
||||
"gitea.alexandru.macocian.me/Charlie/sherlock/internal/keyring"
|
||||
fakekeyring "gitea.alexandru.macocian.me/Charlie/sherlock/internal/keyring/fake"
|
||||
"gitea.alexandru.macocian.me/Charlie/sherlock/internal/rpc"
|
||||
)
|
||||
|
||||
func newMethods(t *testing.T) *Methods {
|
||||
t.Helper()
|
||||
return &Methods{
|
||||
Store: fakekeyring.New(),
|
||||
Cfg: authn.Config{},
|
||||
Store: fakekeyring.New(),
|
||||
Cfg: authn.Config{},
|
||||
AgentNames: func() []string { return []string{"copilot"} },
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ func (s *Server) Close() error {
|
||||
|
||||
func (s *Server) serveConn(ctx context.Context, conn net.Conn) {
|
||||
defer s.wg.Done()
|
||||
defer conn.Close()
|
||||
defer func() { _ = conn.Close() }()
|
||||
|
||||
scanner := bufio.NewScanner(conn)
|
||||
scanner.Buffer(make([]byte, 64*1024), maxFrameBytes)
|
||||
|
||||
@@ -49,7 +49,7 @@ func TestRoundTrip_Success(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("Dial: %v", err)
|
||||
}
|
||||
defer c.Close()
|
||||
defer func() { _ = c.Close() }()
|
||||
|
||||
var resp map[string]string
|
||||
if err := c.Call(context.Background(), "echo", nil, &resp); err != nil {
|
||||
@@ -71,7 +71,7 @@ func TestRoundTrip_Error(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("Dial: %v", err)
|
||||
}
|
||||
defer c.Close()
|
||||
defer func() { _ = c.Close() }()
|
||||
|
||||
err = c.Call(context.Background(), "status", nil, nil)
|
||||
if err == nil {
|
||||
@@ -98,7 +98,7 @@ func TestRoundTrip_BadJSON(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("dialRaw: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
defer func() { _ = conn.Close() }()
|
||||
|
||||
if _, err := conn.Write([]byte("not json\n")); err != nil {
|
||||
t.Fatalf("Write: %v", err)
|
||||
@@ -140,7 +140,7 @@ func TestConcurrentCalls(t *testing.T) {
|
||||
t.Errorf("Dial: %v", err)
|
||||
return
|
||||
}
|
||||
defer c.Close()
|
||||
defer func() { _ = c.Close() }()
|
||||
for j := 0; j < callsEach; j++ {
|
||||
var out map[string]int
|
||||
err := c.Call(context.Background(), "echo",
|
||||
|
||||
Reference in New Issue
Block a user