diff --git a/DEVLOG.md b/DEVLOG.md
index 597f1b17..0a9c386a 100644
--- a/DEVLOG.md
+++ b/DEVLOG.md
@@ -2,6 +2,17 @@
> Append-only session log. Read at session start. Update at session end.
+## 2026-07-25 — Canonical build version across system surfaces
+
+- Replaced the Admin System router's independent `npm_package_version` /
+ `1.0.0` fallback with Forge's shared build-identity reader, so its version,
+ SHA, and build time now use the same runtime source as the application shell
+ and MCP server metadata.
+- Removed the sign-in footer's hardcoded `1.0.0`; unauthenticated operators now
+ see the packaged application version too.
+- Added unit and instance-admin integration coverage for the standalone-runtime
+ case where npm lifecycle metadata is absent.
+
## 2026-07-25 — v0.29.7 release preparation
Prepared the serialized patch release for AXI-151 after implementation PR #87
diff --git a/docs/guide/instance-admin.md b/docs/guide/instance-admin.md
index 74878897..d160b50f 100644
--- a/docs/guide/instance-admin.md
+++ b/docs/guide/instance-admin.md
@@ -33,9 +33,11 @@ The admin shell has a fixed set of pages, each backed by an
### Overview / System (`/admin/system`)
Build identity and instance-wide rollups. Reports the running
-`version`, `buildSha`, and `buildTime` (baked into the Docker image as
-`FORGE_GIT_SHA` / `FORGE_BUILD_TIME` — the same source as
-`system.buildInfo` and the Settings → About line), plus counts:
+`version`, `buildSha`, and `buildTime`. The packaged `package.json` version is
+canonical when npm lifecycle metadata is unavailable; the Docker image bakes
+`FORGE_GIT_SHA` / `FORGE_BUILD_TIME`. These are the same sources used by the
+sign-in footer, `system.buildInfo`, MCP metadata, and the Settings → About line.
+The page also reports counts for:
tenants, users, admins, runtimes, profiles, connections, and runs in
the last 24h.
diff --git a/src/app/(auth)/signin/page.tsx b/src/app/(auth)/signin/page.tsx
index a6a8b4ac..cb43a53a 100644
--- a/src/app/(auth)/signin/page.tsx
+++ b/src/app/(auth)/signin/page.tsx
@@ -6,8 +6,7 @@ import { Input } from "@/components/ui/input";
import { Kbd } from "@/components/ui/kbd";
import { LiveStatusPanel, LiveLoopCard } from "./live-status-panel";
import { listEnabledSsoProviders, type PublicSsoProvider } from "@/server/sso";
-
-const APP_VERSION = "1.0.0";
+import { readPackageVersion } from "@/server/build-info";
/** Short mono badge shown in the provider button. */
function providerBadge(p: PublicSsoProvider): string {
@@ -65,6 +64,7 @@ export default async function SignInPage({
const providers = await listEnabledSsoProviders();
const hasProviders = providers.length > 0;
const host = instanceHost();
+ const appVersion = await readPackageVersion();
async function credentialsAction(formData: FormData) {
"use server";
@@ -126,7 +126,7 @@ export default async function SignInPage({
- forge v{APP_VERSION} · self-hosted
+ forge v{appVersion} · self-hosted
{host}
diff --git a/src/server/routers/__tests__/instance-admin-system.test.ts b/src/server/routers/__tests__/instance-admin-system.test.ts
new file mode 100644
index 00000000..72c4f763
--- /dev/null
+++ b/src/server/routers/__tests__/instance-admin-system.test.ts
@@ -0,0 +1,48 @@
+import { InstanceRole } from "@prisma/client";
+import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+import { instanceAdminRouter } from "@/server/routers/instance-admin";
+import {
+ buildContext,
+ createWorkspaceFixture,
+ disconnectPrisma,
+ getPrisma,
+ type TestFixture,
+} from "@/server/routers/__tests__/helpers";
+import { _resetBuildInfoCacheForTests } from "@/server/build-info";
+import packageJson from "../../../../package.json";
+
+describe("instanceAdmin.system", () => {
+ let fixture: TestFixture;
+
+ beforeEach(async () => {
+ fixture = await createWorkspaceFixture({ keyPrefix: "IA" });
+ await getPrisma().user.update({
+ where: { id: fixture.user.id },
+ data: { instanceRole: InstanceRole.INSTANCE_ADMIN },
+ });
+ });
+
+ afterEach(async () => {
+ vi.unstubAllEnvs();
+ _resetBuildInfoCacheForTests();
+ await fixture.cleanup();
+ });
+
+ afterAll(async () => {
+ await disconnectPrisma();
+ });
+
+ it("uses the canonical packaged version and baked build metadata", async () => {
+ vi.stubEnv("npm_package_version", "");
+ vi.stubEnv("FORGE_GIT_SHA", "abc1234");
+ vi.stubEnv("FORGE_BUILD_TIME", "2026-07-25T15:05:43Z");
+
+ const caller = instanceAdminRouter.createCaller(await buildContext(fixture));
+
+ await expect(caller.system()).resolves.toMatchObject({
+ version: packageJson.version,
+ buildSha: "abc1234",
+ buildTime: "2026-07-25T15:05:43Z",
+ });
+ });
+});
diff --git a/src/server/routers/instance-admin.ts b/src/server/routers/instance-admin.ts
index 8fa1ad23..989c9647 100644
--- a/src/server/routers/instance-admin.ts
+++ b/src/server/routers/instance-admin.ts
@@ -11,6 +11,7 @@ import {
resolveSubjectLabels,
subjectKey,
} from "@/server/services/subject-labels";
+import { forgeBuildIdentity } from "@/server/build-info";
const slugSchema = z
.string()
@@ -185,20 +186,23 @@ export const instanceAdminRouter = router({
/** System / build info + instance-wide counts for the admin overview. */
system: instanceAdminProcedure.query(async ({ ctx }) => {
- const [tenants, users, admins, runtimes, profiles, connections, runs24] = await Promise.all([
- ctx.db.workspace.count({ where: { deletedAt: null } }),
- ctx.db.user.count(),
- ctx.db.user.count({ where: { instanceRole: "INSTANCE_ADMIN" } }),
- ctx.db.runtime.count({ where: { archivedAt: null } }),
- ctx.db.agentProfile.count({ where: { archivedAt: null } }),
- ctx.db.connection.count(),
- ctx.db.agentRun.count({ where: { startedAt: { gte: new Date(Date.now() - 24 * 60 * 60 * 1000) } } }),
- ]);
+ const [build, tenants, users, admins, runtimes, profiles, connections, runs24] =
+ await Promise.all([
+ forgeBuildIdentity(),
+ ctx.db.workspace.count({ where: { deletedAt: null } }),
+ ctx.db.user.count(),
+ ctx.db.user.count({ where: { instanceRole: "INSTANCE_ADMIN" } }),
+ ctx.db.runtime.count({ where: { archivedAt: null } }),
+ ctx.db.agentProfile.count({ where: { archivedAt: null } }),
+ ctx.db.connection.count(),
+ ctx.db.agentRun.count({
+ where: { startedAt: { gte: new Date(Date.now() - 24 * 60 * 60 * 1000) } },
+ }),
+ ]);
return {
- // Match system.buildInfo: the Dockerfile bakes FORGE_GIT_SHA / FORGE_BUILD_TIME.
- version: process.env.npm_package_version ?? "1.0.0",
- buildSha: process.env.FORGE_GIT_SHA || null,
- buildTime: process.env.FORGE_BUILD_TIME || null,
+ version: build.version,
+ buildSha: build.gitSha,
+ buildTime: build.buildTime,
counts: { tenants, users, admins, runtimes, profiles, connections, runs24 },
};
}),
diff --git a/tests/unit/build-info.test.ts b/tests/unit/build-info.test.ts
index cf323771..9ac9fc1d 100644
--- a/tests/unit/build-info.test.ts
+++ b/tests/unit/build-info.test.ts
@@ -3,7 +3,9 @@ import {
_resetBuildInfoCacheForTests,
forgeBuildIdentity,
mcpServerInfo,
+ readPackageVersion,
} from "@/server/build-info";
+import packageJson from "../../package.json";
describe("build identity", () => {
afterEach(() => {
@@ -23,6 +25,12 @@ describe("build identity", () => {
});
});
+ it("reads the packaged application version when npm lifecycle metadata is absent", async () => {
+ vi.stubEnv("npm_package_version", "");
+
+ await expect(readPackageVersion()).resolves.toBe(packageJson.version);
+ });
+
it("includes build identity in MCP serverInfo", async () => {
vi.stubEnv("npm_package_version", "9.8.7");
vi.stubEnv("FORGE_GIT_SHA", "abc1234");