Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
2 changes: 1 addition & 1 deletion .github/actions/setup-deps/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,4 @@ runs:
- name: Create workspace
shell: bash
run: |
printf 'go 1.26.5\n\nuse .\n\nreplace (\n\tgithub.com/GrayCodeAI/hawk-core-contracts => ./external/hawk-core-contracts\n\tgithub.com/GrayCodeAI/eyrie => ./external/eyrie\n\tgithub.com/GrayCodeAI/inspect => ./external/inspect\n\tgithub.com/GrayCodeAI/sight => ./external/sight\n\tgithub.com/GrayCodeAI/tok => ./external/tok\n\tgithub.com/GrayCodeAI/trace => ./external/trace\n\tgithub.com/GrayCodeAI/yaad => ./external/yaad\n\tgithub.com/GrayCodeAI/hawk-mcpkit => ./external/hawk-mcpkit\n)\n' > go.work
printf 'go 1.26.6\n\nuse .\n\nreplace (\n\tgithub.com/GrayCodeAI/hawk-core-contracts => ./external/hawk-core-contracts\n\tgithub.com/GrayCodeAI/eyrie => ./external/eyrie\n\tgithub.com/GrayCodeAI/inspect => ./external/inspect\n\tgithub.com/GrayCodeAI/sight => ./external/sight\n\tgithub.com/GrayCodeAI/tok => ./external/tok\n\tgithub.com/GrayCodeAI/trace => ./external/trace\n\tgithub.com/GrayCodeAI/yaad => ./external/yaad\n\tgithub.com/GrayCodeAI/hawk-mcpkit => ./external/hawk-mcpkit\n)\n' > go.work
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ concurrency:
cancel-in-progress: true

env:
GO_VERSION: "1.26.5"
GO_VERSION: "1.26.6"
# GrayCodeAI sibling modules are resolved from the local external/ submodules via
# go.work; their go.mod require versions (v0.1.0) intentionally do not match the
# frozen public proxy/sumdb snapshot, so bypass the proxy + checksum DB for them.
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/compatibility-matrix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ jobs:
git submodule update --init external/hawk-mcpkit
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version: "1.26.5"
go-version: "1.26.6"
cache: true
- name: Structural validation (schema + version pins)
run: make compat-check
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version: "1.26.5"
go-version: "1.26.6"
cache: true

- name: Run GoReleaser
Expand Down
5 changes: 5 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ linters:
- unconvert
- whitespace
- gosec
# Catch fmt.Errorf("%s", err) — discards the wrapped chain; should be %w.
- errorlint
settings:
errcheck:
check-type-assertions: true
Expand Down Expand Up @@ -89,6 +91,9 @@ linters:
- errcheck
- unused
- gosec
# Tests conventionally compare errors for exact identity (err != sentinel)
# or type-assert with a concrete type; enforcing errors.Is/As there is noise.
- errorlint
issues:
max-issues-per-linter: 0
max-same-issues: 0
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Build stage
# Supply-chain hardening: both stages are pinned by digest so a mutable tag
# cannot silently change the build.
FROM golang:1.26.5-alpine@sha256:0178a641fbb4858c5f1b48e34bdaabe0350a330a1b1149aabd498d0699ff5fb2 AS builder
FROM golang:1.26.6-alpine@sha256:af8d6740070b8906d12eae1c3e3ea0957fb63f492051ea05e354c38ef9fe88df AS builder

RUN apk upgrade --no-cache && \
apk add --no-cache git ca-certificates tzdata
Expand Down Expand Up @@ -43,7 +43,7 @@ COPY . .
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
rm -f go.work go.work.sum && \
{ echo "go 1.26.5"; echo; echo "use ."; echo; echo "replace ("; \
{ echo "go 1.26.6"; echo; echo "use ."; echo; echo "replace ("; \
for repo in hawk-core-contracts eyrie inspect sight tok trace yaad; do \
echo " github.com/GrayCodeAI/${repo} => ./external/${repo}"; \
done; echo ")"; } > go.work && \
Expand Down
4 changes: 2 additions & 2 deletions Dockerfile.daemon
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
#
# Build: docker build -f Dockerfile.daemon -t hawk-daemon .
# Run: docker run -p 4590:4590 -e HAWK_DAEMON_API_KEY=... hawk-daemon
FROM golang:1.26.5-alpine@sha256:0178a641fbb4858c5f1b48e34bdaabe0350a330a1b1149aabd498d0699ff5fb2 AS builder
FROM golang:1.26.6-alpine@sha256:af8d6740070b8906d12eae1c3e3ea0957fb63f492051ea05e354c38ef9fe88df AS builder

RUN apk upgrade --no-cache && \
apk add --no-cache git ca-certificates tzdata
Expand All @@ -24,7 +24,7 @@ COPY . .
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
rm -f go.work go.work.sum && \
{ echo "go 1.26.5"; echo; echo "use ."; echo; echo "replace ("; \
{ echo "go 1.26.6"; echo; echo "use ."; echo; echo "replace ("; \
for repo in hawk-core-contracts eyrie inspect sight tok trace yaad; do \
echo " github.com/GrayCodeAI/${repo} => ./external/${repo}"; \
done; echo ")"; } > go.work && \
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ setup: ## Set up local development environment (go.work + external repos).
fi; \
done
@echo "Generating go.work..."
@echo "go 1.26.5" > go.work
@echo "go 1.26.6" > go.work
@echo "" >> go.work
@echo "use ." >> go.work
@echo "" >> go.work
Expand Down
2 changes: 1 addition & 1 deletion cmd/chat_config_deployment.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ func saveCredentialAsync(inference hawkconfig.CredentialInference, secret string
if refreshErr := hawkconfig.RefreshCatalogAfterCredentials(ctx, nil); refreshErr == nil {
result, err = hawkconfig.ApplyEyrieCredentialsForProvider(ctx, inference.ProviderID)
} else {
err = fmt.Errorf("%w; automatic catalog refresh failed: %v", err, refreshErr)
err = fmt.Errorf("%w; automatic catalog refresh failed: %w", err, refreshErr)
}
}
if err != nil {
Expand Down
3 changes: 2 additions & 1 deletion cmd/chat_print.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bufio"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"os"
Expand Down Expand Up @@ -319,7 +320,7 @@ func runRepl() error {
_, _ = fmt.Fprint(os.Stderr, "\n> ")
input, err := reader.ReadString('\n')
if err != nil {
if err == io.EOF {
if errors.Is(err, io.EOF) {
fmt.Fprintln(os.Stderr, "")
return nil
}
Expand Down
16 changes: 16 additions & 0 deletions cmd/chat_subcommand_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,22 @@ func TestSubcommandRegistry_NamesIsSorted(t *testing.T) {
}
}

// TestPackageSubcommandRegistry_IsPopulated verifies the real package-level
// registry (built by the per-file init() functions) actually registered
// subcommands and that the canonical entry points resolve (M12). A broken or
// no-op init() would leave the registry empty or drop a command.
func TestPackageSubcommandRegistry_IsPopulated(t *testing.T) {
if subcommandRegistry.Size() == 0 {
t.Fatal("package subcommandRegistry is empty — no init() registrations took effect")
}
// Core commands that must always resolve.
for _, name := range []string{"help", "config", "status", "quit"} {
if _, ok := subcommandRegistry.Lookup(name); !ok {
t.Errorf("package subcommandRegistry.Lookup(%q) = false", name)
}
}
}

// --- subcommand-interface contract ---

func TestSubcommandInterface_Accessors(t *testing.T) {
Expand Down
23 changes: 23 additions & 0 deletions cmd/chat_tools_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,29 @@ func TestMergedMCPHeaders_ConfiguredAuthorizationTakesPrecedence(t *testing.T) {
}
}

// TestEssentialOptionalTools_NoOverlapOrDuplicates guards against drift between
// the hand-maintained essentialTools() and optionalTools() lists (M11): a tool
// must appear in exactly one list. Duplicates within a list or an overlap across
// lists would silently double-register or misclassify a tool.
func TestEssentialOptionalTools_NoOverlapOrDuplicates(t *testing.T) {
essential := essentialTools()
optional := optionalTools()

seen := make(map[string]struct{})
for _, tl := range essential {
if _, dup := seen[tl.Name()]; dup {
t.Fatalf("essentialTools() duplicates tool %q", tl.Name())
}
seen[tl.Name()] = struct{}{}
}
for _, tl := range optional {
if _, dup := seen[tl.Name()]; dup {
t.Fatalf("optionalTools() tool %q also appears in essentialTools()", tl.Name())
}
seen[tl.Name()] = struct{}{}
}
}

func TestDefaultRegistry_SkipsFailedStartupMCPServers(t *testing.T) {
orig := defaultRegistryLoadMCPTools
t.Cleanup(func() { defaultRegistryLoadMCPTools = orig })
Expand Down
6 changes: 3 additions & 3 deletions cmd/chat_view.go
Original file line number Diff line number Diff line change
Expand Up @@ -408,9 +408,9 @@ func (m chatModel) View() tea.View {
}
if slashOpen {
if sugs := m.slashSuggestionsFor(m.input.Value()); len(sugs) > 0 {
if m.slashSel < 0 || m.slashSel >= len(sugs) {
m.slashSel = 0
}
// NOTE: slashSel is clamped in Update (chat_update.go) and
// submit (chat_submit.go), not here — View is by-value, so any
// mutation would be silently discarded.
cmdStyle := slashCmdStyle
descStyle := slashDescStyle
selCmdStyle := slashSelCmdStyle
Expand Down
3 changes: 2 additions & 1 deletion cmd/cli_contracts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package cmd
import (
"bytes"
"context"
"errors"
"os"
"path/filepath"
"strings"
Expand Down Expand Up @@ -197,7 +198,7 @@ func TestPromptInputReadLine_WithoutInteractiveReader(t *testing.T) {
if err == nil {
t.Fatal("expected an error when no interactive prompt input is available")
}
if err != errNoInteractivePromptInput {
if !errors.Is(err, errNoInteractivePromptInput) {
t.Fatalf("error = %v, want %v", err, errNoInteractivePromptInput)
}
}
Expand Down
10 changes: 9 additions & 1 deletion cmd/container_boot.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package cmd
import (
"context"
"fmt"
"os"
"time"

tea "charm.land/bubbletea/v2"
Expand All @@ -29,8 +30,15 @@ func shouldUseContainer() bool {

// startRequiredContainer starts Hawk's mandatory Docker sandbox. It fails
// closed with an actionable error; there is deliberately no host fallback.
// Egress is restricted to a domain allowlist via NetworkProxy by default; set
// HAWK_DISABLE_EGRESS_PROXY=1 to opt out (unrestricted bridge egress).
func startRequiredContainer(projectDir string) (*sandbox.ContainerSandbox, error) {
cs := sandbox.NewContainerSandbox(projectDir)
var cs *sandbox.ContainerSandbox
if os.Getenv("HAWK_DISABLE_EGRESS_PROXY") == "1" {
cs = sandbox.NewContainerSandbox(projectDir)
} else {
cs = sandbox.NewContainerSandboxWithEgressProxy(projectDir)
}
sandbox.ResetDockerAvailabilityCache()
if !dockerAvailable() {
return nil, fmt.Errorf("docker is required but is not running — start Docker and retry")
Expand Down
2 changes: 1 addition & 1 deletion external/eyrie
Submodule eyrie updated 0 files
4 changes: 2 additions & 2 deletions go.mod

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions go.sum

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion go.work
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
go 1.26.5
go 1.26.6

use .

Expand Down
4 changes: 3 additions & 1 deletion internal/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"crypto/rand"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"os"
"os/exec"
Expand Down Expand Up @@ -278,7 +279,8 @@ func execCommand(name string, args ...string) (string, error) {
cmd := exec.CommandContext(context.Background(), name, args...) // #nosec G204 -- executable is selected by the platform credential backend
out, err := cmd.Output()
if err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
var exitErr *exec.ExitError
if errors.As(err, &exitErr) {
return "", fmt.Errorf("%s: %w: %s", name, err, strings.TrimSpace(string(exitErr.Stderr)))
}
return "", fmt.Errorf("%s: %w", name, err)
Expand Down
3 changes: 2 additions & 1 deletion internal/bridge/trace/lib_bridge.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ package tracebridge

import (
"context"
"errors"
"fmt"
"strings"
"sync"
Expand Down Expand Up @@ -253,7 +254,7 @@ func (a *SubprocessBridgeAdapter) Enable(ctx context.Context, _ string) error {
// Disable stops the active capture session, mirroring the `trace disable` subprocess call.
func (a *SubprocessBridgeAdapter) Disable(ctx context.Context, _ string) error {
_, err := a.capture.StopCapture(ctx)
if err == ErrNotActive {
if errors.Is(err, ErrNotActive) {
return nil // idempotent: disable on an already-disabled session is a no-op
}
return err
Expand Down
3 changes: 2 additions & 1 deletion internal/bridge/trace/lib_bridge_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package tracebridge

import (
"context"
"errors"
"testing"
"time"
)
Expand Down Expand Up @@ -144,7 +145,7 @@ func TestGetTranscriptPath_EmptyBeforeCapture(t *testing.T) {
func TestStopCapture_WithoutStart_ReturnsError(t *testing.T) {
sc := NewSessionCapture(CaptureConfig{RepoPath: "/repo"}, nil)
result, err := sc.StopCapture(context.Background())
if err != ErrNotActive {
if !errors.Is(err, ErrNotActive) {
t.Errorf("StopCapture error = %v, want ErrNotActive", err)
}
if result != nil {
Expand Down
4 changes: 3 additions & 1 deletion internal/container/lifecycle.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package container
import (
"bytes"
"context"
"errors"
"fmt"
"os/exec"
"strings"
Expand Down Expand Up @@ -108,7 +109,8 @@ func ExecWithStdin(ctx context.Context, containerID string, cmd []string, stdin

exitCode := 0
if err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
var exitErr *exec.ExitError
if errors.As(err, &exitErr) {
exitCode = exitErr.ExitCode()
} else {
return nil, &ContainerError{
Expand Down
4 changes: 3 additions & 1 deletion internal/container/lifecycle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package container
import (
"bytes"
"context"
"errors"
"fmt"
"os/exec"
"strings"
Expand Down Expand Up @@ -144,7 +145,8 @@ func TestExecWithStdin(t *testing.T) {
if err == nil {
t.Fatal("expected error for empty container ID")
}
ce, ok := err.(*ContainerError)
ce := &ContainerError{}
ok := errors.As(err, &ce)
if !ok {
t.Fatalf("expected *ContainerError, got %T", err)
}
Expand Down
Loading
Loading