From 7798bad7ae5e52f1664984261cf51d2cf6249ba9 Mon Sep 17 00:00:00 2001 From: Bailey Dixon Date: Mon, 19 Oct 2026 07:56:20 -0400 Subject: [PATCH] fix: unify admin build labels and clarify agent setup (AXI-212) --- DEVLOG.md | 32 +++ docs/guide/navigation-and-agent-setup.md | 23 ++- .../settings/agents/agent-setup-dialog.tsx | 191 ++++++++++++------ .../(app)/settings/agents/agents-content.tsx | 8 +- .../(app)/w/[slug]/settings/runtimes/page.tsx | 26 ++- src/components/admin-shell/admin-overview.tsx | 4 +- src/components/admin-shell/admin-shell.tsx | 27 +-- src/components/admin-shell/admin-system.tsx | 2 +- src/components/global-shell/version-chip.tsx | 24 ++- src/lib/agent-setup.ts | 5 + .../mission-control-agent-management.spec.ts | 9 +- tests/unit/agent-setup-dialog.test.ts | 54 +++++ tests/unit/agent-setup.test.ts | 20 ++ tests/unit/build-labels.test.ts | 56 +++++ 14 files changed, 376 insertions(+), 105 deletions(-) create mode 100644 tests/unit/agent-setup-dialog.test.ts create mode 100644 tests/unit/build-labels.test.ts diff --git a/DEVLOG.md b/DEVLOG.md index 8e574598..ee9d9612 100644 --- a/DEVLOG.md +++ b/DEVLOG.md @@ -2,6 +2,38 @@ > Append-only session log. Read at session start. Update at session end. +## 2026-10-19 — AXI-212 build labels and runtime-first setup + +- Reused the shared system.buildInfo VersionChip in the admin header instead + of optional props with a vdev fallback. Header/footer show a bounded + seven-character SHA; their full build, release and timestamp remain in the + title/accessibility description. Removed false dev placeholders from other + admin version panels when data is unavailable. +- Managed setup now starts with an explicit choice between connecting a + runtime and configuring an agent identity. Host registration requires only + an authorized home workspace, not an existing agent. Existing runtime + browsing and personal/agent MCP credential paths remain separate. Runtime + registration opens the existing host form and preserves server permission + checks; opening the flow never registers a host or dispatches work. +- Added focused build-label, setup-rendering and routing/role regression + coverage. No schema changes, production configuration changes or release. +- Initial visual verification was blocked by the in-app browser and Docker's + Inference Manager startup during an operator update. After the update, + verified against a fresh local fixture database and an authorized isolated + Playwright browser. No Docker data was reset or deleted. +- Rendered review caught a related mobile admin sidebar clipping issue: its + navigation now scrolls independently and preserves the footer. Both build + chips fit in 129 CSS pixels with the full SHA retained in their descriptions; + the 390px page has no horizontal overflow. Verified runtime registration + (Hermes/Codex choices), existing-agent routing, and personal MCP token setup + without creating credentials, registering hosts, or dispatching runs. +- Focused regression suite: 19 tests, final TypeScript and focused ESLint all + passed. Desktop/mobile captures are kept + in local output/playwright; the final browser page reported zero console + errors or warnings. Operator accepted the local iteration and authorized + commit, PR, merge and a separately assembled v0.36.1 patch release. Hosted + checks must pass on each exact PR head before merge. + ## 2026-10-15 — AXI-210 v0.36.0 release assembly - Assembled the operator-approved release from implementation PR #104, diff --git a/docs/guide/navigation-and-agent-setup.md b/docs/guide/navigation-and-agent-setup.md index c1ae5ef5..575aa4d4 100644 --- a/docs/guide/navigation-and-agent-setup.md +++ b/docs/guide/navigation-and-agent-setup.md @@ -24,9 +24,18 @@ Begin in **Agents** and choose the execution intent: the work. Choose the workspace and either your personal identity or an agent. Personal credentials act as the current user. Agent credentials identify the agent and record the issuing operator. No managed runtime is required. -- **Let Forge run an agent**: choose the workspace and agent, then configure - its managed execution host. Workspace owner/admin authority is required for - managed setup; instance administration alone is not workspace membership. +- **Connect a managed runtime**: choose **Connect a runtime** and its home + workspace, then continue to the runtime-host form. Choose the supported + adapter (for example Hermes or Codex app server) and supply the host's + connection details. No existing agent identity is required. You can also + view existing runtimes without opening a new registration. +- **Configure an agent** inside managed setup: choose an existing identity or + create/request one, then review its workspace access and runtime. Agent + identities and hosts are separate: several identities can use one host. + +Workspace owner/admin authority is required for managed setup; instance +administration alone is not workspace membership. Opening setup does not +create a credential, register a host, grant access, or dispatch work. An existing agent with workspace access can continue directly to its scoped MCP credential setup. Otherwise its detail page explains how an authorized @@ -35,6 +44,14 @@ instance policy under Administration → Agent governance. The compatibility default remains approval required; changing policy does not approve existing requests, share identities, or grant workspace membership. +## Running build + +The admin header and shell footers use the same `system.buildInfo` source. +They show the running version and a seven-character commit identifier in a +bounded single line. Hover/accessibility descriptions retain the full commit, +release and build timestamp; the link opens What's New. While build metadata +is unavailable the label says Forge, not an invented development version. + ## Ownership, availability and authorization These are separate facts: diff --git a/src/app/(app)/settings/agents/agent-setup-dialog.tsx b/src/app/(app)/settings/agents/agent-setup-dialog.tsx index dc575b3a..d06bee58 100644 --- a/src/app/(app)/settings/agents/agent-setup-dialog.tsx +++ b/src/app/(app)/settings/agents/agent-setup-dialog.tsx @@ -5,7 +5,11 @@ import { useState } from "react"; import { Button } from "@/components/ui/button"; import { CenterModal } from "@/components/ui/modal"; import { Combobox } from "@/components/ui/combobox"; -import { agentSetupDestination, type AgentSetupMode } from "@/lib/agent-setup"; +import { + agentSetupDestination, + type AgentSetupMode, + type ManagedSetupTask, +} from "@/lib/agent-setup"; import { personalAgentCreationCopy, type PersonalAgentCreationPolicy, @@ -49,10 +53,12 @@ export function AgentSetupDialog({ : "", ); const [profileId, setProfileId] = useState(""); + const [managedTask, setManagedTask] = useState("runtime"); + const choosingRuntime = mode === "managed" && managedTask === "runtime"; const workspace = workspaces.find((item) => item.id === workspaceId); const profile = profiles.find((item) => item.id === profileId); const canManage = workspace?.role === "OWNER" || workspace?.role === "ADMIN"; - const destination = agentSetupDestination({ mode, workspace, profile }); + const destination = agentSetupDestination({ mode, workspace, profile, managedTask }); const binding = profile?.bindings.find((item) => item.workspace.id === workspaceId); const creation = personalAgentCreationCopy(personalCreationPolicy); @@ -63,11 +69,11 @@ export function AgentSetupDialog({ if (!open) onClose(); }} size="md" - title={mode === "external" ? "Connect an external client" : "Let Forge run an agent"} + title={mode === "external" ? "Connect an external client" : "Set up managed execution"} description={ mode === "external" ? "Use Codex Desktop, a CLI, or another MCP client. Your client executes the work; no Forge runtime is required." - : "Forge dispatches work to a managed execution runtime. Choose the workspace and agent before configuring its host." + : "Connect a Hermes or Codex execution host first, or configure an agent identity to use a runtime. These are separate steps." } footer={
@@ -79,19 +85,61 @@ export function AgentSetupDialog({ className="focus-ring inline-flex h-8 items-center rounded-md bg-ember px-3 text-xs font-medium text-ember-foreground" href={destination} > - Continue + {choosingRuntime ? "Connect runtime" : "Continue"} ) : ( )}
} >
+ {mode === "managed" && ( +
+ What would you like to set up? + {( + [ + [ + "runtime", + "Connect a runtime", + "Register a Hermes, Codex, or other supported host. No agent identity is required yet.", + ], + [ + "agent", + "Configure an agent", + "Choose or create an identity, then give it workspace access and an execution runtime.", + ], + ] as const + ).map(([value, label, hint]) => ( + + ))} +
+ )}
-

Workspace access

+

+ {choosingRuntime ? "Runtime home workspace" : "Workspace access"} +

- Credentials and permissions apply only to this workspace. You can add other workspaces - separately. -

-
-
-

Acting identity

- setProfileId(value ?? "")} - options={[ - ...(mode === "external" - ? [{ value: "", label: "My account", secondary: "Personal MCP token" }] - : []), - ...profiles - .filter( - (item) => - item.availability.state === "AVAILABLE" && - (isInstanceAdmin || item.ownedByMe || item.instanceShared), - ) - .map((item) => ({ - value: item.id, - label: item.name, - secondary: - item.ownerType === "SERVICE" - ? "Service-owned" - : item.ownedByMe - ? "Owned by you" - : "Available agent", - })), - ]} - placeholder={ - mode === "external" ? "My account · Personal MCP token" : "Choose an agent…" - } - className="w-full" - /> -

- {profile - ? "The agent is the acting identity. The issued key also records the human operator; sharing an identity does not grant workspace access." - : mode === "external" - ? "Actions are attributed to your user account. Choose an agent above for a separate agent identity." - : "Choose an existing agent or create/request a new identity below."} + {choosingRuntime + ? "Choose where to register and manage the host. Registration alone does not grant agents access or start any work." + : "Credentials and permissions apply only to this workspace. You can add other workspaces separately."}

+ {!choosingRuntime && ( +
+

+ {mode === "external" ? "Act as · agent identity is optional" : "Agent identity"} +

+ setProfileId(value ?? "")} + options={[ + ...(mode === "external" + ? [{ value: "", label: "My account", secondary: "Personal MCP token" }] + : []), + ...profiles + .filter( + (item) => + item.availability.state === "AVAILABLE" && + (isInstanceAdmin || item.ownedByMe || item.instanceShared), + ) + .map((item) => ({ + value: item.id, + label: item.name, + secondary: + item.ownerType === "SERVICE" + ? "Service-owned" + : item.ownedByMe + ? "Owned by you" + : "Available agent", + })), + ]} + placeholder={ + mode === "external" ? "My account · Personal MCP token" : "Choose an agent…" + } + className="w-full" + /> +

+ {profile + ? mode === "external" + ? "The agent is the acting identity. The issued key also records the human operator; sharing an identity does not grant workspace access." + : "This is who does the work, not the host that executes it. Continue to review workspace access and its runtime." + : mode === "external" + ? "Actions are attributed to your user account. Choose an agent above for a separate agent identity." + : "Choose an existing agent or create/request a new identity below."} +

+
+ )} {workspace?.role === "GUEST" && (

Guests cannot create credentials. Ask a workspace administrator for access. @@ -175,26 +230,28 @@ export function AgentSetupDialog({ href={`/w/${encodeURIComponent(workspace.slug)}/settings/runtimes`} className="focus-ring inline-flex text-sm text-ember hover:underline" > - Register or inspect a runtime in {workspace.name} → + View existing runtimes in {workspace.name} → )} -

- -

- {isInstanceAdmin - ? "Choose personal or service ownership when creating an identity. Workspace access and catalog sharing remain separate controls." - : creation.description} -

-
+ {!choosingRuntime && ( +
+ +

+ {isInstanceAdmin + ? "Choose personal or service ownership when creating an identity. Workspace access and catalog sharing remain separate controls." + : creation.description} +

+
+ )}
); diff --git a/src/app/(app)/settings/agents/agents-content.tsx b/src/app/(app)/settings/agents/agents-content.tsx index 2b55cc47..6727396d 100644 --- a/src/app/(app)/settings/agents/agents-content.tsx +++ b/src/app/(app)/settings/agents/agents-content.tsx @@ -179,9 +179,10 @@ export function AgentsContent({
-

Let Forge run an agent

+

Connect a managed runtime

- Choose an identity and runtime for work dispatched and monitored by Forge. + Register a Hermes or Codex host, then choose which agent identities use it. An + existing agent is not required to connect a runtime.

- Ownership, catalog availability, and workspace access are separate. + These are identities, not runtime hosts or MCP clients. Multiple identities can use + the same runtime.

diff --git a/src/app/(app)/w/[slug]/settings/runtimes/page.tsx b/src/app/(app)/w/[slug]/settings/runtimes/page.tsx index 485df622..638aa490 100644 --- a/src/app/(app)/w/[slug]/settings/runtimes/page.tsx +++ b/src/app/(app)/w/[slug]/settings/runtimes/page.tsx @@ -1,6 +1,7 @@ "use client"; import { useEffect, useState } from "react"; import Link from "next/link"; +import { usePathname, useRouter, useSearchParams } from "next/navigation"; import { toast } from "sonner"; import { Cloud, @@ -155,6 +156,10 @@ function ChatModeBadge({ chatMode }: { chatMode: string }) { export default function RuntimesPage() { const ws = useWorkspace(); + const router = useRouter(); + const pathname = usePathname(); + const search = useSearchParams(); + const canRegister = ws.role === "OWNER" || ws.role === "ADMIN"; const utils = trpc.useUtils(); const [includeArchived, setIncludeArchived] = useState(false); const { data: runtimes, isLoading } = trpc.runtime.list.useQuery({ @@ -166,6 +171,13 @@ export default function RuntimesPage() { name: string; } | null>(null); const [createOpen, setCreateOpen] = useState(false); + useEffect(() => { + if (search?.get("create") !== "1" || !canRegister) return; + setCreateOpen(true); + const next = new URLSearchParams(search.toString()); + next.delete("create"); + router.replace(`${pathname}${next.size ? `?${next}` : ""}`, { scroll: false }); + }, [search, canRegister, pathname, router]); const [editTarget, setEditTarget] = useState<{ id: string; name: string; @@ -262,7 +274,17 @@ export default function RuntimesPage() { > {includeArchived ? "Hide archived" : "Show archived"} - @@ -461,7 +483,7 @@ export default function RuntimesPage() {
setCreateOpen(false)} onSubmit={async (vals) => { diff --git a/src/components/admin-shell/admin-overview.tsx b/src/components/admin-shell/admin-overview.tsx index f3331798..68190f85 100644 --- a/src/components/admin-shell/admin-overview.tsx +++ b/src/components/admin-shell/admin-overview.tsx @@ -162,7 +162,7 @@ export function AdminOverview() {
{( [ - ["Version", system.data?.version ?? "dev"], + ["Version", system.data?.version ?? "—"], ["Build", system.data?.buildSha ?? "—"], ["Built at", system.data?.buildTime ?? "—"], ["Tenants", String(counts?.tenants ?? "—")], @@ -329,7 +329,7 @@ export function AdminOverview() { License · Self-hosted
- Unlimited tenants · {counts?.users ?? "—"} seats in use · version {system.data?.version ?? "dev"} + Unlimited tenants · {counts?.users ?? "—"} seats in use · version {system.data?.version ?? "—"}
License details diff --git a/src/components/admin-shell/admin-shell.tsx b/src/components/admin-shell/admin-shell.tsx index 8f2db464..e611374e 100644 --- a/src/components/admin-shell/admin-shell.tsx +++ b/src/components/admin-shell/admin-shell.tsx @@ -56,7 +56,7 @@ function AdminSidebar({ activePath, instanceUrl }: { activePath: string; instanc }} >
@@ -81,7 +81,10 @@ function AdminSidebar({ activePath, instanceUrl }: { activePath: string; instanc
-