Skip to content

feat: deterministic source project identity via stdio proxy (#40)#140

Merged
thebtf merged 11 commits into
mainfrom
feat/deterministic-source-project
Apr 12, 2026
Merged

feat: deterministic source project identity via stdio proxy (#40)#140
thebtf merged 11 commits into
mainfrom
feat/deterministic-source-project

Conversation

@thebtf
Copy link
Copy Markdown
Owner

@thebtf thebtf commented Apr 12, 2026

Summary

  • New cmd/engram/ binary: stdio MCP proxy with git-derived project identity
  • Server enforces X-Engram-Project header for source_project attribution
  • Agents can no longer spoof source project — identity derived from git remote

Architecture

CLI (CC/Gemini/Codex) --stdio--> engram binary (local, has git) --HTTP+header--> engram server (remote)

Changes

  • internal/proxy/identity.go — ResolveProjectSlug from git remote (SHA-256, 8 hex chars)
  • cmd/engram/main.go — stdio-to-HTTP proxy with git identity + Streamable HTTP + SSE fallback
  • internal/mcp/context.go — X-Engram-Project header extraction + context pipeline
  • internal/mcp/streamable.go + sse.go — wire header extraction before tool dispatch
  • internal/mcp/tools_issues.go + tools_memory.go + tools_store_consolidated.go — use header-derived project for write ops
  • internal/config/config.go — ENGRAM_ENFORCE_SOURCE_PROJECT feature flag (default true)
  • plugin/engram/.mcp.json — switch from HTTP to command-based transport

Verified

  • go build ./... clean
  • go test ./... -count=1 all pass (8 new tests)
  • Live smoke test: proxy → unleashed.lan:37777 → correct initialize response + project header logged

Test plan

  • CI green
  • Live: build binary, run against server, verify X-Engram-Project in request headers
  • Verify observations stored with header-derived project, not agent-supplied

Closes #40

Summary by CodeRabbit

  • New Features

    • Новый интерфейс командной строки для работы с сервисом.
    • Добавлена опция конфигурации для принудительного использования идентификатора проекта из контекста запроса.
  • Chores

    • Обновлена версия плагина.
    • Переконфигурирована схема подключения плагина с HTTP на команду.

thebtf added 5 commits April 12, 2026 21:07
ResolveProjectSlug computes project slug from git remote URL,
identical to the plugin's ProjectIDWithName algorithm:
SHA-256(remoteURL + "/" + relativePath)[:8].

Worktrees produce the same hash suffix as the main repo.
Fallback to path-based slug when git is unavailable.

4 unit tests covering git repo, non-git dir, consistency, and worktree.
- context.go: extractProjectFromHeader, contextWithProject, projectFromContext
  for X-Engram-Project header → context.Context pipeline
- config.go: EnforceSourceProject bool (default true), env ENGRAM_ENFORCE_SOURCE_PROJECT
…3-T006]

- T003: ENGRAM_URL/ENGRAM_API_TOKEN env vars with CLI flag override
- T004: ResolveProjectSlug at startup, log to stderr
- T005: X-Engram-Project header on all HTTP requests
- T006: Streamable HTTP (POST /mcp) with SSE fallback
…8, T009]

- streamable.go: extract header, store in context before handleRequest
- sse.go: extract header in handleMessage, store in context
- tools_issues.go: all write handlers use projectFromContext when EnforceSourceProject=true
- tools_memory.go: store uses projectFromContext for observation project
- tools_store_consolidated.go: extract uses projectFromContext
- Read handlers (recall, issues list) unchanged — cross-project reads allowed
- .mcp.json: switch from HTTP to command-based transport (engram binary)
- plugin.json: version bump 3.7.5 → 3.7.6
- context_test.go: 4 unit tests for header extraction and context roundtrip
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 12, 2026

Warning

Rate limit exceeded

@thebtf has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 5 minutes and 16 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 5 minutes and 16 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: eb47581d-2d6d-4a19-bded-d34c690bf97e

📥 Commits

Reviewing files that changed from the base of the PR and between 30a1299 and 176c7ad.

📒 Files selected for processing (1)
  • cmd/engram/main.go

Walkthrough

Переорганизация MCP-прокси с введением контекстной передачи проекта: удалён старый cmd/mcp-stdio-proxy/main.go, добавлена новая CLI точка входа cmd/engram/main.go с поддержкой HTTP и SSE режимов; расширена конфигурация, добавлены помощники для извлечения проекта из заголовков и хранения в контексте; обновлены обработчики MCP для использования проекта из контекста с опциональным принудительным применением.

Changes

Cohort / File(s) Summary
CLI Entrypoint Migration
cmd/engram/main.go, cmd/mcp-stdio-proxy/main.go
Заменена старая SSE-только реализация на новую универсальную CLI точку входа с поддержкой режимов streamable HTTP и SSE, автоматическим определением транспорта, разрешением идентификации проекта и управлением флагами --url/--token.
Configuration & Project Context
internal/config/config.go, internal/mcp/context.go, internal/mcp/context_test.go
Добавлено новое поле конфига EnforceSourceProject, введены помощники для извлечения проекта из HTTP-заголовков и управления контекстом с тестовым покрытием.
MCP HTTP Handlers
internal/mcp/sse.go, internal/mcp/streamable.go
Оба обработчика теперь извлекают проект из заголовков запроса и передают его в контексте к handleRequest.
Project-Aware Tools
internal/mcp/tools_issues.go, internal/mcp/tools_memory.go, internal/mcp/tools_store_consolidated.go
Введена функция разрешения проекта с учётом флага EnforceSourceProject: при включении использует проект из контекста, иначе fallback на пользовательский ввод; применено во всех issue и memory обработчиках.
Project Identity Resolution
internal/proxy/identity.go, internal/proxy/identity_test.go
Добавлена новая функция ResolveProjectSlug для получения идентификации проекта на основе git удалённого и относительного пути с хешированием, включая полное тестовое покрытие с проверкой worktree.
Plugin Configuration
plugin/engram/.claude-plugin/plugin.json, plugin/engram/.mcp.json
Обновлена версия с 3.7.5 на 3.7.6; переконфигурирована интеграция с MCP с HTTP на командную конфигурацию с переменными окружения.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • feat: composite scoring v2 — per-type decay, project scoping, feedback tools #40: Оба PR изменяют internal/mcp/tools_memory.go — основной PR добавляет принудительное применение проекта из контекста, а связанный PR вводит новые обработчики памяти.
  • feat: unify and tune existing systems (roadmap Phase 2) #130: Оба PR модифицируют те же обработчики MCP (напр., handleStoreMemory, handleExtractAndOperate), где один PR добавляет контекстное проектирование, а другой интегрирует проверку дубликатов.
  • fix: Phase 1 — Security & Reliability (P0) #57: Оба PR изменяют HTTP обработчики MCP (internal/mcp/streamable.go, SSE handling) — основной PR добавляет контекстную передачу проекта, а связанный модифицирует конструктор обработчика.

