diff --git a/.changeset/generic-agent-runtime-config.md b/.changeset/generic-agent-runtime-config.md new file mode 100644 index 0000000..1156ff9 --- /dev/null +++ b/.changeset/generic-agent-runtime-config.md @@ -0,0 +1,5 @@ +--- +"@jmfederico/pi-web": patch +--- + +Add selectable Pi-compatible agent profiles and companion CLIs for isolated auth, models, settings, sessions, Pi packages, plugins, diagnostics, and safe update commands. Settings shows when a session-daemon restart is required, and mixed-version remote saves fail instead of reporting false success. The embedded runtime remains the bundled Pi SDK. diff --git a/docs/config.html b/docs/config.html index 7bbc07d..e1c97d4 100644 --- a/docs/config.html +++ b/docs/config.html @@ -81,7 +81,7 @@
PI WEB configuration covers the machine-local and project-local settings you usually need: bind address, trusted development-host settings, UI preferences, PI WEB plugin enablement, file-explorer path access, - manual upload defaults, upload limits, and session-daemon tools. + manual upload defaults, upload limits, Pi-compatible agent profiles and companion CLIs, and session-daemon tools.
@@ -98,6 +98,7 @@ Config matrix External path access Manual uploads + Agent profile and companion CLI Session tools Completion tools @@ -112,8 +113,8 @@Each PI WEB machine has its own config. When using Fleet/machine federation, Settings uses the selected - machine for config that affects work running there: session daemon tools, PI WEB plugin enablement, - external path access, and upload defaults. Gateway/browser-only settings stay local to the gateway: + machine for config that affects work running there: the Pi-compatible agent profile and companion CLI, + session daemon tools, PI WEB plugin enablement, external path access, and upload defaults. Gateway/browser-only settings stay local to the gateway: keyboard shortcuts, remote machine registry/tokens, and gateway host/port/allowed-hosts. Remote servers that do not advertise selected-machine settings support report those settings as unavailable instead of silently falling back to the gateway. @@ -162,13 +163,15 @@
Environment overrides include PI_WEB_HOST, PI_WEB_PORT / PORT,
- PI_WEB_ALLOWED_HOSTS, PI_WEB_MAX_UPLOAD_BYTES, PI_WEB_SPAWN_SESSIONS,
- and PI_WEB_SUBSESSIONS.
+ PI_WEB_ALLOWED_HOSTS, PI_WEB_MAX_UPLOAD_BYTES, PI_WEB_AGENT_COMMAND,
+ PI_WEB_AGENT_DIR, PI_WEB_AGENT_SESSION_DIR, PI_CODING_AGENT_DIR /
+ PI_CODING_AGENT_SESSION_DIR for Pi compatibility, PI_WEB_SPAWN_SESSIONS, and
+ PI_WEB_SUBSESSIONS.
host / port: restart the gateway web/API service or process.maxUploadBytes: restart both the web/API process and the session daemon on that machine.spawnSessions / subsessions: restart the session daemon on that machine.agent.command / agent.dir / spawnSessions / subsessions: restart the session daemon on that machine.pathAccess: applies on the next request; existing file views may need a browser refresh.uploads.defaultFolder: applies to newly opened Files upload dialogs and new direct drag/drop batches after config/workspace refresh.plugins: reload the browser tab after changing PI WEB plugin enablement.— are runtime-only environment variables, not config-file keys. Global means
machine-global. In Settings, selected-machine-safe global keys (pathAccess, uploads,
- maxUploadBytes, spawnSessions, subsessions, and plugins)
+ maxUploadBytes, agent, spawnSessions, subsessions, and plugins)
are edited for the selected machine; gateway host/port/allowed-hosts, keyboard shortcuts, and machine
registry/tokens stay local.
@@ -326,6 +333,22 @@
agent.commandPI_WEB_AGENT_COMMANDagent.dirPI_WEB_AGENT_DIR (PI_CODING_AGENT_DIR for Pi compatibility)spawnSessionsPI_CODING_AGENT_SESSION_DIRPI_WEB_AGENT_SESSION_DIR (PI_CODING_AGENT_SESSION_DIR for Pi compatibility)PI_CODING_AGENT_DIRPI_WEB_AGENT_DIR (PI_CODING_AGENT_DIR for Pi compatibility)
+ agent.command selects the Pi-compatible companion CLI used by pi-web doctor and,
+ when it can be generated safely, package-managed update commands. It defaults to pi. This
+ setting does not replace the embedded runtime: every session continues to use PI WEB's
+ bundled Pi SDK.
+
+ agent.dir selects the Pi-compatible state profile used for auth providers, models, settings,
+ sessions, Pi packages, and Pi-package-backed PI WEB plugin discovery. It defaults to
+ ~/.pi/agent only for a canonical Pi companion command. The directory must use the data layout
+ supported by the bundled Pi SDK; PI WEB does not load or convert incompatible fork formats, migrate
+ profile data, or repartition PI WEB-managed archives when the profile changes.
+
{
+ "agent": {
+ "command": "pi-lab",
+ "dir": "/opt/pi-profiles/lab"
+ }
+}
+
+ An alternate command always requires an explicit state directory. The command must be a safe bare
+ executable name such as pi-lab or a host-absolute executable path such as
+ /opt/pi/bin/pi; relative paths, shell expressions, and launcher strings are rejected. The
+ state directory must be host-absolute or start with ~. In a federated save, the gateway
+ transports Unix and Windows absolute paths without reinterpreting them, and the target machine validates
+ and returns the persisted profile.
+
+ Environment variables take precedence over the config file. PI_WEB_AGENT_COMMAND selects the
+ companion CLI, PI_WEB_AGENT_DIR sets the profile state directory, and
+ PI_WEB_AGENT_SESSION_DIR overrides session storage separately from agent.dir. The
+ legacy PI_CODING_AGENT_DIR and PI_CODING_AGENT_SESSION_DIR names apply only to a
+ canonical Pi companion command; PI WEB never derives ambient environment-variable names from an arbitrary
+ command. Use the explicit PI_WEB_AGENT_* names for alternate commands.
+ PI_WEB_AGENT_DIR is an unconditional override, while a legacy
+ PI_CODING_AGENT_DIR override stops applying when Settings selects an alternate command so the
+ command and directory can transition together.
+
+ The session daemon resolves the persisted desired values plus its environment once at startup. That + secret-free active profile stays fixed for the daemon lifetime. Settings → Session daemon + saves command and directory together as desired configuration and shows whether the profile is active, + needs a restart, or cannot be compared. Until the daemon restarts, sessions, Pi package operations, + package-backed plugin discovery, status/install detection, and update planning continue to use the + daemon-owned active profile; a web/API restart recovers that same active profile instead of applying the + newly saved values. +
+spawnSessionsEnter the API key for ${state.provider.name}. It will be stored by pi in auth.json.
Enter the API key for ${state.provider.name}. It will be stored in the active Pi-compatible profile's auth.json.
pi-web restart on that machine",
+ "Pi-compatible agent profile restart required on Lab Mac (remote machine)",
+ "Run pi-web restart on that machine",
"Config file",
+ "Companion CLI command",
+ "agent-lab",
+ "Profile state directory",
+ "/srv/agent-lab",
"Allow agents to start sessions",
]);
});
it("orders save/load notices before the restart notice and settings content", () => {
const panel = new SettingsSessiondPanel();
- panel.configResponse = configResponse({ spawnSessions: false });
+ setPanelConfig(panel, configResponse({ agent: { command: "agent-lab", dir: "/srv/agent-lab" }, spawnSessions: false }));
+ panel.activeAgentProfile = activeProfile("pi", "/srv/pi");
panel.error = "Failed to save session-daemon config.";
panel.savedMessage = "Session daemon settings saved.";
@@ -34,11 +44,55 @@ describe("settings-sessiond-panel layout", () => {
expectTextOrder(rendered, [
"Failed to save session-daemon config.",
"Session daemon settings saved.",
- "Restart required on local (local gateway)",
+ "Pi-compatible agent profile restart required on local (local gateway)",
"Config file",
]);
});
+ it("shows the profile as active without restart guidance when desired and active match", () => {
+ const panel = new SettingsSessiondPanel();
+ setPanelConfig(panel, configResponse({ agent: { command: "agent-lab", dir: "/srv/agent-lab" } }));
+ panel.activeAgentProfile = activeProfile("agent-lab", "/srv/agent-lab");
+
+ const rendered = flattenTemplateContent(panel.render());
+
+ expect(rendered).toContain("Profile status");
+ expect(rendered).toContain("Active");
+ expect(rendered).not.toContain("restart required on");
+ });
+
+ it("submits command and directory together as one profile save", async () => {
+ const panel = new SettingsSessiondPanel();
+ const onSave = vi.fn();
+ setPanelConfig(panel, configResponse({ agent: { command: "pi", dir: "/srv/pi" } }));
+ setPanelProperty(panel, "agentDraft", { command: " alternate-agent ", dir: " /srv/alternate " });
+ panel.onSave = onSave;
+ const event = new Event("submit", { cancelable: true });
+
+ await callPanelPromise(panel, "saveAgentProfile", event);
+
+ expect(event.defaultPrevented).toBe(true);
+ expect(onSave.mock.calls).toEqual([[{ agent: { command: "alternate-agent", dir: "/srv/alternate" } }]]);
+ });
+
+ it("preserves a dirty profile draft when an unrelated daemon setting is saved", () => {
+ const panel = new SettingsSessiondPanel();
+ const initial = configResponse({ agent: { command: "pi", dir: "/srv/pi" }, spawnSessions: false });
+ setPanelConfig(panel, initial);
+ callPanelMethod(panel, "updateAgentDraft", { command: "alternate-agent", dir: "/srv/alternate" });
+
+ const toggled = configResponse({ agent: { command: "pi", dir: "/srv/pi" }, spawnSessions: true });
+ panel.configResponse = toggled;
+ callPanelMethod(panel, "willUpdate", new Map([["configResponse", initial]]));
+
+ expect(Reflect.get(panel, "agentDraft")).toEqual({ command: "alternate-agent", dir: "/srv/alternate" });
+
+ const saved = configResponse({ agent: { command: "alternate-agent", dir: "/srv/alternate" }, spawnSessions: true });
+ panel.configResponse = saved;
+ callPanelMethod(panel, "willUpdate", new Map([["configResponse", toggled]]));
+ expect(Reflect.get(panel, "agentDraftDirty")).toBe(false);
+ });
+
it("shows one blocked content state without restart guidance or toggles when config is unavailable", () => {
const panel = new SettingsSessiondPanel();
panel.targetLabel = "Lab Mac (remote machine)";
@@ -57,6 +111,37 @@ describe("settings-sessiond-panel layout", () => {
});
});
+function activeProfile(command: string, dir: string): ActiveAgentProfileDescriptor {
+ return {
+ schemaVersion: 1,
+ revision: `sha256:${"a".repeat(64)}`,
+ command,
+ dir,
+ sessionDirEnvKeys: ["PI_WEB_AGENT_SESSION_DIR"],
+ };
+}
+
+function setPanelConfig(panel: SettingsSessiondPanel, config: PiWebConfigResponse): void {
+ panel.configResponse = config;
+ callPanelMethod(panel, "willUpdate", new Map([["configResponse", undefined]]));
+}
+
+function setPanelProperty(panel: SettingsSessiondPanel, property: string, value: unknown): void {
+ if (!Reflect.set(panel, property, value)) throw new Error(`Failed to set SettingsSessiondPanel property ${property}`);
+}
+
+async function callPanelPromise(panel: SettingsSessiondPanel, methodName: string, ...args: readonly unknown[]): Promise${config.path}
+
spawn_subsession, list_subsessions, check_subsession, read_subsession) and are notified when a child finishes. Requires "Allow agents to start sessions". Off by default.
pi-web restart on that machine (or restart its session daemon service) after changing these settings.`,
+ title: `Pi-compatible agent profile restart required on ${this.targetLabel}`,
+ content: html`The desired profile differs from the active session-daemon profile. Run pi-web restart on that machine (or restart its session daemon service) to apply the command and state directory together.`,
+ });
+ } else if (config !== undefined && activation === "unavailable" && this.agentProfileSupport.state === "supported") {
+ notices.push({
+ type: "info",
+ title: `Active Pi-compatible agent profile unavailable on ${this.targetLabel}`,
+ content: "PI WEB cannot compare the desired profile with the running session daemon. Reload after the daemon is available.",
});
}
return notices;
@@ -102,6 +184,22 @@ export class SettingsSessiondPanel extends LitElement {
return html` {
+async function getSessiondComponentStatus(daemon: PiWebStatusDaemon, options: PiWebStatusOptions = {}): Promise