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
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ jobs:
run: bash ./scripts/check-internal-layer-imports.sh
- name: eyrie client boundary guard
run: bash ./scripts/check-eyrie-client-imports.sh
- name: eyrie engine facade boundary guard
run: bash ./scripts/check-eyrie-engine-boundary.sh

# -------------------------------------------------------------------------
# 2. Module hygiene — tidy, verify (hawk + external ecosystem repos via go.work).
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ jobs:
output: trivy-image.sarif
severity: CRITICAL,HIGH
ignore-unfixed: true
exit-code: '0'
exit-code: '1'

- name: Build and push
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ hawk_bin
.gocache-*/
.gomodcache/
.gomodcache-*/
.golangci-cache/
.gopath/
.tmp/
*.codegraph.db
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ RUN rm -f go.work go.work.sum && \
-o hawk ./cmd/hawk

# Runtime stage — Alpine (hawk requires git + bash for workspace operations; distroless excluded)
FROM alpine:3.21
FROM alpine:3.23

RUN apk add --no-cache ca-certificates git bash tini && \
adduser -D -u 1000 -h /home/hawk hawk
Expand Down
9 changes: 6 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ GORELEASER := $(GOBIN_DIR)/goreleaser
# ---------------------------------------------------------------------------
# Phony declarations (alphabetical).
# ---------------------------------------------------------------------------
.PHONY: all bench boundaries build ci clean contracts-guard ecosystem-guard eyrie-client-guard peer-guard internal-layers-guard submodule-release-parity cover cover-new fmt help install lint lint-fix \
.PHONY: all bench boundaries build ci clean contracts-guard ecosystem-guard eyrie-client-guard eyrie-engine-guard peer-guard internal-layers-guard submodule-release-parity cover cover-new fmt help install lint lint-fix \
release security setup smoke path sync-external test test-10x test-live test-new test-race tidy version vet

# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -104,16 +104,19 @@ contracts-guard: ## Fail on any legacy imports of removed hawk/shared/types.
ecosystem-guard: ## Fail if external ecosystem repos import hawk/internal or removed hawk/shared/types.
bash ./scripts/check-ecosystem-boundaries.sh

eyrie-client-guard: ## Fail on new direct eyrie/client imports outside Hawk transport adapters.
eyrie-client-guard: ## Fail on any production eyrie/client import.
bash ./scripts/check-eyrie-client-imports.sh

eyrie-engine-guard: ## Require all production Eyrie imports to use the stable engine facade.
bash ./scripts/check-eyrie-engine-boundary.sh

peer-guard: ## Fail if support engines import each other instead of depending only on Hawk contracts.
bash ./scripts/check-support-repo-coupling.sh

internal-layers-guard: ## Enforce one-way dependencies across stable Hawk internal layers.
bash ./scripts/check-internal-layer-imports.sh

boundaries: contracts-guard ecosystem-guard eyrie-client-guard peer-guard internal-layers-guard ## Alias for all boundary guards (matches `make boundaries` in engine repos).
boundaries: contracts-guard ecosystem-guard eyrie-client-guard eyrie-engine-guard peer-guard internal-layers-guard ## Alias for all boundary guards (matches `make boundaries` in engine repos).

submodule-release-parity: ## Verify every go.mod ecosystem version resolves to its pinned Gitlink.
bash ./scripts/check-submodule-release-parity.sh
Expand Down
36 changes: 28 additions & 8 deletions api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@ components:
description: The user prompt to send to the agent
session_id:
type: string
description: Reserved for session continuation (currently ignored; every chat creates a fresh engine session)
maxLength: 128
pattern: '^[A-Za-z0-9._-]+$'
description: Durable session ID to continue. The session must already exist; successful continuation returns the same ID.
model:
type: string
description: Optional model override
Expand All @@ -55,10 +57,10 @@ components:
description: Autonomy preset controlling non-interactive permission approval
cwd:
type: string
description: Working directory for the agent session
description: Existing directory associated with session metadata. The daemon executes tools from its startup working directory; it never performs a process-wide chdir per request.
agent:
type: string
description: Named agent persona to run
description: Named Hawk agent persona to apply. Continuations inherit the persisted persona when omitted.

ChatResponse:
type: object
Expand All @@ -79,7 +81,7 @@ components:

Session:
type: object
description: Lightweight in-memory record of a daemon chat session.
description: Lightweight active record for a durably persisted daemon chat session.
properties:
id:
type: string
Expand All @@ -93,6 +95,8 @@ components:
type: integer
cwd:
type: string
agent:
type: string

SessionDetail:
type: object
Expand All @@ -110,6 +114,8 @@ components:
type: string
provider:
type: string
agent:
type: string
cwd:
type: string
name:
Expand Down Expand Up @@ -277,8 +283,9 @@ paths:
tags: [system]
summary: Readiness probe
description: |
Returns 200 when the daemon can serve chat (engine configured),
503 with a reason otherwise.
Returns 200 only when a session factory is configured and Eyrie's local
preflight confirms provider state, catalog, credentials, and model
selection are ready. Returns 503 with the failed dependency otherwise.
security: []
responses:
"200":
Expand All @@ -301,8 +308,10 @@ paths:
description: |
Non-streaming: returns the full response in one JSON object.
Streaming: send `Accept: text/event-stream` to receive Server-Sent Events.
Each request creates a new agent session; there is no standalone
session-creation endpoint.
Omit `session_id` to create and durably persist a new session. Supply a
previously returned `session_id` to load its transcript, append a turn,
and update that same durable session. The returned JSON field and the
`X-Hawk-Session-ID` header identify a retrievable `/v1/sessions/{id}`.
requestBody:
required: true
content:
Expand All @@ -312,6 +321,11 @@ paths:
responses:
"200":
description: Agent response (or SSE stream)
headers:
X-Hawk-Session-ID:
description: Durable session ID created or continued by this request.
schema:
type: string
content:
application/json:
schema:
Expand All @@ -331,6 +345,12 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/Error"
"404":
description: Requested continuation session does not exist
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"503":
description: Engine not configured
content:
Expand Down
14 changes: 4 additions & 10 deletions cmd/chat.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@ import (
tea "charm.land/bubbletea/v2"
lipgloss "charm.land/lipgloss/v2"

"github.com/GrayCodeAI/eyrie/runtime"
"github.com/GrayCodeAI/eyrie/storage"
"github.com/GrayCodeAI/hawk/internal/bridge/sessioncapture"
"github.com/GrayCodeAI/hawk/internal/codegraph"
hawkconfig "github.com/GrayCodeAI/hawk/internal/config"
Expand Down Expand Up @@ -169,9 +167,9 @@ func newChatModelWithRegistry(ref *progRef, systemPrompt string, settings hawkco

// Initialize conversation DAG for branching support
startup.MarkPhase("newChatModel:dag")
dagPath := filepath.Join(hawkstorage.SessionsDir(), "convo.db")
if dag, err := storage.NewDAG(dagPath, sid); err == nil {
sess.SetConvoDAG(dag)
graphPath := filepath.Join(hawkstorage.SessionsDir(), "conversations", sid+".json")
if graph, err := session.OpenConversationGraph(graphPath, sid); err == nil {
sess.SetConversationGraph(graph)
}
startup.EndPhase("newChatModel:dag")

Expand Down Expand Up @@ -268,7 +266,7 @@ func newChatModelWithRegistry(ref *progRef, systemPrompt string, settings hawkco
// Prefetch live models for the active provider so footer ctx/pricing stay current.
go func() {
providerName := effectiveProvider
entries, _ := runtime.ListModels(context.Background(), runtime.ListModelsOpts{ProviderID: providerName, Source: runtime.ListSourceAuto})
entries, _ := hawkconfig.ListEngineModels(context.Background(), providerName, false)
opts := configModelOptionsFromEyrie(entries)
if len(opts) > 0 {
modelCacheMu.Lock()
Expand Down Expand Up @@ -380,10 +378,6 @@ func newChatModelWithRegistry(ref *progRef, systemPrompt string, settings hawkco
}
}(m)

go func() {
_ = hawkconfig.CompiledCatalogV1()
}()

// Load plugins/skills after startup and refresh welcome indicators when ready.
go func() {
runtime := plugin.NewRuntime()
Expand Down
7 changes: 3 additions & 4 deletions cmd/chat_commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (

tea "charm.land/bubbletea/v2"

"github.com/GrayCodeAI/hawk/internal/engine"
"github.com/GrayCodeAI/hawk/internal/multiagent/parallel"
"github.com/GrayCodeAI/hawk/internal/ui/icons"
)
Expand Down Expand Up @@ -410,9 +409,9 @@ func (m *chatModel) handleParallelCommand(parts []string, text string) (tea.Mode
}
taskIdx++

// Create a new session for this agent with same provider/model
agentSession := engine.NewSession(
m.session.Provider(),
// Clone the engine-backed transport so parallel agents cannot bypass
// the parent session's resolved gateway policy.
agentSession := m.session.SubSession(
m.session.Model(),
"You are a coding agent working in an isolated git worktree. Complete the assigned task.",
m.registry,
Expand Down
2 changes: 1 addition & 1 deletion cmd/chat_commands_session.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ func (m *chatModel) handleSessionCommand(cmd string, parts []string, text string

case "/fork":
// If convodag is active, fork from the current head node
if m.session.Persistence().DAG() != nil {
if m.session.Persistence().Graph() != nil {
headID := m.session.ConvoHead()
if headID == "" {
m.messages = append(m.messages, displayMsg{role: "error", content: "No conversation to fork from."})
Expand Down
44 changes: 5 additions & 39 deletions cmd/chat_config_deployment.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ import (
tea "charm.land/bubbletea/v2"
lipgloss "charm.land/lipgloss/v2"

"github.com/GrayCodeAI/eyrie/config"
"github.com/GrayCodeAI/eyrie/runtime"
hawkconfig "github.com/GrayCodeAI/hawk/internal/config"
"github.com/GrayCodeAI/hawk/internal/engine"
"github.com/GrayCodeAI/hawk/internal/ui/icons"
Expand Down Expand Up @@ -48,22 +46,13 @@ func firstRunModelProvider(m chatModel) string {
return ""
}

func credentialOptionFromHawk(in hawkconfig.CredentialInference) runtime.CredentialProviderOption {
return runtime.CredentialProviderOption{
ProviderID: in.ProviderID,
DeploymentID: in.DeploymentID,
EnvVar: in.EnvVar,
DisplayName: in.DisplayName,
}
}

func saveProviderKeyAsync(inference hawkconfig.CredentialInference, secret string) tea.Cmd {
return saveCredentialAsync(inference, secret)
}

func saveOllamaAsync(baseURL string) tea.Cmd {
return func() tea.Msg {
inference, err := runtime.LocalCredentialInference(configProviderOllama)
inference, err := hawkconfig.LocalCredentialInference(configProviderOllama)
if err != nil {
return configApplyCredentialsMsg{err: err}
}
Expand All @@ -80,14 +69,7 @@ func saveOllamaAsync(baseURL string) tea.Cmd {
func saveCredentialAsync(inference hawkconfig.CredentialInference, secret string) tea.Cmd {
return func() tea.Msg {
ctx := context.Background()
if inference.ProviderID == hawkconfig.ProviderXiaomiTokenPlan {
hawkconfig.ApplyXiaomiTokenPlanRegionEnv(ctx)
}
if inference.ProviderID == hawkconfig.ProviderZAICoding {
hawkconfig.ApplyZAIRegionEnv(ctx)
}
rtInf := config.InferenceFromOption(credentialOptionFromHawk(inference))
if err := runtime.SaveCredential(ctx, rtInf, secret); err != nil {
if err := hawkconfig.SaveCredential(ctx, inference, secret); err != nil {
return configApplyCredentialsMsg{
err: err,
providerID: inference.ProviderID,
Expand All @@ -105,7 +87,7 @@ func saveCredentialAsync(inference hawkconfig.CredentialInference, secret string
}
}

entries, listErr := runtime.ListModels(ctx, runtime.ListModelsOpts{ProviderID: inference.ProviderID, Source: runtime.ListSourceAuto})
entries, listErr := hawkconfig.ListEngineModels(ctx, inference.ProviderID, false)
if listErr != nil {
return configApplyCredentialsMsg{
err: listErr,
Expand All @@ -114,11 +96,6 @@ func saveCredentialAsync(inference hawkconfig.CredentialInference, secret string
}
}
opts := configModelOptionsFromEyrie(entries)
if len(opts) == 0 && result.Setup != nil {
fallback := hawkconfig.OptionsFromSetupUI(result.Setup, inference.ProviderID)
opts = toConfigModelOptionsFromHawk(fallback)
}

return configApplyCredentialsMsg{
summary: hawkconfig.FormatApplyCredentialsSummary(result),
providerID: inference.ProviderID,
Expand All @@ -128,17 +105,6 @@ func saveCredentialAsync(inference hawkconfig.CredentialInference, secret string
}
}

func toConfigModelOptionsFromHawk(in []hawkconfig.ModelOption) []configModelOption {
out := make([]configModelOption, len(in))
for i, o := range in {
out[i] = configModelOption{
ID: o.ID,
DisplayName: o.DisplayName,
}
}
return out
}

func (m chatModel) startConfigOllamaURL() (chatModel, tea.Cmd) {
return m.startConfigOllamaURLWithValue(configDefaultOllamaURL)
}
Expand Down Expand Up @@ -249,11 +215,11 @@ func (m chatModel) handleConfigApplyCredentialsMsg(msg configApplyCredentialsMsg
}

func (m chatModel) rebuildSessionTransport() (chatModel, tea.Cmd) {
selection := runtime.EffectiveSelection(context.Background(), runtime.SelectionOpts{
selection := hawkconfig.EffectiveSelectionWithSettings(context.Background(), m.settings, hawkconfig.SelectionOptions{
ProviderOverride: firstNonEmptyTrimmed(m.session.Provider(), m.settings.Provider),
ModelOverride: firstNonEmptyTrimmed(m.session.Model(), m.settings.Model),
})
if err := engine.RebuildSessionTransport(context.Background(), m.session, selection, m.session.Provider()); err != nil {
if err := engine.RebuildSessionTransportForSettings(context.Background(), m.settings, m.session, selection, m.session.Provider()); err != nil {
m.configNotice = sanitizeConfigNotice(err.Error())
}
syncSessionFromPersistedSelection(m.session)
Expand Down
1 change: 1 addition & 0 deletions cmd/chat_config_gateways_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ func chatModelForConfigPasteTest() chatModel {
}

func TestConfigGatewaysView_RequiresKeyForModelCounts(t *testing.T) {
isolateCredentialHome(t)
hawkconfig.InvalidateConfigUICache()
store := &credentials.MapStore{}
credentials.SetDefaultStore(store)
Expand Down
8 changes: 1 addition & 7 deletions cmd/chat_config_hub.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (

tea "charm.land/bubbletea/v2"

"github.com/GrayCodeAI/eyrie/catalog/registry"
hawkconfig "github.com/GrayCodeAI/hawk/internal/config"
)

Expand Down Expand Up @@ -70,12 +69,7 @@ func catalogPricesAreStale(opts []configModelOption) bool {
}

func providerHasLiveFetcher(providerID string) bool {
for _, key := range registry.LiveFetcherKeys() {
if key == providerID {
return true
}
}
return false
return hawkconfig.GatewaySupportsLiveDiscovery(providerID)
}

func (m chatModel) returnToOllamaURLAfterError(err error) (chatModel, tea.Cmd) {
Expand Down
3 changes: 1 addition & 2 deletions cmd/chat_config_keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,11 @@ import (

tea "charm.land/bubbletea/v2"

"github.com/GrayCodeAI/eyrie/credentials"
hawkconfig "github.com/GrayCodeAI/hawk/internal/config"
)

func credentialsStoreLabel() string {
return credentials.PlatformSecretStoreName()
return hawkconfig.CredentialStoreName()
}

func configGatewayRemovePrompt(step int, gatewayName string) string {
Expand Down
Loading
Loading