Poem

🐰 Прыг-скок! Контекст летит,
Проект в заголовке сидит!
CLI новый — раз и взлёт,
От SSE к потокам ход,
Конфиг гибкий — вот успех! ✨

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning PR implements header-based project context plumbing and enforcement but does not address core requirements from #40: per-type decay, feedback tools, migrations, or project-scoped vector search fixes. Implement missing composite scoring features [#40]: per-type half-life decay, feedback tools (rate_memory, suppress_memory), database migrations for user_feedback and is_suppressed, and project-scoped vector search calls.
Out of Scope Changes check ❓ Inconclusive PR introduces stdio proxy infrastructure and project identity resolution that are foundational but not explicitly specified in #40's composite scoring requirements. Clarify whether proxy infrastructure and ResolveProjectSlug are prerequisites for #40 or should be tracked in separate tracking issue; confirm scope alignment with issue author.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR title clearly summarizes the main change: introducing a stdio proxy with git-derived deterministic source project identity.
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%.

✏️ 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/deterministic-source-project

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

@thebtf
Copy link
Copy Markdown
Owner Author

thebtf commented Apr 12, 2026

@coderabbitai review focus:security

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces an engram stdio proxy for MCP endpoints, supporting SSE and Streamable HTTP, and implements project identity resolution using git metadata. The changes include new configuration options to enforce project scoping across tool handlers via HTTP headers and context. Feedback highlights the need for increased bufio.Scanner buffer sizes to handle large messages, the addition of timeouts to HTTP clients for production readiness, and improved error handling in the proxy's forwarding logic. Refactoring redundant project resolution code and enhancing git command error reporting were also recommended.

Comment thread cmd/engram/main.go
Comment thread cmd/engram/main.go
Comment thread cmd/engram/main.go Outdated
Comment thread cmd/engram/main.go Outdated
Comment thread internal/mcp/tools_issues.go Outdated
Comment thread internal/proxy/identity.go Outdated
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 12, 2026

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Read all security-relevant files
echo "=== internal/proxy/identity.go ==="
cat internal/proxy/identity.go 2>/dev/null || fd identity.go | xargs cat

echo "=== cmd/engram/main.go ==="
cat cmd/engram/main.go 2>/dev/null || fd main.go --search-path cmd | xargs cat

echo "=== internal/mcp/context.go ==="
cat internal/mcp/context.go 2>/dev/null || fd context.go --search-path internal/mcp | xargs cat

echo "=== internal/config/config.go ==="
cat internal/config/config.go 2>/dev/null || fd config.go --search-path internal/config | xargs cat

echo "=== internal/mcp/streamable.go ==="
cat internal/mcp/streamable.go 2>/dev/null || fd streamable.go | xargs cat

echo "=== internal/mcp/sse.go ==="
cat internal/mcp/sse.go 2>/dev/null || fd sse.go | xargs cat

echo "=== internal/mcp/tools_issues.go ==="
cat internal/mcp/tools_issues.go 2>/dev/null

echo "=== internal/mcp/tools_memory.go ==="
cat internal/mcp/tools_memory.go 2>/dev/null

echo "=== internal/mcp/tools_store_consolidated.go ==="
cat internal/mcp/tools_store_consolidated.go 2>/dev/null

echo "=== plugin/engram/.mcp.json ==="
cat plugin/engram/.mcp.json 2>/dev/null || fd .mcp.json | xargs cat

Length of output: 107016


@thebtf Here is a security-focused review of PR #140:


🔒 Security Review — PR #140 (stdio proxy + project identity)

🔴 HIGH

1. CORS wildcard on authenticated endpoints

Both streamable.go and sse.go respond with:

Access-Control-Allow-Origin: *
Access-Control-Allow-Headers: Authorization, Content-Type

A wildcard Allow-Origin combined with Authorization in Allow-Headers means any web page a user visits can make credentialed cross-origin requests to the MCP server if the token is available client-side (e.g. stored in localStorage). Since the server is typically localhost or a LAN host, this also opens a browser-based port-scan / SSRF vector.

Recommendation: Restrict to a specific trusted origin, or if open access is truly required, enforce that credentials are never usable cross-origin (note: Allow-Origin: * and allow-credentials: true are mutually exclusive — but omitting allow-credentials does not protect if the JS caller sets the token manually in headers).

2. No validation on X-Engram-Project header value

extractProjectFromHeader in context.go performs a raw r.Header.Get(...) with zero sanitisation:

func extractProjectFromHeader(r *http.Request) string {
    return r.Header.Get("X-Engram-Project")
}

This value flows into database operations (project field on observations, issues, comments) and log output. There is no length cap, no character-set validation, and no format enforcement. An attacker who can reach the MCP endpoint directly (bypassing the proxy) can send arbitrary strings — including very long strings (DoS), null bytes, or SQL-special characters.

Recommendation: Add a validator, e.g.:

var validProjectSlug = regexp.MustCompile(`^[a-zA-Z0-9._\-]{1,128}$`)

func extractProjectFromHeader(r *http.Request) string {
    v := r.Header.Get("X-Engram-Project")
    if !validProjectSlug.MatchString(v) {
        return ""
    }
    return v
}

🟡 MEDIUM

3. EnforceSourceProject falls back to agent-supplied value

