feat: expose daemon-owned active agent profile

This commit is contained in:
Federico Jaramillo Martinez
2026-07-13 22:50:50 +02:00
parent a1a019e7b1
commit 141cda93c8
11 changed files with 362 additions and 25 deletions
+13 -2
View File
@@ -12,6 +12,17 @@ export interface LoadedPiWebConfig {
config: PiWebConfig;
}
export interface EffectivePiWebConfig extends Omit<PiWebConfig, "uploads" | "spawnSessions" | "subsessions" | "agent"> {
uploads: NonNullable<PiWebConfig["uploads"]>;
spawnSessions: boolean;
subsessions: boolean;
agent: Required<NonNullable<PiWebConfig["agent"]>>;
}
export interface LoadedEffectivePiWebConfig extends Omit<LoadedPiWebConfig, "config"> {
config: EffectivePiWebConfig;
}
export interface LoadOptions {
env?: NodeJS.ProcessEnv;
cwd?: string;
@@ -110,11 +121,11 @@ export function loadPiWebConfig(options: LoadOptions = {}): LoadedPiWebConfig {
return { path, exists: true, config: parsePiWebConfig(parsed, path) };
}
export function effectivePiWebConfig(options: LoadOptions = {}): LoadedPiWebConfig {
export function effectivePiWebConfig(options: LoadOptions = {}): LoadedEffectivePiWebConfig {
return resolveEffectivePiWebConfig(loadPiWebConfig(options), options);
}
export function resolveEffectivePiWebConfig(loaded: LoadedPiWebConfig, options: LoadOptions = {}): LoadedPiWebConfig {
export function resolveEffectivePiWebConfig(loaded: LoadedPiWebConfig, options: LoadOptions = {}): LoadedEffectivePiWebConfig {
const env = options.env ?? process.env;
const host = env["PI_WEB_HOST"];
const port = env["PI_WEB_PORT"] ?? env["PORT"];
+23
View File
@@ -90,6 +90,29 @@ describe("PI WEB status", () => {
expect(runtime.capabilities).toEqual(expect.arrayContaining([PI_WEB_CAPABILITIES.piPackagesManage, PI_WEB_CAPABILITIES.selectedMachineSettings]));
});
it("carries the daemon-owned active agent profile through the web runtime response", async () => {
const activeAgentProfile = {
schemaVersion: 1 as const,
revision: `sha256:${"a".repeat(64)}`,
command: "acme-agent",
dir: "/opt/acme-agent/state",
sessionDirEnvKeys: ["PI_WEB_AGENT_SESSION_DIR"],
};
const daemon = daemonWithRuntime({
component: "sessiond",
label: "Session daemon",
runtimeVersion: "1.202605.7",
available: true,
capabilities: [],
activeAgentProfile,
});
const runtime = await getPiWebRuntime(daemon);
expect(runtime.components.sessiond.activeAgentProfile).toEqual(activeAgentProfile);
expect(runtime.components.web.activeAgentProfile).toBeUndefined();
});
it("bypasses cached npm release data for a forced check", async () => {
Reflect.deleteProperty(process.env, "PI_WEB_SKIP_VERSION_CHECK");
process.env["PI_WEB_DOCKER_RUNTIME"] = "1";
+32 -23
View File
@@ -20,11 +20,16 @@ import { TerminalService } from "./terminals/terminalService.js";
import { registerTerminalRoutes } from "./terminals/terminalRoutes.js";
import { getPiWebRuntimeComponent } from "./piWebStatus.js";
import { SESSIOND_RUNTIME_CAPABILITIES } from "../shared/capabilities.js";
import { effectiveAgentConfig, effectivePiWebConfig, maxUploadBytes, spawnSessionsEnabled, subsessionsEnabled } from "../config.js";
import { agentSessionDirEnvKeys, effectivePiWebConfig, maxUploadBytes, spawnSessionsEnabled, subsessionsEnabled } from "../config.js";
import { createActiveAgentProfileDescriptor } from "../sessiond/activeAgentProfile.js";
import { runSessionDaemonStartup } from "./sessiond/sessionDaemonStartup.js";
const { config } = effectivePiWebConfig();
const agent = effectiveAgentConfig(process.env, config);
const activeAgentProfile = createActiveAgentProfileDescriptor({
command: config.agent.command,
dir: config.agent.dir,
sessionDirEnvKeys: agentSessionDirEnvKeys(config.agent.command),
});
const app = Fastify({ logger: true, bodyLimit: maxUploadBytes(process.env, config) });
await app.register(fastifyWebsocket);
@@ -33,46 +38,50 @@ await runSessionDaemonStartup({
createRuntime() {
const eventHub = new SessionEventHub();
const workspaceActivity = new WorkspaceActivityService(eventHub);
const auth = new AuthService({ agentDir: agent.dir });
const auth = new AuthService({ agentDir: activeAgentProfile.dir });
const spawnTargets = spawnSessionsEnabled(process.env, config)
? new ProjectScopedSpawnTargetResolver({ projects: new ProjectService(new ProjectStore()), workspaces: new WorkspaceService() })
: undefined;
const sessions = new PiSessionService(eventHub, {
modelRegistry: auth.modelRegistry,
agentDir: agent.dir,
agentDir: activeAgentProfile.dir,
workspaceActivity,
logger: app.log,
...(spawnTargets === undefined ? {} : { spawnTargets }),
subsessionsEnabled: spawnTargets !== undefined && subsessionsEnabled(process.env, config),
sessionManager: createPiSessionManagerGateway({ agentDir: agent.dir, sessionDirEnvKeys: agent.sessionDirEnvKeys }),
sessionManager: createPiSessionManagerGateway({
agentDir: activeAgentProfile.dir,
sessionDirEnvKeys: activeAgentProfile.sessionDirEnvKeys,
}),
});
auth.subscribe((change) => { sessions.applyAuthChange(change); });
const terminals = new TerminalService(eventHub, workspaceActivity);
return { eventHub, workspaceActivity, auth, sessions, terminals };
const runtimeComponent = Object.freeze({
...getPiWebRuntimeComponent("sessiond", SESSIOND_RUNTIME_CAPABILITIES),
activeAgentProfile,
});
return { eventHub, workspaceActivity, auth, sessions, terminals, activeAgentProfile, runtimeComponent };
},
registerRoutes({ eventHub, workspaceActivity, auth, sessions, terminals }) {
registerRoutes({ eventHub, workspaceActivity, auth, sessions, terminals, runtimeComponent }) {
registerWorkspaceActivityRoutes(app, workspaceActivity);
registerAuthRoutes(app, auth);
registerSessionRoutes(app, sessions, eventHub);
registerTerminalRoutes(app, terminals);
app.get("/health", () => {
const runtime = getPiWebRuntimeComponent("sessiond", SESSIOND_RUNTIME_CAPABILITIES);
return {
ok: true,
activeSessions: sessions.activeCount(),
checkedAt: new Date().toISOString(),
version: {
component: runtime.component,
label: runtime.label,
...(runtime.runtimeVersion === undefined ? {} : { runtimeVersion: runtime.runtimeVersion }),
stale: false,
available: runtime.available,
},
};
});
app.get("/health", () => ({
ok: true,
activeSessions: sessions.activeCount(),
checkedAt: new Date().toISOString(),
version: {
component: runtimeComponent.component,
label: runtimeComponent.label,
...(runtimeComponent.runtimeVersion === undefined ? {} : { runtimeVersion: runtimeComponent.runtimeVersion }),
stale: false,
available: runtimeComponent.available,
},
}));
app.get("/runtime", () => getPiWebRuntimeComponent("sessiond", SESSIOND_RUNTIME_CAPABILITIES));
app.get("/runtime", () => runtimeComponent);
},
async listen({ auth, sessions, terminals }) {
let shuttingDown = false;
+54
View File
@@ -0,0 +1,54 @@
import { describe, expect, it } from "vitest";
import type { EffectivePiWebAgentConfig } from "../config.js";
import { createActiveAgentProfileDescriptor } from "./activeAgentProfile.js";
const baseAgent: EffectivePiWebAgentConfig = {
command: "acme-agent",
dir: "/opt/acme-agent/state",
sessionDirEnvKeys: ["PI_WEB_AGENT_SESSION_DIR"],
};
describe("active agent profile descriptor", () => {
it("builds a stable revision from every effective profile field", () => {
const first = createActiveAgentProfileDescriptor(baseAgent);
const second = createActiveAgentProfileDescriptor({ ...baseAgent, sessionDirEnvKeys: [...baseAgent.sessionDirEnvKeys] });
expect(second).toEqual(first);
expect(first.revision).toMatch(/^sha256:[0-9a-f]{64}$/u);
expect(createActiveAgentProfileDescriptor({ ...baseAgent, command: "other-agent" }).revision).not.toBe(first.revision);
expect(createActiveAgentProfileDescriptor({ ...baseAgent, dir: "/other/state" }).revision).not.toBe(first.revision);
expect(createActiveAgentProfileDescriptor({ ...baseAgent, sessionDirEnvKeys: ["OTHER_SESSION_DIR"] }).revision).not.toBe(first.revision);
});
it("takes an immutable snapshot for the session daemon profile epoch", () => {
const sessionDirEnvKeys = ["PI_WEB_AGENT_SESSION_DIR"];
const profile = createActiveAgentProfileDescriptor({ ...baseAgent, sessionDirEnvKeys });
sessionDirEnvKeys.push("LATE_MUTATION");
expect(Object.isFrozen(profile)).toBe(true);
expect(Object.isFrozen(profile.sessionDirEnvKeys)).toBe(true);
expect(profile.sessionDirEnvKeys).toEqual(["PI_WEB_AGENT_SESSION_DIR"]);
expect(Reflect.set(profile, "command", "mutated-agent")).toBe(false);
expect(Reflect.set(profile.sessionDirEnvKeys, "0", "MUTATED_SESSION_DIR")).toBe(false);
});
it("copies only the secret-free descriptor fields", () => {
const input = {
...baseAgent,
token: "must-not-cross-the-protocol",
auth: { apiKey: "also-secret" },
};
const profile = createActiveAgentProfileDescriptor(input);
expect(profile).toEqual({
schemaVersion: 1,
revision: profile.revision,
command: baseAgent.command,
dir: baseAgent.dir,
sessionDirEnvKeys: baseAgent.sessionDirEnvKeys,
});
expect(profile.revision).toMatch(/^sha256:[0-9a-f]{64}$/u);
expect(JSON.stringify(profile)).not.toContain("secret");
});
});
+22
View File
@@ -0,0 +1,22 @@
import { createHash } from "node:crypto";
import type { EffectivePiWebAgentConfig } from "../config.js";
import type { ActiveAgentProfileDescriptor } from "../shared/apiTypes.js";
import { ACTIVE_AGENT_PROFILE_SCHEMA_VERSION } from "../shared/activeAgentProfile.js";
export function createActiveAgentProfileDescriptor(agent: EffectivePiWebAgentConfig): ActiveAgentProfileDescriptor {
const sessionDirEnvKeys = Object.freeze([...agent.sessionDirEnvKeys]);
const revisionInput = JSON.stringify({
schemaVersion: ACTIVE_AGENT_PROFILE_SCHEMA_VERSION,
command: agent.command,
dir: agent.dir,
sessionDirEnvKeys,
});
return Object.freeze({
schemaVersion: ACTIVE_AGENT_PROFILE_SCHEMA_VERSION,
revision: `sha256:${createHash("sha256").update(revisionInput).digest("hex")}`,
command: agent.command,
dir: agent.dir,
sessionDirEnvKeys,
});
}
+69
View File
@@ -0,0 +1,69 @@
import { describe, expect, it, vi } from "vitest";
import { SessionDaemonClient } from "./sessionDaemonClient.js";
const activeAgentProfile = {
schemaVersion: 1,
revision: `sha256:${"a".repeat(64)}`,
command: "acme-agent",
dir: "/opt/acme-agent/state",
sessionDirEnvKeys: ["PI_WEB_AGENT_SESSION_DIR"],
};
describe("SessionDaemonClient active agent profile protocol", () => {
it("returns the validated immutable profile from the daemon runtime endpoint", async () => {
const client = new SessionDaemonClient();
const request = vi.spyOn(client, "request").mockResolvedValue(runtimeResponse(activeAgentProfile));
const result = await client.getActiveAgentProfile();
expect(request).toHaveBeenCalledWith("GET", "/runtime");
expect(result).toEqual({ status: "available", profile: activeAgentProfile });
if (result.status === "available") {
expect(Object.isFrozen(result.profile)).toBe(true);
expect(Object.isFrozen(result.profile.sessionDirEnvKeys)).toBe(true);
}
});
it("distinguishes invalid protocol responses from daemon unavailability", async () => {
const invalidClient = new SessionDaemonClient();
vi.spyOn(invalidClient, "request").mockResolvedValue(runtimeResponse({
...activeAgentProfile,
token: "must-not-cross-the-protocol",
}));
const unavailableClient = new SessionDaemonClient();
vi.spyOn(unavailableClient, "request").mockRejectedValue(new Error("connect ECONNREFUSED"));
await expect(invalidClient.getActiveAgentProfile()).resolves.toEqual({
status: "invalid",
error: "session daemon runtime response was invalid",
});
await expect(unavailableClient.getActiveAgentProfile()).resolves.toEqual({
status: "unavailable",
error: "connect ECONNREFUSED",
});
});
it("treats a legacy runtime response without a profile as invalid for profile-dependent work", async () => {
const client = new SessionDaemonClient();
vi.spyOn(client, "request").mockResolvedValue(runtimeResponse(undefined));
await expect(client.getActiveAgentProfile()).resolves.toEqual({
status: "invalid",
error: "session daemon runtime response did not include an active agent profile",
});
});
});
function runtimeResponse(profile: unknown) {
return {
statusCode: 200,
headers: { "content-type": "application/json" },
body: JSON.stringify({
component: "sessiond",
label: "Session daemon",
available: true,
capabilities: [],
...(profile === undefined ? {} : { activeAgentProfile: profile }),
}),
};
}
+40
View File
@@ -1,7 +1,14 @@
import http from "node:http";
import { WebSocket } from "ws";
import type { ActiveAgentProfileDescriptor } from "../shared/apiTypes.js";
import { parsePiWebRuntimeComponent } from "../shared/piWebStatusParsing.js";
import { sessiondHttpUrl, sessiondSocketPath } from "./config.js";
export type SessionDaemonAgentProfileResult =
| { status: "available"; profile: ActiveAgentProfileDescriptor }
| { status: "unavailable"; error: string }
| { status: "invalid"; error: string };
export class SessionDaemonClient {
private readonly baseUrl = sessiondHttpUrl();
private readonly socketPath = sessiondSocketPath();
@@ -12,6 +19,35 @@ export class SessionDaemonClient {
return this.requestSocket(method, path, payload);
}
async getActiveAgentProfile(): Promise<SessionDaemonAgentProfileResult> {
let response: Awaited<ReturnType<SessionDaemonClient["request"]>>;
try {
response = await this.request("GET", "/runtime");
} catch (error) {
return { status: "unavailable", error: errorMessage(error) };
}
if (response.statusCode < 200 || response.statusCode >= 300) {
return { status: "unavailable", error: `session daemon runtime request returned HTTP ${String(response.statusCode)}` };
}
let value: unknown;
try {
value = response.body === "" ? undefined : JSON.parse(response.body);
} catch {
return { status: "invalid", error: "session daemon runtime response was not valid JSON" };
}
const runtime = parsePiWebRuntimeComponent(value);
if (runtime?.component !== "sessiond") {
return { status: "invalid", error: "session daemon runtime response was invalid" };
}
if (runtime.activeAgentProfile === undefined) {
return { status: "invalid", error: "session daemon runtime response did not include an active agent profile" };
}
return { status: "available", profile: runtime.activeAgentProfile };
}
connectWebSocket(path: string): WebSocket {
if (this.baseUrl !== undefined && this.baseUrl !== "") {
const url = new URL(path, this.baseUrl);
@@ -66,3 +102,7 @@ export class SessionDaemonClient {
});
}
}
function errorMessage(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}
+43
View File
@@ -0,0 +1,43 @@
import type { ActiveAgentProfileDescriptor } from "./apiTypes.js";
export const ACTIVE_AGENT_PROFILE_SCHEMA_VERSION = 1 as const;
const ACTIVE_AGENT_PROFILE_FIELDS = new Set([
"schemaVersion",
"revision",
"command",
"dir",
"sessionDirEnvKeys",
]);
const SHA256_REVISION_PATTERN = /^sha256:[0-9a-f]{64}$/u;
export function parseActiveAgentProfileDescriptor(value: unknown): ActiveAgentProfileDescriptor | undefined {
if (!isRecord(value) || Object.keys(value).some((key) => !ACTIVE_AGENT_PROFILE_FIELDS.has(key))) return undefined;
const schemaVersion = value["schemaVersion"];
const revision = value["revision"];
const command = value["command"];
const dir = value["dir"];
const sessionDirEnvKeys = value["sessionDirEnvKeys"];
if (schemaVersion !== ACTIVE_AGENT_PROFILE_SCHEMA_VERSION) return undefined;
if (typeof revision !== "string" || !SHA256_REVISION_PATTERN.test(revision)) return undefined;
if (typeof command !== "string" || command === "" || typeof dir !== "string" || dir === "") return undefined;
if (!isNonEmptyStringArray(sessionDirEnvKeys)) return undefined;
if (new Set(sessionDirEnvKeys).size !== sessionDirEnvKeys.length) return undefined;
return Object.freeze({
schemaVersion,
revision,
command,
dir,
sessionDirEnvKeys: Object.freeze([...sessionDirEnvKeys]),
});
}
function isNonEmptyStringArray(value: unknown): value is string[] {
return Array.isArray(value) && value.every((entry: unknown) => typeof entry === "string" && entry !== "");
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
+11
View File
@@ -593,12 +593,23 @@ export interface PiWebComponentStatus {
error?: string;
}
/** Secret-free identity of the Pi-compatible CLI/state profile fixed for one sessiond lifetime. */
export interface ActiveAgentProfileDescriptor {
readonly schemaVersion: 1;
readonly revision: string;
readonly command: string;
readonly dir: string;
readonly sessionDirEnvKeys: readonly string[];
}
export interface PiWebRuntimeComponent {
component: PiWebServiceComponent;
label: string;
runtimeVersion?: string;
available: boolean;
capabilities: PiWebCapability[];
/** Present only for a session daemon that supports active-profile reporting. */
activeAgentProfile?: ActiveAgentProfileDescriptor;
error?: string;
}
+50
View File
@@ -33,6 +33,56 @@ describe("PI WEB status parsing", () => {
})).toBeUndefined();
});
it("parses and freezes a session daemon active agent profile", () => {
const parsed = parsePiWebRuntimeResponse({
packageName: "@jmfederico/pi-web",
generatedAt: "now",
components: {
web: { component: "web", label: "Web/UI", available: true, capabilities: [] },
sessiond: {
component: "sessiond",
label: "Session daemon",
available: true,
capabilities: [],
activeAgentProfile: {
schemaVersion: 1,
revision: `sha256:${"a".repeat(64)}`,
command: "acme-agent",
dir: "/opt/acme-agent/state",
sessionDirEnvKeys: ["PI_WEB_AGENT_SESSION_DIR"],
},
},
},
capabilities: [],
});
expect(parsed?.components.sessiond.activeAgentProfile).toMatchObject({ command: "acme-agent", dir: "/opt/acme-agent/state" });
expect(Object.isFrozen(parsed?.components.sessiond.activeAgentProfile)).toBe(true);
expect(Object.isFrozen(parsed?.components.sessiond.activeAgentProfile?.sessionDirEnvKeys)).toBe(true);
});
it("rejects malformed, secret-bearing, or web-owned active profile descriptors", () => {
const profile = {
schemaVersion: 1,
revision: `sha256:${"a".repeat(64)}`,
command: "acme-agent",
dir: "/opt/acme-agent/state",
sessionDirEnvKeys: ["PI_WEB_AGENT_SESSION_DIR"],
};
const responseFor = (webProfile: unknown, sessiondProfile: unknown) => ({
packageName: "@jmfederico/pi-web",
generatedAt: "now",
components: {
web: { component: "web", label: "Web/UI", available: true, capabilities: [], ...(webProfile === undefined ? {} : { activeAgentProfile: webProfile }) },
sessiond: { component: "sessiond", label: "Session daemon", available: true, capabilities: [], ...(sessiondProfile === undefined ? {} : { activeAgentProfile: sessiondProfile }) },
},
capabilities: [],
});
expect(parsePiWebRuntimeResponse(responseFor(undefined, { ...profile, token: "secret" }))).toBeUndefined();
expect(parsePiWebRuntimeResponse(responseFor(profile, undefined))).toBeUndefined();
});
it("parses Docker installation metadata", () => {
expect(parsePiWebInstallationInfo({ kind: "docker", path: "/srv/pi-web-docker", dockerMode: "runtime" })).toEqual({
kind: "docker",
+5
View File
@@ -1,4 +1,5 @@
import type { PiWebComponentStatus, PiWebInstallationInfo, PiWebRuntimeComponent, PiWebRuntimeResponse, PiWebVersionResponse } from "./apiTypes.js";
import { parseActiveAgentProfileDescriptor } from "./activeAgentProfile.js";
import { parseKnownPiWebCapabilities } from "./capabilities.js";
export function parsePiWebVersionResponse(value: unknown): PiWebVersionResponse | undefined {
@@ -33,15 +34,19 @@ export function parsePiWebRuntimeComponent(value: unknown): PiWebRuntimeComponen
const runtimeVersion = value["runtimeVersion"];
const available = value["available"];
const capabilities = parseKnownPiWebCapabilities(value["capabilities"]);
const activeAgentProfileValue = value["activeAgentProfile"];
const activeAgentProfile = activeAgentProfileValue === undefined ? undefined : parseActiveAgentProfileDescriptor(activeAgentProfileValue);
const error = value["error"];
if (component !== "web" && component !== "sessiond") return undefined;
if (typeof label !== "string" || label === "" || typeof available !== "boolean" || capabilities === undefined) return undefined;
if (activeAgentProfileValue !== undefined && (component !== "sessiond" || activeAgentProfile === undefined)) return undefined;
return {
component,
label,
...(typeof runtimeVersion === "string" ? { runtimeVersion } : {}),
available,
capabilities,
...(activeAgentProfile === undefined ? {} : { activeAgentProfile }),
...(typeof error === "string" ? { error } : {}),
};
}