Skip to content
Open
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
71 changes: 71 additions & 0 deletions .github/workflows/gateway-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
name: Gateway Checks

# Guards the gateway's Go -> TypeScript codegen contract:
# - tygo-check regenerates ui/src/api/types.ts from the Go structs
# in gateway/internal/adminapi and fails on any diff, so a Go
# struct change can't land without its committed TS counterpart
# (drift would silently break the dashboard's typed fetch layer).
# - ui-build type-checks (tsc -b) and bundles the SPA, catching
# imports that the regenerated types.ts no longer satisfies.

on:
pull_request:
paths:
- "gateway/**"
- ".github/workflows/gateway-check.yml"
push:
branches: [main]
paths:
- "gateway/**"
- ".github/workflows/gateway-check.yml"

jobs:
tygo-check:
runs-on: ubuntu-latest
timeout-minutes: 15
defaults:
run:
working-directory: gateway
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: gateway/go.mod
cache-dependency-path: gateway/go.sum

# Pinned so the generated output is byte-stable — a tygo release
# changing its formatting would otherwise fail every PR. Bump in
# lockstep with the version noted in gateway/Makefile.
- name: Install tygo
run: go install github.com/gzuidhof/tygo@v0.2.21

- name: Check generated TS types are up to date
run: make tygo-check

ui-build:
runs-on: ubuntu-latest
timeout-minutes: 15
defaults:
run:
working-directory: gateway
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: 24
cache: npm
# package-lock.json is git-ignored for this SPA, so key the
# npm cache on package.json instead.
cache-dependency-path: gateway/internal/adminapi/ui/package.json

- name: Install SPA dependencies
run: make ui-install

- name: Type-check and build SPA
run: make ui-build
5 changes: 3 additions & 2 deletions gateway/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,9 @@ ui: ui-install ui-build

# ─── tygo (Go -> TS struct codegen) ───────────────────────────────────
#
# Install once per dev machine:
# go install github.com/gzuidhof/tygo@latest
# Install once per dev machine (CI pins the same version in
# .github/workflows/gateway-check.yml — bump both together):
# go install github.com/gzuidhof/tygo@v0.2.21

tygo:
tygo generate
Expand Down
2 changes: 2 additions & 0 deletions gateway/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ github.com/mark3labs/mcp-go v0.43.2 h1:21PUSlWWiSbUPQwXIJ5WKlETixpFpq+WBpbMGDSVy
github.com/mark3labs/mcp-go v0.43.2/go.mod h1:YnJfOL382MIWDx1kMY+2zsRHU/q78dBg9aFb8W6Thdw=
github.com/maximhq/bifrost/core v1.5.18 h1:f1lX3sPesKTScUJHv+K9tWpd1wLLjd7CAzJY0BpbAaE=
github.com/maximhq/bifrost/core v1.5.18/go.mod h1:7vry9xB5kmjT3smAVVQ2+mtfTmmeSN+7N1b8r1buaTI=
github.com/maximhq/bifrost/core v1.6.2 h1:dESW02/iyDznt/VnnjzYnevddUmplXw2YeaK/0dl3HA=
github.com/maximhq/bifrost/core v1.6.2/go.mod h1:GuRwPmx0Kh7lhZ+SnbLjVAclDI360DyKrJtioc3t5jg=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
Expand Down
15 changes: 8 additions & 7 deletions gateway/internal/adminapi/evals.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,9 @@ type evalRunRequest struct {
Agent string `json:"agent,omitempty"`
}

// evalRefResponse is the create/link acknowledgement.
type evalRefResponse struct {
// EvalRefResponse is the create/link acknowledgement. Exported so
// tygo emits the TS binding the SPA's eval mutations decode.
type EvalRefResponse struct {
RefID string `json:"ref_id"`
Linked bool `json:"linked,omitempty"`
}
Expand Down Expand Up @@ -234,7 +235,7 @@ func (h *evalHandlers) createOrLinkForAgent(w http.ResponseWriter, r *http.Reque
writeError(w, http.StatusBadGateway, "catalog_write_failed", "neo4j write failed")
return
}
writeJSON(w, http.StatusOK, evalRefResponse{RefID: req.SetID, Linked: true})
writeJSON(w, http.StatusOK, EvalRefResponse{RefID: req.SetID, Linked: true})
return
}

Expand All @@ -243,7 +244,7 @@ func (h *evalHandlers) createOrLinkForAgent(w http.ResponseWriter, r *http.Reque
writeError(w, http.StatusBadRequest, "missing_field", "name (or set_id) is required")
return
}
var created evalRefResponse
var created EvalRefResponse
if err := h.hive.call(ctx, http.MethodPost, "/api/gateway/evals",
map[string]any{"name": req.Name, "description": req.Description}, &created); err != nil {
relayHiveError(w, err)
Expand All @@ -262,7 +263,7 @@ func (h *evalHandlers) createOrLinkForAgent(w http.ResponseWriter, r *http.Reque
"set created but linking to agent failed")
return
}
writeJSON(w, http.StatusOK, evalRefResponse{RefID: created.RefID})
writeJSON(w, http.StatusOK, EvalRefResponse{RefID: created.RefID})
}

// linkEdge MERGEs HiveAgent-[:HAS_EVAL_SET]->EvalSet and clears any
Expand Down Expand Up @@ -457,7 +458,7 @@ func (h *evalHandlers) createSet(w http.ResponseWriter, r *http.Request) {
writeError(w, http.StatusBadRequest, "missing_field", "name is required")
return
}
var created evalRefResponse
var created EvalRefResponse
if err := h.hive.call(r.Context(), http.MethodPost, "/api/gateway/evals",
map[string]any{"name": req.Name, "description": req.Description}, &created); err != nil {
relayHiveError(w, err)
Expand Down Expand Up @@ -513,7 +514,7 @@ func (h *evalHandlers) createRequirement(w http.ResponseWriter, r *http.Request,
writeError(w, http.StatusBadRequest, "missing_field", "name is required")
return
}
var created evalRefResponse
var created EvalRefResponse
if err := h.hive.call(r.Context(), http.MethodPost,
"/api/gateway/evals/"+urlSeg(setID)+"/requirements", req, &created); err != nil {
relayHiveError(w, err)
Expand Down
4 changes: 2 additions & 2 deletions gateway/internal/adminapi/ui/dist/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
<meta name="viewport" content="width=1280" />
<meta name="color-scheme" content="dark" />
<title>Agent Mothership</title>
<script type="module" crossorigin src="/_plugin/ui/assets/index-hPiEX19Z.js"></script>
<link rel="stylesheet" crossorigin href="/_plugin/ui/assets/index-o3GB2jE-.css">
<script type="module" crossorigin src="/_plugin/ui/assets/index-BjpR7bV8.js"></script>
<link rel="stylesheet" crossorigin href="/_plugin/ui/assets/index-8V8yL1oX.css">
</head>
<body>
<div id="root"></div>
Expand Down
2 changes: 1 addition & 1 deletion gateway/internal/adminapi/ui/src/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
// or auth-refresh logic here lights the trap of having two HTTP
// stacks; Tanstack Query gives us all of that one layer up.

import type { ApiError } from "./types";
import type { ApiError } from "./manual";

const PLUGIN_PREFIX = "/_plugin";

Expand Down
169 changes: 169 additions & 0 deletions gateway/internal/adminapi/ui/src/api/manual.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
// Hand-maintained types that tygo cannot generate. Everything the
// SPA decodes from an exported Go struct in internal/adminapi lives
// in the sibling types.ts, which is generated by `make tygo` and
// checked in CI by `make tygo-check` — never hand-edit that file.
//
// What belongs here instead, and why:
// - String-literal unions (Window / Bucket / Dimension): the Go
// side validates these as plain strings, so there is no struct
// for tygo to translate.
// - ApiError: the phase-7 error envelope is built inline by
// adminapi's writeError helper (a map, not a struct).
// - TrustOrg / TrustStatus: mirror gateway/internal/trust, a
// package outside tygo.yaml's adminapi scope, re-named for the
// dashboard (trust.Org -> TrustOrg, trust.StatusResponse ->
// TrustStatus).
// - Chat / usage shapes (ChatMessage, TokenUsage, CacheDebug, …):
// Bifrost pass-through fields that Go carries as
// json.RawMessage (tygo emits `any`); typed here to the depth
// the RunDetail drawer actually renders.
//
// Keep each block in lockstep with the Go source it names.

// ─── enums validated Go-side as strings ─────────────────────────────

// Window options the SPA exposes to the operator. Kept in lockstep
// with the Go-side validation in observability.go > parseWindow.
export type Window = "1h" | "6h" | "24h" | "7d" | "30d";

// Bucket options for the histogram endpoints — same source-of-truth
// note as Window.
export type Bucket = "1m" | "5m" | "10m" | "1h" | "6h" | "1d";

// Dimension values the histogram endpoint accepts. Phase 11 removed
// `realm-id` — every row in a swarm's logs.db is implicitly for
// that swarm's realm, and the realm is surfaced on the trust-status
// card instead of as a per-row column.
export type Dimension =
| "agent-name"
| "run-id"
| "session-id"
| "user-id";

// ─── error envelope ─────────────────────────────────────────────────

// Phase-7 error envelope (returned on 4xx/5xx). Mirrors the map
// built by internal/adminapi's writeError helper.
export interface ApiError {
error: {
code: string;
message: string;
};
}

// ─── trust registry (gateway/internal/trust) ────────────────────────

// Trust-registry Org entry — mirrors gateway/internal/trust.Org.
// Surfaced on the dashboard's Provenance card so an operator can
// see which org's signature authorized a run, plus the pubkey /
// issuer URL the plugin would verify against.
export interface TrustOrg {
org_id: string;
pubkey: string;
issuer_url: string;
revocation_poll_seconds: number;
grace_pubkeys?: string[];
grace_until?: string;
}

// Trust-registry status — mirrors gateway/internal/trust.StatusResponse.
// The Provenance card on RunDetail uses `realm_id` to show the
// swarm's self-identity ("this run was processed by swarm w1"),
// since phase 11 dropped the per-row realm-id metadata column.
export interface TrustStatus {
claimed: boolean;
org_count: number;
orgs: string[];
seed_source: "" | "env" | "api";
last_modified: string;
/** Set on multi-swarm deployments; absent / empty on single-swarm. */
realm_id?: string;
}

// ─── Bifrost pass-through shapes (json.RawMessage on the Go side) ───

// One message in a chat-style input_history / output_message.
// Mirrors Bifrost's `schemas.ChatMessage` to the depth the drawer
// renders: role, content (string OR an array of content blocks),
// optional tool-call list (assistant) and tool_call_id (tool reply).
// Everything is optional because providers vary in which fields
// they populate, and the drawer falls back to JSON for anything it
// doesn't recognize.
export interface ChatMessage {
role?: string;
name?: string;
/** OpenAI/Anthropic-style: either a plain string or an array of
* typed content blocks. */
content?: string | ChatContentBlock[] | null;
/** Tool messages: which prior tool_call this is the result for. */
tool_call_id?: string;
/** Assistant tool calls. */
tool_calls?: ChatToolCall[];
/** Anthropic / OpenAI reasoning summaries. */
reasoning?: string;
refusal?: string;
}

export interface ChatContentBlock {
type: string;
text?: string;
refusal?: string;
/** Anthropic-style cache marker. When present on a block the
* provider charged this block as a cache write (or read on a
* subsequent call). */
cache_control?: { type?: string } | null;
cachePoint?: { type?: string } | null;
image_url?: unknown;
input_audio?: unknown;
file?: unknown;
}

export interface ChatToolCall {
id?: string;
type?: string;
function?: { name?: string; arguments?: string };
}

/** Provider-reported usage breakdown. Source: Bifrost
* `schemas.BifrostLLMUsage`. The `prompt_tokens_details` sub-
* object is where the cache split lives — Anthropic populates
* `cached_write_tokens` (prompt cache writes) and
* `cached_read_tokens`; OpenAI uses `cached_read_tokens` only. */
export interface TokenUsage {
prompt_tokens?: number;
completion_tokens?: number;
total_tokens?: number;
prompt_tokens_details?: {
text_tokens?: number;
audio_tokens?: number;
image_tokens?: number;
cached_read_tokens?: number;
cached_write_tokens?: number;
cached_write_token_details?: {
cached_write_tokens_5m?: number;
cached_write_tokens_1h?: number;
};
};
completion_tokens_details?: {
reasoning_tokens?: number;
accepted_prediction_tokens?: number;
};
cost?: unknown;
}

/** Semantic cache verdict. Source: `schemas.BifrostCacheDebug`.
* Distinct from prompt-cache token splits (those live in
* TokenUsage above). Present only when a semantic cache plugin
* is configured. */
export interface CacheDebug {
cache_hit: boolean;
cache_id?: string;
hit_type?: string;
requested_provider?: string;
requested_model?: string;
provider_used?: string;
model_used?: string;
input_tokens?: number;
threshold?: number;
similarity?: number;
}
10 changes: 6 additions & 4 deletions gateway/internal/adminapi/ui/src/api/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,15 @@ import type {
SpendByAgentResponse,
SpendByAgentUserResponse,
SpendByUserResponse,
TrustOrg,
TrustStatus,
UserDetailResponse,
Window,
} from "./types";
import type {
Bucket,
Dimension,
} from "./types";
TrustOrg,
TrustStatus,
Window,
} from "./manual";

// ─── /me ─────────────────────────────────────────────────────────────
// Fires once at boot, plus on tab refocus (Tanstack default). Cheap
Expand Down
Loading
Loading