In every write tool handler (e.g. handleIssueCreate, handleStoreMemory), when EnforceSourceProject is true but projectFromContext(ctx) is empty, the code falls back to the agent-supplied m["project"]:

if config.Get().EnforceSourceProject {
    sourceProject = projectFromContext(ctx)
    if sourceProject == "" {          // ← fallback breaks the guarantee
        sourceProject = coerceString(m["project"], "")
    }
}

An agent that reaches the server directly (without the stdio proxy) sends no X-Engram-Project header → context value is "" → agent-supplied project is accepted. The PR's stated security property ("agents can no longer spoof source project") is therefore only enforced when the proxy is in the path.

Recommendation: When EnforceSourceProject=true and the header is absent, either reject the write operation with an explicit error, or apply a fixed sentinel (e.g. "unknown") rather than trusting the agent payload. Document the proxy as a required component, not optional.

4. Git remote URL with embedded credentials logged to stderr

In main.go:

fmt.Fprintf(os.Stderr, "[engram] git remote: %s\n", remote)

If the remote URL is https://user:ghp_token@github.com/org/repo, the token appears in stderr output — which may be captured by process supervisors, log aggregators, or shell history.

Recommendation: Scrub credentials from the URL before logging:

// strip userinfo before logging
if u, err := url.Parse(remote); err == nil {
    u.User = nil
    fmt.Fprintf(os.Stderr, "[engram] git remote: %s\n", u.String())
}

5. No body size limits in the proxy (cmd/engram/main.go)

_, _ = io.Copy(os.Stdout, resp.Body)        // runStreamableHTTP
body, err := io.ReadAll(resp.Body)           // tryStreamableHTTP

A malicious or compromised server can stream an unbounded response, causing OOM. Similarly, bufio.NewScanner(os.Stdin) uses the default 64 KB token buffer — lines exceeding that are silently truncated, corrupting JSON-RPC frames.

Recommendation:

resp.Body = http.MaxBytesReader(nil, resp.Body, 10<<20) // 10 MB cap
scanner.Buffer(make([]byte, 1<<20), 1<<20)              // 1 MB scanner buffer

6. SSE endpoint event enables SSRF via server-controlled path

In runSSE, the server-supplied data: field from the endpoint SSE event is passed to resolveMessageEndpoint and used as the POST target for all stdin data:

case "endpoint":
    if messageEndpoint == "" {
        messageEndpoint = resolveMessageEndpoint(serverURL, data)
        go forwardStdin(messageEndpoint, authToken, projectSlug)
    }

A compromised MCP server (or a MitM on an HTTP connection) can send data: http://internal-service:8080/admin and redirect all agent JSON-RPC messages — including auth tokens — to an internal host.

Recommendation: Validate that the resolved endpoint shares the same scheme+host as serverURL before using it, and enforce HTTPS for remote URLs.


🔵 LOW

7. --token flag value visible in ps output

The Bearer token is accepted via flag.String("token", ...). On Linux/macOS, command-line arguments are readable by all users via /proc/<pid>/cmdline or ps aux.

Recommendation: Accept the token only via environment variable (ENGRAM_API_TOKEN), or read it from a file path flag instead.

8. 32-bit project hash collision space

The primary slug uses 8 hex characters of SHA-256 (32 bits). With ~65,000 distinct git remotes the collision probability exceeds 50% (birthday bound). While not immediately exploitable, a collision causes one project's identity to be attributed to another.

Recommendation: Increase to 12 hex chars (48 bits) to match the fallback's safety margin, or document the collision probability explicitly.

9. X-Engram-Project missing from CORS Access-Control-Allow-Headers

The CORS preflight in both handlers lists only Authorization, Content-Type. Browser clients (if any) setting X-Engram-Project would be blocked by preflight, creating an inconsistency with the curl/native client path.


Summary Table

# Severity File Finding
1 🔴 HIGH streamable.go, sse.go CORS wildcard on authenticated endpoints
2 🔴 HIGH context.go No validation of X-Engram-Project header value
3 🟡 MEDIUM all tool handlers EnforceSourceProject fallback allows agent spoofing without proxy
4 🟡 MEDIUM main.go Git remote URL with credentials logged to stderr
5 🟡 MEDIUM main.go No body/scanner size limits → OOM + silent JSON truncation
6 🟡 MEDIUM main.go SSE endpoint SSRF via server-controlled redirect path
7 🔵 LOW main.go Bearer token exposed in process listing via --token flag
8 🔵 LOW identity.go 32-bit hash collision space for project slug
9 🔵 LOW streamable.go, sse.go X-Engram-Project missing from CORS Allow-Headers
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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: 5

🧹 Nitpick comments (5)
internal/proxy/identity.go (1)

51-74: Рассмотрите использование единого контекста для обеих git-операций.

Функция getGitInfo создаёт два отдельных контекста с таймаутами (строки 52-53 и 64-65). Если первая команда git выполняется близко к лимиту в 3 секунды, общее время ожидания может достигнуть 6 секунд. Возможно, стоит использовать единый контекст с общим таймаутом для обеих операций.

Текущая реализация функциональна, так как это происходит только один раз при старте прокси и не блокирует критический путь.

♻️ Опциональный рефакторинг с единым контекстом
 func getGitInfo(cwd string) (remoteURL, relativePath string, err error) {
-	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
+	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 	defer cancel()

 	rawRemote, err := runGit(ctx, cwd, "remote", "get-url", "origin")
 	if err != nil {
 		return "", "", err
 	}
 	remoteURL = strings.TrimSpace(rawRemote)
 	if remoteURL == "" {
 		return "", "", fmt.Errorf("empty remote URL")
 	}

-	ctx2, cancel2 := context.WithTimeout(context.Background(), 3*time.Second)
-	defer cancel2()
-
-	rawPrefix, err := runGit(ctx2, cwd, "rev-parse", "--show-prefix")
+	rawPrefix, err := runGit(ctx, cwd, "rev-parse", "--show-prefix")
 	if err != nil {
 		return "", "", err
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/proxy/identity.go` around lines 51 - 74, getGitInfo creates two
separate 3s contexts so the two runGit calls can cumulatively wait up to 6s;
change it to create a single context with timeout (e.g. context.WithTimeout(...,
3*time.Second)) at the start of getGitInfo and reuse that ctx for both runGit
calls (remote get-url and rev-parse) and call cancel once via defer; update
references to ctx2/cancel2 to use the single ctx and cancel to ensure both
operations share the same overall timeout while keeping function behavior
otherwise unchanged.
internal/proxy/identity_test.go (1)

114-121: Windows-специфичная нормализация путей.

Строки 116-117 используют жёстко заданный разделитель \ для Windows. На Unix-системах это приведёт к некорректному сравнению путей. Рассмотрите использование filepath.Clean и strings.EqualFold без ручной замены разделителей, либо используйте filepath.ToSlash для унификации.

♻️ Кроссплатформенная нормализация путей
-		// Normalise separators for comparison.
-		normPath := strings.ReplaceAll(path, "/", `\`)
-		normMain := strings.ReplaceAll(mainRepo, "/", `\`)
-		if !strings.EqualFold(normPath, normMain) {
+		// Normalise using filepath for cross-platform compatibility.
+		normPath := filepath.Clean(path)
+		normMain := filepath.Clean(mainRepo)
+		if !strings.EqualFold(normPath, normMain) {
 			worktreePaths = append(worktreePaths, path)
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/proxy/identity_test.go` around lines 114 - 121, The Windows-only
replacement of separators (`strings.ReplaceAll(..., "/", "\\")`) causes
incorrect comparisons on non-Windows platforms; in the block handling `path`,
replace the manual ReplaceAll logic with cross-platform normalization using
`filepath.Clean` and a consistent slash form (e.g., `filepath.ToSlash`) for both
`path` and `mainRepo` before comparing with `strings.EqualFold`, and add the
`path/filepath` import; keep the same logic that appends to `worktreePaths` when
the cleaned, normalized values (e.g., `normPath :=
filepath.ToSlash(filepath.Clean(path))` and `normMain :=
filepath.ToSlash(filepath.Clean(mainRepo))`) are not equal by
`strings.EqualFold`.
internal/mcp/tools_issues.go (1)

330-338: Рассмотрите возможность выделения в хелпер-функцию.

Один и тот же 8-строчный блок логики повторяется 5 раз в этом файле (create, update, comment, reopen, close). Это можно было бы вынести в хелпер:

♻️ Опциональный рефакторинг
// resolveSourceProject returns project from context when enforcement is enabled,
// falling back to the explicit parameter.
func resolveSourceProject(ctx context.Context, m map[string]any) string {
	if config.Get().EnforceSourceProject {
		if p := projectFromContext(ctx); p != "" {
			return p
		}
	}
	return coerceString(m["project"], "")
}

Использование:

sourceProject := resolveSourceProject(ctx, m)

Also applies to: 368-376, 398-406, 422-430

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

In `@internal/mcp/tools_issues.go` around lines 330 - 338, Extract the repeated
8-line logic that determines sourceProject into a helper function (e.g.,
resolveSourceProject(ctx context.Context, m map[string]any) string) that uses
config.Get().EnforceSourceProject, projectFromContext(ctx) and falls back to
coerceString(m["project"], "") when needed; then replace the duplicated blocks
in the create/update/comment/reopen/close handlers with a single call
sourceProject := resolveSourceProject(ctx, m) to reduce duplication and improve
clarity.
cmd/engram/main.go (2)

103-104: Ограничение размера буфера Scanner.

bufio.Scanner по умолчанию имеет буфер 64KB. Если JSON-RPC сообщение превысит этот размер, оно будет обрезано. Рекомендуется увеличить буфер для надёжности.

🛡️ Предлагаемое исправление
 scanner := bufio.NewScanner(os.Stdin)
+scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024) // 1MB max
 for scanner.Scan() {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/engram/main.go` around lines 103 - 104, The bufio.Scanner created with
bufio.NewScanner(os.Stdin) uses a 64KB token buffer by default, so increase its
capacity by calling scanner.Buffer(...) to provide a larger initial slice and a
larger max token size (e.g., several megabytes) to avoid truncating large
JSON-RPC messages; modify the code around the scanner variable and the loop
where scanner.Scan() is used (referencing scanner and bufio.NewScanner) to call
scanner.Buffer and then after the loop check scanner.Err() and handle/report any
scanning error.

162-167: SSE-соединение также требует таймаута на подключение.

Для SSE используется долгоживущее соединение, но начальное подключение должно иметь таймаут. Рассмотрите использование контекста с таймаутом для начального запроса.

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

In `@cmd/engram/main.go` around lines 162 - 167, The SSE connection currently uses
http.DefaultClient.Do(req) without a connection timeout; wrap the initial
request in a context with timeout (e.g., ctx, cancel :=
context.WithTimeout(req.Context(), connectTimeout) and defer cancel()) and
attach it to the request via req = req.WithContext(ctx) before calling
http.DefaultClient.Do(req) so the initial connect will fail after the timeout
while still allowing the long-lived resp.Body stream to continue once
established.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cmd/engram/main.go`:
- Around line 227-229: В функции forwardStdin замените тихий выход с os.Exit(0)
на корректный обработчик ошибки: логируйте ошиб (через log.Fatalf /
processLogger или fmt.Fprintf(os.Stderr,...)) и завершайте процесс с ненулевым
кодом (например os.Exit(1)); если поведение с кодом 0 было намеренным, оставьте
os.Exit(0) и добавьте явный комментарий с объяснением причины. Проверьте и
аналогично исправьте второе место (строки около 241-243) чтобы все ошибки
HTTP/запросов не маскировались нулевым кодом выхода, ссылаясь на функцию
forwardStdin и соответствующие os.Exit вызовы.
- Around line 77-80: Исправьте использование http.DefaultClient без таймаута:
добавьте импорт "time", создайте в том же пакете глобальный или локальный
httpClient := &http.Client{Timeout: 10 * time.Second} (или другой подходящий
таймаут) и замените все вызовы http.DefaultClient.Do(req) (включая места, где
используются переменные resp/req) на httpClient.Do(req); убедитесь, что
обработка ошибок и закрытие тела ответа остаются прежними.

In `@internal/proxy/identity_test.go`:
- Around line 69-92: The test uses a hardcoded Windows path (const repoDir =
`D:\Dev\engram`) which breaks portability; change
TestResolveProjectSlug_ConsistentAcrossCalls to create and use a
platform-independent temporary directory (e.g., call t.TempDir() or os.MkdirTemp
and assign to repoDir) and use that variable for both ResolveProjectSlug calls
so the test runs consistently across OSes; update the reference to repoDir in
the test and ensure any necessary minimal setup for a repo path is performed
before calling proxy.ResolveProjectSlug.
- Around line 94-125: The test TestResolveProjectSlug_WorktreeMatchesMain (and
the other two tests that share the same hard-coded mainRepo constant) use a
fixed path `D:\Dev\engram`, which breaks on other machines; replace that
constant with a dynamic setup: either discover the repository root at runtime
(e.g., use os.Getwd or read GIT_DIR to locate the current repo) or create an
isolated test repo using t.TempDir() and run git init + any required git
worktree commands to produce the same conditions; update the constant mainRepo
and any test helpers in TestResolveProjectSlug_WorktreeMatchesMain (and the two
sibling tests) to use the dynamic path so the tests run in any environment.
- Around line 16-42: The test TestResolveProjectSlug_GitRepo uses a hard-coded
Windows path (repoDir = `D:\Dev\engram`) which breaks portability; replace that
with a dynamic repo root lookup (e.g., add a helper like findRepoRoot that runs
`git rev-parse --show-toplevel` and trims whitespace) and pass its result to
proxy.ResolveProjectSlug instead of the fixed path; ensure the helper fails the
test via t.Fatalf on error so ResolveProjectSlug is always called with a valid
repo root.

---

Nitpick comments:
In `@cmd/engram/main.go`:
- Around line 103-104: The bufio.Scanner created with bufio.NewScanner(os.Stdin)
uses a 64KB token buffer by default, so increase its capacity by calling
scanner.Buffer(...) to provide a larger initial slice and a larger max token
size (e.g., several megabytes) to avoid truncating large JSON-RPC messages;
modify the code around the scanner variable and the loop where scanner.Scan() is
used (referencing scanner and bufio.NewScanner) to call scanner.Buffer and then
after the loop check scanner.Err() and handle/report any scanning error.
- Around line 162-167: The SSE connection currently uses
http.DefaultClient.Do(req) without a connection timeout; wrap the initial
request in a context with timeout (e.g., ctx, cancel :=
context.WithTimeout(req.Context(), connectTimeout) and defer cancel()) and
attach it to the request via req = req.WithContext(ctx) before calling
http.DefaultClient.Do(req) so the initial connect will fail after the timeout
while still allowing the long-lived resp.Body stream to continue once
established.

In `@internal/mcp/tools_issues.go`:
- Around line 330-338: Extract the repeated 8-line logic that determines
sourceProject into a helper function (e.g., resolveSourceProject(ctx
context.Context, m map[string]any) string) that uses
config.Get().EnforceSourceProject, projectFromContext(ctx) and falls back to
coerceString(m["project"], "") when needed; then replace the duplicated blocks
in the create/update/comment/reopen/close handlers with a single call
sourceProject := resolveSourceProject(ctx, m) to reduce duplication and improve
clarity.

In `@internal/proxy/identity_test.go`:
- Around line 114-121: The Windows-only replacement of separators
(`strings.ReplaceAll(..., "/", "\\")`) causes incorrect comparisons on
non-Windows platforms; in the block handling `path`, replace the manual
ReplaceAll logic with cross-platform normalization using `filepath.Clean` and a
consistent slash form (e.g., `filepath.ToSlash`) for both `path` and `mainRepo`
before comparing with `strings.EqualFold`, and add the `path/filepath` import;
keep the same logic that appends to `worktreePaths` when the cleaned, normalized
values (e.g., `normPath := filepath.ToSlash(filepath.Clean(path))` and `normMain
:= filepath.ToSlash(filepath.Clean(mainRepo))`) are not equal by
`strings.EqualFold`.

In `@internal/proxy/identity.go`:
- Around line 51-74: getGitInfo creates two separate 3s contexts so the two
runGit calls can cumulatively wait up to 6s; change it to create a single
context with timeout (e.g. context.WithTimeout(..., 3*time.Second)) at the start
of getGitInfo and reuse that ctx for both runGit calls (remote get-url and
rev-parse) and call cancel once via defer; update references to ctx2/cancel2 to
use the single ctx and cancel to ensure both operations share the same overall
timeout while keeping function behavior otherwise unchanged.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0b491792-41e9-4fd8-bd68-1057394031ae

📥 Commits

Reviewing files that changed from the base of the PR and between 1ea9ace and 5b6f5be.

📒 Files selected for processing (14)
  • cmd/engram/main.go
  • cmd/mcp-stdio-proxy/main.go
  • internal/config/config.go
  • internal/mcp/context.go
  • internal/mcp/context_test.go
  • internal/mcp/sse.go
  • internal/mcp/streamable.go
  • internal/mcp/tools_issues.go
  • internal/mcp/tools_memory.go
  • internal/mcp/tools_store_consolidated.go
  • internal/proxy/identity.go
  • internal/proxy/identity_test.go
  • plugin/engram/.claude-plugin/plugin.json
  • plugin/engram/.mcp.json
💤 Files with no reviewable changes (1)
  • cmd/mcp-stdio-proxy/main.go

Comment thread cmd/engram/main.go Outdated
Comment thread cmd/engram/main.go
Comment thread internal/proxy/identity_test.go
Comment thread internal/proxy/identity_test.go
Comment thread internal/proxy/identity_test.go
- Replace http.DefaultClient with httpClient (30s timeout) in cmd/engram/main.go
  to prevent indefinite hangs on unresponsive servers (MAJOR)
- Add stderr logging to forwardStdin error paths; explain intentional exit 0
  rationale in comments (MINOR)
- Replace hardcoded D:\Dev\engram paths in identity_test.go with findRepoRoot()
  helper that runs git rev-parse --show-toplevel (MAJOR x3)
- Use filepath.Clean for cross-platform worktree path comparison (MINOR)
- Fix test assertion to check slug prefix against actual dirName, not hardcoded
  "engram_" — allows tests to pass from any worktree

Reviewers: CodeRabbit
Comment ids: PRRC_kwDORY-ek862--Xc, PRRC_kwDORY-ek862--Xk, PRRC_kwDORY-ek862--Xm,
             PRRC_kwDORY-ek862--Xo, PRRC_kwDORY-ek862--Xh
- internal/proxy/identity.go: use single shared context for both git calls
  in getGitInfo so total wall time is bounded to 5s (not 6s)
- internal/proxy/identity.go: capture stderr from failed git commands via
  exec.Cmd.Stderr so error messages include diagnostic text (e.g. "not a git
  repository") rather than bare exit-status strings
- cmd/engram/main.go: increase bufio.Scanner buffer to 4 MB on all three
  scanners (runStreamableHTTP stdin, runSSE body, forwardStdin stdin) to
  handle large JSON-RPC messages without truncation

Comment ids: coderabbit-nitpick-c5b27a4b-51, coderabbit-nitpick-354bdef6-103,
             PRRC_kwDORY-ek862-6uq, PRRC_kwDORY-ek862-6ur, PRRC_kwDORY-ek862-6u6
@thebtf
Copy link
Copy Markdown
Owner Author

thebtf commented Apr 12, 2026

🤖 PR Review MCP State (auto-managed, do not edit)
{
  "version": 2,
  "parentChildren": {},
  "resolvedNitpicks": {
    "coderabbit-nitpick-c5b27a4b-51": {
      "resolvedAt": "2026-04-12T18:37:54.620Z",
      "resolvedBy": "agent"
    },
    "coderabbit-nitpick-dcf30349-114": {
      "resolvedAt": "2026-04-12T18:37:58.293Z",
      "resolvedBy": "agent"
    },
    "coderabbit-nitpick-354bdef6-103": {
      "resolvedAt": "2026-04-12T18:38:01.315Z",
      "resolvedBy": "agent"
    },
    "coderabbit-nitpick-c5c109b8-330": {
      "resolvedAt": "2026-04-12T18:41:45.809Z",
      "resolvedBy": "agent"
    },
    "coderabbit-nitpick-c2356718-162": {
      "resolvedAt": "2026-04-12T18:43:59.996Z",
      "resolvedBy": "agent"
    },
    "coderabbit-nitpick-2b5e5370-240": {
      "resolvedAt": "2026-04-12T18:48:36.361Z",
      "resolvedBy": "agent"
    }
  },
  "updatedAt": "2026-04-12T18:48:36.720Z"
}

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: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cmd/engram/main.go`:
- Around line 52-53: Вместо прямого вывода переменной remote в stderr (в коде
рядом с вызовом ResolveProjectSlug и переменной remote) уберите возможный
userinfo: распарсьте remote через net/url.Parse, обнулите поле User (или
извлеките только Host+Path) и логируйте безопасную часть (host/path) либо строку
URL без userinfo; используйте полученное значение в fmt.Fprintf вместо исходного
remote, чтобы предотвратить утечку токенов в логах/CI.
- Around line 138-142: Удалите лишний вызов fmt.Fprintln(os.Stdout) в
cmd/engram/main.go: оставьте io.Copy(os.Stdout, resp.Body) и resp.Body.Close(),
но уберите строку, которая добавляет дополнительный перевод строки; серверная
сторона (json.NewEncoder(w).Encode) уже добавляет нужную newline, поэтому
удаление fmt.Fprintln устранит пустую строку между JSON-документами.
- Line 110: The bufio.Scanner instances used to read JSON-RPC from stdin and SSE
event streams in runStreamableHTTP and forwardStdin hit the default 64KiB token
limit; update each scanner (the scanner created with bufio.NewScanner in
runStreamableHTTP, the SSE response scanner in the event stream handling, and
the scanner in forwardStdin) to call scanner.Buffer(make([]byte,
bufio.MaxScanTokenSize), maxFrameSize) (or similar) to raise the max token size
to the provided maxFrameSize constant; apply this change to all three scanner
variables so long JSON-RPC messages and SSE events don’t produce "token too
long" errors.
- Around line 229-263: forwardStdin currently calls os.Exit(0) on EOF and on
POST errors which forcibly kills the whole process from a goroutine; change
forwardStdin to stop calling os.Exit and instead signal its completion/error to
the SSE controller (runSSE/main) via a channel or an errgroup.Context so the
main reader can finish processing buffered SSE messages before exiting.
Specifically, remove os.Exit calls in forwardStdin, have forwardStdin return (or
send an error/nil on a provided doneCh/errorCh) when scanner.Scan() ends or
httpClient.Do fails, and update the caller of forwardStdin/runSSE to wait on
that signal and perform graceful shutdown/finalization and then exit if
appropriate.
- Around line 94-101: The probe in tryStreamableHTTP currently uses
io.ReadAll(resp.Body) which can exhaust memory for large responses; change it to
read with a size limit (e.g., wrap resp.Body with io.LimitReader or use
json.Decoder with an enforced limit) and validate JSON only within that limit.
Add a clear constant like maxProbeBodySize and replace the io.ReadAll call with
a limited read (or stream-decoding) so json.Unmarshal/json.Decoder only sees at
most maxProbeBodySize bytes and the probe returns an error if the response
exceeds that limit.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b79cfa1e-8272-4a6a-95b0-c234056c7f92

📥 Commits

Reviewing files that changed from the base of the PR and between 5b6f5be and bacc2cf.

📒 Files selected for processing (2)
  • cmd/engram/main.go
  • internal/proxy/identity_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/proxy/identity_test.go

Comment thread cmd/engram/main.go Outdated
Comment thread cmd/engram/main.go Outdated
Comment thread cmd/engram/main.go
Comment thread cmd/engram/main.go Outdated
Comment thread cmd/engram/main.go Outdated
thebtf added 3 commits April 12, 2026 21:41
- internal/proxy/identity.go: use a single shared context for both runGit
  calls in getGitInfo so the total timeout is bounded to 5s (not 6s)
- internal/proxy/identity.go: capture git stderr in runGit error messages
  so "git identity failed" log includes diagnostic text (e.g. "not a git
  repository") instead of a bare exit-status
- internal/mcp/tools_issues.go: extract repeated 8-line sourceProject
  resolution into resolveSourceProject helper; replace all 5 duplicate
  blocks across create/update-comment/comment/reopen/close handlers

Note: resolveSourceProject preserves the existing fallback-to-parameter
semantics when the header is absent under EnforceSourceProject — changing
that behavior to an error would be a breaking design change requiring
explicit user approval.

Comment ids: coderabbit-nitpick-c5b27a4b-51, coderabbit-nitpick-c5c109b8-330,
             PRRC_kwDORY-ek862-6u3, PRRC_kwDORY-ek862-6u6
…tions

The shared httpClient has a 30s Timeout which would kill any SSE stream
after 30 seconds. Introduce a separate sseClient backed by a custom
Transport with a 15s dial/TLS-handshake timeout but no response timeout,
so the initial connection fails fast while the long-lived stream stays open.

httpClient (30s timeout) — probe ping, Streamable HTTP POSTs
sseClient (dial-timeout only) — SSE GET + SSE message POSTs

Comment id: coderabbit-nitpick-c2356718-162
…ndings

- Redact userinfo from git remote URL before logging to stderr to prevent
  token leaks in CI output and bug reports (MAJOR)
  Uses net/url.Parse → u.User = nil to strip embedded credentials

- Limit probe response body to 64 KB using io.LimitReader to prevent an
  untrusted server from exhausting proxy memory (MAJOR)

- Remove extra fmt.Fprintln(os.Stdout) after io.Copy in runStreamableHTTP;
  json.NewEncoder on the server already appends a newline — the extra call
  was emitting a blank line between JSON documents, breaking JSON Lines
  consumers (MINOR)

- Refactor forwardStdin to accept a done chan<-struct{} and close it on
  return instead of calling os.Exit from a goroutine; runSSE now waits on
  that channel before exiting so in-flight POSTs can complete gracefully
  without racing against os.Exit (MAJOR)

Comment ids: PRRC_kwDORY-ek862_EKm, PRRC_kwDORY-ek862_EKo,
             PRRC_kwDORY-ek862_EKv, PRRC_kwDORY-ek862_EKx
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: 1

♻️ Duplicate comments (3)
cmd/engram/main.go (3)

143-144: ⚠️ Potential issue | 🟡 Minor

Удалите лишний вызов fmt.Fprintln.

Серверная сторона использует json.NewEncoder(w).Encode(...), который добавляет символ новой строки после JSON. io.Copy копирует полное тело ответа включая эту новую строку. Дополнительный fmt.Fprintln создаёт пустую строку между JSON-документами, что может нарушить парсинг JSON Lines у клиентов.

,

🔧 Предлагаемое исправление
 		_, _ = io.Copy(os.Stdout, resp.Body)
 		_ = resp.Body.Close()
-
-		// Ensure newline separator between responses
-		fmt.Fprintln(os.Stdout)
 	}

Если сервер не гарантирует trailing newline, лучше проверять последний байт ответа и добавлять newline условно.

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

In `@cmd/engram/main.go` around lines 143 - 144, Remove the extraneous
fmt.Fprintln call that writes an extra newline after copying the response;
instead either delete the fmt.Fprintln(os.Stdout) invocation in main (or the
function containing it) or replace it with conditional logic that inspects the
last byte of the copied response and only writes a '\n' if the response does not
already end with one (so that json.NewEncoder(w).Encode(...) which already
appends '\n' is not duplicated).

52-54: ⚠️ Potential issue | 🟠 Major

Санируйте git remote перед логированием.

Переменная remote может содержать встроенные учётные данные в формате https://token@host/.... Прямое логирование в stderr приводит к утечке секретов в логи и CI.

,

🛡️ Предлагаемое исправление
+import "net/url"
+
+// sanitizeRemote removes userinfo (credentials) from a URL for safe logging.
+func sanitizeRemote(remote string) string {
+	u, err := url.Parse(remote)
+	if err != nil {
+		return "<invalid-url>"
+	}
+	u.User = nil
+	return u.String()
+}

Затем замените:

 if remote != "" {
-	fmt.Fprintf(os.Stderr, "[engram] git remote: %s\n", remote)
+	fmt.Fprintf(os.Stderr, "[engram] git remote: %s\n", sanitizeRemote(remote))
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/engram/main.go` around lines 52 - 54, The code logs the raw git remote in
the variable remote which may include embedded credentials (e.g.
https://token@host/...), so parse remote using net/url (url.Parse), clear the
User info (e.g. set u.User = nil) and then log the sanitized URL (u.String());
update the fmt.Fprintf call that prints remote to use the sanitized string and
fall back to a redacted placeholder if parsing fails or the URL contains
unexpected sensitive parts.

94-101: ⚠️ Potential issue | 🟠 Major

Ограничьте размер тела ответа в probe-запросе.

io.ReadAll(resp.Body) читает весь ответ без лимита. Недоверенный сервер может исчерпать память прокси, отправив произвольно большой ответ. Для probe, которому нужна только валидация JSON, достаточно небольшого лимита.

,

🛡️ Предлагаемое исправление
+const maxProbeBodySize = 64 * 1024 // 64 KB достаточно для JSON-RPC ответа
+
 func tryStreamableHTTP(serverURL, authToken, projectSlug string) bool {
 	...
-	body, err := io.ReadAll(resp.Body)
+	body, err := io.ReadAll(io.LimitReader(resp.Body, maxProbeBodySize))
 	if err != nil {
 		return false
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/engram/main.go` around lines 94 - 101, The probe currently uses
io.ReadAll(resp.Body) which can load an unbounded response into memory; replace
this with a size-limited read using io.LimitReader(resp.Body, <maxBytes>) (or
http.MaxBytesReader) when reading into body in the probe function, then pass the
limited body to json.Unmarshal (the check json.RawMessage and json.Unmarshal
call) and handle truncated responses (e.g., treat reads that hit the limit as
invalid and return false or an appropriate error) so an untrusted server cannot
exhaust memory.
🧹 Nitpick comments (1)
cmd/engram/main.go (1)

240-246: Рассмотрите замену os.Exit на channel-based сигнализацию.

Понимаю обоснование в комментариях — exit code 0 чтобы не маскировать здоровую SSE сессию. Однако os.Exit() из горутины обходит все deferred функции и делает невозможным graceful shutdown (например, финализация буферизованных SSE сообщений перед выходом).

Альтернатива: использовать канал или context.CancelFunc для сигнализации завершения forwardStdin, позволяя основному reader'у контролировать жизненный цикл процесса.

Also applies to: 257-263

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

In `@cmd/engram/main.go` around lines 240 - 246, The goroutine in main.go
currently calls os.Exit(0) on forward error which bypasses deferred cleanup;
change this to signal the main SSE reader instead (e.g., add a shared done
channel or pass a context with CancelFunc into forwardStdin) so forwardStdin
sends a non-fatal completion/error signal instead of exiting; update the caller
and the SSE reader goroutine to listen for that channel/context cancellation and
perform graceful shutdown/flush of buffered SSE messages; apply the same change
to the other os.Exit usage around the forwardStdin/error handling (the second
block at 257-263) so all early exits use the same channel/context-based
signaling.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cmd/engram/main.go`:
- Around line 212-216: The SSE-supplied messageEndpoint is used directly for
POSTs (forwardStdin) which creates an SSRF risk; update
resolveMessageEndpoint/runSSE flow so the resolved URL is parsed and validated
before use: parse messageEndpoint and serverURL, ensure the origins (scheme +
host + port) match the serverURL origin, reject or ignore endpoints that fail
validation (log an error) and only call forwardStdin(messageEndpoint, authToken,
projectSlug) when validation succeeds; reference resolveMessageEndpoint, runSSE,
messageEndpoint, and forwardStdin when making this change.

---

Duplicate comments:
In `@cmd/engram/main.go`:
- Around line 143-144: Remove the extraneous fmt.Fprintln call that writes an
extra newline after copying the response; instead either delete the
fmt.Fprintln(os.Stdout) invocation in main (or the function containing it) or
replace it with conditional logic that inspects the last byte of the copied
response and only writes a '\n' if the response does not already end with one
(so that json.NewEncoder(w).Encode(...) which already appends '\n' is not
duplicated).
- Around line 52-54: The code logs the raw git remote in the variable remote
which may include embedded credentials (e.g. https://token@host/...), so parse
remote using net/url (url.Parse), clear the User info (e.g. set u.User = nil)
and then log the sanitized URL (u.String()); update the fmt.Fprintf call that
prints remote to use the sanitized string and fall back to a redacted
placeholder if parsing fails or the URL contains unexpected sensitive parts.
- Around line 94-101: The probe currently uses io.ReadAll(resp.Body) which can
load an unbounded response into memory; replace this with a size-limited read
using io.LimitReader(resp.Body, <maxBytes>) (or http.MaxBytesReader) when
reading into body in the probe function, then pass the limited body to
json.Unmarshal (the check json.RawMessage and json.Unmarshal call) and handle
truncated responses (e.g., treat reads that hit the limit as invalid and return
false or an appropriate error) so an untrusted server cannot exhaust memory.

---

Nitpick comments:
In `@cmd/engram/main.go`:
- Around line 240-246: The goroutine in main.go currently calls os.Exit(0) on
forward error which bypasses deferred cleanup; change this to signal the main
SSE reader instead (e.g., add a shared done channel or pass a context with
CancelFunc into forwardStdin) so forwardStdin sends a non-fatal completion/error
signal instead of exiting; update the caller and the SSE reader goroutine to
listen for that channel/context cancellation and perform graceful shutdown/flush
of buffered SSE messages; apply the same change to the other os.Exit usage
around the forwardStdin/error handling (the second block at 257-263) so all
early exits use the same channel/context-based signaling.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 017e2b41-221c-4714-a6d9-bd50598a20b7

📥 Commits

Reviewing files that changed from the base of the PR and between bacc2cf and 30a1299.

📒 Files selected for processing (3)
  • cmd/engram/main.go
  • internal/mcp/tools_issues.go
  • internal/proxy/identity.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • internal/mcp/tools_issues.go
  • internal/proxy/identity.go

Comment thread cmd/engram/main.go
The SSE server delivers a message endpoint URL which the proxy uses as the
target for stdin POST forwarding. A malicious server could supply an arbitrary
URL and redirect proxy traffic to internal or third-party services.

Add validateEndpoint(baseURL, endpoint) that checks the resolved endpoint's
host against the configured server host and rejects any mismatch. Relative
paths (empty host) are allowed as they inherit the base origin.

Comment id: PRRC_kwDORY-ek862_Hbq
@thebtf thebtf merged commit c961779 into main Apr 12, 2026
1 of 2 checks passed
@thebtf thebtf deleted the feat/deterministic-source-project branch April 12, 2026 18:50
thebtf added a commit that referenced this pull request Apr 12, 2026
…arseSync

- Dockerfile: update build path after cmd/mcp-stdio-proxy was renamed to cmd/engram in PR #140
- useMarkdown.ts: marked.parseSync → marked.parse (parseSync removed in marked v13+)
thebtf added a commit that referenced this pull request Apr 12, 2026
* fix(ui): replace removed parseSync with parse in marked v18

marked v13+ removed parseSync(). The parse() method is synchronous
by default (unless async: true is passed). Fixes CI build failure.

* fix: Dockerfile reference cmd/mcp-stdio-proxy → cmd/engram + marked parseSync

- Dockerfile: update build path after cmd/mcp-stdio-proxy was renamed to cmd/engram in PR #140
- useMarkdown.ts: marked.parseSync → marked.parse (parseSync removed in marked v13+)

---------

Co-authored-by: Kirill Turanskiy <thebtf@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant