Skip to content

feat: server utilities from dpcode#67

Open
aaditagrawal wants to merge 4 commits intomainfrom
feat/dpcode-server-utils
Open

feat: server utilities from dpcode#67
aaditagrawal wants to merge 4 commits intomainfrom
feat/dpcode-server-utils

Conversation

@aaditagrawal
Copy link
Copy Markdown
Owner

@aaditagrawal aaditagrawal commented Apr 12, 2026

Summary

Ports server-side utilities from the dpcode fork.

  • Provider health monitoring — CLI probes for codex + claude availability
  • Provider discovery service — capabilities, skills, models, and plugin discovery
  • Managed terminal wrappers — CLI wrapper scripts for terminal identity tracking (cross-platform)
  • Project favicon route — serve project favicons via HTTP
  • Project metadata projection — project info tracking in orchestration layer
  • Git error types — structured error classes for git operations
  • Repo detection — utility to check if a path is a git repository

Test plan

  • Provider health tests
  • Favicon route tests

Summary by CodeRabbit

  • New Features

    • Project favicon serving—automatically resolves and serves project-specific icons from root or linked sources, with fallback support.
    • Provider health monitoring for Codex and Claude with authentication status checks at startup.
    • Enhanced terminal wrapper integration for managed CLI tools with improved environment handling.
    • Improved project metadata orchestration for lifecycle event handling.
  • Tests

    • Added comprehensive test coverage for favicon resolution and provider health checks.

- Provider health monitoring (CLI probes for codex + claude)
- Provider discovery service (capabilities, skills, models)
- Managed terminal wrappers (CLI identity tracking scripts)
- Project favicon route handler
- Project metadata projection for orchestration
- Git error types and repo detection utility
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 12, 2026

Warning

Rate limit exceeded

@aaditagrawal has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 7 minutes and 42 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 7 minutes and 42 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 059fa0f1-eb27-4c5d-94c4-70c935f07f72

📥 Commits

Reviewing files that changed from the base of the PR and between 5fb0a44 and 75e2edd.

📒 Files selected for processing (5)
  • apps/server/src/provider/Layers/ProviderHealth.test.ts
  • apps/server/src/provider/Layers/ProviderHealth.ts
  • apps/server/src/provider/Services/ProviderHealth.ts
  • packages/shared/package.json
  • packages/shared/src/terminalThreads.ts
📝 Walkthrough

Walkthrough

This PR introduces foundational infrastructure across multiple domains: Git error handling and repository validation, project metadata orchestration with event-based snapshots, project favicon serving with fallback resolution, provider health monitoring for external CLIs, and managed terminal wrapper setup for CLI interception and configuration.

Changes

Cohort / File(s) Summary
Git Foundation
git/Errors.ts, git/isRepo.ts
Defines four tagged error classes (GitCommandError, GitHubCliError, TextGenerationError, GitManagerError) with formatted messages, and adds helper to check for .git directory existence.
Project Metadata Orchestration
orchestration/projectMetadataProjection.ts
Introduces event-filtered projection logic for project lifecycle events; routes project.created/project.meta-updated/project.deleted to upsert operations with conditional field merging and timestamp tracking.
Favicon Serving
projectFaviconRoute.ts, projectFaviconRoute.test.ts
HTTP route handler resolving project-specific favicons via multi-phase lookup (well-known files, then HTML link extraction) with fallback SVG support; comprehensive test coverage for success paths, edge cases, and fallback modes.
Provider Health Checks
provider/Layers/ProviderHealth.ts, provider/Layers/ProviderHealth.test.ts, provider/Services/ProviderHealth.ts
Startup-time health probing for Codex and Claude CLIs via spawned processes; parses version/auth status with config-driven auth-skip logic; caches results in service layer; extensive test suite covering parsing, config detection, and provider availability scenarios.
Terminal Wrapper Management
terminal/managedTerminalWrappers.ts
Builds wrapper shell scripts and Zsh dotfiles to intercept codex/claude invocations, inject environment variables, and enable hook-based event emission for terminal session tracking.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Server as HTTP Server
    participant FS as FileSystem
    participant Resolver as Icon Resolver

    Client->>Server: GET /api/project-favicon?cwd=...
    Server->>FS: stat ./favicon.svg
    alt Well-known favicon found
        FS-->>Server: file exists
        Server->>FS: read file
        FS-->>Server: file content
        Server-->>Client: 200 (image/svg+xml)
    else Well-known favicon missing
        FS-->>Server: ENOENT
        Server->>Resolver: parse index.html for link rel=icon
        Resolver->>FS: read index.html
        FS-->>Resolver: file content
        Resolver->>Resolver: extract href from link tag
        Resolver->>FS: stat public/{href}
        alt Icon file found
            FS-->>Resolver: file exists
            Resolver->>FS: read icon file
            FS-->>Resolver: file content
            Resolver-->>Server: icon path & content
            Server-->>Client: 200 (icon content)
        else Icon missing & fallback enabled
            FS-->>Resolver: ENOENT
            Resolver-->>Server: no icon found
            Server-->>Client: 200 (fallback SVG)
        else fallback=none
            Server-->>Client: 204 (no content)
        end
    end
Loading
sequenceDiagram
    participant Layer as ProviderHealthLive
    participant Spawner as ChildProcessSpawner
    participant CLI as CLI (codex/claude)
    participant FS as FileSystem
    participant Cache as Service Cache

    Layer->>Layer: layer init (fork both providers)
    par Codex Health Check
        Layer->>Spawner: spawn codex --version
        Spawner->>CLI: execute
        CLI-->>Spawner: stdout + exit code
        Spawner-->>Layer: {stdout, stderr, code}
        Layer->>FS: read ~/.codex/config.toml
        FS-->>Layer: config content
        Layer->>Layer: parse model_provider
        alt Custom model_provider detected
            Layer->>Layer: skip auth probe, mark ready
        else OpenAI (default)
            Layer->>Spawner: spawn codex login status
            Spawner->>CLI: execute
            CLI-->>Spawner: JSON/text output
            Spawner-->>Layer: result
            Layer->>Layer: parseAuthStatusFromOutput
        end
    and Claude Health Check
        Layer->>Spawner: spawn claude --version
        Spawner->>CLI: execute
        CLI-->>Spawner: stdout + exit code
        Layer->>Spawner: spawn claude auth status
        Spawner->>CLI: execute
        CLI-->>Spawner: JSON output
        Layer->>Layer: parseClaudeAuthStatusFromOutput
    end
    Layer->>Cache: store joined status results
    Cache-->>Layer: ProviderHealthShape ready
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰 A rabbit hops through Git's new garden,
Favicons bloom where paths uncharted,
Provider health glows, CLIs greet,
Terminal wrappers make workflows sweet—
Infrastructure springs eternal! 🌱✨

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'feat: server utilities from dpcode' is too vague and generic. It references 'dpcode' without clearly describing the specific utilities being added or their purpose to someone unfamiliar with that codebase. Consider a more descriptive title that highlights key utilities (e.g., 'feat: add provider health checks, favicon route, and git error handling' or similar) rather than referencing an external fork name.
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed The description is well-structured with clear sections summarizing each utility added, but lacks detailed explanation of why these changes are needed and what problems they solve.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/dpcode-server-utils

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XXL 1,000+ effective changed lines (test files excluded in mixed PRs). labels Apr 12, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (4)
apps/server/src/projectFaviconRoute.ts (1)

119-136: Extract shared candidate traversal to avoid security drift.

tryResolvedPaths and tryCandidates duplicate near-identical stat/containment/serve flow. This already drifted from the Effect resolver hardening. A shared helper would reduce future divergence.

Also applies to: 163-180

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/projectFaviconRoute.ts` around lines 119 - 136, The two
functions tryResolvedPaths and tryCandidates duplicate the same containment
check, fs.stat and serve logic (using isPathWithinProject and serveFaviconFile),
which causes security drift; refactor by extracting a single helper (e.g.,
iterateAndServeCandidate or validateAndServeCandidate) that accepts an array of
candidate paths and a callback/onExhausted, performs isPathWithinProject checks,
fs.stat to ensure isFile, and calls serveFaviconFile for the first valid file,
then replace both tryResolvedPaths and tryCandidates with calls to this shared
helper to remove duplication and centralize hardening.
apps/server/src/projectFaviconRoute.test.ts (1)

88-181: Add a regression test for symlink-based escape.

Once the resolver hardening is in place, add a test ensuring a symlinked favicon that points outside cwd is rejected (and falls back/204). This will lock in the security fix.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/projectFaviconRoute.test.ts` around lines 88 - 181, Add a
regression test in projectFaviconRoute.test.ts that creates a projectDir via
makeTempDir, creates an external file outside that cwd (e.g.,
externalDir/logo.svg), then fs.symlinkSync a link inside the project (e.g.,
public/brand/logo.svg -> external path) and calls the route via withRouteServer
+ request to /api/project-favicon?cwd=... to assert the symlinked favicon is
rejected: when no fallback param expect a served fallback (response.contentType
contains image/svg+xml and response.body contains
'data-fallback="project-favicon"'), and when calling with &fallback=none expect
a 204 and empty body; use the same helpers (makeTempDir, withRouteServer,
request) and paths used in existing tests so the spec locks in the resolver
hardening.
apps/server/src/provider/Layers/ProviderHealth.ts (2)

17-17: Remove unused Array import.

The Array module from Effect is imported but never used. Line 66 uses native Array.isArray(), not the Effect variant.

♻️ Proposed fix
-import { Array, Effect, Fiber, FileSystem, Layer, Option, Path, Result, Stream } from "effect";
+import { Effect, Fiber, FileSystem, Layer, Option, Path, Result, Stream } from "effect";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/provider/Layers/ProviderHealth.ts` at line 17, The import
list includes the unused Effect Array symbol; remove Array from the import
statement (the list containing Array, Effect, Fiber, FileSystem, Layer, Option,
Path, Result, Stream) so only used symbols remain—keep all other imports intact;
confirm there are no references to the Effect Array API (the code uses native
Array.isArray) and run a quick typecheck to ensure no other code depends on
Array.

242-282: Consider extracting shared command runner.

runCodexCommand and runClaudeCommand are nearly identical, differing only in the CLI binary name. A shared helper could reduce duplication.

♻️ Example refactor
+const runCliCommand = (binary: string, args: ReadonlyArray<string>) =>
+  Effect.gen(function* () {
+    const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
+    const command = ChildProcess.make(binary, [...args], {
+      shell: process.platform === "win32",
+    });
+
+    const child = yield* spawner.spawn(command);
+
+    const [stdout, stderr, exitCode] = yield* Effect.all(
+      [
+        collectStreamAsString(child.stdout),
+        collectStreamAsString(child.stderr),
+        child.exitCode.pipe(Effect.map(Number)),
+      ],
+      { concurrency: "unbounded" },
+    );
+
+    return { stdout, stderr, code: exitCode } satisfies CommandResult;
+  }).pipe(Effect.scoped);
+
+const runCodexCommand = (args: ReadonlyArray<string>) => runCliCommand("codex", args);
+const runClaudeCommand = (args: ReadonlyArray<string>) => runCliCommand("claude", args);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/provider/Layers/ProviderHealth.ts` around lines 242 - 282,
runCodexCommand and runClaudeCommand are duplicated; extract a shared helper
like makeRunner or runCommand(binary: string, args: ReadonlyArray<string>) that
encapsulates getting ChildProcessSpawner.ChildProcessSpawner, creating the
command via ChildProcess.make, spawning, collecting stdout/stderr with
collectStreamAsString and mapping child.exitCode to Number, and applying
.pipe(Effect.scoped); then have runCodexCommand and runClaudeCommand delegate to
that helper (e.g., return runCommand("codex", args) and runCommand("claude",
args)) while preserving the returned shape that satisfies CommandResult.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/server/src/git/isRepo.ts`:
- Around line 4-5: The isGitRepository function should not just check for the
existence of a .git path; update isGitRepository to validate that .git is a real
git repository by: (1) using fs.stat to confirm join(cwd, ".git") is a directory
(stat.isDirectory()), and if not a directory, (2) read the .git file to detect a
"gitdir: <path>" pointer and resolve that path and confirm it exists and
contains git metadata (e.g., HEAD and refs) before returning true; otherwise
return false. Target the isGitRepository function for these changes and ensure
you handle errors (ENOENT, permission) cleanly and synchronously/asynchronously
consistent with the surrounding code.

In `@apps/server/src/orchestration/projectMetadataProjection.ts`:
- Around line 42-78: The projection currently silently ignores
"project.meta-updated" and "project.deleted" events when
projectionProjectRepository.getById(...) returns None, which allows
advanceProjectMetadataSnapshotState(...) to mark the sequence applied and
permanently drift; fix by failing fast: in both the "project.meta-updated" and
"project.deleted" cases, detect Option.isNone(existingRow) and return/throw an
explicit projection error (or call the module's projection-failure helper)
rather than silently continuing, so the snapshot advancement is prevented until
the base row exists. Ensure you reference the existingRow variable and the
projectionProjectRepository.getById(...) call and produce a clear error (e.g.,
throw new Error or a ProjectionError) so the caller/runner can stop and
retry/backfill before advancing state.

In `@apps/server/src/projectFaviconRoute.ts`:
- Around line 68-71: The lexical containment check in isPathWithinProject allows
symlink escapes; update the route to resolve canonical paths like
ProjectFaviconResolver does: use fs.promises.realpath (or fs.realpathSync) to
get realProject = realpath(projectCwd) and realCandidate =
realpath(candidatePath) before checking containment, then ensure realCandidate
=== realProject or realCandidate.startsWith(realProject + path.sep) before
calling serveFaviconFile; also catch realpath errors (treat non-existent or
unreadable targets as disallowed) and keep the existing fallback behavior.

In `@apps/server/src/provider/Layers/ProviderHealth.test.ts`:
- Around line 4-5: The PlatformError import path is wrong; replace the import of
PlatformError from "effect/PlatformError" with the correct module
"@effect/platform/Error" so that references like PlatformError.systemError()
resolve; update the import statement that currently brings in PlatformError to
import { PlatformError } from "@effect/platform/Error" ensuring other imports
(e.g., ChildProcessSpawner) remain unchanged.

In `@apps/server/src/provider/Services/ProviderHealth.ts`:
- Around line 10-21: The file is missing the Context import and has an unused
ServiceMap import: add an import for Context from the "effect" package and
remove the unused ServiceMap import; specifically update the top imports so
ProviderHealth (which uses Context.Service) and ProviderHealthShape compile
correctly by importing Context and deleting ServiceMap from the import list.

In `@apps/server/src/terminal/managedTerminalWrappers.ts`:
- Around line 173-257: The script built by buildCodexWrapperScript can orphan
the background watcher (T3CODE_CODEX_START_WATCHER_PID) on signals; add a trap
right before the watcher is started that kills and waits for
$T3CODE_CODEX_START_WATCHER_PID on INT, TERM, HUP and EXIT (e.g. trap 'kill
"$T3CODE_CODEX_START_WATCHER_PID" >/dev/null 2>&1 || true; wait
"$T3CODE_CODEX_START_WATCHER_PID" 2>/dev/null || true' INT TERM HUP EXIT) so the
watcher is cleaned up whether the script exits normally or via a signal.

---

Nitpick comments:
In `@apps/server/src/projectFaviconRoute.test.ts`:
- Around line 88-181: Add a regression test in projectFaviconRoute.test.ts that
creates a projectDir via makeTempDir, creates an external file outside that cwd
(e.g., externalDir/logo.svg), then fs.symlinkSync a link inside the project
(e.g., public/brand/logo.svg -> external path) and calls the route via
withRouteServer + request to /api/project-favicon?cwd=... to assert the
symlinked favicon is rejected: when no fallback param expect a served fallback
(response.contentType contains image/svg+xml and response.body contains
'data-fallback="project-favicon"'), and when calling with &fallback=none expect
a 204 and empty body; use the same helpers (makeTempDir, withRouteServer,
request) and paths used in existing tests so the spec locks in the resolver
hardening.

In `@apps/server/src/projectFaviconRoute.ts`:
- Around line 119-136: The two functions tryResolvedPaths and tryCandidates
duplicate the same containment check, fs.stat and serve logic (using
isPathWithinProject and serveFaviconFile), which causes security drift; refactor
by extracting a single helper (e.g., iterateAndServeCandidate or
validateAndServeCandidate) that accepts an array of candidate paths and a
callback/onExhausted, performs isPathWithinProject checks, fs.stat to ensure
isFile, and calls serveFaviconFile for the first valid file, then replace both
tryResolvedPaths and tryCandidates with calls to this shared helper to remove
duplication and centralize hardening.

In `@apps/server/src/provider/Layers/ProviderHealth.ts`:
- Line 17: The import list includes the unused Effect Array symbol; remove Array
from the import statement (the list containing Array, Effect, Fiber, FileSystem,
Layer, Option, Path, Result, Stream) so only used symbols remain—keep all other
imports intact; confirm there are no references to the Effect Array API (the
code uses native Array.isArray) and run a quick typecheck to ensure no other
code depends on Array.
- Around line 242-282: runCodexCommand and runClaudeCommand are duplicated;
extract a shared helper like makeRunner or runCommand(binary: string, args:
ReadonlyArray<string>) that encapsulates getting
ChildProcessSpawner.ChildProcessSpawner, creating the command via
ChildProcess.make, spawning, collecting stdout/stderr with collectStreamAsString
and mapping child.exitCode to Number, and applying .pipe(Effect.scoped); then
have runCodexCommand and runClaudeCommand delegate to that helper (e.g., return
runCommand("codex", args) and runCommand("claude", args)) while preserving the
returned shape that satisfies CommandResult.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b3f3c980-4bd3-4b26-8598-6508935ed4b8

📥 Commits

Reviewing files that changed from the base of the PR and between b3bd5c3 and 5fb0a44.

📒 Files selected for processing (9)
  • apps/server/src/git/Errors.ts
  • apps/server/src/git/isRepo.ts
  • apps/server/src/orchestration/projectMetadataProjection.ts
  • apps/server/src/projectFaviconRoute.test.ts
  • apps/server/src/projectFaviconRoute.ts
  • apps/server/src/provider/Layers/ProviderHealth.test.ts
  • apps/server/src/provider/Layers/ProviderHealth.ts
  • apps/server/src/provider/Services/ProviderHealth.ts
  • apps/server/src/terminal/managedTerminalWrappers.ts

Comment on lines +4 to +5
export function isGitRepository(cwd: string): boolean {
return existsSync(join(cwd, ".git"));
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Harden repo detection to avoid false positives from arbitrary .git artifacts.

The current predicate returns true for any existing .git path, even malformed files. Since this result gates git workflows, a malformed .git can incorrectly enable downstream git operations.

🔧 Proposed hardening
-import { existsSync } from "node:fs";
+import { existsSync, lstatSync, readFileSync } from "node:fs";
 import { join } from "node:path";
 
 export function isGitRepository(cwd: string): boolean {
-  return existsSync(join(cwd, ".git"));
+  const gitPath = join(cwd, ".git");
+  if (!existsSync(gitPath)) return false;
+
+  try {
+    const stat = lstatSync(gitPath);
+    if (stat.isDirectory()) return true;
+    if (stat.isFile()) {
+      return readFileSync(gitPath, "utf8").startsWith("gitdir:");
+    }
+    return false;
+  } catch {
+    return false;
+  }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/git/isRepo.ts` around lines 4 - 5, The isGitRepository
function should not just check for the existence of a .git path; update
isGitRepository to validate that .git is a real git repository by: (1) using
fs.stat to confirm join(cwd, ".git") is a directory (stat.isDirectory()), and if
not a directory, (2) read the .git file to detect a "gitdir: <path>" pointer and
resolve that path and confirm it exists and contains git metadata (e.g., HEAD
and refs) before returning true; otherwise return false. Target the
isGitRepository function for these changes and ensure you handle errors (ENOENT,
permission) cleanly and synchronously/asynchronously consistent with the
surrounding code.

Comment on lines +42 to +78
case "project.meta-updated": {
const existingRow = yield* input.projectionProjectRepository.getById({
projectId: input.event.payload.projectId,
});
if (Option.isSome(existingRow)) {
yield* input.projectionProjectRepository.upsert({
...existingRow.value,
...(input.event.payload.title !== undefined
? { title: input.event.payload.title }
: {}),
...(input.event.payload.workspaceRoot !== undefined
? { workspaceRoot: input.event.payload.workspaceRoot }
: {}),
...(input.event.payload.defaultModelSelection !== undefined
? { defaultModelSelection: input.event.payload.defaultModelSelection }
: {}),
...(input.event.payload.scripts !== undefined
? { scripts: input.event.payload.scripts }
: {}),
updatedAt: input.event.payload.updatedAt,
});
}
break;
}

case "project.deleted": {
const existingRow = yield* input.projectionProjectRepository.getById({
projectId: input.event.payload.projectId,
});
if (Option.isSome(existingRow)) {
yield* input.projectionProjectRepository.upsert({
...existingRow.value,
deletedAt: input.event.payload.deletedAt,
updatedAt: input.event.payload.deletedAt,
});
}
break;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fail fast when update/delete arrives before the row exists.

If getById(...) returns None on Line 43 or Line 68, these branches silently drop the event. In the same module, advanceProjectMetadataSnapshotState(...) can still record the sequence as applied, which makes the projection drift permanent after replay gaps or partial backfills. Return an explicit projection error here, or otherwise prevent snapshot advancement until the base row exists.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/orchestration/projectMetadataProjection.ts` around lines 42 -
78, The projection currently silently ignores "project.meta-updated" and
"project.deleted" events when projectionProjectRepository.getById(...) returns
None, which allows advanceProjectMetadataSnapshotState(...) to mark the sequence
applied and permanently drift; fix by failing fast: in both the
"project.meta-updated" and "project.deleted" cases, detect
Option.isNone(existingRow) and return/throw an explicit projection error (or
call the module's projection-failure helper) rather than silently continuing, so
the snapshot advancement is prevented until the base row exists. Ensure you
reference the existingRow variable and the
projectionProjectRepository.getById(...) call and produce a clear error (e.g.,
throw new Error or a ProjectionError) so the caller/runner can stop and
retry/backfill before advancing state.

Comment on lines +68 to +71
function isPathWithinProject(projectCwd: string, candidatePath: string): boolean {
const relative = path.relative(path.resolve(projectCwd), path.resolve(candidatePath));
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify canonical-path guard parity between the two implementations.
rg -n -C2 'realPath|realpath|isPathWithinProject|serveFaviconFile' \
  apps/server/src/project/Layers/ProjectFaviconResolver.ts \
  apps/server/src/projectFaviconRoute.ts

Repository: aaditagrawal/t3code

Length of output: 4506


🏁 Script executed:

#!/bin/bash
# Check serveFaviconFile function and surrounding context
sed -n '73,90p' apps/server/src/projectFaviconRoute.ts
echo "---"
# Check full context at lines 120-140
sed -n '120,140p' apps/server/src/projectFaviconRoute.ts
echo "---"
# Check full context at lines 165-180
sed -n '165,180p' apps/server/src/projectFaviconRoute.ts

Repository: aaditagrawal/t3code

Length of output: 1655


Block symlink escape before serving favicon files.

The isPathWithinProject() function (line 68-71) enforces only lexical path containment without resolving symlinks. At lines 125-134 and 169-178, the code checks containment and immediately serves the file without canonical-path validation. A symlink inside projectCwd (e.g., favicon.svg -> /etc/passwd) passes the lexical check and leaks files outside the project when serveFaviconFile() reads its target.

The ProjectFaviconResolver.ts implementation correctly uses realPath() to resolve both projectCwd and candidate paths before serving, preventing this attack. Align this route handler with that pattern.

Suggested fix
 function isPathWithinProject(projectCwd: string, candidatePath: string): boolean {
   const relative = path.relative(path.resolve(projectCwd), path.resolve(candidatePath));
   return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
 }
+
+function withValidatedRealPath(
+  projectCwd: string,
+  candidate: string,
+  onValid: (realCandidate: string) => void,
+  onInvalid: () => void,
+): void {
+  fs.realpath(projectCwd, (rootErr, realProjectCwd) => {
+    const effectiveRoot = rootErr ? path.resolve(projectCwd) : realProjectCwd;
+    fs.realpath(candidate, (candidateErr, realCandidate) => {
+      if (candidateErr || !isPathWithinProject(effectiveRoot, realCandidate)) {
+        onInvalid();
+        return;
+      }
+      onValid(realCandidate);
+    });
+  });
+}
@@
     fs.stat(candidate, (err, stats) => {
       if (err || !stats?.isFile()) {
         tryResolvedPaths(paths, index + 1, onExhausted);
         return;
       }
-      serveFaviconFile(candidate, res);
+      withValidatedRealPath(
+        projectCwd,
+        candidate,
+        (safePath) => serveFaviconFile(safePath, res),
+        () => tryResolvedPaths(paths, index + 1, onExhausted),
+      );
     });
   };
@@
     fs.stat(candidate, (err, stats) => {
       if (err || !stats?.isFile()) {
         tryCandidates(index + 1);
         return;
       }
-      serveFaviconFile(candidate, res);
+      withValidatedRealPath(
+        projectCwd,
+        candidate,
+        (safePath) => serveFaviconFile(safePath, res),
+        () => tryCandidates(index + 1),
+      );
     });
   };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/projectFaviconRoute.ts` around lines 68 - 71, The lexical
containment check in isPathWithinProject allows symlink escapes; update the
route to resolve canonical paths like ProjectFaviconResolver does: use
fs.promises.realpath (or fs.realpathSync) to get realProject =
realpath(projectCwd) and realCandidate = realpath(candidatePath) before checking
containment, then ensure realCandidate === realProject or
realCandidate.startsWith(realProject + path.sep) before calling
serveFaviconFile; also catch realpath errors (treat non-existent or unreadable
targets as disallowed) and keep the existing fallback behavior.

Comment on lines +4 to +5
import * as PlatformError from "effect/PlatformError";
import { ChildProcessSpawner } from "effect/unstable/process";
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

effect-ts PlatformError import path effect/PlatformError

💡 Result:

The import path for PlatformError in Effect-TS (Effect Platform) is from "@effect/platform/Error". Usage example: import { PlatformError, isPlatformError } from "@effect/platform/Error" PlatformError is a union type: type PlatformError = BadArgument | SystemError It includes refinements like isPlatformError(u: unknown): u is PlatformError, and is used in platform services like FileSystem, Terminal, etc., where operations can fail with PlatformError (e.g., Effect.Effect<void, PlatformError, FileSystem>).

Citations:


🏁 Script executed:

sed -n '1,10p' apps/server/src/provider/Layers/ProviderHealth.test.ts

Repository: aaditagrawal/t3code

Length of output: 468


🏁 Script executed:

grep -n "PlatformError" apps/server/src/provider/Layers/ProviderHealth.test.ts

Repository: aaditagrawal/t3code

Length of output: 158


Fix incorrect PlatformError import path.

The import at line 4 should be changed from effect/PlatformError to @effect/platform/Error. The current path does not resolve correctly in Effect-TS and will cause a module resolution error. Since PlatformError.systemError() is used at line 52, this import must be corrected.

Correct import:
import { PlatformError } from "@effect/platform/Error";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/provider/Layers/ProviderHealth.test.ts` around lines 4 - 5,
The PlatformError import path is wrong; replace the import of PlatformError from
"effect/PlatformError" with the correct module "@effect/platform/Error" so that
references like PlatformError.systemError() resolve; update the import statement
that currently brings in PlatformError to import { PlatformError } from
"@effect/platform/Error" ensuring other imports (e.g., ChildProcessSpawner)
remain unchanged.

Comment thread apps/server/src/provider/Services/ProviderHealth.ts Outdated
Comment on lines +173 to +257
function buildCodexWrapperScript(input: {
codexHomeDir: string;
notifyHookPath: string;
targetPath: string;
}): string {
const { codexHomeDir, notifyHookPath, targetPath } = input;
return [
`export CODEX_HOME=${shellQuote(codexHomeDir)}`,
`if [ -f ${shellQuote(notifyHookPath)} ]; then`,
" export CODEX_TUI_RECORD_SESSION=1",
' if [ -z "${CODEX_TUI_SESSION_LOG_PATH:-}" ]; then',
' _t3code_codex_ts="$(date +%s 2>/dev/null || echo "$$")"',
' export CODEX_TUI_SESSION_LOG_PATH="${TMPDIR:-/tmp}/t3code-codex-session-$$_${_t3code_codex_ts}.jsonl"',
" fi",
" (",
' _t3code_log="$CODEX_TUI_SESSION_LOG_PATH"',
` _t3code_notify=${shellQuote(notifyHookPath)}`,
' _t3code_last_turn_id=""',
' _t3code_last_approval_id=""',
' _t3code_last_exec_call_id=""',
" _t3code_approval_fallback_seq=0",
"",
" _t3code_emit_event() {",
' _t3code_event="$1"',
` _t3code_payload=$(printf '{"hook_event_name":"%s"}' "$_t3code_event")`,
' "$_t3code_notify" "$_t3code_payload" >/dev/null 2>&1 || true',
" }",
"",
" _t3code_i=0",
' while [ ! -f "$_t3code_log" ] && [ "$_t3code_i" -lt 200 ]; do',
" _t3code_i=$((_t3code_i + 1))",
" sleep 0.05",
" done",
' if [ ! -f "$_t3code_log" ]; then',
" exit 0",
" fi",
"",
' tail -n 0 -F "$_t3code_log" 2>/dev/null | while IFS= read -r _t3code_line; do',
' case "$_t3code_line" in',
` *'"dir":"to_tui"'*'"kind":"codex_event"'*'"msg":{"type":"task_started"'*)`,
` _t3code_turn_id=$(printf '%s\n' "$_t3code_line" | awk -F'"turn_id":"' 'NF > 1 { sub(/".*/, "", $2); print $2; exit }')`,
' [ -n "$_t3code_turn_id" ] || _t3code_turn_id="task_started"',
' if [ "$_t3code_turn_id" != "$_t3code_last_turn_id" ]; then',
' _t3code_last_turn_id="$_t3code_turn_id"',
' _t3code_emit_event "Start"',
" fi",
" ;;",
` *'"dir":"to_tui"'*'"kind":"codex_event"'*'"msg":{"type":"'*'_approval_request"'*)`,
` _t3code_approval_id=$(printf '%s\n' "$_t3code_line" | awk -F'"id":"' 'NF > 1 { sub(/".*/, "", $2); print $2; exit }')`,
` [ -n "$_t3code_approval_id" ] || _t3code_approval_id=$(printf '%s\n' "$_t3code_line" | awk -F'"approval_id":"' 'NF > 1 { sub(/".*/, "", $2); print $2; exit }')`,
` [ -n "$_t3code_approval_id" ] || _t3code_approval_id=$(printf '%s\n' "$_t3code_line" | awk -F'"call_id":"' 'NF > 1 { sub(/".*/, "", $2); print $2; exit }')`,
' if [ -z "$_t3code_approval_id" ]; then',
" _t3code_approval_fallback_seq=$((_t3code_approval_fallback_seq + 1))",
' _t3code_approval_id="approval_request_${_t3code_approval_fallback_seq}"',
" fi",
' if [ "$_t3code_approval_id" != "$_t3code_last_approval_id" ]; then',
' _t3code_last_approval_id="$_t3code_approval_id"',
' _t3code_emit_event "PermissionRequest"',
" fi",
" ;;",
` *'"dir":"to_tui"'*'"kind":"codex_event"'*'"msg":{"type":"exec_command_begin"'*)`,
` _t3code_exec_call_id=$(printf '%s\n' "$_t3code_line" | awk -F'"call_id":"' 'NF > 1 { sub(/".*/, "", $2); print $2; exit }')`,
' if [ -n "$_t3code_exec_call_id" ]; then',
' if [ "$_t3code_exec_call_id" != "$_t3code_last_exec_call_id" ]; then',
' _t3code_last_exec_call_id="$_t3code_exec_call_id"',
' _t3code_emit_event "Start"',
" fi",
" else",
' _t3code_emit_event "Start"',
" fi",
" ;;",
" esac",
" done",
" ) &",
" T3CODE_CODEX_START_WATCHER_PID=$!",
"fi",
`${shellQuote(targetPath)} --enable codex_hooks -c ${shellQuote(`notify=["bash",${JSON.stringify(notifyHookPath)}]`)} "$@"`,
"_t3code_status=$?",
'if [ -n "${T3CODE_CODEX_START_WATCHER_PID:-}" ]; then',
' kill "$T3CODE_CODEX_START_WATCHER_PID" >/dev/null 2>&1 || true',
' wait "$T3CODE_CODEX_START_WATCHER_PID" 2>/dev/null || true',
"fi",
'exit "$_t3code_status"',
].join("\n");
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Consider adding signal trap for background watcher cleanup.

The background session watcher (spawned at line 246) is cleaned up on normal exit (lines 251-254), but if the script receives a signal (SIGTERM/SIGINT) during codex execution, the watcher process will be orphaned.

🛡️ Proposed fix to add signal trap

Add a trap before spawning the background process:

   ) &
   T3CODE_CODEX_START_WATCHER_PID=$!
+  trap 'kill "$T3CODE_CODEX_START_WATCHER_PID" 2>/dev/null || true' INT TERM
 fi
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/terminal/managedTerminalWrappers.ts` around lines 173 - 257,
The script built by buildCodexWrapperScript can orphan the background watcher
(T3CODE_CODEX_START_WATCHER_PID) on signals; add a trap right before the watcher
is started that kills and waits for $T3CODE_CODEX_START_WATCHER_PID on INT,
TERM, HUP and EXIT (e.g. trap 'kill "$T3CODE_CODEX_START_WATCHER_PID" >/dev/null
2>&1 || true; wait "$T3CODE_CODEX_START_WATCHER_PID" 2>/dev/null || true' INT
TERM HUP EXIT) so the watcher is cleaned up whether the script exits normally or
via a signal.

- Define ServerProviderStatus/ServerProviderStatusState locally in
  Services/ProviderHealth instead of importing non-existent names from
  contracts
- Fix Context.Service usage in ProviderHealth service definition
- Add missing unref property to ChildProcessHandle mock in test
- Add terminalThreads.ts from shared terminal utilities and register
  its subpath export in @t3tools/shared
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL 1,000+ effective changed lines (test files excluded in mixed PRs). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant