Skip to content

feat(pi): add Pi and Oh My Pi support#221

Merged
vakovalskii merged 2 commits into
vakovalskii:mainfrom
timseriakov:feat/pi-agent-support
May 25, 2026
Merged

feat(pi): add Pi and Oh My Pi support#221
vakovalskii merged 2 commits into
vakovalskii:mainfrom
timseriakov:feat/pi-agent-support

Conversation

@timseriakov
Copy link
Copy Markdown
Contributor

@timseriakov timseriakov commented May 24, 2026

Summary

Adds first-class Pi and Oh My Pi support to Codbash.

Pi and Oh My Pi (Fork of Pi) sessions now show up in the dashboard alongside the existing agents, can be searched, previewed, opened in detail, resumed from the UI/CLI, included in analytics, and shown separately in leaderboard stats.

Companion PR

What's new

Surface Change
Agent detection Detects Pi via pi and Oh My Pi via omp, with Settings detection metadata and tests.
Session loading Reads Pi / Oh My Pi JSONL sessions from their agent directories, including canonical nested session layouts.
Session detail Parses messages, timestamps, projects, models, token usage, and cost metadata into the existing session/detail UI shape.
Resume / launch Generates shell-safe variant-specific resume commands (pi --session for Pi, omp --resume for Oh My Pi) and launches terminal resumes through the existing /api/launch flow.
Sidebar Adds separate Pi and Oh My Pi filters in the Agents section and Sidebar settings.
Analytics Includes Pi / Oh My Pi sessions in cost and usage analytics using real usage/cost data when present.
Leaderboard Splits Pi and Oh My Pi into separate leaderboard agent keys and badges.
Docs Updates supported-agent documentation and architecture notes for Pi / Oh My Pi data sources.

Implementation details

Backend

  • Adds Pi / Oh My Pi session scanners in src/data.js.
  • Normalizes Pi / Oh My Pi usage fields into Codbash token and cost fields.
  • Tracks agent_variant so Pi and Oh My Pi can share the pi tool integration while remaining distinguishable in filters and analytics.
  • Stores resume_target for JSONL-backed sessions so resumes can target the exact session file.
  • Validates Pi / Oh My Pi resume targets against scanned session files and keeps safe session IDs separate from JSONL resume paths for terminal tracking.
  • Extends agent detection, settings validation, CLI listing/resume support, WSL home discovery, active-session detection, markdown export, and terminal launch command construction.

Frontend

  • Adds Pi / Oh My Pi labels, badges, filters, sidebar entries, install metadata, and detail actions.
  • Wires Pi / Oh My Pi into calendar, heatmap, analytics, leaderboard, and session rendering paths.
  • Sends JSONL resume targets for Pi / Oh My Pi detail and project “Last” resume flows.
  • Keeps normal UI casing as Pi / Oh My Pi; leaderboard badges remain lowercase (pi, ohmypi) to match existing leaderboard style.

Tests

  • Adds Pi / Oh My Pi parser, scanner, nested cache invalidation, resume-target parsing, and leaderboard coverage in test/pi-session.test.js.
  • Extends agent detection, settings, sidebar config, frontend escaping, WSL discovery, terminal launch, and Pi feature-parity tests for Pi / Oh My Pi behavior.

Test plan

  • node --test
  • Focused regression tests: node --test test/pi-session.test.js test/sidebar-config.test.js test/frontend-escaping.test.js
  • Isolated dashboard smoke fixture: /api/sessions returns Pi and Oh My Pi JSONL sessions with resume_target.
  • Browser smoke: All Sessions renders Pi and Oh My Pi sessions.
  • Browser smoke: sidebar filters isolate Pi and Oh My Pi separately.
  • Browser smoke: Pi detail view renders messages, model/title context, and Resume action.
  • Browser smoke: Resume action sends safe sessionId plus JSONL resumeTarget to /api/launch (fetch intercepted; no real terminal/agent launched).
  • Browser smoke: Analytics includes Pi/OhMyPi cost coverage.
  • Browser smoke: Leaderboard shows separate pi and ohmypi rows with violet badge styling.
  • Manual: click Resume for real Pi and Oh My Pi sessions and confirm the terminal resumes them.

Verified locally

  • node --test → 142 pass, 0 fail, 2 skipped
  • Focused regression tests: node --test test/pi-session.test.js test/sidebar-config.test.js test/frontend-escaping.test.js test/terminals-windows-launch.test.js → 55 pass, 0 fail
  • Manual Pi resume by JSONL path: pi --session <jsonl path> --print 'respond ok'ok
  • Manual Oh My Pi resume by JSONL path: omp --resume <jsonl path> --print 'respond ok' → observed respond ok

Files

  • README.md
  • docs/ARCHITECTURE.md
  • bin/cli.js
  • src/agents-detect.js
  • src/data.js
  • src/frontend/analytics.js
  • src/frontend/app.js
  • src/frontend/calendar.js
  • src/frontend/detail.js
  • src/frontend/heatmap.js
  • src/frontend/index.html
  • src/frontend/leaderboard.js
  • src/frontend/sidebar-config.js
  • src/frontend/styles.css
  • src/server.js
  • src/settings.js
  • src/terminals.js
  • test/agents-detect.test.js
  • test/frontend-escaping.test.js
  • test/pi-session.test.js
  • test/settings.test.js
  • test/sidebar-config.test.js
  • test/terminals-windows-launch.test.js
  • test/wsl-windows.test.js

Adds Pi and Oh My Pi as first-class agents instead of treating them as a combined sidebar entry. The sidebar exposes separate Pi and Oh My Pi filters, while leaderboard aggregation keeps their stats distinct and renders lowercase badges only in the leaderboard context.

Resume reliability:

- Scan Pi and Oh My Pi session directories recursively enough to find canonical nested ~/.omp/agent/sessions/<project> JSONL files.
- Store resume_target on scanned sessions and use the JSONL path for Pi and Oh My Pi copy, CLI, and UI resume commands.
- Pass resumeTarget through /api/launch separately from sessionId so terminal tracking remains keyed by safe IDs while Pi and Oh My Pi resumes can use validated session file paths.
- Quote resume targets before passing them to pi/omp launch commands.

Analytics/UI:

- Split leaderboard agent keys for Pi vs Oh My Pi instead of aggregating under pi.
- Remove the combined Pi/OhMyPi install/sidebar item; keep Pi and Oh My Pi as separate sidebar entries.
- Keep proper display casing outside Leaderboard and lowercase badges inside Leaderboard.

Tests:

- node --test
- Manual: omp --resume <jsonl path> --print 'respond ok'
Copy link
Copy Markdown
Owner

@vakovalskii vakovalskii left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM ✅ Welcome to the project @timseriakov — really solid first contribution!

Verified:

  • node --test test/pi-session.test.js → 11/11 pass
  • Full suite locally: 144 pass / 0 fail / 2 skipped (up from 127 baseline, +17 new tests across pi-session + extensions)
  • Diff stat against fresh main — no unexpected deletions, additive across all 24 files
  • Squash-merging now since mergeable=MERGEABLE and #220 already landed

