Skip to content

Add GitHub session logic, and refactor logging#855

Open
PaytonWebber wants to merge 22 commits intomainfrom
refactor/improved-logging
Open

Add GitHub session logic, and refactor logging#855
PaytonWebber wants to merge 22 commits intomainfrom
refactor/improved-logging

Conversation

@PaytonWebber
Copy link
Collaborator

@PaytonWebber PaytonWebber commented Feb 11, 2026

This PR introduces GitHub based trigger event support, which allows users to now trigger Cyrus via @ mentions on GitHub issues or PR's. When users post a comment that contains '@cyrusagent', an agent session is triggered in a dedicated Git worktree, with the context of the comment that Cyrus was triggered by.


Open with Devin

cyrusagent and others added 21 commits January 26, 2026 13:37
Summary subroutines (concise-summary, verbose-summary, question-answer,
plan-summary, etc.) have disallowAllTools: true, but MCP tools like
Linear's create_comment were still accessible because MCP config was
always provided regardless of this setting.

This change conditionally disables mcpConfig and mcpConfigPath when
disallowAllTools is true, ensuring the agent truly has no tool access
during summary subroutines.

Closes CYPACK-760

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…YPACK-725)

Implements Phase 1 of CYPACK-724 architectural refactor. Creates new
GlobalSessionRegistry class that centralizes session storage across all
repositories, enabling cross-repository session lookups for orchestrator
workflows.

Key features:
- Session CRUD operations (create, get, update, delete, getAll)
- Entry management (add, get, update entries)
- Parent-child session mapping for orchestrator workflows
- EventEmitter with lifecycle events (sessionCreated, sessionUpdated, sessionCompleted)
- Serialization/deserialization (v3.0 format)
- Cleanup method for removing old sessions
- Comprehensive unit tests (45 tests)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Add changelog entry for GlobalSessionRegistry implementation with PR link.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Fix TypeScript error in updateEntry method by ensuring type and content
fields are never undefined when applying partial updates.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…tation (CYPACK-726)

- Create IActivitySink interface for decoupling activity posting
- Implement LinearActivitySink wrapping IIssueTrackerService
- Add comprehensive unit tests (20 tests, all passing)
- Export from edge-worker package

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…726)

Add changelog entry documenting the extraction of IActivitySink interface
and LinearActivitySink implementation.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Add GlobalSessionRegistry instance to EdgeWorker
- Replace childToParentAgentSession Map calls with GlobalSessionRegistry methods
- Pass GlobalSessionRegistry to AgentSessionManager for future migration
- All session lookups for cross-repo parent resume now use GlobalSessionRegistry

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…(CYPACK-728)

- Rename linearAgentActivitySessionId to id for clarity
- Add optional externalSessionId for tracker-specific IDs (e.g., Linear's AgentSession ID)
- Add optional issueContext object with trackerId, issueId, issueIdentifier
- Make issue and issueId optional for standalone sessions
- Update PersistenceManager to v3.0 with automatic migration from v2.0
- Update all field references in GlobalSessionRegistry, AgentSessionManager, EdgeWorker
- Add 7 new tests for persistence migration

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…PACK-728)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Fix missed linearAgentActivitySessionId → id rename in EdgeWorker.ts:1639
  (code added in main after the Phase 4 schema changes)
- Fix unused variable lint warning in GlobalSessionRegistry.ts:264

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Update getAgentRunnersForIssue, getSessionsByIssueId, and
getActiveSessionsByIssueId to resolve issue ID from issueContext first,
falling back to deprecated issueId field. This ensures future standalone
sessions that only populate issueContext will be found correctly.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…gentSession (CYPACK-724)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…ding max-turns. Now, when the result message of a single-turn sub-routine is an error, the result text from the previous sub-routine is emitted to Linear as a result message.
* feat: implement GitHub webhook endpoint and event transport

Add /github-webhook endpoint for receiving forwarded GitHub webhooks
from CYHOST. When a valid @cyrusagent mention is received on a PR
comment, a new Claude session is created on the PR branch.

New package: cyrus-github-event-transport
- GitHubEventTransport: EventEmitter-based transport with proxy (Bearer
  token) and signature (HMAC-SHA256) verification modes
- GitHubCommentService: REST API client for posting replies to GitHub PRs
- Utility functions for extracting data from GitHub webhook payloads
- Handles both issue_comment and pull_request_review_comment events

EdgeWorker integration:
- registerGitHubEventTransport: registers /github-webhook endpoint
- handleGitHubWebhook: full session creation flow (validates PR comment,
  finds matching repo, creates workspace, starts ClaudeRunner)
- postGitHubReply: posts session results back to GitHub

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* docs: update changelogs for GitHub webhook endpoint feature

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: use forwarded GitHub installation token for PR comment replies

Updates GitHub webhook handling to extract and use the X-GitHub-Installation-Token
header forwarded from CYHOST instead of relying on process.env.GITHUB_TOKEN.

Changes:
- Extended GitHubWebhookEvent type with optional installationToken field
- GitHubEventTransport now extracts X-GitHub-Installation-Token header from incoming webhooks
- EdgeWorker.postGitHubReply() prefers forwarded token over process.env.GITHUB_TOKEN
- Added comprehensive tests for token extraction behavior (2 new tests)

This enables self-hosted Cyrus processes to post PR comment replies using
short-lived (1-hour) GitHub App installation tokens.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: use forwarded installation token in fetchPRBranchRef

Update EdgeWorker.fetchPRBranchRef() to prefer event.installationToken
over process.env.GITHUB_TOKEN for authenticating GitHub API calls to
fetch PR branch details. This fixes 404 errors in self-hosted setups
where process.env.GITHUB_TOKEN doesn't exist, allowing private repo
PR details to be fetched using forwarded GitHub App installation tokens.

- Changed line 866 in EdgeWorker.ts from process.env.GITHUB_TOKEN to
  event.installationToken || process.env.GITHUB_TOKEN
- Updated comment to reflect new token preference behavior
- Added comprehensive test coverage in EdgeWorker.fetchPRBranchRef.test.ts
- Updated CHANGELOG.internal.md to document the change

Fixes CYPACK-774

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: guard activity-posting methods for non-Linear sessions

- Add externalSessionId guard to postAnalyzingThought and
  postProcedureSelectionThought so they skip posting (and don't error)
  for GitHub/Slack sessions
- Remove activeWebhookCount tracking from handleMessage() to avoid
  double-counting with legacy webhook handlers
- Normalize all activity-posting guard clauses from warn to debug level,
  since non-Linear sessions hitting these paths is expected behavior
- Add tests verifying GitHub sessions skip all Linear activity posting

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add unified internal message bus with platform translators

Introduce a platform-agnostic message layer that translates webhook
payloads from Linear and GitHub into a unified InternalMessage format.
This enables handleMessage() in EdgeWorker to process events from all
platforms through a single code path.

- Add InternalMessage type system in core (SessionStart, UserPrompt,
  StopSignal, ContentUpdate, Unassign) with type guards and platform refs
- Add IMessageTranslator interface for platform-specific translators
- Implement LinearMessageTranslator and GitHubMessageTranslator
- Wire translators into event transports to emit 'message' alongside
  legacy 'event' for backward compatibility
- Replace old linear-event-transport tests with translator-focused tests

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: agentclear <agentops@ceedar.ai>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* feat: implement GitHub webhook endpoint and event transport

Add /github-webhook endpoint for receiving forwarded GitHub webhooks
from CYHOST. When a valid @cyrusagent mention is received on a PR
comment, a new Claude session is created on the PR branch.

New package: cyrus-github-event-transport
- GitHubEventTransport: EventEmitter-based transport with proxy (Bearer
  token) and signature (HMAC-SHA256) verification modes
- GitHubCommentService: REST API client for posting replies to GitHub PRs
- Utility functions for extracting data from GitHub webhook payloads
- Handles both issue_comment and pull_request_review_comment events

EdgeWorker integration:
- registerGitHubEventTransport: registers /github-webhook endpoint
- handleGitHubWebhook: full session creation flow (validates PR comment,
  finds matching repo, creates workspace, starts ClaudeRunner)
- postGitHubReply: posts session results back to GitHub

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* docs: update changelogs for GitHub webhook endpoint feature

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: use forwarded GitHub installation token for PR comment replies

Updates GitHub webhook handling to extract and use the X-GitHub-Installation-Token
header forwarded from CYHOST instead of relying on process.env.GITHUB_TOKEN.

Changes:
- Extended GitHubWebhookEvent type with optional installationToken field
- GitHubEventTransport now extracts X-GitHub-Installation-Token header from incoming webhooks
- EdgeWorker.postGitHubReply() prefers forwarded token over process.env.GITHUB_TOKEN
- Added comprehensive tests for token extraction behavior (2 new tests)

This enables self-hosted Cyrus processes to post PR comment replies using
short-lived (1-hour) GitHub App installation tokens.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* refactor: consolidate messsage emitting to a single function

---------

Co-authored-by: agentclear <agentops@ceedar.ai>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When a GitHub webhook arrives for a PR branch that's already checked out
in a Linear session's worktree, git worktree add fails. Instead of
falling back to an empty directory, detect and reuse the existing
worktree path — both proactively (before attempting creation) and as a
safety net (parsing the git error message in the catch block).

Also adds a concurrency warning log when a GitHub session shares a
workspace with an active Linear session.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 potential issue.

View 8 additional findings in Devin Review.

Open in Devin Review

}

try {
const body = JSON.stringify(request.body);

Choose a reason for hiding this comment

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

🔴 HMAC signature verification uses re-serialized JSON instead of raw request body

In signature verification mode, the code computes the HMAC-SHA256 digest over JSON.stringify(request.body) (the re-serialized parsed JSON) instead of the original raw request body bytes. GitHub computes its x-hub-signature-256 over the exact raw bytes sent in the HTTP request. Re-serializing the already-parsed body with JSON.stringify() can produce different output than the original payload (e.g., different whitespace, key ordering, unicode escaping, number formatting), causing the signature comparison to fail.

Root Cause

The route is correctly configured with rawBody: true at GitHubEventTransport.ts:75, which tells Fastify to preserve the raw body as request.rawBody. However, at line 116, the code ignores request.rawBody and instead uses JSON.stringify(request.body):

const body = JSON.stringify(request.body); // ← re-serialized, NOT raw bytes
const isValid = this.verifyGitHubSignature(body, signature, this.config.secret);

The verifyGitHubSignature method at line 252 then computes:

const expectedSignature = `sha256=${createHmac("sha256", secret).update(body).digest("hex")}`;

Since body is JSON.stringify(request.body) rather than the original raw bytes, any difference between the two (whitespace, key order, etc.) causes timingSafeEqual to return false, rejecting valid webhooks.

Impact: Signature-mode verification (verificationMode: "signature") will reject legitimate GitHub webhooks whenever JSON.stringify(parsed) does not byte-for-byte match the original payload. This effectively breaks the cloud/signature verification mode entirely.

Suggested change
const body = JSON.stringify(request.body);
const body = (request as any).rawBody ?? JSON.stringify(request.body);
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

* fix: use rawBody for GitHub webhook HMAC signature verification

JSON.stringify(request.body) re-serializes parsed JSON which may differ
from the original bytes GitHub signed. Use request.rawBody instead, which
preserves the exact payload bytes. The rawBody config was already enabled.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: rename misleading _sessionId param to _runnerSessionId

Clarifies that the parameter receives the runner session ID (Claude/Gemini),
not the internal session ID.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: migrate AgentSessionManager from IIssueTrackerService to IActivitySink

Decouple AgentSessionManager from Linear-specific IIssueTrackerService by
routing all activity posting through the platform-agnostic IActivitySink
interface. This enables future support for non-Linear activity sinks.

- Expand IActivitySink with ActivitySignal, ActivityPostOptions, ActivityPostResult types
- Update LinearActivitySink to map string signals to AgentActivitySignal enum
- Replace issueTracker constructor param with activitySink in AgentSessionManager
- Rename syncEntryToLinear → syncEntryToActivitySink
- Create LinearActivitySink at both EdgeWorker construction sites
- Update all test files to use IActivitySink mock pattern

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add "eyes" emoji reaction to GitHub comments that trigger agent sessions

Gives users instant visual feedback that their @mention was received before
the potentially long-running session begins.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <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.

2 participants