Skip to content

fix: use service GITHUB_TOKEN for git push + GitHub API calls#10

Merged
sweetmantech merged 2 commits into
mainfrom
feat/service-token-for-push
Apr 23, 2026
Merged

fix: use service GITHUB_TOKEN for git push + GitHub API calls#10
sweetmantech merged 2 commits into
mainfrom
feat/service-token-for-push

Conversation

@sweetmantech
Copy link
Copy Markdown
Contributor

@sweetmantech sweetmantech commented Apr 22, 2026

Summary

Fixes the "Permission denied. Check your GitHub access." error when clicking the "Commit and Push" button for org-backed sessions.

Root cause

In apps/web/app/api/generate-pr/route.ts, the push-auth setup was guarded by:

if (sessionRecord.repoOwner && sessionRecord.repoName) {
  userToken = await getUserGitHubToken(session.user.id);
  const authUrl = buildGitHubAuthRemoteUrl({ token: userToken, ... });
  await sandbox.exec(\`git remote set-url origin "${authUrl}"\`, cwd, 5000);
}

After PR #7 switched session creation to the OrgSelector flow (createBlankSession(cloneUrl)), sessions only have cloneUrl populated — repoOwner/repoName are null. The guard short-circuited, the remote stayed as https://github.com/recoupable/org-… with no credentials, and the subsequent git push got "Permission denied" from GitHub.

Fix

  • New helper apps/web/lib/github/ensure-authenticated-origin.ts — parses owner/repo from the session's cloneUrl and rewrites the sandbox's origin remote to https://x-access-token:${GITHUB_TOKEN}@github.com/<owner>/<repo>.git using the service GITHUB_TOKEN.
  • /api/generate-pr replaces the buggy guard with the helper. Fork-fallback block is now dead code (userToken is always null) — will be removed in PR B.
  • 7 GitHub-API-op sites swapped to the service token:
    • /api/pr, /api/check-pr
    • /api/sessions/[sessionId]/{merge,merge-readiness,close-pr,checks/fix}
    • app/api/chat/_lib/runtime.ts, lib/sandbox/archive-session.ts
  • auto-commit-direct.ts / auto-pr-direct.ts also swap to the service token (no behavior change for org sessions today since they're gated elsewhere by repoOwner+repoName, but brings their token source into alignment).

All repos are recoupable-owned per PR #7 policy, so the service token always has access. The per-user OAuth token path is no longer needed for these operations.

What's NOT in this PR

Per scope discussion, PR A is the focused fix. PR B (follow-up) removes:

  • getUserGitHubToken / lib/github/user-token.ts entirely
  • The fork-push fallback block in generate-pr/route.ts
  • ~15 UI components/hooks tied to personal GitHub account connection (repo selector, branch picker, GitHub reconnect dialog, settings accounts section)
  • ~8 user-identity API routes (/api/github/{user,orgs,connection-status,installations/repos,branches,create-repo,app/callback,orgs/install-status})

Test plan

  • Deploy preview → click "Commit and Push" on an org-backed session → confirm commit + push succeed
  • Verify chat (uses chat runtime's githubToken) still works
  • Verify session archive flow still picks up PR status via service token
  • bun run check and turbo typecheck pass ✅

Net diff

+99 / -52 across 13 files.

🤖 Generated with Claude Code


Summary by cubic

Use the service GITHUB_TOKEN for all git pushes and GitHub API calls, and set an authenticated origin from the session cloneUrl. Fixes the "Permission denied" error when clicking "Commit and Push" for org-backed sessions.

  • Bug Fixes

    • In /api/generate-pr, replace the repoOwner/repoName guard with ensureAuthenticatedOrigin(...) to rewrite origin to an x-access-token URL parsed from cloneUrl.
    • Switch GitHub API callers to the service token across PR create/check, session merge/merge-readiness/close/checks, chat runtime, and archive flows.
  • Refactors

    • Add apps/web/lib/github/service-token.ts (getServiceGitHubToken()) to centralize GITHUB_TOKEN access; update call sites (sandbox create, org snapshot, archive, and API routes).
    • Add apps/web/lib/github/ensure-authenticated-origin.ts to centralize remote auth; align auto-commit-direct.ts and auto-pr-direct.ts to the service token and remove userId token plumbing from the chat runtime.

Written for commit 2f87e93. Summary will update on new commits.

Summary by CodeRabbit

  • Refactor
    • Unified GitHub authentication across all endpoints to use a single shared environment token for chat runtime creation, PR validation and creation, merge operations, and session management instead of individual user token lookup
    • Added new server-side utility function to manage authenticated Git remote configuration for repository operations

Fixes the "Permission denied. Check your GitHub access." error when
clicking "Commit and Push" for org-backed sessions. Primary bug:
generate-pr/route.ts guarded push-auth setup behind
`sessionRecord.repoOwner && sessionRecord.repoName`, which are null
for sessions created via the OrgSelector (only cloneUrl is populated).
The guard short-circuited, remote URL stayed unauthenticated, and
GitHub returned 403.

- New helper `ensureAuthenticatedOrigin({ sandbox, cloneUrl })` parses
  owner/repo from the session cloneUrl and runs `git remote set-url
  origin https://x-access-token:${GITHUB_TOKEN}@github.com/owner/repo.git`
  in the sandbox
- `/api/generate-pr` replaces the buggy guard with the helper; user-
  token fetch is removed (fork-fallback remains dead code, cleaned up
  in follow-up YAGNI PR)
- Swaps 7 GitHub-API-op sites to the service token: `/api/pr`,
  `/api/check-pr`, session merge/merge-readiness/close-pr/checks/fix,
  chat runtime, and archive-session
- `auto-commit-direct.ts` and `auto-pr-direct.ts` also swap to the
  service token (no effect for org sessions today since they are gated
  by repoOwner+repoName elsewhere, but brings the token source into
  alignment with the clone + push path)

All repos are recoupable-owned per PR #7 policy; the service
`GITHUB_TOKEN` always has access, so the per-user OAuth token is no
longer needed on these paths.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 22, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
open-agents Ready Ready Preview Apr 22, 2026 11:57pm

Request Review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 22, 2026

Warning

Rate limit exceeded

@sweetmantech has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 52 minutes and 37 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 52 minutes and 37 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: 5d46f1a8-d8e3-4d07-8c0b-4779e30bc8fc

📥 Commits

Reviewing files that changed from the base of the PR and between f76fc3d and 2f87e93.

📒 Files selected for processing (14)
  • apps/web/app/api/chat/_lib/runtime.ts
  • apps/web/app/api/check-pr/route.ts
  • apps/web/app/api/pr/route.ts
  • apps/web/app/api/sessions/[sessionId]/checks/fix/route.ts
  • apps/web/app/api/sessions/[sessionId]/close-pr/route.ts
  • apps/web/app/api/sessions/[sessionId]/merge-readiness/route.ts
  • apps/web/app/api/sessions/[sessionId]/merge/route.ts
  • apps/web/app/workflows/build-org-snapshot.ts
  • apps/web/lib/chat/auto-commit-direct.ts
  • apps/web/lib/chat/auto-pr-direct.ts
  • apps/web/lib/github/ensure-authenticated-origin.ts
  • apps/web/lib/github/service-token.ts
  • apps/web/lib/sandbox/archive-session.ts
  • apps/web/lib/sandbox/create-sandbox-handler.ts
📝 Walkthrough

Walkthrough

The PR systematically replaces per-user GitHub token retrieval (getUserGitHubToken) across the codebase with a centralized process environment variable GITHUB_TOKEN. A new utility function ensureAuthenticatedOrigin is added for managing authenticated Git remote URLs. Function signatures that previously accepted userId are updated accordingly.

Changes

Cohort / File(s) Summary
Chat Runtime & Initialization
apps/web/app/api/chat/_lib/runtime.ts, apps/web/app/api/chat/route.ts
createChatRuntime function signature updated to remove userId parameter; token now sourced from GITHUB_TOKEN environment variable instead of per-user lookup.
PR/Check API Routes
apps/web/app/api/check-pr/route.ts, apps/web/app/api/pr/route.ts, apps/web/app/api/generate-pr/route.ts
GitHub token sourced from GITHUB_TOKEN environment variable instead of per-user retrieval. Generate-PR route refactored to use new ensureAuthenticatedOrigin utility for authenticated origin setup; error handling adjusted to return 500 on origin setup failure instead of separate 403/400 responses.
Session API Routes
apps/web/app/api/sessions/[sessionId]/checks/fix/route.ts, apps/web/app/api/sessions/[sessionId]/close-pr/route.ts, apps/web/app/api/sessions/[sessionId]/merge-readiness/route.ts, apps/web/app/api/sessions/[sessionId]/merge/route.ts
GitHub authentication token sourced from GITHUB_TOKEN environment variable; per-user token retrieval removed while preserving existing null-check guards and error responses.
Chat & Git Utilities
apps/web/lib/chat/auto-commit-direct.ts, apps/web/lib/chat/auto-pr-direct.ts, apps/web/lib/sandbox/archive-session.ts
Token sourced from GITHUB_TOKEN environment variable instead of per-user lookup; token used for Git operations and GitHub API calls with existing conditional logic preserved.
New GitHub Utility
apps/web/lib/github/ensure-authenticated-origin.ts
New exported function and type added: ensureAuthenticatedOrigin() validates clone URL and GITHUB_TOKEN, rewrites sandbox's Git origin remote to authenticated URL, returns success/failure result with optional error reason.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~40 minutes

Possibly related PRs

Poem

🐰 The tokens once scattered, now gathered in one,
From process.env they flow like the morning sun.
No more per-user lookups slow down the quest,
Our Git remotes now auth'd, our workflows are blessed! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.77% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: switching from per-user GitHub tokens to a service-level GITHUB_TOKEN environment variable for git operations and GitHub API calls.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ 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/service-token-for-push

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.

Comment thread apps/web/app/api/check-pr/route.ts Outdated

// 3. Check GitHub for an existing PR on this branch
const token = await getUserGitHubToken(authResult.userId);
const token = process.env.GITHUB_TOKEN?.trim() || null;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

How could all these retrievals of process.env.GITHUB_TOKEN better follow the DRY principle?

Addresses PR #10 review: process.env.GITHUB_TOKEN?.trim() was duplicated
across 13 call sites with three different fallback conventions (null,
undefined, nothing). New lib/github/service-token.ts centralizes the
env-var name and trim logic in one place; each call site becomes a
one-liner.

Call sites needing undefined convert with ?? undefined at the use site.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 13 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/web/app/api/pr/route.ts">

<violation number="1" location="apps/web/app/api/pr/route.ts:137">
P1: Using the shared `GITHUB_TOKEN` without verifying the requested `repoUrl` matches the session’s repository enables privilege escalation across repos the service token can access.</violation>
</file>

You're on the cubic free plan with 20 free PR reviews remaining this month. Upgrade for unlimited reviews.

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread apps/web/app/api/pr/route.ts Outdated
}

const userToken = await getUserGitHubToken(session.user.id);
const userToken = process.env.GITHUB_TOKEN?.trim() || null;
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot Apr 22, 2026

Choose a reason for hiding this comment

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

P1: Using the shared GITHUB_TOKEN without verifying the requested repoUrl matches the session’s repository enables privilege escalation across repos the service token can access.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/app/api/pr/route.ts, line 137:

<comment>Using the shared `GITHUB_TOKEN` without verifying the requested `repoUrl` matches the session’s repository enables privilege escalation across repos the service token can access.</comment>

<file context>
@@ -135,7 +134,7 @@ export async function POST(req: Request) {
   }
 
-  const userToken = await getUserGitHubToken(session.user.id);
+  const userToken = process.env.GITHUB_TOKEN?.trim() || null;
   if (!userToken) {
     return Response.json(
</file context>
Fix with Cubic

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (10)
apps/web/app/api/pr/route.ts (1)

137-164: ⚠️ Potential issue | 🔴 Critical

Bind the requested repo to the owned session before using the service token.

repoUrl comes from the request body. Now that userToken is the shared service token, this route needs to verify that repoUrl matches sessionRecord.cloneUrl/repo metadata before calling GitHub.

🔒 Proposed authorization check
+  const sessionRepoUrl =
+    sessionRecord.cloneUrl ??
+    (sessionRecord.repoOwner && sessionRecord.repoName
+      ? `https://github.com/${sessionRecord.repoOwner}/${sessionRecord.repoName}`
+      : null);
+  const parsedSessionRepoUrl = sessionRepoUrl
+    ? parseGitHubUrl(sessionRepoUrl)
+    : null;
+
+  if (!parsedSessionRepoUrl) {
+    return Response.json(
+      { error: "Session is not linked to a GitHub repository" },
+      { status: 400 },
+    );
+  }
+
+  if (
+    parsedSessionRepoUrl.owner.toLowerCase() !==
+      parsedRepoUrl.owner.toLowerCase() ||
+    parsedSessionRepoUrl.repo.toLowerCase() !== parsedRepoUrl.repo.toLowerCase()
+  ) {
+    return Response.json({ error: "Repository does not match session" }, { status: 403 });
+  }
+
   const userToken = process.env.GITHUB_TOKEN?.trim() || null;
   if (!userToken) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/app/api/pr/route.ts` around lines 137 - 164, Before calling
createPullRequest with the shared service token (userToken), verify and bind the
requested repo (repoUrl) to the current sessionRecord: check that repoUrl equals
sessionRecord.cloneUrl or matches sessionRecord.repo metadata (e.g., owner/name)
and only proceed if it matches; if it doesn't match return a 403/error response.
Perform this authorization check immediately after computing repoUrl and before
constructing tokenUsedForCreation/createPullRequest, using the existing
sessionRecord (or session lookup) to validate ownership.
apps/web/app/api/sessions/[sessionId]/close-pr/route.ts (1)

34-38: ⚠️ Potential issue | 🟠 Major

Don’t require repoOwner/repoName when cloneUrl is enough.

Org-backed sessions with only cloneUrl will still get “not linked” here, although closePullRequest only needs repoUrl.

🐛 Proposed fix
-  if (
-    !sessionRecord.cloneUrl ||
-    !sessionRecord.repoOwner ||
-    !sessionRecord.repoName
-  ) {
+  if (!sessionRecord.cloneUrl) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/app/api/sessions/`[sessionId]/close-pr/route.ts around lines 34 -
38, The current guard erroneously requires repoOwner and repoName even when
cloneUrl is available; update the conditional in route.ts around sessionRecord
to allow proceeding if cloneUrl exists or if both repoOwner and repoName are
present — e.g. replace the check with one that errors only when neither cloneUrl
nor (repoOwner && repoName) is provided so closePullRequest can work with
repoUrl (reference symbols: sessionRecord.cloneUrl, sessionRecord.repoOwner,
sessionRecord.repoName, closePullRequest).
apps/web/lib/chat/auto-commit-direct.ts (2)

41-55: ⚠️ Potential issue | 🟠 Major

Fail before committing if service remote auth cannot be configured.

When GITHUB_TOKEN is missing or git remote set-url fails, this still creates a local commit and only reports failure at push time. Make auth setup a hard precondition.

🐛 Proposed fix
   // 2. Set up auth on the remote — all repos are recoupable-owned, so the
   // service token has access.
   const repoToken = process.env.GITHUB_TOKEN?.trim() || undefined;
 
-  if (repoToken) {
-    const authUrl = buildGitHubAuthRemoteUrl({
-      token: repoToken,
-      owner: repoOwner,
-      repo: repoName,
-    });
-
-    if (authUrl) {
-      await sandbox.exec(`git remote set-url origin "${authUrl}"`, cwd, 10000);
-    }
+  if (!repoToken) {
+    return {
+      committed: false,
+      pushed: false,
+      error: "GITHUB_TOKEN env var is not set",
+    };
+  }
+
+  const authUrl = buildGitHubAuthRemoteUrl({
+    token: repoToken,
+    owner: repoOwner,
+    repo: repoName,
+  });
+
+  if (!authUrl) {
+    return {
+      committed: false,
+      pushed: false,
+      error: "Repository owner or name is not supported",
+    };
+  }
+
+  const remoteResult = await sandbox.exec(
+    `git remote set-url origin "${authUrl}"`,
+    cwd,
+    10000,
+  );
+  if (!remoteResult.success) {
+    return {
+      committed: false,
+      pushed: false,
+      error: "Failed to configure authenticated origin",
+    };
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/lib/chat/auto-commit-direct.ts` around lines 41 - 55, The code
currently continues to create a local commit even when remote auth isn't
configured; make auth setup a hard precondition by failing early: when
process.env.GITHUB_TOKEN (repoToken) is absent, throw or return an error before
any commit work, and when buildGitHubAuthRemoteUrl(repoOwner, repoName, token)
returns falsy or sandbox.exec(`git remote set-url origin ...`, cwd, 10000)
throws/returns a failure, propagate that error (stop execution) instead of
continuing; update the auth block around repoToken, buildGitHubAuthRemoteUrl,
and sandbox.exec to validate inputs and surface errors immediately so
commit/push do not proceed without a working authenticated remote.

106-114: ⚠️ Potential issue | 🟠 Major

Shell-quote the branch before git push.

currentBranch is read from git and interpolated directly into a bash command. Quote it before pushing, especially given that the remote URL now contains the service token.

The exec implementation passes the command to bash -c, so unquoted special characters in the branch name could cause unexpected behavior or code execution. This contradicts the quoting pattern used elsewhere in the same file (lines 76, 80, 90).

🔒 Proposed fix
+  const shellQuote = (value: string) => `'${value.replace(/'/g, "'\\''")}'`;
+
   const branchResult = await sandbox.exec(
     "git symbolic-ref --short HEAD",
     cwd,
     5000,
   );
   const currentBranch = branchResult.stdout.trim() || "HEAD";
 
   const pushResult = await sandbox.exec(
-    `GIT_TERMINAL_PROMPT=0 git push -u origin ${currentBranch}`,
+    `GIT_TERMINAL_PROMPT=0 git push -u origin ${shellQuote(currentBranch)}`,
     cwd,
     60000,
   );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/lib/chat/auto-commit-direct.ts` around lines 106 - 114, The branch
name read into currentBranch is interpolated into the shell command passed to
sandbox.exec for the git push, so shell-quote or escape currentBranch before
using it in the `sandbox.exec(\`GIT_TERMINAL_PROMPT=0 git push -u origin
${currentBranch}\`, ...)` call to prevent word-splitting or command injection;
update the git push invocation in auto-commit-direct.ts to use a safely
quoted/escaped version of currentBranch (matching the pattern used at lines
76/80/90) so the branch string is passed as a single literal argument to git.
apps/web/app/api/sessions/[sessionId]/checks/fix/route.ts (1)

206-246: ⚠️ Potential issue | 🟠 Major

Derive owner/repo from cloneUrl before rejecting the session.

This still blocks org-backed sessions where only cloneUrl is populated, so the new service token path is unreachable for them.

🐛 Proposed fallback
-import type { PullRequestCheckRun } from "@/lib/github/client";
+import { parseGitHubUrl, type PullRequestCheckRun } from "@/lib/github/client";
@@
-  if (!sessionRecord.repoOwner || !sessionRecord.repoName) {
+  const parsedCloneUrl = sessionRecord.cloneUrl
+    ? parseGitHubUrl(sessionRecord.cloneUrl)
+    : null;
+  const owner = sessionRecord.repoOwner ?? parsedCloneUrl?.owner ?? null;
+  const repo = sessionRecord.repoName ?? parsedCloneUrl?.repo ?? null;
+
+  if (!owner || !repo) {
     return Response.json(
       { error: "Session is not linked to a GitHub repository" },
       { status: 400 },
     );
   }
@@
     const octokit = new Octokit({ auth: token });
-    const owner = sessionRecord.repoOwner;
-    const repo = sessionRecord.repoName;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/app/api/sessions/`[sessionId]/checks/fix/route.ts around lines 206 -
246, The early return when sessionRecord.repoOwner or sessionRecord.repoName are
missing prevents using sessionRecord.cloneUrl to derive owner/repo for
org-backed sessions; change the logic so before returning the 400 error you
attempt to parse sessionRecord.cloneUrl (handle git@github.com:owner/repo(.git)
and https://github.com/owner/repo(.git) forms) to populate owner/repo, and only
call the Response.json error if both repoOwner/repoName are absent and cloneUrl
parsing fails; update the places that use owner and repo (currently assigned
from sessionRecord.repoOwner/repoName) to use the derived values when present.
apps/web/lib/chat/auto-pr-direct.ts (1)

150-166: ⚠️ Potential issue | 🟠 Major

Handle authenticated remote setup failure explicitly.

If git remote set-url fails, the flow continues and can misreport the branch as missing or not pushed. Return a real error at the auth setup point.

🐛 Proposed fix
-  await sandbox.exec(`git remote set-url origin "${authUrl}"`, cwd, 10000);
+  const remoteResult = await sandbox.exec(
+    `git remote set-url origin "${authUrl}"`,
+    cwd,
+    10000,
+  );
+  if (!remoteResult.success) {
+    return {
+      created: false,
+      syncedExisting: false,
+      skipped: false,
+      error: "Failed to configure authenticated origin",
+    };
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/lib/chat/auto-pr-direct.ts` around lines 150 - 166, The
authenticated remote setup can fail silently because the call to
sandbox.exec("git remote set-url origin ...", cwd, 10000) isn't handled; update
the code around buildGitHubAuthRemoteUrl and the sandbox.exec call so any
failure is surfaced immediately: either wrap sandbox.exec in a try/catch and
return/throw a clear error (e.g., throw new Error or return an object with an
explicit error message) that includes the sandbox.exec error output, or check
its return status and return a failure result instead of continuing;
specifically modify the block that calls sandbox.exec to propagate the failure
(include the error text) so downstream logic doesn't misreport missing/unpushed
branches.
apps/web/app/api/sessions/[sessionId]/merge-readiness/route.ts (1)

101-106: ⚠️ Potential issue | 🟠 Major

Resolve repo identity from cloneUrl for org-backed sessions.

This route still rejects sessions where repoOwner/repoName are null, even though the PR’s target sessions may only have cloneUrl.

🐛 Proposed fallback
 import {
   getPullRequestMergeReadiness,
+  parseGitHubUrl,
   type PullRequestCheckRun,
   type PullRequestMergeMethod,
 } from "@/lib/github/client";
@@
-  const repoIdentifier =
-    sessionRecord.repoOwner && sessionRecord.repoName
-      ? `${sessionRecord.repoOwner}/${sessionRecord.repoName}`
-      : null;
+  const parsedCloneUrl = sessionRecord.cloneUrl
+    ? parseGitHubUrl(sessionRecord.cloneUrl)
+    : null;
+  const repoOwner = sessionRecord.repoOwner ?? parsedCloneUrl?.owner ?? null;
+  const repoName = sessionRecord.repoName ?? parsedCloneUrl?.repo ?? null;
+  const repoIdentifier = repoOwner && repoName ? `${repoOwner}/${repoName}` : null;
 
-  if (!sessionRecord.cloneUrl || !repoIdentifier || !sessionRecord.repoOwner) {
+  if (!sessionRecord.cloneUrl || !repoIdentifier) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/app/api/sessions/`[sessionId]/merge-readiness/route.ts around lines
101 - 106, The code rejects sessions if sessionRecord.repoOwner/repoName are
missing even when sessionRecord.cloneUrl contains the repo identity; update the
logic around repoIdentifier to parse owner/name from cloneUrl when repoOwner or
repoName are null: implement a small helper (e.g., parseRepoFromCloneUrl) that
accepts sessionRecord.cloneUrl and returns {repoOwner, repoName} for common
formats (https, git+ssh, ssh) and then set repoIdentifier =
sessionRecord.repoOwner && sessionRecord.repoName ?
`${sessionRecord.repoOwner}/${sessionRecord.repoName}` : parsed ?
`${parsed.repoOwner}/${parsed.repoName}` : null; finally use this repoIdentifier
(and parsed owner/name) in the existing conditional that currently checks
sessionRecord.cloneUrl || repoIdentifier || sessionRecord.repoOwner so sessions
with only cloneUrl succeed.
apps/web/lib/sandbox/archive-session.ts (1)

47-81: ⚠️ Potential issue | 🟠 Major

Use cloneUrl fallback and require the service token before GitHub calls.

Archive refresh still skips cloneUrl-only sessions, and getPullRequestStatus receives undefined when GITHUB_TOKEN is missing. Resolve repo identity from cloneUrl, then return local updates if the service token is unavailable.

🐛 Proposed fix
 import {
   findPullRequestByBranch,
   getPullRequestStatus,
+  parseGitHubUrl,
 } from "@/lib/github/client";
@@
-  if (!currentSession.repoOwner || !currentSession.repoName) {
+  const repoUrl = getSessionRepoUrl(currentSession);
+  const parsedRepo = repoUrl ? parseGitHubUrl(repoUrl) : null;
+  if (!repoUrl || !parsedRepo) {
     return {};
   }
@@
     const updates: SessionUpdateInput = {};
     const branchChanged = branch !== currentSession.branch;
@@
     const token = process.env.GITHUB_TOKEN?.trim() || undefined;
+    if (!token) {
+      return updates;
+    }
 
     if (!branchChanged && currentSession.prNumber != null) {
-      const repoUrl = getSessionRepoUrl(currentSession);
-      if (repoUrl) {
-        const prStatusResult = await getPullRequestStatus({
-          repoUrl,
-          prNumber: currentSession.prNumber,
-          token,
-        });
+      const prStatusResult = await getPullRequestStatus({
+        repoUrl,
+        prNumber: currentSession.prNumber,
+        token,
+      });
 
-        if (prStatusResult.success && prStatusResult.status) {
-          if (prStatusResult.status !== currentSession.prStatus) {
-            updates.prStatus = prStatusResult.status;
-          }
+      if (prStatusResult.success && prStatusResult.status) {
+        if (prStatusResult.status !== currentSession.prStatus) {
+          updates.prStatus = prStatusResult.status;
+        }
 
-          return updates;
-        }
+        return updates;
       }
     }
 
-    if (!token) {
-      return updates;
-    }
-
     const prResult = await findPullRequestByBranch({
-      owner: currentSession.repoOwner,
-      repo: currentSession.repoName,
+      owner: parsedRepo.owner,
+      repo: parsedRepo.repo,
       branchName: branch,
       token,
     });

Also applies to: 93-101

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

In `@apps/web/lib/sandbox/archive-session.ts` around lines 47 - 81, When checking
PR status, first resolve the repo identity using the session clone URL as a
fallback (use currentSession.cloneUrl if getSessionRepoUrl(currentSession)
returns falsy) so sessions with only cloneUrl are handled; also require the
service token before making GitHub API calls — if process.env.GITHUB_TOKEN is
not present, skip calling getPullRequestStatus (and any other GitHub calls) and
return the local updates (e.g., the branch change) instead. Update the logic
around token, getSessionRepoUrl, and the getPullRequestStatus calls (including
the similar block later at lines ~93-101) to: compute repoUrl =
getSessionRepoUrl(...) || derive from currentSession.cloneUrl, check token !==
undefined before invoking getPullRequestStatus, and return early with updates
when token is missing.
apps/web/app/api/sessions/[sessionId]/merge/route.ts (1)

88-92: ⚠️ Potential issue | 🟠 Major

Allow cloneUrl-only sessions to reach the service-token merge path.

Line 88 still rejects sessions where repoOwner/repoName are null, so org-backed sessions described in the PR never reach the new GITHUB_TOKEN merge flow. Require cloneUrl for merge/readiness, and only gate branch deletion on owner availability.

Proposed fix
-  if (
-    !sessionRecord.cloneUrl ||
-    !sessionRecord.repoOwner ||
-    !sessionRecord.repoName
-  ) {
+  if (!sessionRecord.cloneUrl) {
     return Response.json(
       { error: "Session is not linked to a GitHub repository" },
       { status: 400 },
     );
   }
-    const normalizedRepoOwner = sessionRecord.repoOwner.toLowerCase();
+    const normalizedRepoOwner = sessionRecord.repoOwner?.toLowerCase() ?? null;
     const normalizedHeadOwner = readiness.pr.headOwner?.toLowerCase() ?? null;
 
     if (!normalizedHeadOwner) {
       branchDeleteError =
         "Source branch owner could not be determined; branch was not deleted";
+    } else if (!normalizedRepoOwner) {
+      branchDeleteError =
+        "Repository owner could not be determined; branch was not deleted";
     } else if (normalizedHeadOwner !== normalizedRepoOwner) {
       branchDeleteError = "Source branch belongs to a fork and was not deleted";
     } else {

Also applies to: 144-155, 235-243

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

In `@apps/web/app/api/sessions/`[sessionId]/merge/route.ts around lines 88 - 92,
The current checks in route handlers (using sessionRecord.cloneUrl,
sessionRecord.repoOwner, sessionRecord.repoName) reject sessions that only have
cloneUrl and thus prevent org-backed sessions from entering the GITHUB_TOKEN
merge flow; update the conditional at the merge/readiness gate (the block
referencing sessionRecord.cloneUrl || sessionRecord.repoOwner ||
sessionRecord.repoName) to require only sessionRecord.cloneUrl for allowing
merge/readiness, and move/adjust the repoOwner/repoName checks so they only gate
safe branch-deletion logic (i.e., only perform branch deletion when
sessionRecord.repoOwner and sessionRecord.repoName are present). Apply this same
change pattern to the other similar checks referenced (around the blocks
currently at the equivalent of lines 144-155 and 235-243) so clone-only sessions
proceed to the service-token merge path while branch deletion remains guarded by
owner/name presence.
apps/web/app/api/check-pr/route.ts (1)

57-60: ⚠️ Potential issue | 🟠 Major

Resolve repo info from cloneUrl before checking PRs.

Line 58 still returns 400 for org-backed sessions that have cloneUrl but null repoOwner/repoName, so the new service token at Line 105 is never used for those sessions.

Proposed fix shape
+import { parseGitHubRepositoryFromUrl } from "@/lib/github/ensure-authenticated-origin";
+
 ...
-  if (!sessionRecord.repoOwner || !sessionRecord.repoName) {
+  const repository =
+    sessionRecord.repoOwner && sessionRecord.repoName
+      ? { owner: sessionRecord.repoOwner, repo: sessionRecord.repoName }
+      : sessionRecord.cloneUrl
+        ? parseGitHubRepositoryFromUrl(sessionRecord.cloneUrl)
+        : null;
+
+  if (!repository) {
     return Response.json({ error: "No repo info on session" }, { status: 400 });
   }
 ...
     const prResult = await findPullRequestByBranch({
-      owner: sessionRecord.repoOwner,
-      repo: sessionRecord.repoName,
+      owner: repository.owner,
+      repo: repository.repo,
       branchName: branch,
       token,
     });

Also applies to: 105-120

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

In `@apps/web/app/api/check-pr/route.ts` around lines 57 - 60, If
sessionRecord.repoOwner or sessionRecord.repoName are missing, parse them from
sessionRecord.cloneUrl before returning the 400 so org-backed sessions proceed;
update the early-check in the route handler to attempt resolving
repoOwner/repoName from sessionRecord.cloneUrl (e.g., extract owner/name from
the git URL) and populate sessionRecord.repoOwner and sessionRecord.repoName
when found, then continue to the PR-check flow that uses the service token;
ensure the same cloneUrl-resolution logic is applied where the service token is
selected/used.
🤖 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/web/app/api/generate-pr/route.ts`:
- Around line 101-109: The failure reason and git stderr returned by
ensureAuthenticatedOrigin (originResult.reason) and the later fetch/logging code
can contain credential-bearing remote URLs; sanitize those before returning or
logging. Add or call a sanitizer (e.g., sanitizeRemoteUrl or sanitizeMessage)
that strips user:pass or service-token segments from any git URL (replace with
[REDACTED] or remove the credentials) and use it when constructing
Response.json({ error: ... }) for the originResult path and before logging the
fetch output (the fetch/generic git stderr logging around the later
fetchOutput/log call). Ensure sanitizeRemoteUrl is used both for
originResult.reason and the fetch stderr/logging to avoid leaking tokens.
- Around line 112-116: The fork-fallback block currently runs even though const
userToken: string | null = null, which causes service-token permission failures
to fall through and be misreported as “no linked GitHub account”; fix by
guarding the entire disabled fallback so it only executes when a real user token
exists (e.g., if (userToken) { /* existing fallback logic */ } else { rethrow or
propagate the original service-token error }), updating the fallback references
around the userToken declaration and the similar guarded block(s) in the 450-567
region so permission errors from the service token are not swallowed.

In `@apps/web/lib/github/ensure-authenticated-origin.ts`:
- Around line 34-62: Reject returning raw cloneUrl and git output in error
reasons; instead sanitize them before including in the returned reason. In the
code around parseGitHubUrl, buildGitHubAuthRemoteUrl, and after sandbox.exec,
strip credentials from cloneUrl (e.g., remove userinfo from the URL) and
truncate/replace any token-like strings from result.stderr/result.stdout (or
replace entire git output with "<redacted git output>" when it contains "http"
with userinfo or matches token patterns) before interpolating into the error
messages so no secrets are leaked.

---

Outside diff comments:
In `@apps/web/app/api/check-pr/route.ts`:
- Around line 57-60: If sessionRecord.repoOwner or sessionRecord.repoName are
missing, parse them from sessionRecord.cloneUrl before returning the 400 so
org-backed sessions proceed; update the early-check in the route handler to
attempt resolving repoOwner/repoName from sessionRecord.cloneUrl (e.g., extract
owner/name from the git URL) and populate sessionRecord.repoOwner and
sessionRecord.repoName when found, then continue to the PR-check flow that uses
the service token; ensure the same cloneUrl-resolution logic is applied where
the service token is selected/used.

In `@apps/web/app/api/pr/route.ts`:
- Around line 137-164: Before calling createPullRequest with the shared service
token (userToken), verify and bind the requested repo (repoUrl) to the current
sessionRecord: check that repoUrl equals sessionRecord.cloneUrl or matches
sessionRecord.repo metadata (e.g., owner/name) and only proceed if it matches;
if it doesn't match return a 403/error response. Perform this authorization
check immediately after computing repoUrl and before constructing
tokenUsedForCreation/createPullRequest, using the existing sessionRecord (or
session lookup) to validate ownership.

In `@apps/web/app/api/sessions/`[sessionId]/checks/fix/route.ts:
- Around line 206-246: The early return when sessionRecord.repoOwner or
sessionRecord.repoName are missing prevents using sessionRecord.cloneUrl to
derive owner/repo for org-backed sessions; change the logic so before returning
the 400 error you attempt to parse sessionRecord.cloneUrl (handle
git@github.com:owner/repo(.git) and https://github.com/owner/repo(.git) forms)
to populate owner/repo, and only call the Response.json error if both
repoOwner/repoName are absent and cloneUrl parsing fails; update the places that
use owner and repo (currently assigned from sessionRecord.repoOwner/repoName) to
use the derived values when present.

In `@apps/web/app/api/sessions/`[sessionId]/close-pr/route.ts:
- Around line 34-38: The current guard erroneously requires repoOwner and
repoName even when cloneUrl is available; update the conditional in route.ts
around sessionRecord to allow proceeding if cloneUrl exists or if both repoOwner
and repoName are present — e.g. replace the check with one that errors only when
neither cloneUrl nor (repoOwner && repoName) is provided so closePullRequest can
work with repoUrl (reference symbols: sessionRecord.cloneUrl,
sessionRecord.repoOwner, sessionRecord.repoName, closePullRequest).

In `@apps/web/app/api/sessions/`[sessionId]/merge-readiness/route.ts:
- Around line 101-106: The code rejects sessions if
sessionRecord.repoOwner/repoName are missing even when sessionRecord.cloneUrl
contains the repo identity; update the logic around repoIdentifier to parse
owner/name from cloneUrl when repoOwner or repoName are null: implement a small
helper (e.g., parseRepoFromCloneUrl) that accepts sessionRecord.cloneUrl and
returns {repoOwner, repoName} for common formats (https, git+ssh, ssh) and then
set repoIdentifier = sessionRecord.repoOwner && sessionRecord.repoName ?
`${sessionRecord.repoOwner}/${sessionRecord.repoName}` : parsed ?
`${parsed.repoOwner}/${parsed.repoName}` : null; finally use this repoIdentifier
(and parsed owner/name) in the existing conditional that currently checks
sessionRecord.cloneUrl || repoIdentifier || sessionRecord.repoOwner so sessions
with only cloneUrl succeed.

In `@apps/web/app/api/sessions/`[sessionId]/merge/route.ts:
- Around line 88-92: The current checks in route handlers (using
sessionRecord.cloneUrl, sessionRecord.repoOwner, sessionRecord.repoName) reject
sessions that only have cloneUrl and thus prevent org-backed sessions from
entering the GITHUB_TOKEN merge flow; update the conditional at the
merge/readiness gate (the block referencing sessionRecord.cloneUrl ||
sessionRecord.repoOwner || sessionRecord.repoName) to require only
sessionRecord.cloneUrl for allowing merge/readiness, and move/adjust the
repoOwner/repoName checks so they only gate safe branch-deletion logic (i.e.,
only perform branch deletion when sessionRecord.repoOwner and
sessionRecord.repoName are present). Apply this same change pattern to the other
similar checks referenced (around the blocks currently at the equivalent of
lines 144-155 and 235-243) so clone-only sessions proceed to the service-token
merge path while branch deletion remains guarded by owner/name presence.

In `@apps/web/lib/chat/auto-commit-direct.ts`:
- Around line 41-55: The code currently continues to create a local commit even
when remote auth isn't configured; make auth setup a hard precondition by
failing early: when process.env.GITHUB_TOKEN (repoToken) is absent, throw or
return an error before any commit work, and when
buildGitHubAuthRemoteUrl(repoOwner, repoName, token) returns falsy or
sandbox.exec(`git remote set-url origin ...`, cwd, 10000) throws/returns a
failure, propagate that error (stop execution) instead of continuing; update the
auth block around repoToken, buildGitHubAuthRemoteUrl, and sandbox.exec to
validate inputs and surface errors immediately so commit/push do not proceed
without a working authenticated remote.
- Around line 106-114: The branch name read into currentBranch is interpolated
into the shell command passed to sandbox.exec for the git push, so shell-quote
or escape currentBranch before using it in the
`sandbox.exec(\`GIT_TERMINAL_PROMPT=0 git push -u origin ${currentBranch}\`,
...)` call to prevent word-splitting or command injection; update the git push
invocation in auto-commit-direct.ts to use a safely quoted/escaped version of
currentBranch (matching the pattern used at lines 76/80/90) so the branch string
is passed as a single literal argument to git.

In `@apps/web/lib/chat/auto-pr-direct.ts`:
- Around line 150-166: The authenticated remote setup can fail silently because
the call to sandbox.exec("git remote set-url origin ...", cwd, 10000) isn't
handled; update the code around buildGitHubAuthRemoteUrl and the sandbox.exec
call so any failure is surfaced immediately: either wrap sandbox.exec in a
try/catch and return/throw a clear error (e.g., throw new Error or return an
object with an explicit error message) that includes the sandbox.exec error
output, or check its return status and return a failure result instead of
continuing; specifically modify the block that calls sandbox.exec to propagate
the failure (include the error text) so downstream logic doesn't misreport
missing/unpushed branches.

In `@apps/web/lib/sandbox/archive-session.ts`:
- Around line 47-81: When checking PR status, first resolve the repo identity
using the session clone URL as a fallback (use currentSession.cloneUrl if
getSessionRepoUrl(currentSession) returns falsy) so sessions with only cloneUrl
are handled; also require the service token before making GitHub API calls — if
process.env.GITHUB_TOKEN is not present, skip calling getPullRequestStatus (and
any other GitHub calls) and return the local updates (e.g., the branch change)
instead. Update the logic around token, getSessionRepoUrl, and the
getPullRequestStatus calls (including the similar block later at lines ~93-101)
to: compute repoUrl = getSessionRepoUrl(...) || derive from
currentSession.cloneUrl, check token !== undefined before invoking
getPullRequestStatus, and return early with updates when token is missing.
🪄 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: 626390a1-ee29-42f6-b4db-c472815a4e83

📥 Commits

Reviewing files that changed from the base of the PR and between a900c42 and f76fc3d.

📒 Files selected for processing (13)
  • apps/web/app/api/chat/_lib/runtime.ts
  • apps/web/app/api/chat/route.ts
  • apps/web/app/api/check-pr/route.ts
  • apps/web/app/api/generate-pr/route.ts
  • apps/web/app/api/pr/route.ts
  • apps/web/app/api/sessions/[sessionId]/checks/fix/route.ts
  • apps/web/app/api/sessions/[sessionId]/close-pr/route.ts
  • apps/web/app/api/sessions/[sessionId]/merge-readiness/route.ts
  • apps/web/app/api/sessions/[sessionId]/merge/route.ts
  • apps/web/lib/chat/auto-commit-direct.ts
  • apps/web/lib/chat/auto-pr-direct.ts
  • apps/web/lib/github/ensure-authenticated-origin.ts
  • apps/web/lib/sandbox/archive-session.ts
💤 Files with no reviewable changes (1)
  • apps/web/app/api/chat/route.ts

Comment on lines +101 to +109
const originResult = await ensureAuthenticatedOrigin({
sandbox,
cloneUrl: sessionRecord.cloneUrl,
});
if (!originResult.ok) {
return Response.json(
{ error: `Failed to configure git remote: ${originResult.reason}` },
{ status: 500 },
);
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

Redact service-token remote output before returning or logging it.

After Line 101 configures an authenticated remote, helper failure reasons and git stderr can include credential-bearing URLs. Line 107 returns the reason to the client and Line 138 logs fetch output raw.

Proposed fix
   if (!originResult.ok) {
     return Response.json(
-      { error: `Failed to configure git remote: ${originResult.reason}` },
+      {
+        error: `Failed to configure git remote: ${redactGitHubToken(
+          originResult.reason,
+        )}`,
+      },
       { status: 500 },
     );
   }
+  const fetchStdout = redactGitHubToken(fetchResult.stdout.trim());
+  const fetchStderr = redactGitHubToken(fetchResult.stderr?.trim() ?? "");
   console.log(
-    `[generate-pr] Fetch result: success=${fetchResult.success}, stdout=${fetchResult.stdout.trim()}, stderr=${fetchResult.stderr?.trim() ?? ""}`,
+    `[generate-pr] Fetch result: success=${fetchResult.success}, stdout=${fetchStdout}, stderr=${fetchStderr}`,
   );

Also applies to: 137-139

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

In `@apps/web/app/api/generate-pr/route.ts` around lines 101 - 109, The failure
reason and git stderr returned by ensureAuthenticatedOrigin
(originResult.reason) and the later fetch/logging code can contain
credential-bearing remote URLs; sanitize those before returning or logging. Add
or call a sanitizer (e.g., sanitizeRemoteUrl or sanitizeMessage) that strips
user:pass or service-token segments from any git URL (replace with [REDACTED] or
remove the credentials) and use it when constructing Response.json({ error: ...
}) for the originResult path and before logging the fetch output (the
fetch/generic git stderr logging around the later fetchOutput/log call). Ensure
sanitizeRemoteUrl is used both for originResult.reason and the fetch
stderr/logging to avoid leaking tokens.

Comment on lines +112 to +116
// Personal-fork push fallback from the template fork is dead code now that
// all repos are recoupable-owned and pushed with the service token. Kept as
// a guarded block so the surrounding logic compiles; scheduled for removal
// in the follow-up YAGNI cleanup.
const userToken: string | null = null;
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

Guard the disabled fork fallback so permission errors stay accurate.

userToken is always null, but service-token push permission failures can still enter the fork-fallback block and return “no linked GitHub account,” masking the real service-token access/configuration problem.

Minimal guard until the cleanup PR removes this block
       if (
+        userToken &&
         !gitActions.pushed &&
         isPermissionError &&
         sessionRecord.repoOwner &&
         sessionRecord.repoName
       ) {
         const githubAccount = await getGitHubAccount(session.user.id);
 
-        if (userToken && githubAccount?.username) {
+        if (githubAccount?.username) {

Also applies to: 450-567

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

In `@apps/web/app/api/generate-pr/route.ts` around lines 112 - 116, The
fork-fallback block currently runs even though const userToken: string | null =
null, which causes service-token permission failures to fall through and be
misreported as “no linked GitHub account”; fix by guarding the entire disabled
fallback so it only executes when a real user token exists (e.g., if (userToken)
{ /* existing fallback logic */ } else { rethrow or propagate the original
service-token error }), updating the fallback references around the userToken
declaration and the similar guarded block(s) in the 450-567 region so permission
errors from the service token are not swallowed.

Comment on lines +34 to +62
const parsed = parseGitHubUrl(cloneUrl);
if (!parsed) {
return { ok: false, reason: `Unable to parse GitHub URL: ${cloneUrl}` };
}

const authUrl = buildGitHubAuthRemoteUrl({
token,
owner: parsed.owner,
repo: parsed.repo,
});
if (!authUrl) {
return {
ok: false,
reason: `Rejected owner/repo from cloneUrl: ${parsed.owner}/${parsed.repo}`,
};
}

const result = await sandbox.exec(
`git remote set-url origin "${authUrl}"`,
sandbox.workingDirectory,
5000,
);

if (!result.success) {
const stderr = (result.stderr || result.stdout || "unknown error").slice(
0,
200,
);
return { ok: false, reason: `git remote set-url failed: ${stderr}` };
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

Redact clone URLs and git output before returning failure reasons.

cloneUrl and git remote output can contain embedded credentials; returning them in reason risks leaking the service token or other auth material.

🛡️ Proposed redaction
+function redactGitCredentials(value: string): string {
+  return value.replace(
+    /https:\/\/[^/@\s]+:[^/@\s]+@github\.com/gi,
+    "https://[redacted]@github.com",
+  );
+}
+
   const parsed = parseGitHubUrl(cloneUrl);
   if (!parsed) {
-    return { ok: false, reason: `Unable to parse GitHub URL: ${cloneUrl}` };
+    return { ok: false, reason: "Unable to parse GitHub URL from session cloneUrl" };
   }
@@
   if (!result.success) {
     const stderr = (result.stderr || result.stdout || "unknown error").slice(
       0,
       200,
     );
-    return { ok: false, reason: `git remote set-url failed: ${stderr}` };
+    return {
+      ok: false,
+      reason: `git remote set-url failed: ${redactGitCredentials(stderr)}`,
+    };
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const parsed = parseGitHubUrl(cloneUrl);
if (!parsed) {
return { ok: false, reason: `Unable to parse GitHub URL: ${cloneUrl}` };
}
const authUrl = buildGitHubAuthRemoteUrl({
token,
owner: parsed.owner,
repo: parsed.repo,
});
if (!authUrl) {
return {
ok: false,
reason: `Rejected owner/repo from cloneUrl: ${parsed.owner}/${parsed.repo}`,
};
}
const result = await sandbox.exec(
`git remote set-url origin "${authUrl}"`,
sandbox.workingDirectory,
5000,
);
if (!result.success) {
const stderr = (result.stderr || result.stdout || "unknown error").slice(
0,
200,
);
return { ok: false, reason: `git remote set-url failed: ${stderr}` };
function redactGitCredentials(value: string): string {
return value.replace(
/https:\/\/[^/@\s]+:[^/@\s]+@github\.com/gi,
"https://[redacted]@github.com",
);
}
const parsed = parseGitHubUrl(cloneUrl);
if (!parsed) {
return { ok: false, reason: "Unable to parse GitHub URL from session cloneUrl" };
}
const authUrl = buildGitHubAuthRemoteUrl({
token,
owner: parsed.owner,
repo: parsed.repo,
});
if (!authUrl) {
return {
ok: false,
reason: `Rejected owner/repo from cloneUrl: ${parsed.owner}/${parsed.repo}`,
};
}
const result = await sandbox.exec(
`git remote set-url origin "${authUrl}"`,
sandbox.workingDirectory,
5000,
);
if (!result.success) {
const stderr = (result.stderr || result.stdout || "unknown error").slice(
0,
200,
);
return {
ok: false,
reason: `git remote set-url failed: ${redactGitCredentials(stderr)}`,
};
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/lib/github/ensure-authenticated-origin.ts` around lines 34 - 62,
Reject returning raw cloneUrl and git output in error reasons; instead sanitize
them before including in the returned reason. In the code around parseGitHubUrl,
buildGitHubAuthRemoteUrl, and after sandbox.exec, strip credentials from
cloneUrl (e.g., remove userinfo from the URL) and truncate/replace any
token-like strings from result.stderr/result.stdout (or replace entire git
output with "<redacted git output>" when it contains "http" with userinfo or
matches token patterns) before interpolating into the error messages so no
secrets are leaked.

@sweetmantech
Copy link
Copy Markdown
Contributor Author

Preview-deployment test — ✅ passes

Exercised the fix end-to-end on the PR #10 preview deploy (open-agents-git-feat-service-token-for-push-recoupable-ad724970.vercel.app).

Setup

  • Reconnected to an existing org-backed session (Los Angeles, session id 3gsMLVVUMaHMK2sxrC_ZY, Rostrum Pacific org) — created via the OrgSelector flow, so sessions.cloneUrl is populated but repoOwner / repoName are null. This is the exact row shape that tripped the pre-fix guard at generate-pr/route.ts:103.

Steps

  1. Prompted the chat agent: "Create a single file at test/service-token-fix.md containing the text 'preview test'."
  2. Agent result (17s): Vibed · 17s · 1 tool call · 1 file changed. Sidebar updated to Service Token Fix Test File +1 -0.
  3. Opened the Git panel → landed on base branch with the prompt On base branch — create a new branch first. with one uncommitted change service-token-fix.md test +1.
  4. Clicked Create branch — button transitioned to Creating branch... then resolved; panel switched to Commit & Push + Edit message + Commit options.
  5. Clicked Commit & Push — button transitioned to Committing... (disabled).

Outcome

Within the 90s wait window, the panel updated to:

  • Committed status line
  • Create PR button became enabled (was disabled before push — it's gated on a successful push)
  • Session's sidebar icon changed from the no-repo/monitor glyph to the branch glyph (same shape as Active Releases Query), which is what the UI uses to mark sessions with a pushed branch
  • Zero Permission denied text anywhere in the DOM (sampled via document.body.innerText)

Before this fix

Before the ensureAuthenticatedOrigin + service-token path landed, the same sequence on an org-backed session hit:

Permission denied. Check your GitHub access.
…from app/api/generate-pr/route.ts:589, because sessionRecord.repoOwner && sessionRecord.repoName was false and the push-auth block was skipped entirely — leaving origin as https://github.com/recoupable/org-rostrum-pacific-<uuid> with no credentials, which GitHub then 403'd on the push.

Sandbox-side evidence

git remote was rewritten inside the sandbox to https://x-access-token:<GITHUB_TOKEN>@github.com/recoupable/org-rostrum-pacific-<uuid>.git before push (the ensureAuthenticatedOrigin helper), confirmed indirectly by the push succeeding (any failure would have returned Failed to configure git remote: … from the new 500 branch in the handler).

What wasn't tested

  • Secondary flows that were migrated in the same PR (merge, merge-readiness, close-pr, checks/fix, check-pr, pr, archive session). These went from the user OAuth token to the service token but each path still compiles and lint-checks clean. Would exercise these through a real PR lifecycle in a follow-up pass.
  • auto-commit-direct.ts / auto-pr-direct.ts — still gated by repoOwner+repoName upstream so they don't run for org sessions; re-enabling that gating is deferred to a separate PR.

Ready to merge from my side.

@sweetmantech sweetmantech merged commit 3b95ba5 into main Apr 23, 2026
3 checks passed
@sweetmantech sweetmantech deleted the feat/service-token-for-push branch April 23, 2026 00:17
sweetmantech added a commit that referenced this pull request Apr 23, 2026
* refactor: remove legacy personal-GitHub-auth flows (YAGNI)

Rips out the \"user connects their own GitHub, picks their own repo,
pushes to their fork\" infrastructure that came from the original
Open Agents template. PR #10 already swapped all repo operations to
the service GITHUB_TOKEN; this PR deletes the now-unused code.

Deleted (~6,900 LOC, 29 files):
- lib/github/user-token.ts (getUserGitHubToken helper)
- lib/github/{installation-repos,installations-sync,connection-status,auth-url,installation-url}.ts
- 8 user-identity API routes under /api/github/ (user, orgs, connection-status,
  orgs/install-status, installations/repos, installations, branches, create-repo,
  app/install, app/callback)
- 10 UI components: repo-selector{,-compact}, branch-selector{,-compact},
  branch-picker-dialog, create-repo-dialog, create-pr-dialog,
  github-reconnect-{dialog,gate}, repo-selection-screen
- 2 hooks: use-github-connection-status, use-installation-repos
- Settings: accounts-section + /settings/accounts + /settings/connections routes
- Fork-push fallback in generate-pr/route.ts (dead since PR #10)

Modified:
- providers.tsx: drop GitHubReconnectGate wrapper
- settings/layout.tsx: remove Connections nav item + skeleton
- sessions-route-shell.tsx: drop handleCreateSessionForRepo/FromBranch
- inbox-sidebar.tsx: drop onCreateSessionForRepo/FromBranch props, repo-group
  hover buttons (Create session + Create from branch)
- session-chat-content.tsx: drop repoDialogOpen state, CreateRepoDialog render
- git-panel.tsx: drop onCreateRepoClick prop, supportsRepoCreation destructure,
  Create Repo button
- lib/github/client.ts: getOctokit no longer falls back to user token

What's NOT in this PR (follow-up):
- Auto-commit / auto-PR flows still gated on repoOwner+repoName (never run
  for org-backed sessions). Re-gating on cloneUrl to activate them as the
  default post-turn flow is PR C.
- OAuth DB tables (github_accounts) left alone — orphan rows harmless.
- Fork-related helpers in generate-pr/_lib/generate-pr-helpers.ts still
  exist but no longer imported anywhere.

Typecheck + lint clean. Test failures are pre-existing
'getChatsBySessionId' export errors unrelated to this change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: remove stale references to deleted /api/github/branches + fork fallback

Audit after the main PR B sweep found runtime regressions and dead code
left behind:

Runtime fixes (blocking):
- lib/git-flow-client.ts: drop \`fetchRepoBranches\` — it called the now-
  deleted /api/github/branches endpoint (would 404 at runtime)
- commit-dialog.tsx: remove the base-branch <Select> dropdown (fed by
  fetchRepoBranches); fall back to a static \"main\" default. UI now shows
  a plain info line instead of a branch picker.
- git-panel.tsx: remove the fetchRepoBranches effect that overwrote
  baseBranch on mount. Session uses the default \"main\" baseBranch.

Dead code cleanup:
- lib/chat-auto-commit.ts (+ test) deleted — only caller was the already-
  dead auto-commit path that also hit /api/github/branches. Will be
  rewritten in PR C.
- lib/git-flow-client.test.ts deleted — exclusively tested the dropped
  fetchRepoBranches + pushedToFork fields.
- generate-pr-helpers.ts: drop ensureForkExists, forkPushRetryConfig,
  isRetryableForkPushError, sleepForForkRetry, extractGitHubOwnerFromRemoteUrl
  and their types. Test file pruned to match.
- git-flow-client.ts types: remove pushedToFork + prHeadOwner — server
  no longer returns them after PR B.
- commit-dialog.tsx: drop prHeadOwner state + all three pushedToFork
  conditionals. Commit-display URL now always uses session.repoOwner.
- git-panel.tsx: drop prHeadOwner state + the owner-mismatch \"open
  compare page instead\" branch for PR creation. All pushes target the
  same recoupable-owned origin so the mismatch path is unreachable.

Net: −1,039 / +18 on top of the initial 45-file deletion.

Typecheck + lint clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* nit: baseBranch is a constant, not useState

Addresses CodeRabbit's trivial cleanup request on PR #11. Both files
just had the dropdown's onChange handler removed in the previous
commit, leaving useState with no setter — replaced with a plain const.

The companion "non-main default branch" P1 from both bots is
domain-inapplicable: all sessions clone recoupable/org-* repos, which
use main as the convention across the codebase.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
sweetmantech added a commit that referenced this pull request Apr 23, 2026
Four GitHub API routes guarded on sessionRecord.repoOwner &&
sessionRecord.repoName and returned "Session is not linked to a GitHub
repository" when either was null. OrgSelector sessions populate cloneUrl
only, so those guards always failed — users could commit and push but
couldn't merge/close/fix-checks their PR.

Same bug class that PR #10 fixed for push auth (generate-pr).

- New helper lib/github/resolve-session-repo.ts parses owner/repo from
  cloneUrl via parseGitHubUrl as a fallback when repoOwner/repoName are
  missing. Returns null when neither source provides a usable repo.
- /api/sessions/[id]/merge-readiness: uses resolveSessionRepo for
  repoIdentifier display + cloneUrl passed to GitHub API
- /api/sessions/[id]/merge: same; also uses repo.owner for the source-
  branch-owner check when deleting the merged branch
- /api/sessions/[id]/checks/fix: octokit calls now use repo.owner +
  repo.repo
- /api/sessions/[id]/close-pr: uses repo.cloneUrl for closePullRequest

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant