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
+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);
}