Skip to content

Latest commit

 

History

History
194 lines (114 loc) · 56.8 KB

File metadata and controls

194 lines (114 loc) · 56.8 KB

Nexus Operator Toolkit Architecture

The operator toolkit is nexus, a single Go binary that operates and observes the gateway from the terminal. It lives in its own module packages/nexus-cli (module path github.com/AlphaBitCore/nexus-gateway/packages/nexus-cli, joined to the workspace in go.work), with the binary built from packages/nexus-cli/cmd/nexus. A separate module keeps the terminal-UI dependency set out of the server modules' vetted dependencies.

The toolkit is a pure client: it reaches the gateway only through the existing Control Plane admin API and the AI Gateway /v1/* data plane. It registers no admin endpoint, owns no database connection, and carries no authorization logic of its own — every call is made as an ordinary IAM principal, so IAM, audit, and validation are never bypassed.

One core, multiple faces

The kernel — core + agent + capabilities — lives in a separate shared module, packages/nexus-agent-core, so a server can embed the same agent (the web assistant does); the nexus binary in packages/nexus-cli adds thin presenter faces (cli / tui) over it, plus the local-machine implementations in internal/local (OS keychain, on-disk config, browser login) that a server does not need:

  • core (in nexus-agent-core) — the only package that authenticates, holds the active profile, references the secret seam, and calls the gateway. The OS-keychain implementation of that seam lives in the CLI's internal/local (a server supplies its own credential source); core itself never touches the keychain. It exposes typed capability functions; the faces never build HTTP requests themselves.
  • agent (in nexus-agent-core) — the operator-agent kernel: a tool-using model loop, pure and self-contained, that depends only on the standard library and three seam interfaces (Model, Tool, SituationProvider).
  • capabilities (in nexus-agent-core) — the concrete implementations of those seams over core: the gateway-backed model, the tools, the situation snapshotter, and the tool registry behind the agent loop. One pure-library sub-package carves out a self-contained engine: capabilities/resource (the OpenAPI-driven resource engine — catalog, operation model, distiller, and the TUI/CLI accessors; a dependency-free leaf that imports only stdlib + yaml). The funcTool tool-wiring that adapts the resource engine to the agent kernel stays in the root package, so resource never depends on the kernel.
  • internal/cli — the Cobra command tree (nexus <noun> <verb>), each command a thin presenter that calls a core capability and renders a table or stable JSON.
  • internal/tui — the Bubble Tea operator console: an entry wizard followed by a tabbed dashboard. Its shared contract + leaf widgets live in internal/tui/kit (the Session/ViewModel/Gateway contract, the pure render widgets, the Allow/Deny confirm gate, and the cross-boundary messages) — a dependency-free leaf the shell and views import; the root re-exports the public names via type aliases so the cli is unaffected. The dashboard Model, the views, and the resident conversation stay one cohesive package: their integration tests drive the Model and assert concrete view internals, so they form a single Bubble Tea unit rather than further sub-packages (the kit leaf is the decomposition; oversized files are split by responsibility in place).

The kernel core owns its own wire structs (packages/nexus-agent-core/core/types_*.go); it does not import the server's packages/shared types, which keeps the client decoupled and the server modules' dependency sets unchanged.

The core package

Profiles and environments

local.Config (packages/nexus-cli/internal/local/config.go) is the on-disk profile set, stored as TOML at ~/.config/nexus/config.toml (directory 0700, file 0600). The TOML store lives in the CLI module, not the shared kernel, so a server that embeds the kernel (e.g. the web assistant) is not forced to carry the BurntSushi/toml dependency. The kernel keeps only the transport-shaped core.Env descriptor (packages/nexus-agent-core/core/config.go) — its toml: struct tags are inert without the lib. Each core.Env holds only non-secret references: the environment name, the Control Plane and AI Gateway base URLs, the OAuth client id and redirect URI, a prod flag, and the remembered model slug + Virtual Key id/name. A default_env key points at one environment, and the built-in local target lets a fresh install run without hand-writing a config first.

Config.Resolve(flagEnv, sessionEnv) selects the active environment with the precedence --env flag > in-session switch > default_env. Config.Save writes the profile set back, and never writes secrets — Env holds none.

Secrets and the keychain

core.SecretStore (packages/nexus-agent-core/core/secretstore.go) is the secret seam; the production implementation, local.KeyringStore (packages/nexus-cli/internal/local/secretstore.go), is backed by the OS keychain (macOS Keychain, Linux libsecret, Windows Credential Manager). The keychain implementation lives in the CLI module, not the shared kernel, so a server that embeds the kernel (e.g. the web assistant) is not forced to carry the OS-keychain dependency (it supplies its own bearer-token source). The toolkit stores exactly four secret kinds per environment — the access token, the refresh token, an admin API key, and a selected Virtual Key secret — keyed <env>:<key>. No secret is ever written to the plaintext config.

Authentication

core.TokenSource (packages/nexus-agent-core/core/auth.go) yields the credential header for an admin-authed request. NewTokenSource selects between the two surfaces the admin API already accepts: when an admin API key is stored it attaches x-admin-key (machine profile); otherwise it attaches the auth-server JWT as a bearer token and refreshes it through the OAuth refresh_token grant when the access token nears expiry, falling back to a re-login prompt only when refresh fails.

core.Authenticator (packages/nexus-agent-core/core/auth_pkce.go) drives login. LoginBrowser runs the browser-loopback OAuth2 Authorization Code + PKCE (S256) flow: it starts a 127.0.0.1 listener, opens the system browser at /oauth/authorize, captures the redirected code, and exchanges it for tokens. LoginHeadless performs the non-interactive password exchange against /authserver/password for environments where a browser cannot open. JWT expiry is read without verification by jwtExpiry (packages/nexus-agent-core/core/jwt.go) purely to time refreshes.

The typed client

core.Client (packages/nexus-agent-core/core/client.go) is the typed capability surface. A single roundtrip helper attaches the credential, sends the request, maps any non-2xx to a classified *core.APIError, and returns the raw body; do decodes that body into a typed value, while passthrough callers keep the raw bytes. APIError (packages/nexus-agent-core/core/errors.go) wraps one of the sentinel kinds — ErrUnauthorized, ErrForbidden, ErrNotFound, ErrTransport — so callers classify failures with errors.Is and the CLI maps them to documented exit codes.

The client's read capabilities cover the analytics sparkline and aggregates, the traffic list and single event (raw and normalized, including request/response bodies and hook decisions), service instances, registered nodes, firing alerts, the dead-letter-queue depth, per-provider usage, the compliance KPI overview, scheduled jobs, config-sync drift, the provider catalog (id ↔ name ↔ display name), single-provider SLO detail, virtual keys, the grouped model catalog, the grouped cost report, cache ROI, routing fallbacks, per-group latency percentiles, the current global kill-switch state, and the emergency-passthrough snapshot. The kill switch and the emergency passthrough are distinct controls: the kill switch (SetKillSwitch) halts TLS bumping fleet-wide, while the emergency passthrough is a three-tier (global / per-adapter / per-provider) config that bypasses the compliance hooks, cache, or normalization. The kill-switch route is write-only, so KillSwitchStatus reads the current state from the newest killswitch config-sync history event (the same read-side surface the web console uses); PassthroughSnapshot reads the full three-tier passthrough state. The per-provider analytics group by the denormalized provider name while the single-provider detail endpoint keys on the provider id; the provider catalog (Providers) bridges the two, and is also the source of the human-friendly display name shown wherever a provider is surfaced (never the bare id). It also runs a routing dry-run (RoutingSimulate) that resolves which provider/model a request would take without firing it, and creates a personal Virtual Key (CreateVK) returning the once-shown plaintext. SparklineResult.Totals (packages/nexus-agent-core/core/types_*.go) sums the per-bucket series — keyed by the snake_case metric instrument names — when the endpoint returns no top-level summary, which is the shape the sparkline endpoint actually returns.

Two capabilities use the Virtual Key as the upstream credential rather than the admin token:

  • ChatStream and ChatToolStream (packages/nexus-agent-core/core/chat.go) POST to the AI Gateway /v1/chat/completions with the VK as a bearer token, force stream: true with usage on, and parse the OpenAI SSE frames. ChatStream is the plain content+usage path the chat playground uses; ChatToolStream is the function-calling path the agent's model uses — it sends a tools array, accumulates streamed delta.tool_calls fragments by index across frames, and returns the assembled content, the tool calls, the finish reason, and usage. ChatStream delegates to the same scanner, discarding the tool calls.
  • SimulatorForward POSTs to the Control Plane admin simulator-forward endpoint with the admin credential and the VK carried in the request body; the endpoint forwards the body through the real gateway pipeline and returns the upstream response verbatim.

The mutating capabilities — SetKillSwitch (the global kill switch via POST /compliance/killswitch), SetPassthroughGlobal (the global emergency-passthrough tier via PUT /passthrough/global), SetProviderEnabled (enable/disable a provider via PUT /providers/:id), CacheFlush (invalidate the gateway's cached config via POST /cache/flush), RevokeVK (revoke a virtual key via POST /virtual-keys/:id/revoke), RegenerateVK (rotate a virtual key's secret via POST /virtual-keys/:id/regenerate, returning the new plaintext exactly once), and SetRoutingRuleEnabled (enable/disable a routing rule via PUT /routing-rules/:id) — each call an existing admin endpoint as the admin principal (no new endpoint or IAM action is introduced; the reach is exactly the principal's policy — provider.update, settings.update, virtual-key.revoke, virtual-key.update, routing-rule.update). Every write is gated in the view by the shared confirm component (packages/nexus-cli/internal/tui/kit/confirm.go), raised in every environment — no write fires without the operator authorizing it: the operator picks Allow or Deny (the selection defaults to Deny, so a stray enter never applies a change) before the call fires, and a prod environment adds a red PRODUCTION banner with the env-named Apply button while off-prod shows a neutral prompt. The global kill switch and emergency passthrough (packages/nexus-cli/internal/tui/views/kill.go) route through this same gate rather than a bespoke confirmation. A revoke is offered only for a key in active status (the endpoint rejects any other), so an operator never sees the prompt for a no-op; a regenerated secret is shown once in a dismissible panel and never persisted.

Every VK-authed path (chat, the request lab, the synthetic generator) uses a Virtual Key the operator holds the plaintext for — pasted, stored in the keychain, or freshly minted by CreateVK. Because VK secrets are stored hashed server-side and are not recoverable, there is no way to drive real traffic under a key the operator does not possess, so the toolkit cannot consume another principal's traffic.

The agent kernel

agent (packages/nexus-agent-core/agent/) is the operator-agent core: a tool-using model loop that turns a plain-language request into grounded, tool-backed action. It is a self-contained package that depends only on the standard library and three interfaces it defines itself — Model, Tool, and SituationProvider — so the whole loop is exercised in tests with a fake model and stub tools, and a concrete model, the gateway tools, and a live situation source plug in at those seams.

A conversation is modelled as content blocks rather than flat strings (packages/nexus-agent-core/agent/message.go): a Message is a role and an ordered list of Blocks, each a text block, a tool_use block (a call id, the tool name, and JSON arguments), or a tool_result block (the answered call id, its text, and an error flag). This is the shape the loop appends to and the model consumes.

The model seam (packages/nexus-agent-core/agent/model.go) is a single method: Generate takes a ModelRequest — the system prompt, the transcript, and the tool schemas currently exposed — and returns the assistant Message, a stop reason, and token Usage, streaming assistant text to one callback and the model's reasoning/thinking channel to a separate callback as each arrives. Reasoning is display-only: it streams for live rendering but is never written into the returned Message, so it never enters the transcript a later turn replays — keeping the system-plus-history prefix byte-stable for prompt-cache reuse. Usage is the kernel's own token-accounting type, so the package carries no dependency on the data-plane client; an implementation translates its provider's usage into it.

Tools are typed and named (packages/nexus-agent-core/agent/tool.go): a Tool exposes a name, a description, a JSON schema for its arguments, a risk Tier, and a Run. A Registry holds tools by name and renders their schemas for a request. The tier is either TierAuto — reads, navigation, and simulation, which carry no production side effect — or TierConfirm, the mitigations that change gateway state.

The loop (packages/nexus-agent-core/agent/loop.go) is the heart. Each round it assembles the request, calls the model, streams the assistant text and any reasoning, and — if the turn produced tool_use blocks — runs them and feeds the tool_results back before calling again, stopping when a turn has no tool call or a step cap is reached (it then returns ErrStepCap so the caller can ask whether to continue). Independent auto-tier tools run concurrently; a confirm-tier tool is routed through the permission gate first. A tool that errors, an unknown tool name, and a declined call each become a tool_result — the decline a non-error "user declined" — so the model adapts in the next round rather than the turn aborting.

Before a tool runs, the permission gate decides (packages/nexus-agent-core/agent/permission.go). An auto-tier tool runs immediately, a confirm-tier tool always asks, and a danger Classifier can escalate an otherwise-auto tool for a specific dangerous input. When a confirm-tier tool can describe its concrete write — the optional ConfirmDetailer seam, by which resource_invoke resolves the METHOD /substituted-path (operationId) and each mitigate_* the named entity action — the gate uses that as its prompt, so the operator authorizes the exact change rather than a generic "mitigation". A parallel optional ImpactDetailer seam lets a high-blast-radius confirm-tier tool read current state and return a structured impact preview (current → effect); the web assistant face surfaces it in the confirm card before Allow, failing open to an "unavailable" note so an emergency mitigation is never blocked by a degraded read — the CLI/TUI face does not use it. A regression test asserts no non-GET catalog operation is reachable at the auto tier, so a write can never bypass the gate. The default classifier is fail-safe for the shell: a run_command runs without a prompt only when it begins with a known-safe read / inspect / build prefix and carries no shell metacharacters; a compound or redirecting command, an unrecognized command, and the common destructive operations — recursive or forced deletes in any flag form, force pushes including the +-refspec form, database drops, raw disk writes, filesystem formats, and host-halting commands — all require authorization. A file write to a system path requires authorization while a write to an ordinary path does not. An operator allowlist pre-approves specific patterns, and an opt-in bypass auto-approves everything for power use. When the gate asks, a confirmation callback decides; a nil callback declines, so a misconfigured wiring fails safe rather than silently mutating.

Three persistence layers are deliberately distinct. Context assembly (packages/nexus-agent-core/agent/context.go) prepends a fresh bundle to each turn — a situation snapshot from the SituationProvider, the active view's data, and loaded memory — and a snapshot failure is soft, noted in the bundle rather than aborting the turn. Compaction (packages/nexus-agent-core/agent/compact.go) trims the in-window transcript: past a size threshold it summarizes the older turns in one model call and keeps the most recent turns verbatim. Memory (packages/nexus-agent-core/agent/memory.go) is a scope-split, per-fact-file store of durable facts the agent learns — modeled on Claude Code's file memory so the CLI gets smarter the more it is used. Global facts (operator preferences, procedures) live under <base>/global and env-specific facts (baselines, named entities) under <base>/<env>; each fact is one markdown file with a name/description/type frontmatter header, and re-remembering the same title updates it in place rather than duplicating. Only the merged one-line index (global + the active env) rides each turn's context — a full fact is read on demand with recall — so the per-turn bundle stays small, holds operational facts and never secrets (a secret-looking value is refused on write), and the cached system prefix is untouched (the index is part of the volatile per-turn tail). Sessions (packages/nexus-agent-core/agent/session.go) persist the full transcript and navigation trail as JSON per environment, written atomically and listed newest-first, so a past conversation resumes with its complete history — compaction shapes only what the model sees and never deletes from the stored session.

The system prompt is assembled in one place (packages/nexus-agent-core/agent/prompt.go): the operator-agent persona, the tool-use and safety guidance (propose a mitigation and let the human confirm, never fabricate, cite the data behind a claim, adapt when a tool is declined), the user-facing terminology boundary (node, config sync, applied config — not the internal engineering vocabulary), and any per-feature domain context the caller injects; a production environment is flagged so the model treats every mitigation with extra care.

Agent ties the units together (packages/nexus-agent-core/agent/agent.go). A turn compacts an in-window view of the transcript, assembles the system prompt and the context bundle, runs the loop, appends only the turn's new messages to the full persisted session, and returns the assistant's final text — persisting even when the step cap is hit so a partial turn is resumable. It registers the recall, remember, update_memory, and forget built-ins so the model can manage its own learning memory. The kernel is the shared brain the agent surfaces drive; it introduces no admin endpoint and no IAM action of its own, reaching gateway state only through the tools wired into its registry.

The concrete capabilities

capabilities (packages/nexus-agent-core/capabilities/) implements the kernel's three seams over core, plus the tool registry. It is the layer that turns the pure kernel into a live gateway agent; it imports both agent (the seams) and core (the data plane), while the kernel itself imports neither.

The concrete model (packages/nexus-agent-core/capabilities/runtime/model.go) implements agent.Model over core.ChatToolStream. It translates the kernel's content-block messages to the OpenAI wire and back: the system prompt becomes a leading system message; a user message's tool_result blocks each become a role:"tool" row keyed by the call id while its text becomes a role:"user" row; an assistant message's text and tool_use blocks become content plus a tool_calls array. On the way back, the streamed content and accumulated tool calls become the assistant message's text and tool_use blocks, the finish reason maps to the kernel's stop reason, and the gateway usage translates into the kernel's Usage. Empty tool arguments normalize to {} in both directions.

The tools are built as one funcTool adapter (packages/nexus-agent-core/capabilities/runtime/tool.go) so each capability is a value rather than a bespoke type. The gateway tools (packages/nexus-agent-core/capabilities/runtime/tools_gateway.go) are the observe / analyze / simulate reads at TierAuto and the mitigate writes at TierConfirm, each calling a core capability through a Gateway interface (packages/nexus-agent-core/capabilities/runtime/gateway.go) that *core.Client satisfies. A gateway or domain failure becomes a recoverable error result the model can read and adapt to, never an aborting error; the entity-scoped mitigations resolve a human-friendly name to the id the admin endpoint needs through the provider / routing-rule / virtual-key catalogs and refuse an ambiguous name rather than mutating the wrong entity (packages/nexus-agent-core/capabilities/runtime/resolve.go). The canvas tools (packages/nexus-agent-core/capabilities/runtime/canvas.go) — navigate, show_event, highlight — are TierAuto and drive a Canvas seam the TUI implements, so the agent visibly operates the cockpit. The system tools (packages/nexus-agent-core/capabilities/runtime/system.go) — run_command, read_file, write_file — are TierAuto and carry the exact names the kernel's danger classifier inspects, so a destructive command or a system-path write is escalated to a confirmation by the permission gate rather than the tool itself.

The situation snapshotter (packages/nexus-agent-core/capabilities/runtime/situation.go) implements agent.SituationProvider over the same Gateway. Each snapshot renders compact aggregates — request and error totals, the top cost lines, firing-alert counts and names, the kill-switch and passthrough state, the out-of-sync node count, and the most recent errors — and never raw request or response bodies, so the bundle that rides the Virtual-Key data plane minimizes what the gateway's compliance hooks scan. Every sub-call failure degrades to an empty or partial field rather than failing the whole snapshot.

The registry profile is built from these tools (packages/nexus-agent-core/capabilities/runtime/registry.go): the gateway reads + analyses + mitigate writes, plus the canvas and system tools, which are meaningful only with a live console and a permission gate — a web/server embedding leaves the system tools off (no run_command on a server). BuildAgent wires the profile, the concrete model, the situation snapshotter, the scope-split learning memory and the per-environment session store, and the permission gate into a ready agent.Agent for the console to drive.

The OpenAPI-driven resource engine

Alongside the hand-built gateway tools and dedicated views, the toolkit carries a generic engine that makes every Control Plane admin endpoint reachable from one model — driven entirely by the Control Plane's own OpenAPI specs, with no per-endpoint client code. It is the mechanism by which a traditional REST API, described only by its OpenAPI document, becomes uniformly callable by an agent, a script, and an interactive operator.

The embedded catalog

The Control Plane admin API is generated to OpenAPI 3.1, one spec file per resource kind plus an _index.yaml catalog that lists every kind and, per kind, each operation's method, path, operationId, IAM tier, and IAM action (packages/nexus-agent-core/capabilities/resource/openapi/control-plane/). The toolkit embeds this catalog into the binary with go:embed so the engine works on a distributed CLI with no repository on disk. The embedded copy is kept byte-identical to the doc source by go generate ./capabilities/resource (packages/nexus-agent-core/capabilities/resource/generate.go); a guard test fails the build on drift. The catalog is parsed once at init into a by-kind index (packages/nexus-agent-core/capabilities/resource/catalog.go).

The catalog reflects only the active CP Admin API surface — removed endpoints are absent. Notably, rotateAgentCert (POST /api/admin/agent-devices/{id}/rotate-cert) and bulkRotateCertGroup (POST /api/admin/device-groups/{id}/rotate-cert) were removed from both agent-devices.yaml and device-groups.yaml when Hub-issued P-256 mTLS device certificates were deprecated (agent self-signs its device identity; Hub CA now issues only Ed25519 attestation certificates). When the CP Admin API changes, re-run go generate ./capabilities/resource and update both the canonical doc source and the embedded copy in the same PR.

The operation model

packages/nexus-agent-core/capabilities/resource/operation.go treats the catalog as a flat list of operations and derives everything an agent or a UI needs from each one, with no per-kind special-casing. An Operation carries its kind, method, path, operationId, IAM tier and action, and the ordered list of path-parameter names parsed from the path (pathParams). SubstituteParams fills every {placeholder} in a path from a name-keyed value map and returns an error if any placeholder is left unfilled, so a half-substituted path never reaches the server — the engine handles a two-parameter path such as /nodes/{id}/overrides/{configKey} as readily as a flat collection. CanonicalVerb labels an operation that matches a conventional CRUD shape (list/get/create/update/delete/action:<name>) and Label falls back to the path tail for the long tail of reports, singleton config, RPCs, and nested sub-resources that have no CRUD verb, so no operation is ever hidden. Mutating reports whether an operation changes state (anything but GET).

Discovery is code-level, not model-level: Search ranks the whole catalog against a free-text query by substring and token overlap over the kind, operationId, path, label, and a precomputed per-operation corpus of the operation's summary and its query-parameter names and descriptions — so a question phrased in the words of a filter ("filter traffic by provider") or of the operation's purpose ("why was a request blocked") still surfaces the right op, not just one whose path happens to contain the words. That corpus — and the full DistilledOp per operation — is built once at init (buildOpIndexopIndex + distilledIdx, distilling each kind a single time) so neither a query nor card assembly ever re-distills the specs. The search answer is two-segment (SearchCards): the top K candidates (default 5, max 8 — K=5 pinned by the eval baseline, where ranks 6–8 added zero hits) come back as full executable cards carrying the operation's summary, write flag, parameters (with descriptions), and request-body skeleton, so a card that matches is executable in the same turn; the rest of the window (up to 20) is a thin 4-field tail for recall (+7pp top-20 over top-5 at baseline — recorded sunset condition: if a future eval shows top-K recall ≈ top-20 recall, delete the tail). A two-segment result is strictly smaller than a search plus a whole-kind describe round trip. Token cost stays flat as the catalog (or any external API) grows — the same grep-first pattern a human uses to find a command before running it.

The distiller resolves same-document $refs (#/components/schemas/...) at the body root and at each property, with an inclusive 3-hop chain cap and a visited set for cycles — 37 of the catalog's 116 JSON request bodies are a root $ref — without resolution they distill to an empty skeleton, shipping cards (and describe rows) with no field list for those writes (TestDistillRefBodiesNonEmpty guards the corpus-wide invariant). Search-quality itself is pinned by a deterministic golden-set eval (eval_search_test.go, 30 operator questions in three phrasing classes) whose measured baseline — top-1 80% / top-5 86% / top-20 93% — is pinned by CI regression floors (80 / 86 / 93).

The four search-first agent tools

The agent reaches the catalog through four operation-driven tools sharing one execution path (packages/nexus-agent-core/capabilities/runtime/tools_resource.go):

  • resource_search (TierAuto) — ranked candidates for a query, the discovery entry point. The top K come back as full executable cards (kind, operationId, method, path, summary, write flag, params, body skeleton) and the rest as a thin 4-field tail — never the whole catalog. A matching card removes the describe round trip entirely.
  • resource_describe (TierAuto) — a compact, distilled schema for one kind: every operation with its operationId, label, method, path, the operation's summary, its parameters (path + query, each with its description), and request-body fields (name / type / required / enum). Carrying the summary and the per-parameter descriptions is what lets the model pick the right operation and the right filter rather than guessing from names alone. The distiller (packages/nexus-agent-core/capabilities/resource/distill.go) emits every operation in the kind and returns this in place of the raw OpenAPI document, keeping the tool result small in the agent's context.
  • resource_read (TierAuto) — execute a GET by operationId, filling path placeholders from a name-keyed params map and attaching query. It refuses a write operationId and points the model at resource_invoke.
  • resource_invoke (TierConfirm) — execute a write (POST/PUT/PATCH/DELETE) by operationId, with params, query, and a JSON body. It refuses a GET and points the model at resource_read. The TierConfirm classification maps resource_invoke to a mandatory human authorization at the agent.Gate before execution — fail-closed: a nil Confirm handler, a Confirm error, or a context timeout all deny and the operation never runs. The generic single-tool design does not weaken this guarantee: each individual invocation is still gated, and confirmDetail resolves the exact METHOD /substituted-path (operationId) so the operator authorizes the concrete mutation rather than a generic prompt. A regression test (loop_confirm_test.go) verifies that no non-GET catalog operation is reachable at the auto tier.

A shared executeOp substitutes the path parameters, attaches the query, sends the body, and relays the HTTP status and body verbatim so a 400 (validation) or 403 (IAM) teaches the model to self-correct. The read tools are in the auto-tier discovery set and resource_invoke is in the confirm-tier write set, so the same permission-gate rules that govern the hand-built tools apply unchanged. The system prompt teaches the search-first flow — search returns executable cards, so read or invoke directly from a matching card and reach for describe only when no card covers the target — and that data questions have no dead ends: because the agent is the specialist for this gateway's own data, when no curated tool fits it must resource_search before saying it can't answer, reformulate and retry a missed search, compose across two or three kinds (using one read's result as the next read's input), and filter on the server with the query parameters the search card (or resource_describe) lists rather than scanning everything by eye (packages/nexus-agent-core/agent/prompt.go).

The shared faces

The same engine backs all three faces through public accessors (packages/nexus-agent-core/capabilities/resource/api.go): Kinds (the kinds plus a total capability profile — crud/config/report/action:<seg>, deduplicated, never empty — and operation count), Operations (every operation of a kind), ResolveOperation (an operationId + params → concrete method and path), SearchCards (the code-level search, cards + thin tail), Distill (a kind's full compact schema), and DescribeOperation (one operation's parameter and body schema, for building input forms). Result rendering is shared too: packages/nexus-cli/internal/restable/ holds the presentation-agnostic logic — detect a renderable collection in a response body, extract its rows, infer a bounded, identity-first column set, reduce a cell to text, and paginate — so the CLI (a tabwriter table) and the TUI (a styled table) render the same data shape from the same logic, and a non-collection body falls back to an indented record on either surface.

The CLI face

internal/cli builds the Cobra command tree (packages/nexus-cli/internal/cli/root.go). cli.Main is the process entry point invoked from cmd/nexus; it executes the root command and returns a documented exit code: 0 success, 1 generic/transport, 2 usage, 3 auth required, 4 IAM denied, 5 not found. Persistent flags --env and --output {table,json} apply to every command.

The commands are thin presenters over core: setup (interactively create/edit an environment), login (browser PKCE, or --admin-key from stdin for a machine profile), env ls/env use/env add/env rm, models ls, traffic ls/traffic get, health, cost, slo, chat, simulate, route explain (the routing dry-run), vk create (mint a personal key, secret printed once), killswitch status/killswitch on/killswitch off (read or toggle the global kill switch), passthrough status/passthrough global on/passthrough global off (read the three-tier emergency-passthrough snapshot or toggle its global tier — engaging bypasses the hooks by default, with --bypass-cache/--bypass-normalize for the other tiers), and resource (the CLI face of the OpenAPI-driven engine — resource kinds, resource search <query>, resource describe <kind>, resource read <kind> <operationId>, and resource invoke <kind> <operationId>).The resource catalog subcommands (kinds, search, describe) read the embedded catalog and run offline with no environment or credential; read and invoke make a live admin call, fill path placeholders from repeatable --param name=value flags and the query from --query name=value, take a write body from --body or --body-file, and gate every write behind a y/N prompt unless --yes is passed (a non-interactive write without --yes is refused, not silently applied). read refuses a write operation and invoke refuses a read, each pointing at the other. Each renders a table by default and stable JSON under --output json. The VK-authed commands (chat, simulate) resolve the model from --model or the remembered selection and the Virtual Key secret from --vk or the keychain. killswitch and passthrough global refuse to toggle in a prod environment without --yes.

The root PersistentPreRunE is a setup/login guard: config commands (setup, env) and the bare TUI launch carry annotations that skip env resolution or the credential check, but every gateway command first resolves an environment and verifies a stored credential (loggedIn: a JWT access token or an admin key). When no environment resolves it guides the operator to nexus setup; when an environment exists but no credential is stored it guides them to nexus login — both returning the auth exit code rather than letting a raw 401 surface deeper in the call.

With no subcommand on an interactive terminal, the root command launches the TUI; piped or non-TTY invocations print help so scripting stays predictable.

The TUI face

internal/tui is the Bubble Tea console, styled from one shared Lip Gloss palette (packages/nexus-cli/internal/tui/styles/styles.go) whose accent is the institutional brand blue and whose status / health colors are semantic.

tui.Run (packages/nexus-cli/internal/tui/shell/run.go) starts the top-level Shell (packages/nexus-cli/internal/tui/shell/shell.go). The shell runs the entry wizard on first use or when the stored selection is missing/invalid, and otherwise goes straight to the dashboard. tui.Deps (packages/nexus-cli/internal/tui/shell/wizard.go) carries the gateway, the auth/persistence callbacks (login, VK-secret storage, remembered-selection persistence, VK creation) the wizard needs, and a BuildAgent seam that constructs the conversation's agent; the Control Plane CLI assembles those from core and capabilities in App.tuiDeps (packages/nexus-cli/internal/cli/root.go).

The entry wizard (packages/nexus-cli/internal/tui/shell/wizard.go) walks choose-environment → login → pick model → pick Virtual Key. The environment step lists the configured environments (and a "create new" row that captures a name + Control Plane URL and persists a profile); selecting or creating one calls back into the CLI to rebuild the gateway for that deployment (Deps.SwitchEnv / Deps.CreateEnv), so the rest of the wizard and the dashboard talk to the chosen target — and a switch to an already fully-configured, logged-in environment skips straight to the dashboard. At the key step the operator either pastes a held secret or creates a new personal key in-session; the secret is stored in the keychain and the model/VK selection in the config so subsequent launches skip the wizard.

The dashboard (packages/nexus-cli/internal/tui/shell/app.go) stacks a top canvas — the active view — over an always-resident conversation pane, with a red banner only when the environment is prod, a breadcrumb trail heading the canvas, and a footer that carries the keybar (or the agent's working spinner while a turn runs) on the left and a bottom-right location indicator (the active profile name + scheme-stripped Control Plane address, reddened on prod) so the operator can always confirm which deployment they are acting on; the slash palette, when open, owns the footer line. The split heights are computed by focus (packages/nexus-cli/internal/tui/shell/layout.go): the focused pane takes the larger share and the other keeps a usable minimum, the boundary eases over a few animation frames on a focus change, and a very short terminal collapses to the single focused pane. The breadcrumb (packages/nexus-cli/internal/tui/shell/breadcrumb.go) renders the path from the cockpit down to the active view, and a nav stack records the drill path so that, with the canvas focused, esc — or , an alias for it since no view uses the arrow for in-view navigation — walks back one level; past the root it lands on the cockpit. A view that holds its own detail drawer consumes the back key first through an optional backHandler (closing the drawer), and only when it has nothing to back out of does the root pop the nav stack.

Interaction is modal (packages/nexus-cli/internal/tui/shell/mode.go), keyed by which pane is focused. tab toggles focus between the chat and the canvas. With the chat focused — the launch default — the whole keyboard, including IME / multibyte text, routes to the conversation prompt (INPUT mode). With the canvas focused the root dispatches single-key hotkeys (NORMAL mode), and an IME guard (isHotkey) accepts only a single printable-ASCII rune or a named key so multibyte / composed runes are dropped and an active CJK or other input method can never leak composition into the interface or misfire a shortcut; a canvas view that itself captures text is INPUT mode too. / opens a fuzzy slash command palette (packages/nexus-cli/internal/tui/shell/slash.go) over the view registry (packages/nexus-cli/internal/tui/shell/registry.go) — from an empty chat prompt or from the focused canvas — whose entries are either a view command (open a view, optionally with an argument such as an event id) or an agent control routed to the conversation; /model switches the chat's model, either directly from a slug argument or by opening the Models catalog in a pick mode where enter selects the highlighted model; f filters the active view; number keys jump between views.

The conversation pane drives the gateway agent, and the agent's turn is synchronous and blocking, so a bridge (packages/nexus-cli/internal/tui/shell/bridge.go) runs the turn on a background goroutine and pumps every agent callback — streamed text, streamed reasoning, tool starts, the canvas drives, the confirm request, and the terminal result — back into the Bubble Tea loop as messages via a re-issuing drain command, the same pattern the chat streamer (packages/nexus-cli/internal/tui/kit/stream.go) uses. The bridge implements the capabilities.Canvas seam (packages/nexus-agent-core/capabilities/runtime/canvas.go): Navigate, ShowEvent, and Highlight enqueue a message and return immediately, so a canvas drive never blocks the agent loop, and the root applies them to follow the agent live (open a view, drill an event, spotlight a row). The bridge also supplies the agent's ConfirmFunc, bound to the in-flight turn's context: a mitigation request rides back to the UI as a confirm message, the agent goroutine parks until the human's decision returns on the reply channel, and a turn torn down by timeout or teardown declines rather than acting on a late authorization. The CLI builds the agent in App.tuiDeps's BuildAgent, wiring the bridge as the canvas and confirm, the typed client as the model and gateway, mitigation enabled behind that confirm, and the learning-memory base directory and the per-environment session path (packages/nexus-agent-core/capabilities/runtime/paths.go).

The conversation pane (packages/nexus-cli/internal/tui/shell/conversation.go) is docked at the bottom and always on screen; tab moves focus to it and the split grows it. It builds the agent lazily on the first turn, folds the bridge's streamed text and tool-progress lines into a transcript, and reveals streamed assistant text with a paced typewriter — deltas accumulate in a buffer that an animation clock uncovers a few runes per tick, a working spinner with an elapsed-time counter shows in the footer while a turn runs, and the buffer is flushed in full at turn end so the final answer is never withheld behind the cadence. When the model emits a reasoning channel, those deltas stream live (no typewriter) into a distinct dimmed block rendered above that turn's answer; the block is display-only and, like the model seam guarantees, never enters the transcript — and because reasoning can resume between two answer segments, opening a new answer segment first finalizes the prior one to its complete text so an interleaved reasoning block never leaves the earlier answer truncated. The prompt stays editable while a turn runs: enter then enqueues the message (rendered as a dim queued line) and the queue drains one message at a time as turns finish, so turns stay strictly serial; esc interrupts the running turn and keeps the queue. Any mitigation the agent proposes routes through the shared Allow/Deny confirm gate (packages/nexus-cli/internal/tui/kit/confirm.go), raised in every environment: the operator picks Allow or Deny (defaulting to Deny) before the agent's write proceeds, with the prod banner reddening the prompt on a production environment. A confirm request forces focus to the chat so the gate is always on the focused surface, and a turn that ends with a confirm still pending drops the gate so a later keystroke can never resolve a finished turn's confirmation. Switching the chat's model resets the agent so the next turn rebuilds against the new model, persists the selection through the CLI, and broadcasts it to the session-bearing views.

The Mission Control cockpit (packages/nexus-cli/internal/tui/views/cockpit.go) is the landing view: a row of metric cards (requests, cost, errors) each with a braille trend over the sparkline series and a delta arrow; a live traffic waterfall with cache-hit, blocked, and PII badges; a provider leaderboard ranked by request volume and labeled by the friendly provider name; and system status lights for node health, firing alerts, the kill switch, and emergency passthrough that pulse on a problem state via an animation tick that advances a phase counter without refetching. Each source is fetched independently, so one failing endpoint degrades a single panel rather than blanking the cockpit.

Each resource view is a self-contained model that polls its endpoint on a throttled interval and renders last-good data with an inline error note rather than blanking on a transient failure. The views are Radar (the polled traffic list with the cache-hit / blocked / PII badges, filterable to errors, enter to open an event), Event (a single event with its hook decisions, request/response bodies, latency-phase waterfall, trace id, an on-demand LLM explanation streamed over the normalized view, and a replay that re-fires the captured request through the simulator), SLO (per-provider latency percentiles, availability, and routing-fallback activity; selecting a provider drills into its detail — availability, error and cache-hit rates, average latency/TTFB, and cost — resolved through the provider catalog so the drill is keyed by the provider id while the friendly name is shown; t enable/disables the drilled provider behind the confirm gate), Cost (per-provider spend with average latency, cache ROI, and a burn-rate + 30-day projection; f flushes the gateway cache behind the confirm gate), Chat (the VK-authed streaming playground with a running session-cost tally and slash commands — /system, /temp, /clear, and /compare <model> an A/B mode that fans one prompt to two models side by side with the faster reply flagged — over the shared chatStreamer, which owns the background goroutine, channels, and cancel; A/B mode runs two such streams concurrently), Lab (a synthetic traffic generator, a request lab over the simulator-forward endpoint, and a routing dry-run), Kill (the emergency-controls surface: the current global kill-switch state and the emergency-passthrough snapshot — what the global tier is bypassing plus a count of any per-adapter/per-provider overrides — with e/d and p/o toggles behind the confirm gate in prod), Alerts (the firing alerts), Nodes (per-node heartbeat and version with config drift), Jobs (the scheduled background jobs with their interval and last run), Models (the model catalog grouped by provider — code, friendly name, context window, per-million pricing, and enabled state), Compliance (block-rate and TLS/hook-error KPIs), Sync (the fleet-wide config-drift rollup), Keys (the virtual keys with their type, approval status, and enabled state; r revokes the selected active key and g rotates its secret — showing the new plaintext once — both behind the confirm gate), and Rules (the routing rules in priority order with their strategy and stage; t enable/disables the selected rule behind the confirm gate). The list views that carry per-record detail — Alerts, Nodes, Jobs, Models, Keys, and Rules, plus the SLO provider drill — share a row cursor with an enter-to-drill read-only detail drawer (a firing alert's full record, a node's target-vs-applied version drift, a job's schedule and last run, a model's type / status and full pricing, a key's source app / rate limit / owner-and-project scope / id, a rule's description and pretty-printed strategy config / match conditions / fallback chain) and /esc back. Keys and Rules keep their in-place mitigation keystrokes (r/g on Keys, t on Rules) alongside the drawer. The cockpit, Cost (a per-provider spend ranking whose per-provider health detail lives in the SLO drill), Compliance (a scalar KPI rollup), and Sync (a fleet rollup that defers per-node detail to the Nodes drill) have no per-row drawer of their own.

A generic Resource view (packages/nexus-cli/internal/tui/resource/resourceview.go) complements the dedicated views with the interactive face of the OpenAPI-driven engine: a deterministic, local, zero-token browser over every admin kind in the embedded catalog. It is a fuzzy-filtered kind picker over a navigation stack of frames, where each frame binds the path parameters accumulated by its ancestors so a resource at any nesting depth resolves fully. Opening a kind lands on its collection table when it has a canonical list, or on an operation menu when it does not (reports, singleton config, two-level resources). A collection frame renders a real multi-column table — columns inferred from the rows, identity fields (name / code / displayName) ahead of the id, paginated through the shared restable logic (packages/nexus-cli/internal/restable/). Selecting a row binds its id to the next path parameter and drills into that record plus its child operations — nested sub-collections, item actions, and writes — so the operator walks kind → collection → record → sub-collection → sub-record to any depth, the same flow the agent reaches by operationId. Operations that take query parameters open a filter form, and writes a body form, both built from the operation's OpenAPI schema (packages/nexus-cli/internal/tui/resource/resourceform.go — an enum field becomes a choice picker, a free field a text input). Reads run immediately; every write (create, update, delete, singleton config, RPC, item action) is gated by the shared Allow/Deny confirm before the call fires (packages/nexus-cli/internal/tui/kit/confirm.go). The view resolves each call through the public accessors and issues it via the gateway's AdminRequest, with no agent turn.

The web face

The Control Plane embeds the same kernel as the "Chat with Nexus" widget's backend (packages/control-plane/internal/assistant/). Where the TUI runs one resident agent over local files, the web face builds a fresh agent per turn, bound to the calling admin: BuildWebAgent (builder.go) wires the caller-authenticated core.Client for admin self-calls, the system-VK model for inference, DB-backed per-user memory and a spill-backed session store (dbstore.go — the row is the metadata index, the transcript content lives in object storage), the per-user file sandbox (webfiles.go, quota-capped), the PII redactor over tool output (redactor.go), and a TTL situation cache so a rapid follow-up turn reuses the ~8-read snapshot (situation_cache.go). The web profile registers canvas navigation and the mitigate writes but never the system tools — run_command cannot reach a server.

A turn is split command/data-stream (handler_turn.go + bus.go): POST /assistant/sessions/:id/chat returns 202 and runs the turn detached; the client observes it over the GET …/stream SSE channel, which reconnects with ?lastSeq= and replays missed events from a per-session ring. A per-session serialization guard 409s a second concurrent turn, and a per-user concurrent-turn cap 429s before any inference spends the shared system VK. Confirm-tier writes park server-side (confirm.go + dbconfirm.go — a durable mirror distinguishes "pod restarted, re-issue" from "expired"), surface an impact preview for the highest-blast-radius actions, and on production demand a backend-enforced second confirmation.

Admin self-calls dispatch in-process through internal/platform/selfdispatch (no loopback HTTP): the transport stamps the unforgeable initiator context value the audit writer folds into the via column — "assistant", suffixed by internal/platform/subagentmark when a dispatched child made the call — and audit.StripInitiatorHeader scrubs any inbound copy of the marker header at ingress. Every persisted transcript revision (and each deletion tombstone) appends to a per-session, tamper-evident hash chain (chataudit.go over internal/platform/hashchain — canonical JSON + SHA-256 links); the session read path verifies the chain on every load and serves a named verdict (verified / unchained / chain_broken / content_mismatch) rather than ever bricking the conversation. The nine assistant routes gate on the dedicated IAM resource (admin:assistant.read/.write).

Authorization

There is exactly one authorization control for the whole tool: the Nexus IAM, reached only through the admin API and /v1/*. This holds uniformly across the faces and across every read and write. A human authenticates with the auth-server JWT (via PKCE) and a machine with an admin API key bound to a scoped service user; both resolve to an admin principal that passes through the same per-route IAM checks as any other caller. The prod confirmation on the kill-switch toggle is an additional guard layered on top of IAM, never a substitute for it.

References

  • packages/nexus-cli/cmd/nexus/main.go — binary entry point
  • packages/nexus-agent-core/core/ — the shared kernel: auth, profiles, the secret-store seam, typed client, wire structs
  • packages/nexus-cli/internal/local/ — local-machine seam impls the shared kernel does not carry: the OS-keychain KeyringStore (drops go-keyring from the kernel) and the on-disk TOML Config store (drops BurntSushi/toml). The browser-login PKCE Authenticator deliberately stays in core/auth_pkce.go: it is stdlib-only, so moving it would shed no third-party dependency (the bar this package exists to clear), while forcing the tokenResponse wire type to be shared with the runtime jwtTokenSource that must remain in the kernel. The kernel keeps it; servers embedding the kernel simply never construct an Authenticator (they supply their own TokenSource).
  • packages/nexus-agent-core/agent/ — agent kernel: message model, model seam, tool registry, permission gate, context assembly, compaction, memory, system prompt, sessions, the loop, the subagent chassis
  • packages/nexus-agent-core/capabilities/ — concrete seams over core: model, gateway/canvas/system tools, name resolution, situation snapshotter, the registry profile, agent assembly, subagent dispatch
  • packages/nexus-agent-core/core/chat.go — VK-authed chat: ChatStream and the tool-calling ChatToolStream
  • packages/nexus-cli/internal/cli/ — Cobra command tree + exit-code mapping
  • packages/nexus-cli/internal/tui/ — Bubble Tea shell, entry wizard, dashboard views
  • packages/nexus-cli/internal/tui/shell/app.go — root model: prod banner, breadcrumb, pane focus + split, modal routing, view registry, model switch
  • packages/nexus-cli/internal/tui/shell/layout.go — focus type + split-height math + focus-change ease
  • packages/nexus-cli/internal/tui/shell/bridge.go — agent↔TUI channel bridge: canvas impl + blocking-confirm handshake
  • packages/nexus-cli/internal/tui/shell/conversation.go — resident agent chat: transcript, typewriter, reasoning block, markdown render, scrollback, working spinner, message queue, mitigate-confirm, model switch
  • packages/nexus-cli/internal/tui/kit/markdown.go — glamour markdown renderer for finalized chat answers
  • packages/nexus-cli/internal/tui/kit/keyhints.go — OS-aware key hints + global-shortcut strip + the /help reference
  • packages/nexus-agent-core/capabilities/resource/operation.go — the operation model: path-param parsing, named substitution, canonical verb / label, code-level operation search, init-time DistilledOp memo (opIndex/distilledIdx)
  • packages/nexus-agent-core/capabilities/runtime/tools_resource.go — the OpenAPI-driven agent tools (resource_search/resource_describe/resource_read/resource_invoke) + shared executor
  • packages/nexus-agent-core/capabilities/resource/api.go — public catalog accessors (Kinds/Operations/ResolveOperation/SearchCards/DescribeOperation)
  • packages/nexus-agent-core/capabilities/resource/distill.go — distills a kind's OpenAPI into the compact resource_describe schema
  • packages/nexus-agent-core/capabilities/resource/openapi/control-plane/ — embedded Control Plane admin OpenAPI catalog + _index.yaml
  • packages/nexus-cli/internal/restable/ — shared collection rendering: row extraction, column inference, cell text, pagination
  • packages/nexus-cli/internal/tui/resource/resourceview.go — interactive Resource cascade (navigation stack, table, drill, writes) over the catalog
  • packages/nexus-cli/internal/tui/resource/resourceform.go — OpenAPI-driven filter / parameter / body input form
  • packages/nexus-cli/internal/cli/resource.go — the nexus resource command tree
  • packages/nexus-cli/internal/tui/views/cockpit.go — Mission Control landing cockpit
  • packages/nexus-cli/internal/tui/shell/mode.go — modal NORMAL/INPUT grammar + IME hotkey guard
  • packages/nexus-cli/internal/tui/shell/slash.go/ slash command palette
  • packages/nexus-cli/internal/tui/shell/breadcrumb.go — breadcrumb trail + nav stack
  • packages/nexus-cli/internal/tui/styles/styles.go — shared palette
  • go.work — workspace membership