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
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Security
- **Session lock TOCTOU eliminated**: `AcquireLock`'s stat → stale-if->5min → remove → O_EXCL dance could delete a live lock on misjudged staleness and let two instances open the same session. Mutual exclusion now uses an OS advisory lock (`gofrs/flock`, promoted to a direct dependency); a crashed holder's lock is reclaimed instantly because the kernel drops the flock at process death. The lock file keeps PID/timestamps purely as diagnostics.
- **Hardened atomic writes for state files**: global settings, checkpoint file contents and restores, handovers, and named checkpoints now go through `internal/safewrite` (same 0600 mode as before, plus fsync+rename atomicity and symlink refusal at the destination).
- **Dead shell-injection surface removed**: `AssumptionTracker.VerifyCommandSucceeds` ran caller-supplied strings through `sh -c`, bypassing the permission stack; it had zero callers and is deleted. `SelfHealer.RunScript` no longer shell-evaluates the script path (double evaluation) and invokes it directly via `/bin/sh`.

### Fixed
- **Engine subprocesses are bounded and observable**: experiment-loop rollback, auto-commit git calls, and post-edit syntax validators (`go vet`, `python3`, `node`, `npx tsc`) ran on `context.Background()` with ignored errors; they are now time-bounded and log failures instead of discarding them.
- **Memory and config failures no longer silently dropped**: stream-loop memory persists (assistant learnings, skills, conversation summaries, insights) and the self-improve lesson store log failures via slog, corrupt lesson stores are reported, and the config panel surfaces failed `provider` setting saves instead of ignoring them.

### Changed
- **Makefile lint pin matches CI**: `make lint`/`lint-fix`/`setup` install `golangci-lint@v2.1.0` (was `@latest`), the same version CI enforces.
- **Docs truth and housekeeping**: SECURITY.md/CONTRIBUTING.md now describe the actual Go toolchain (golangci-lint, go vet, govulncheck) instead of the polyglot template's ruff/mypy/pip-audit/pnpm-lock language, CONTRIBUTING documents `make setup`/`boundaries`/`test-10x`/`smoke`, and the planning docs (`SPEC_DRIVEN_PLAN.md`, `SPEC_DRIVEN_PHASE2_PLAN.md`, `internal/engine/REFACTOR_PLAN.md`) moved to `docs/plans/`.

## [0.2.0] — 2026-07-13

### Changed
Expand Down
15 changes: 12 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,21 @@ for the full list. The most common targets:
| `make test` | Run unit tests |
| `make test-race` | Run unit tests with the race detector |
| `make cover` | Generate a coverage report |
| `make lint` | Run the linter (`golangci-lint` / `ruff`) |
| `make lint` | Run `golangci-lint` (pinned to CI's version) |
| `make fmt` | Format source files |
| `make vet` | Run `go vet` / `mypy` |
| `make security` | Run `govulncheck` / `pip-audit` |
| `make vet` | Run `go vet` |
| `make security` | Run `govulncheck` |
| `make ci` | Run everything CI runs (the gate before pushing) |

Repo-specific dev targets:

| Target | What it does |
| ------------------ | ------------------------------------------------------------------- |
| `make setup` | Set up local development environment (go.work + external repos). |
| `make boundaries` | Alias for all boundary guards (matches `make boundaries` in engine repos). |
| `make test-10x` | Run tests 10 times to surface flakes. |
| `make smoke` | Quick build + doctor + ecosystem verification. |

## Commit message convention

We use [Conventional Commits](https://www.conventionalcommits.org/). This
Expand Down
8 changes: 5 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ LDFLAGS := -s -w \
# ---------------------------------------------------------------------------
GOBIN_DIR := $(shell go env GOPATH)/bin
GOLANGCI := $(GOBIN_DIR)/golangci-lint
# Keep in sync with the pin in .github/workflows/ci.yml (lint job).
GOLANGCI_VERSION := v2.1.0
GOFUMPT := $(GOBIN_DIR)/gofumpt
GOIMPORTS := $(GOBIN_DIR)/goimports
GOVULNCHECK := $(GOBIN_DIR)/govulncheck
Expand Down Expand Up @@ -134,11 +136,11 @@ submodule-release-parity: ## Verify every go.mod ecosystem version resolves to i
bash ./scripts/check-submodule-release-parity.sh

lint: ## Run golangci-lint.
@command -v $(GOLANGCI) >/dev/null 2>&1 || (echo "install: go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest" && exit 1)
@command -v $(GOLANGCI) >/dev/null 2>&1 || (echo "install: go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@$(GOLANGCI_VERSION)" && exit 1)
$(GOLANGCI) run ./... --timeout=5m

lint-fix: ## Run golangci-lint with --fix.
@command -v $(GOLANGCI) >/dev/null 2>&1 || (echo "install: go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest" && exit 1)
@command -v $(GOLANGCI) >/dev/null 2>&1 || (echo "install: go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@$(GOLANGCI_VERSION)" && exit 1)
$(GOLANGCI) run ./... --fix --timeout=5m

security: ## Run govulncheck.
Expand Down Expand Up @@ -228,7 +230,7 @@ setup: ## Set up local development environment (go.work + external repos).
@echo "=== Installing development tools ==="
@command -v $(GOFUMPT) >/dev/null 2>&1 || go install mvdan.cc/gofumpt@latest || echo " ⚠ Could not install gofumpt"
@command -v $(GOIMPORTS) >/dev/null 2>&1 || go install golang.org/x/tools/cmd/goimports@latest || echo " ⚠ Could not install goimports"
@command -v $(GOLANGCI) >/dev/null 2>&1 || go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest || echo " ⚠ Could not install golangci-lint"
@command -v $(GOLANGCI) >/dev/null 2>&1 || go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@$(GOLANGCI_VERSION) || echo " ⚠ Could not install golangci-lint"
@command -v $(GOVULNCHECK) >/dev/null 2>&1 || go install golang.org/x/vuln/cmd/govulncheck@latest || echo " ⚠ Could not install govulncheck"
@command -v lefthook >/dev/null 2>&1 || go install github.com/evilmartians/lefthook@latest || echo " ⚠ Could not install lefthook"
@echo "✓ All tools installed"
Expand Down
11 changes: 6 additions & 5 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,12 @@ We follow [coordinated vulnerability disclosure](https://en.wikipedia.org/wiki/C

- **Dependency monitoring:** vulnerable dependencies are detected by
`govulncheck`, which runs on every CI build (see "Vulnerability scanning").
- **Static analysis:** `golangci-lint` / `ruff` / `mypy` enforced in CI.
- **Vulnerability scanning:** `govulncheck` (Go) / `pip-audit` (Python) run
on every CI build.
- **Lockfiles:** `go.sum` / `pnpm-lock.yaml` / `pyproject.toml` are pinned
and committed.
- **Static analysis:** `golangci-lint` (including `gosec` rules) and `go vet`
are enforced in CI.
- **Vulnerability scanning:** `govulncheck` runs on every CI build.
- **Dependency pinning:** `go.sum` is pinned and committed; ecosystem
submodules are pinned to exact Gitlinks and verified by
`make submodule-release-parity`.
- **Reproducible builds:** release artefacts ship with SHA-256 checksums via
goreleaser.
- **No secrets in source:** API keys are configuration, not constants. Pre-
Expand Down
14 changes: 11 additions & 3 deletions cmd/chat_config_panel.go
Original file line number Diff line number Diff line change
Expand Up @@ -965,12 +965,20 @@ func (m chatModel) selectConfigModelFromOptions(opts []configModelOption) (chatM
return m.closeConfigPanel(), nil
}
m.session.SetModel(modelID)
// Same precedence as before (manual pick > gateway > provider), but a
// failed provider persist is surfaced instead of silently dropped.
provider := ""
if gw := strings.TrimSpace(m.configModelProvider); gw != "" {
_ = hawkconfig.SetGlobalSetting("provider", gw)
provider = gw
} else if gw := strings.TrimSpace(selected.GatewayID); gw != "" {
_ = hawkconfig.SetGlobalSetting("provider", gw)
provider = gw
} else if prov := strings.TrimSpace(selected.ProviderID); prov != "" {
_ = hawkconfig.SetGlobalSetting("provider", prov)
provider = prov
}
if provider != "" {
if err := hawkconfig.SetGlobalSetting("provider", provider); err != nil {
m.messages = append(m.messages, displayMsg{role: "error", content: "model set, but saving provider failed: " + err.Error()})
}
}
m.applyModelThinkingPref(selected)
m.syncSessionSelection()
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
2 changes: 1 addition & 1 deletion external/hawk-core-contracts
2 changes: 1 addition & 1 deletion external/hawk-mcpkit
2 changes: 1 addition & 1 deletion external/inspect
2 changes: 1 addition & 1 deletion external/sight
Submodule sight updated from 84c96e to 481d3d
2 changes: 1 addition & 1 deletion external/tok
Submodule tok updated from 5355fc to 643b66
2 changes: 1 addition & 1 deletion external/trace
Submodule trace updated from a31e98 to 59b437
2 changes: 1 addition & 1 deletion external/yaad
Submodule yaad updated from ab5e64 to 42bdda
18 changes: 9 additions & 9 deletions go.mod

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

32 changes: 16 additions & 16 deletions go.sum

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

6 changes: 4 additions & 2 deletions internal/config/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (

"github.com/GrayCodeAI/hawk/internal/provider/gateway"
"github.com/GrayCodeAI/hawk/internal/provider/routing"
"github.com/GrayCodeAI/hawk/internal/safewrite"
"github.com/GrayCodeAI/hawk/internal/storage"

"github.com/GrayCodeAI/hawk/internal/types"
Expand Down Expand Up @@ -401,8 +402,9 @@ func SaveGlobal(s Settings) error {
if err != nil {
return err
}
// 0600: per-user config; keep it unreadable to other local users.
if err := os.WriteFile(globalSettingsPath(), data, 0o600); err != nil {
// safewrite keeps the previous 0600 mode (per-user config, unreadable to
// other local users) while making the write atomic and symlink-resistant.
if err := safewrite.WriteFile(globalSettingsPath(), data); err != nil {
return err
}
// Invalidate the in-process byte cache so subsequent loads within the
Expand Down
2 changes: 1 addition & 1 deletion internal/engine/agent/aliases.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
// Package agent is the namespace for sub-agent orchestration types.
// See ../REFACTOR_PLAN.md.
// See ../../docs/plans/engine-refactor-plan.md.
package agent
2 changes: 1 addition & 1 deletion internal/engine/agent_reexports.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// This file re-exports symbols from the agent sub-package so that existing
// callers of engine.SubAgentMode, engine.NewSubAgentBudget, etc. keep compiling
// during the Stage 2 migration. See REFACTOR_PLAN.md.
// during the Stage 2 migration. See docs/plans/engine-refactor-plan.md.
package engine

import (
Expand Down
19 changes: 2 additions & 17 deletions internal/engine/assumptions.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
package engine

import (
"context"
"fmt"
"os"
"os/exec"
"strings"
"sync"

Expand Down Expand Up @@ -60,21 +58,8 @@ func (at *AssumptionTracker) VerifyFileExists(text, path string) {
at.Assumptions = append(at.Assumptions, a)
}

// VerifyCommandSucceeds checks if a command-based assumption holds.
func (at *AssumptionTracker) VerifyCommandSucceeds(text, cmd string) {
at.mu.Lock()
defer at.mu.Unlock()
a := Assumption{Text: text}
out, err := exec.CommandContext(context.Background(), "sh", "-c", cmd).CombinedOutput() // #nosec G204 -- intentional assumption-check command boundary
if err == nil {
a.Status = AssumptionConfirmed
a.Proof = "command succeeded"
} else {
a.Status = AssumptionFailed
a.Proof = strings.TrimSpace(string(out))
}
at.Assumptions = append(at.Assumptions, a)
}
// VerifyCommandSucceeds was removed: it ran caller-supplied strings through
// `sh -c`, bypassing the permission/safety stack, and had no callers.

// Failed returns all assumptions that were proven wrong.
func (at *AssumptionTracker) Failed() []Assumption {
Expand Down
Loading
Loading