Security spot-checks:

  • quotePosixArg correctly POSIX-escapes ('value' with internal ''\\'' — the standard one)
  • isValidPiResumeTarget has a defense-in-depth chain: sessionId regex → .jsonl suffix → reject ['\$\\n\r\0]→ **resolved path must equalfindSessionFile(sessionId).file**. The last check is the killer — even if a crafted resumeTarget` slipped past the regex, it has to point at the same file codbash discovered during scan. Attacker can't smuggle in arbitrary paths.
  • Server now refuses unknown-but-not-detected agents (agent not installed: <tool>) — closes a small gap where an unknown agent could fall back to claude silently
  • Pi-specific resume target only flows when isValidPiResumeTarget returns true; otherwise the existing safe-sessionId path is taken

Design notes I like:

  • agent_variant distinguishes Pi from Oh My Pi while sharing the pi tool integration — clean way to avoid duplicating scanners
  • resume_target separated from sessionId so PID-tracking + window-tagging stay simple while resumes can target the exact JSONL file
  • The companion leaderboard PR is exactly the right way to keep cross-repo style consistent

Thanks again!

@vakovalskii
Copy link
Copy Markdown
Owner

Approved above and tested locally (144/0/2). This PR is in Draft state though — could you flip it to Ready for review? After that I'll merge immediately.

GitHub's CLI also won't run the CI matrix while it's in Draft, so the green checks need to land before the squash anyway.

@timseriakov timseriakov marked this pull request as ready for review May 25, 2026 06:47
@NovakPAai
Copy link
Copy Markdown
Collaborator

Code Review + Security Review — PR #221

@timseriakov impressive first contribution — 24 files, clean patterns, solid test coverage. Two independent reviews (code + security) found findings below. The owner's security spot-checks on isValidPiResumeTarget and quotePosixArg are correct; these findings are what was missed.


HIGH (Code)

1. All Resume buttons route through launchPiSession, not just Pi sessions

In detail.js, the else branch in openDetail was changed so that all agents (Claude, Codex, Qwen, Kiro, etc.) now call launchPiSession(...) instead of launchSession(...). It currently works by accident — non-Pi sessions have no resume_target, so hasResumeTarget is false on the server. But this is structurally wrong and will break if any future agent adds a resume_target field.

Fix: Scope launchPiSession to Pi sessions only:

if (s.tool === 'pi') {
  // launchPiSession(...)
} else {
  // launchSession(...)
}

2. findSessionFile(sessionId) called without project — can return null on cold cache

In isValidPiResumeTarget, findSessionFile(sessionId) is called without the project argument. If the session index hasn't been built yet (e.g., a Pi resume request arrives before the dashboard is opened), the lookup may return null and reject a valid resume. The project is available from the request — passing it would make the lookup more robust.

HIGH (Security)

3. TOCTOU between isValidPiResumeTarget and shell execution

The validation resolves and compares the path at validation time. Between validation and exec(), a local attacker (same-user process) could swap the validated file with a symlink to an arbitrary target. The window is milliseconds, so this requires a co-located malicious process.

Mitigations (pick one):

  • Have buildAgentCommand re-fetch the path from the in-memory index by sessionId rather than accepting the client-supplied resumeTarget
  • Check lstatSync().isSymbolicLink() === false at validation time
  • Open and verify the JSONL header immediately before building the command

4. resumeTarget forwarded to openInTerminal unconditionally

After the guard check, line 214 passes resumeTarget to openInTerminal regardless of tool:

openInTerminal(..., fresh ? '' : (resumeTarget || ''));

For non-Pi tools, buildAgentCommand currently discards it. But any future tool added without updating the guard inherits an unsanitized path in the shell command.

Fix: isSafePiTarget ? resumeTarget : ''

5. sessionId from JSONL header.id not validated at parse time

parsePiSessionFile does let sessionId = String(header.id) with no format check. If a crafted JSONL has header.id matching an existing Claude session UUID, it shadows that session in the index. The client-side regex blocks exploitation via resumeTarget, but session confusion and leaderboard pollution are possible.

Fix: Validate against SAFE_SESSION_ID (/^[A-Za-z0-9._-]{1,128}$/) in parsePiSessionFile and return null on mismatch.


MEDIUM

6. Agent label mismatch in AGENT_DEFS

{ id: 'pi', label: 'OhMyPi', customCheck: 'piPath' } — when only pi is installed (not omp), Settings shows "OhMyPi" as detected. Should be 'Pi' or 'Pi/OhMyPi'.

7. _ompSessionDirMtimes tracks both Pi and OhMyPi directories

Variable name suggests OhMyPi-only but accumulates mtimes for both PI_AGENT_DIR and OMP_AGENT_DIR. Rename to _piOmpSessionDirMtimes.

8. extractSessionIdFromCommand Pi pattern uses safePiId regex that excludes / — will never match actual Pi resume commands

The --resume/--session target is a full JSONL path containing /, but safePiId is [A-Za-z0-9._:-]{1,128}. This function is dead code for Pi sessions. extractPiResumeTargetFromCommand (which handles quoted paths) exists but is only called from the PID-matching block, not from extractSessionIdFromCommand.

9. Session ID collision: Pi IDs not namespaced

parsePiSessionFile stores header.id as-is. If a Pi session has the same ID as a Claude/Codex session, one silently overwrites the other in loadSessions() (last-wins). Consider prefixing Pi IDs with pi_.

10. resume_target (absolute filesystem path) exposed to frontend

The full path (e.g., /Users/alice/.pi/agent/sessions/project/2026-05-24T...jsonl) is served in the sessions API and visible in browser JS. Any XSS or DNS rebinding attack learns home directory layout and project names. Consider keeping resume_target server-side only and having the frontend send just sessionId.

11. listPiSessionFiles follows symlinks

Dirent.isFile() returns true for symlink targets. A symlink inside ~/.pi/sessions/ pointing outside the expected directory would be read and indexed. Skip entries where entry.isSymbolicLink() is true.

12. Environment variables (PI_CONFIG_DIR, OMP_CONFIG_DIR, etc.) used in path.join without sanitization

If an attacker controls these env vars, they redirect session scanning to arbitrary filesystem paths. Validate resolved paths are under os.homedir().


LOW

13. quoteShellArg duplicated in three places

bin/cli.js line 104, src/frontend/app.js line 1089, src/terminals.js line 21 — three independent POSIX single-quote escaping implementations. The backend copies should share a utility.

14. decodeShellToken has dead code in the double-quoted branch

if (token[0] === '"' && token[token.length - 1] === '"') {
  if (token[0] === "'") return body.replace(...); // unreachable

The inner if (token[0] === "'") is never true. Remove the dead branch.

15. getResumeCommand uses getPiCommand() (detected binary) instead of session.agent_variant

A user with both pi and omp installed will see pi --session in Copy Command for OhMyPi sessions. Should use session.agent_variant === 'ohmypi' as the branch condition.

16. ps aux grep pattern for pi is overly broad

The ERE (^|[[:space:]/])pi([[:space:]]|$) applied to full ps aux lines can match usernames or paths containing pi. The downstream match: regex is tighter and is the real guard, so this is acceptable as a pre-filter but worth noting.

17. agent not installed: ' + tool reflects user input in error response

The tool value from the request body ends up verbatim in the error message. Use a static message: throw new Error('agent not installed').

18. DAILY_RESULT_CACHE_FILE bumped to v2 without documentation

A comment explaining the version bump reason would help future maintainers.


Summary

Severity Count
CRITICAL 0
HIGH 5 (2 code, 3 security)
MEDIUM 7
LOW 6

The most actionable items before merge:

  1. HIGH-1 — scope launchPiSession to Pi sessions only (behavioral regression)
  2. HIGH-4 — pass isSafePiTarget ? resumeTarget : '' instead of unconditional forwarding (defense-in-depth)
  3. HIGH-5 — validate header.id format at parse time (session index integrity)

Also: the owner noted this is still in Draft — flip to Ready for Review when the fixes land.

@timseriakov
Copy link
Copy Markdown
Contributor Author

timseriakov commented May 25, 2026

Update: this review-fix commit was pushed after #221 had already been merged, so it is not part of the merged PR.

I moved the fixes into a dedicated follow-up PR instead:

#224

That follow-up PR explains why it exists and contains the hardening fixes from the post-merge review:

  • scope launchPiSession to Pi sessions only
  • forward only server-validated Pi resume targets to terminal launch
  • validate Pi JSONL header.id before indexing
  • skip symlinked Pi/OhMyPi session files
  • fix the Pi/OhMyPi Settings label and Copy Resume variant selection

Verification recorded in #224:

  • node --check src/server.js && node --check src/frontend/detail.js && node --check src/data.js && node --check src/frontend/app.js
  • node --test test/pi-session.test.js test/frontend-escaping.test.js test/agents-detect.test.js test/terminals-windows-launch.test.js → 31 pass, 0 fail
  • node --test → 146 pass, 0 fail, 2 skipped

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants