Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .vscodeignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# ── Build / test output (only dist/ ships) ──
# Ship only the team-shared VS Code workspace files; everything else in .vscode stays out.
.vscode/**
!.vscode/settings.json
!.vscode/launch.json
!.vscode/extensions.json
.vscode-test/**
out/**
dist-tsc/**
Expand Down Expand Up @@ -91,8 +95,18 @@ media/screenshot.png
*.vsix
*.log
.DS_Store
# Environment/secret files at ANY depth (root .env entries above don't cover
# nested ones like .claude/.env or media/.env.local).
.env
.env.*
**/.env
**/.env.*
*.pem
*.key
**/*.pem
**/*.key
credentials*.json
**/credentials*.json
**/*.ts.map
**/tsconfig.json
**/tslint.json
32 changes: 32 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,38 @@

All notable changes to the PiLot Studio for VS Code extension will be documented in this file.

## [Unreleased]

### Security

- Replaced shell-string `execSync` interpolation with argv-array `execFileSync` / `node:fs` in `postinstall-patch.mjs`, `native-addons.ts`, and the VS Code download/clean-up scripts (command/shell injection).
- Escaped quotes in `MessageBubble`'s `escapeHtml`, stopped emitting inline `onclick` attributes, and tightened the webview CSP (no `unsafe-inline`, explicit `connect-src`) — closes the attribute-injection XSS vector.
- Removed the remote Google Fonts `@import` from the webview styles (blocked by CSP anyway; leaked a request per load).
- Constrained `@mention` file resolution in `session-resources.ts` to the workspace root (path traversal like `@../../etc/passwd`).
- Constrained `MOCHA_TEST_FILE` resolution in the test scaffold and sub-path imports in `loader.cjs` against escaping their intended roots.
- All webview `message` handlers (`ContextIndicator`, `PiPackagesPanel`, `SessionTree`, `VoiceCapture`) now validate `event.data` shape via a shared guard before trusting payloads.
- `message-serializer.ts` no longer trusts untyped upstream payload shapes (defensive parsing before serialization).

### Fixed

- `pi-binary.ts`: bare-name `pi` lookup now resolves again (spawned a shell builtin with `shell: false`); arg-env quoting hardened.
- `loader.cjs`: fixed the `@earendel-works` package-name typo, extracted doc URLs to constants, added timeouts to `which`/`where` probes, hooked `Module._resolveFilename` without leaking a process-wide duplicate hook, and resolved bare imports via the package `exports` map before guessing `dist/index.js`.
- `shell.ts`: Windows callers can no longer hit cmd.exe `shell: true` interpolation (quoting seam enforced).
- `verify.mjs` / `fallow-audit.mjs`: signal-terminated and spawn-failed runs are reported distinctly instead of collapsing into a generic exit code; `result.error` is no longer ignored (fail-open CI).
- `run-node-tests.mjs`, `runTest.ts`, test scaffold: watchdog/no-result paths now exit non-zero — no more green CI for failed or misconfigured runs; each test file gets a pid-suffixed tmpdir (parallel-safe) and VS Code discovery is cross-platform instead of hardcoded.
- `dl-tmp.mjs` / `dl-vscode.mjs`: top-level awaits are handled; VS Code version comes from env/config instead of a hardcoded 1.85.0.
- `run-clean.mjs`: removed the machine-specific `/home/lenovo/...` path; VS Code discovery matches `runTest.ts`.
- `session-manager.ts`: multi-session delete now settles per item, reporting which sessions failed and why, instead of failing the whole batch.
- `voice-manager.ts`: temp recording files are cleaned up and stdio listeners detached on stop/dispose.
- `MermaidDiagram`: render cache is capped with eviction; stale async renders can no longer overwrite a newer diagram.
- `MessageBubble`: markdown re-render effect no longer loops unboundedly; tool-call state is cleared on stream errors and new sessions in `App.svelte`.
- `PiPackagesPanel`: registry lookups are batched (no N+1 fan-out), stale responses are discarded, install log is capped, and the installing overlay can no longer deadlock.
- `Toast` timers are cleared on unmount and the stack is capped; `ContextIndicator` clamps percent to 0–100; `OnboardingTour` guards step bounds.
- Accessibility: tooltips wired via `aria-describedby`, `role="button"` handles Space, dialogs focusable; `prefers-reduced-motion` respected by `ActivityBar` and `SkeletonLoader` animations.
- Test mocks: `pi-sdk-mocks` Proxy is overrideable and `disposeCalls` tracks; `session-mock` no longer double-registers handlers or force-casts; `vscode-facade` fires listeners exactly once.
- Deduplicated `PiAgentConfig`/`ThinkingLevel`/`SessionNode` definitions and the cross-component `sendMessage` helper (shared in `webview/messages.ts`); native-addon ABI scan logic consolidated.
- `pnpm-workspace.yaml`: removed invalid `allowBuilds`/`minimumReleaseAgeExclude` keys; `.vscodeignore` no longer ships nested `.env`/secret files and keeps shared `.vscode` config; removed redundant tsconfig globs/excludes; removed dead `wrapperEl` in `HelpTooltip`.

## [2.6.0] - 2026-09-18

### Added
Expand Down
10 changes: 8 additions & 2 deletions dl-tmp.mjs
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
import { downloadAndUnzipVSCode } from "@vscode/test-electron";
const p = await downloadAndUnzipVSCode();
console.log("VSCODE_PATH=" + p);

try {
const p = await downloadAndUnzipVSCode();
console.log("VSCODE_PATH=" + p);
} catch (err) {
console.error("dl-tmp: download failed:", err instanceof Error ? err.message : err);
process.exit(1);
}
38 changes: 10 additions & 28 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,38 +5,20 @@ overrides:
'fast-uri@<3.1.6': '3.1.6'
# Security: qs DoS advisories (patched in 6.16.0, moderate)
'qs@<6.16.0': '6.16.0'

# Dependencies allowed to run postinstall build scripts (pnpm v10+).
onlyBuiltDependencies:
- '@google/genai'
- '@vscode/vsce-sign'
- esbuild
- keytar
- koffi
- protobufjs

allowBuilds:
'@google/genai': true
'@vscode/vsce-sign': true
esbuild: true
keytar: true
koffi: true
protobufjs: true
minimumReleaseAgeExclude:
- '@earendil-works/pi-agent-core@0.77.0'
- '@earendil-works/pi-ai@0.77.0'
- '@earendil-works/pi-coding-agent@0.77.0'
- '@earendil-works/pi-tui@0.77.0'
- '@mariozechner/clipboard-darwin-arm64@0.3.9'
- '@mariozechner/clipboard-darwin-universal@0.3.9'
- '@mariozechner/clipboard-darwin-x64@0.3.9'
- '@mariozechner/clipboard-linux-arm64-gnu@0.3.9'
- '@mariozechner/clipboard-linux-arm64-musl@0.3.9'
- '@mariozechner/clipboard-linux-riscv64-gnu@0.3.9'
- '@mariozechner/clipboard-linux-x64-gnu@0.3.9'
- '@mariozechner/clipboard-linux-x64-musl@0.3.9'
- '@mariozechner/clipboard-win32-arm64-msvc@0.3.9'
- '@mariozechner/clipboard-win32-x64-msvc@0.3.9'
- '@mariozechner/clipboard@0.3.9'
- '@typescript-eslint/eslint-plugin@8.60.1'
- '@typescript-eslint/parser@8.60.1'
- '@typescript-eslint/project-service@8.60.1'
- '@typescript-eslint/scope-manager@8.60.1'
- '@typescript-eslint/tsconfig-utils@8.60.1'
- '@typescript-eslint/type-utils@8.60.1'
- '@typescript-eslint/types@8.60.1'
- '@typescript-eslint/typescript-estree@8.60.1'
- '@typescript-eslint/utils@8.60.1'
- '@typescript-eslint/visitor-keys@8.60.1'
- svelte-check@4.5.0
- typescript-eslint@8.60.1
87 changes: 84 additions & 3 deletions run-clean.mjs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { accessSync, readdirSync } from "node:fs";
import { join } from "node:path";
import { spawn } from "node:child_process";

const blockPrefixes = [
Expand All @@ -16,12 +18,91 @@ for (const k of Object.keys(process.env)) {
delete process.env[k];
}
}
process.env.VSCODE_PATH =
"/home/lenovo/Applications/PiLot/.vscode-test/vscode-linux-x64-1.134.0/bin/code";
process.env.DISPLAY = process.env.DISPLAY || ":0";

// VSCODE_PATH must come from the environment (CI) or be discovered the same way
// runTest.ts does it. The previous hardcoded /home/lenovo/... path only worked
// on one machine and silently pointed CI at a missing binary.
/** Per-platform install locations of the desktop VS Code Electron binary. */
const PLATFORM_VSCODE_PATHS = {
darwin: ["/Applications/Visual Studio Code.app/Contents/MacOS/Electron"],
win32: [
"Programs/Microsoft VS Code/Code.exe", // under LOCALAPPDATA
"Microsoft VS Code/Code.exe", // under ProgramFiles
],
linux: ["/usr/share/code/code", "/usr/bin/code", "/snap/bin/code"],
};

function platformVscodeCandidates() {
if (process.platform === "win32") {
const roots = [process.env.LOCALAPPDATA, process.env.ProgramFiles].filter(Boolean);
return PLATFORM_VSCODE_PATHS.win32.flatMap((rel) => roots.map((root) => join(root, rel)));
}
return PLATFORM_VSCODE_PATHS[process.platform] ?? [];
}

/** First installed candidate, or undefined. */
function findInstalledVscode() {
for (const candidate of platformVscodeCandidates()) {
if (!candidate) continue;
try {
accessSync(candidate);
return candidate;
} catch {
/* not present */
}
}
return undefined;
}

/** Newest @vscode/test-electron download-cache install, or undefined. */
function findCachedVscode() {
const home = process.env.HOME || process.env.USERPROFILE;
if (!home) return undefined;
try {
const cacheRoot = join(home, ".vscode-test");
const dirPrefix = `vscode-${process.platform}-${process.arch}`;
return newestCacheEntry(cacheRoot, dirPrefix);
} catch {
return undefined; // no cache
}
}

/** `bin/code` inside the newest cache dir matching prefix, or undefined. */
function newestCacheEntry(cacheRoot, dirPrefix) {
const matches = readdirSync(cacheRoot)
.filter((e) => e.startsWith(dirPrefix))
.sort();
if (matches.length === 0) return undefined;
return join(cacheRoot, matches[matches.length - 1], "bin", "code");
}

function discoverVscodePath() {
return process.env.VSCODE_PATH || findInstalledVscode() || findCachedVscode();
}

const vscodePath = discoverVscodePath();
if (!vscodePath) {
console.error(
"run-clean: no VS Code executable found. Set VSCODE_PATH (e.g. output of `node scripts/dl-vscode.mjs`) or install VS Code.",
);
process.exit(1);
}
process.env.VSCODE_PATH = vscodePath;

const child = spawn("node", ["./dist-tsc/test/runTest.js"], {
stdio: "inherit",
env: process.env,
});
child.on("exit", (code) => process.exit(code ?? 0));
child.on("error", (err) => {
console.error("run-clean: failed to spawn node:", err);
process.exit(1);
});
child.on("exit", (code, signal) => {
// Preserve both exit code and signal semantics; never map a signal to 0.
if (signal) {
console.error(`run-clean: test runner terminated by signal ${signal}`);
process.exit(1);
}
process.exit(code ?? 1);
});
16 changes: 13 additions & 3 deletions scripts/dl-vscode.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
import { downloadAndUnzipVSCode } from "@vscode/test-electron";

const version = process.argv[2] || "1.85.0";
const exe = await downloadAndUnzipVSCode({ version });
console.log(exe);
// Default mirrors package.json's engines.vscode; overridable via argv[1].
const DEFAULT_VSCODE_VERSION = "1.85.0";
const version = process.argv[2] || DEFAULT_VSCODE_VERSION;
try {
const exe = await downloadAndUnzipVSCode({ version });
console.log(exe);
} catch (err) {
console.error(
`dl-vscode: failed to download VS Code ${version}:`,
err instanceof Error ? err.message : err,
);
process.exit(1);
}
61 changes: 56 additions & 5 deletions scripts/fallow-audit.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,50 @@
import { spawnSync } from "node:child_process";
import process from "node:process";

function resolveBaseRef() {
if (process.env.FALLOW_BASE_REF) return process.env.FALLOW_BASE_REF;
/** Abort with the audit's "invocation error" exit code and a reason. */
function invocationError(reason) {
console.error(`fallow-audit: ${reason}`);
process.exit(2);
}

/** True when the probe never ran to completion (spawn failure or signal). */
function hasSpawnAnomaly(head) {
return Boolean(head.error || head.signal);
}

/** Human-readable reason for a spawn anomaly (only valid after the check). */
function anomalyReason(head) {
if (head.error) return `git probe failed: ${head.error.message}`;
return `git probe terminated by signal ${head.signal}`;
}

/**
* Read the probe result; null means "no answer" (non-zero status), while any
* spawn failure or signal termination aborts with the invocation-error exit
* code instead of silently degrading to the origin/main fallback.
*/
function probeAnswer(head) {
if (hasSpawnAnomaly(head)) invocationError(anomalyReason(head));
if (head.status === 0 && head.stdout.trim()) return head.stdout.trim();
return null;
}

/** Probe the repo's default branch via git; null when unresolvable. */
function probeGitDefaultBranch() {
const head = spawnSync("git", ["symbolic-ref", "--short", "refs/remotes/origin/HEAD"], {
encoding: "utf8",
});
if (head.status === 0 && head.stdout.trim()) return head.stdout.trim();
return "origin/main";
// A signal-terminated or spawn-failed probe is not "no result": don't
// silently fall through to origin/main with a generic code.
return probeAnswer(head);
}

function envBaseRef() {
return process.env.FALLOW_BASE_REF || null;
}

function resolveBaseRef() {
return envBaseRef() ?? probeGitDefaultBranch() ?? "origin/main";
}

const base = resolveBaseRef();
Expand Down Expand Up @@ -51,7 +88,21 @@ const args = [
];
console.log(`fallow audit --base ${base}`);
const result = spawnSync("pnpm", args, { stdio: "inherit" });
const code = result.status ?? 2;
if (result.error) {
console.error(`\n✖ fallow audit could not run pnpm: ${result.error.message}`);
process.exit(2);
}
if (result.signal) {
console.error(
`\n✖ fallow audit terminated by signal ${result.signal} — not a quality verdict.`,
);
process.exit(2);
}
const code = result.status;
if (code === null) {
console.error("\n✖ fallow audit exited without a status — check the Fallow invocation.");
process.exit(2);
}
if (code === 1) {
console.error(
`\n✖ fallow audit gate FAILED (findings against ${base}). Fix the findings or agree on a recorded baseline before merging.`,
Expand Down
Loading
Loading