pi-web-ui 0.6.0 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -11,7 +11,7 @@
11
11
  */
12
12
  import { spawn } from "node:child_process";
13
13
  import { existsSync, readFileSync, statSync, writeFileSync, mkdirSync, } from "node:fs";
14
- import { dirname, join, resolve } from "node:path";
14
+ import { dirname, join, relative, resolve, sep } from "node:path";
15
15
  import { fileURLToPath } from "node:url";
16
16
  import { createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, getAgentDir, SessionManager, } from "@earendil-works/pi-coding-agent";
17
17
  import { serializeMessage, serializeStreamingMessage, } from "./serialize.js";
@@ -429,6 +429,18 @@ function extractPartialText(partial) {
429
429
  }
430
430
  return null;
431
431
  }
432
+ /**
433
+ * Resolve a workspace-relative path against a root, refusing traversal
434
+ * (".." escapes). Returns { abs, rel } — rel is normalized and slash-
435
+ * separated — or null when the path leaves the workspace.
436
+ */
437
+ export function workspacePath(root, raw) {
438
+ const abs = resolve(root, raw);
439
+ const rel = relative(root, abs);
440
+ if (rel.startsWith("..") || rel.includes(`${sep}..`))
441
+ return null;
442
+ return { abs, rel };
443
+ }
432
444
  /**
433
445
  * Persists which workspace each browser client last used + which workspaces it
434
446
  * has opened, so a server restart / page reload restores the same project and
@@ -1810,11 +1822,9 @@ export class ClientSession {
1810
1822
  async readFile(relPath) {
1811
1823
  try {
1812
1824
  const fs = await import("node:fs/promises");
1813
- const { resolve, sep, relative } = await import("node:path");
1814
1825
  const root = resolve(this.cwd);
1815
- const abs = resolve(root, relPath);
1816
- const rel = relative(root, abs);
1817
- if (rel.startsWith("..") || rel.includes(`${sep}..`)) {
1826
+ const wp = workspacePath(root, relPath);
1827
+ if (!wp) {
1818
1828
  this.emit({
1819
1829
  type: "notice",
1820
1830
  level: "warning",
@@ -1822,6 +1832,7 @@ export class ClientSession {
1822
1832
  });
1823
1833
  return;
1824
1834
  }
1835
+ const { abs, rel } = wp;
1825
1836
  const stat = await fs.stat(abs);
1826
1837
  if (!stat.isFile()) {
1827
1838
  this.emit({
@@ -15,13 +15,13 @@
15
15
  import { existsSync } from "node:fs";
16
16
  import { stat } from "node:fs/promises";
17
17
  import { createServer } from "node:http";
18
- import { basename, dirname, join, relative, resolve, sep } from "node:path";
18
+ import { basename, dirname, join, resolve } from "node:path";
19
19
  import { fileURLToPath } from "node:url";
20
20
  import { randomUUID } from "node:crypto";
21
21
  import express from "express";
22
22
  import { WebSocket, WebSocketServer } from "ws";
23
23
  import { VERSION } from "@earendil-works/pi-coding-agent";
24
- import { AgentService, previewKind } from "./agent-service.js";
24
+ import { AgentService, previewKind, workspacePath } from "./agent-service.js";
25
25
  const PORT = Number(process.env.PORT ?? 8787);
26
26
  const CWD = resolve(process.env.PI_WEB_CWD ?? process.cwd());
27
27
  const DATA_DIR = resolve(process.env.PI_WEB_DATA_DIR ?? join(CWD, ".pi-web"));
@@ -40,12 +40,18 @@ app.get("/api/health", (_req, res) => {
40
40
  app.get("/api/file", async (req, res) => {
41
41
  try {
42
42
  const raw = typeof req.query.path === "string" ? req.query.path : "";
43
- const abs = resolve(CWD, raw);
44
- const rel = relative(CWD, abs);
45
- if (rel.startsWith("..") || rel.includes(`${sep}..`)) {
43
+ // Resolve against the requesting client's workspace (the opened
44
+ // project), not the server's startup cwd — they can differ when the
45
+ // client switched projects or restored a previous workspace. Fall
46
+ // back to the server cwd for requests without a known client.
47
+ const cid = typeof req.query.clientId === "string" ? req.query.clientId : "";
48
+ const cs = cid ? service.get(cid) : undefined;
49
+ const wp = workspacePath(cs?.cwd ?? CWD, raw);
50
+ if (!wp) {
46
51
  res.status(400).end("path outside workspace");
47
52
  return;
48
53
  }
54
+ const abs = wp.abs;
49
55
  const name = basename(abs);
50
56
  const kind = previewKind(name);
51
57
  if (kind !== "image" && kind !== "video") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-web-ui",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "description": "Web chat interface for the pi coding agent, powered by the pi SDK (@earendil-works/pi-coding-agent) — one-command run, Docker/systemd/launchd deployable",
5
5
  "license": "MIT",
6
6
  "type": "module",