Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Agent Instructions

## Project Overview

`go.kenn.io/kit` is a Go module of reusable building blocks for Kenn CLI and
developer tools. Keep packages small, app-neutral, and usable by more than one
caller. Do not add product-specific config, database state, UI behavior, or
provider workflows to this repo unless the package already owns that concern.

## How To Work Here

- Start from the package intent, not from a calling app's current needs. The
reusable package should stay useful to future callers with similar mechanics.
- Prefer the repo's existing helpers and test fixtures over recreating local
variants in a caller.
- Follow the Go version and dependency choices in `go.mod`; do not pin guidance
here to a specific version unless the code requires that version for a reason.
- Keep Unix and Windows behavior explicit when permissions, ownership, sockets,
paths, or process behavior differ.
- When behavior changes, update the package-level `AGENTS.md` nearest the change
if it captures an invariant future agents need to preserve.
- Let CI, `go.mod`, and tool config files define the exact verification
commands. Do not duplicate command recipes here unless the command carries a
repo-specific intent that is not encoded elsewhere.

## Go Conventions

- Use standard library APIs first and add dependencies only when they pay for
themselves.
- Keep public package APIs narrow and app-neutral. Callers should not need to
import unrelated kit packages to use one package correctly.
- Surface errors with enough context for callers to act on them. Do not turn
failures into success-shaped fallbacks.
- Use `context.Context` for subprocesses, network calls, update checks, probes,
and other work that can block.
- Avoid broad cleanup or mutation. Operate on exact paths, runtime records, and
repositories that the caller supplied or the test created.

## Tests

- Use `github.com/stretchr/testify` for new and changed tests. Prefer
`require` for setup, preconditions, and values used later; use `assert` for
independent checks where more failures help diagnosis.
- Do not add new `t.Fatal`, `t.Fatalf`, `t.Error`, `t.Errorf`, `t.Fail`, or
`t.FailNow` calls. Existing tests still contain some stdlib assertions; when
editing those checks, migrate the touched checks to testify if it keeps the
test readable.
- When a test repeats package-level testify calls, create local helpers such as
`assert := assert.New(t)` or `require := require.New(t)` and use the helper
methods for the repeated checks.
- Prefer table tests when they make input and expected behavior clearer.
- Use `t.TempDir()` for files created by tests unless the test specifically
needs a fixed OS temp path to exercise permissions or runtime-dir behavior.
- Tests must not depend on the user's git config, global credentials, real
repositories, home directory state, or live provider availability.

## Git Workflow

- Do not change branches unless the user explicitly asks.
- Do not amend commits unless the user explicitly asks.
- Never revert user changes. If existing edits touch the same files, read them
and work with them.
1 change: 1 addition & 0 deletions CLAUDE.md
34 changes: 34 additions & 0 deletions daemon/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Daemon Package Instructions

## Scope

`daemon/` owns shared lifecycle pieces for local background daemons: endpoint
parsing, loopback or Unix-socket clients, runtime records, liveness probes,
listen locks, and caller-driven auto-start. Application server setup, auth,
databases, command parsing, and shutdown policy belong to the caller.

## Invariants

- Never infer live daemon state from a runtime record alone. Probe the endpoint
before claiming a process is reachable.
- Default network behavior stays local. Do not add public listen addresses or
broad bind behavior without an explicit caller option.
- Unix sockets and runtime records must live under private current-user
directories.
- Do not remove an existing path unless it is known to be the stale Unix socket
this package created. Refuse paths whose type or ownership does not match
that intent.
- Use listen locks to serialize startup and bind attempts.
- Keep platform-specific behavior in build-tagged files when ownership, sockets,
process checks, or permissions differ.
- Auto-start goes through the caller-provided `StartFunc`; this package must not
invent application launch commands.

## Tests

- Use local HTTP handlers, temporary dirs, and per-test runtime stores.
- Assert what a client observes: status code, response body, runtime record, or
returned error.
- Avoid timing assumptions. Prefer explicit locks, probes, and short contexts
over sleeps.
- Do not require elevated privileges or external daemons.
1 change: 1 addition & 0 deletions daemon/CLAUDE.md
31 changes: 19 additions & 12 deletions daemon/endpoint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,17 @@ import (
)

func TestParseEndpointDefaultTCP(t *testing.T) {
assert := assert.New(t)

ep, err := daemon.ParseEndpoint("", daemon.ParseEndpointOptions{
DefaultTCPAddress: "127.0.0.1:7373",
TCPPolicy: daemon.RequireLoopback,
})
require.NoError(t, err)
assert.Equal(t, daemon.NetworkTCP, ep.Network)
assert.Equal(t, "127.0.0.1:7373", ep.Address)
assert.Equal(t, "http://127.0.0.1:7373", ep.BaseURL())
assert.Equal(t, 7373, ep.Port())
assert.Equal(daemon.NetworkTCP, ep.Network)
assert.Equal("127.0.0.1:7373", ep.Address)
assert.Equal("http://127.0.0.1:7373", ep.BaseURL())
assert.Equal(7373, ep.Port())
}

func TestParseEndpointRejectsPublicTCPWhenPolicyRequiresNonPublic(t *testing.T) {
Expand Down Expand Up @@ -53,12 +55,14 @@ func TestParseEndpointUnixDefault(t *testing.T) {
}

func TestUnixHTTPClientDialsSocket(t *testing.T) {
require := require.New(t)

socketDir, err := os.MkdirTemp("", "kitd")
require.NoError(t, err)
require.NoError(err)
t.Cleanup(func() { _ = os.RemoveAll(socketDir) })
socketPath := filepath.Join(socketDir, "daemon.sock")
listener, err := net.Listen("unix", socketPath)
require.NoError(t, err)
require.NoError(err)
t.Cleanup(func() {
_ = listener.Close()
_ = os.Remove(socketPath)
Expand All @@ -72,7 +76,7 @@ func TestUnixHTTPClientDialsSocket(t *testing.T) {

ep := daemon.Endpoint{Network: daemon.NetworkUnix, Address: socketPath}
resp, err := ep.HTTPClient(daemon.HTTPClientOptions{}).Get(ep.BaseURL())
require.NoError(t, err)
require.NoError(err)
defer func() { _ = resp.Body.Close() }()
assert.Equal(t, http.StatusOK, resp.StatusCode)
}
Expand All @@ -88,20 +92,23 @@ func TestTCPHTTPClientBypassesEnvironmentProxy(t *testing.T) {
}

func TestDefaultSocketPathCreatesPrivateTempFallback(t *testing.T) {
assert := assert.New(t)
require := require.New(t)

service := fmt.Sprintf("kitdtest%d", os.Getpid())
t.Setenv("TMPDIR", "/tmp")
t.Setenv("XDG_RUNTIME_DIR", "")

socketPath := daemon.DefaultSocketPath(service)
require.NotEmpty(t, socketPath)
require.NotEmpty(socketPath)
t.Cleanup(func() { _ = os.RemoveAll(filepath.Dir(socketPath)) })
assert.Equal(t, filepath.Join(filepath.Dir(socketPath), "daemon.sock"), socketPath)
assert.Equal(filepath.Join(filepath.Dir(socketPath), "daemon.sock"), socketPath)

info, err := os.Stat(filepath.Dir(socketPath))
require.NoError(t, err)
assert.True(t, info.IsDir())
require.NoError(err)
assert.True(info.IsDir())
if runtime.GOOS != "windows" {
assert.Zero(t, info.Mode().Perm()&0o077)
assert.Zero(info.Mode().Perm() & 0o077)
}
}

Expand Down
32 changes: 19 additions & 13 deletions daemon/endpoint_unix_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,24 @@ import (
)

func TestDefaultSocketPathRepairsPublicTempFallback(t *testing.T) {
assert := assert.New(t)
require := require.New(t)

tempDir := "/tmp"
service := fmt.Sprintf("kitdpublic%d", os.Getpid())
t.Setenv("TMPDIR", tempDir)
t.Setenv("XDG_RUNTIME_DIR", "")
parent := filepath.Join(tempDir, fmt.Sprintf("%s-%d", service, os.Getuid()))
t.Cleanup(func() { _ = os.RemoveAll(parent) })
require.NoError(t, os.MkdirAll(parent, 0o700))
require.NoError(t, os.Chmod(parent, 0o777))
require.NoError(os.MkdirAll(parent, 0o700))
require.NoError(os.Chmod(parent, 0o777))

socketPath := daemon.DefaultSocketPath(service)
require.NotEmpty(t, socketPath)
assert.Equal(t, filepath.Join(parent, "daemon.sock"), socketPath)
require.NotEmpty(socketPath)
assert.Equal(filepath.Join(parent, "daemon.sock"), socketPath)
info, err := os.Stat(parent)
require.NoError(t, err)
assert.Zero(t, info.Mode().Perm()&0o077)
require.NoError(err)
assert.Zero(info.Mode().Perm() & 0o077)
}

func TestDefaultSocketPathRejectsSymlinkedTempFallback(t *testing.T) {
Expand All @@ -49,20 +52,23 @@ func TestDefaultSocketPathRejectsSymlinkedTempFallback(t *testing.T) {
}

func TestDefaultSocketPathCreatesPrivateXDGRuntimeDir(t *testing.T) {
assert := assert.New(t)
require := require.New(t)

xdg := filepath.Join("/tmp", fmt.Sprintf("kitdxdg%d", os.Getpid()))
service := "svc"
t.Cleanup(func() { _ = os.RemoveAll(xdg) })
require.NoError(t, os.MkdirAll(xdg, 0o700))
require.NoError(t, os.Chmod(xdg, 0o700))
require.NoError(os.MkdirAll(xdg, 0o700))
require.NoError(os.Chmod(xdg, 0o700))
t.Setenv("XDG_RUNTIME_DIR", xdg)

socketPath := daemon.DefaultSocketPath(service)
require.NotEmpty(t, socketPath)
require.NotEmpty(socketPath)
parent := filepath.Join(xdg, service)
assert.Equal(t, filepath.Join(parent, "daemon.sock"), socketPath)
assert.Equal(filepath.Join(parent, "daemon.sock"), socketPath)

info, err := os.Stat(parent)
require.NoError(t, err)
assert.True(t, info.IsDir())
assert.Zero(t, info.Mode().Perm()&0o077)
require.NoError(err)
assert.True(info.IsDir())
assert.Zero(info.Mode().Perm() & 0o077)
}
Loading
Loading