feat: add OMP runtime support

This commit is contained in:
Jeff Scott Ward
2026-06-26 12:49:58 -04:00
parent 4605a4f1d8
commit 84a485d62e
26 changed files with 639 additions and 83 deletions
+37 -3
View File
@@ -1,5 +1,5 @@
import type { FastifyInstance } from "fastify";
import { effectivePiWebConfig, loadPiWebConfig, parseUploadsConfig, savePiWebConfig, type LoadOptions, type PiWebConfig } from "../config.js";
import { effectiveAgentConfig, effectivePiWebConfig, hasAgentDirEnvOverride, hasAgentSessionDirEnvOverride, loadPiWebConfig, parseUploadsConfig, savePiWebConfig, type LoadOptions, type PiWebConfig } from "../config.js";
import type { PiWebConfigEnvOverrides, PiWebConfigResponse, PiWebConfigValues } from "../shared/apiTypes.js";
import { isPiWebPluginId } from "../shared/pluginIds.js";
@@ -27,7 +27,7 @@ export function currentPiWebConfigResponse(options: LoadOptions = {}): PiWebConf
exists: loaded.exists,
config: loaded.config,
effectiveConfig: effective.config,
envOverrides: piWebConfigEnvOverrides(env),
envOverrides: piWebConfigEnvOverrides(env, loaded.config),
};
}
@@ -63,6 +63,7 @@ function parseConfigRequest(value: unknown): PiWebConfig {
const maxUploadBytes = value["maxUploadBytes"];
const spawnSessions = value["spawnSessions"];
const subsessions = value["subsessions"];
const agent = value["agent"];
if (host !== undefined) {
if (typeof host !== "string") throw new Error("PI WEB config host must be a string");
config.host = host;
@@ -85,6 +86,7 @@ function parseConfigRequest(value: unknown): PiWebConfig {
if (typeof subsessions !== "boolean") throw new Error("PI WEB config subsessions must be a boolean");
config.subsessions = subsessions;
}
if (agent !== undefined) config.agent = parseAgentRequest(agent);
return config;
}
@@ -128,6 +130,30 @@ function parseMaxUploadBytesRequest(value: unknown): number {
return value;
}
function parseAgentRequest(value: unknown): NonNullable<PiWebConfig["agent"]> {
if (!isRecord(value)) throw new Error("PI WEB config agent must be an object");
const command = value["command"];
const dir = value["dir"];
return {
...(command === undefined ? {} : { command: parseAgentCommandRequest(command) }),
...(dir === undefined ? {} : { dir: parseAgentDirRequest(dir) }),
};
}
function parseAgentCommandRequest(value: unknown): string {
if (typeof value !== "string" || value.trim() === "") throw new Error("PI WEB config agent.command must be a non-empty string");
const command = value.trim();
if (/[\s;&|`$<>]/u.test(command)) throw new Error("PI WEB config agent.command must be a single command name or path without shell metacharacters");
return command;
}
function parseAgentDirRequest(value: unknown): string {
if (typeof value !== "string" || value.trim() === "") throw new Error("PI WEB config agent.dir must be a non-empty string");
const dir = value.trim();
if (!isAbsoluteOrHomePath(dir)) throw new Error("PI WEB config agent.dir must be an absolute path or start with ~");
return dir;
}
function parsePluginsRequest(value: unknown): NonNullable<PiWebConfig["plugins"]> {
if (!isRecord(value) || Array.isArray(value)) throw new Error("PI WEB config plugins must be an object");
return Object.fromEntries(Object.entries(value).map(([pluginId, config]) => {
@@ -141,13 +167,17 @@ function parsePluginsRequest(value: unknown): NonNullable<PiWebConfig["plugins"]
}));
}
function piWebConfigEnvOverrides(env: NodeJS.ProcessEnv): PiWebConfigEnvOverrides {
function piWebConfigEnvOverrides(env: NodeJS.ProcessEnv, config: PiWebConfig = {}): PiWebConfigEnvOverrides {
const agent = effectiveAgentConfig(env, config);
return {
host: isEnvSet(env["PI_WEB_HOST"]),
port: isEnvSet(env["PI_WEB_PORT"]) || isEnvSet(env["PORT"]),
allowedHosts: isEnvSet(env["PI_WEB_ALLOWED_HOSTS"]),
spawnSessions: isEnvSet(env["PI_WEB_SPAWN_SESSIONS"]),
subsessions: isEnvSet(env["PI_WEB_SUBSESSIONS"]),
agentCommand: isEnvSet(env["PI_WEB_AGENT_COMMAND"]),
agentDir: hasAgentDirEnvOverride(env, agent.command),
agentSessionDir: hasAgentSessionDirEnvOverride(env, agent.command),
};
}
@@ -163,6 +193,10 @@ function errorMessage(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}
function isAbsoluteOrHomePath(value: string): boolean {
return value === "~" || value.startsWith("~/") || value.startsWith("~\\") || value.startsWith("/") || value.startsWith("\\") || /^[A-Za-z]:[\\/]/u.test(value);
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}