Archived
feat: expose daemon-owned active agent profile
This commit is contained in:
+13
-2
@@ -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"];
|
||||
|
||||
@@ -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";
|
||||
|
||||
+26
-17
@@ -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 {
|
||||
app.get("/health", () => ({
|
||||
ok: true,
|
||||
activeSessions: sessions.activeCount(),
|
||||
checkedAt: new Date().toISOString(),
|
||||
version: {
|
||||
component: runtime.component,
|
||||
label: runtime.label,
|
||||
...(runtime.runtimeVersion === undefined ? {} : { runtimeVersion: runtime.runtimeVersion }),
|
||||
component: runtimeComponent.component,
|
||||
label: runtimeComponent.label,
|
||||
...(runtimeComponent.runtimeVersion === undefined ? {} : { runtimeVersion: runtimeComponent.runtimeVersion }),
|
||||
stale: false,
|
||||
available: runtime.available,
|
||||
available: runtimeComponent.available,
|
||||
},
|
||||
};
|
||||
});
|
||||
}));
|
||||
|
||||
app.get("/runtime", () => getPiWebRuntimeComponent("sessiond", SESSIOND_RUNTIME_CAPABILITIES));
|
||||
app.get("/runtime", () => runtimeComponent);
|
||||
},
|
||||
async listen({ auth, sessions, terminals }) {
|
||||
let shuttingDown = false;
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -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 }),
|
||||
}),
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user