feat: implement credential provisioner, queue dispatch, CODEOWNERS stubs#83
feat: implement credential provisioner, queue dispatch, CODEOWNERS stubs#83chitcommit merged 2 commits intomainfrom
Conversation
The OAuthProvider encodes authorization codes as userId:grantId:secret and splits on : expecting exactly 3 parts. Using mcp-client:clientId created 4 parts and broke token exchange. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…EOWNERS - Implement createScopedServiceToken via ChittyAuth derivative token API - Implement createGitHubInstallationToken via GitHub App installation API - Add fetchChangedFiles for PR automation (labels + reviewer assignment) - Wire CODEOWNERS parsing into requestReviewers (was already implemented but not connected) - Fix trust-resolver entity_type extraction from ChittyID segments (supports DRL reckoning migration from ChittyTrust to ChittyScore) - Log normalized MCP events for observability (bus dispatch deferred to v3) - Update tests for ChittyScore DRL response format (ty/vy/ry) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ❌ Deployment failed View logs |
chittyconnect | fa7b647 | Mar 23 2026, 07:12 PM |
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (7)
📝 WalkthroughWalkthroughExtended pull request review automation by integrating CODEOWNERS support and changed files tracking. Refactored trust resolution from ChittyTrust endpoint to ChittyScore DRL API with component-based trust scoring. Implemented GitHub installation token and scoped service token provisioning. Removed OAuth clientId sanitization. Changes
Sequence DiagramssequenceDiagram
participant Queue as Queue Handler
participant GitHub as GitHub API
participant Reviewers as Reviewers Module
participant CodeOwners as CODEOWNERS Resolver
participant Label as Label Automation
Queue->>GitHub: fetchChangedFiles(prNumber)
GitHub-->>Queue: [changed filenames]
Queue->>Label: autoLabelPullRequest(..., changedFiles)
Label-->>Queue: labels applied
Queue->>Reviewers: requestReviewers(..., {changedFiles})
Reviewers->>CodeOwners: getCodeOwners(changedFiles)
CodeOwners-->>Reviewers: {users, teams}
Reviewers->>GitHub: POST /repos/.../pulls/.../requested_reviewers
GitHub-->>Reviewers: success
Reviewers-->>Queue: reviewers assigned
sequenceDiagram
participant Client as Client
participant CredProv as Credential Provisioner
participant ChittyAuth as ChittyAuth API
participant GitHub as GitHub API
Client->>CredProv: createScopedServiceToken(parentToken, scopes)
CredProv->>ChittyAuth: POST /api/v1/tokens/derivative
ChittyAuth-->>CredProv: {token, expires_at, scopes}
CredProv-->>Client: {value, expires_at, scopes}
Client->>CredProv: createGitHubInstallationToken(owner, repo)
CredProv->>GitHub: POST /app with JWT
GitHub-->>CredProv: JWT validated
CredProv->>GitHub: GET /app/installations
GitHub-->>CredProv: [installations]
CredProv->>GitHub: POST /app/installations/{id}/access_tokens
GitHub-->>CredProv: {token, expires_at, permissions, repositories}
CredProv-->>Client: {value, expires_at, permissions, repositories}
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested labels
Poem
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: fa7b647f09
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // @canon: chittycanon://gov/governance#core-types | ||
| const safeClientId = (oauthReqInfo.clientId || "anonymous").replace(/:/g, "-"); | ||
| const actorId = `mcp-client-${safeClientId}`; | ||
| const actorId = `mcp-client-${oauthReqInfo.clientId || "anonymous"}`; |
There was a problem hiding this comment.
Preserve colon sanitization when deriving
userId
This reintroduces : into the userId passed to completeAuthorization. The comment directly above explains that the OAuth provider encodes authorization codes as userId:grantId:secret and later splits on :; dynamically registered OAuth client IDs are commonly URLs like https://…, so /authorize now generates codes that /token cannot parse for those clients.
Useful? React with 👍 / 👎.
| return { | ||
| value: data.token, | ||
| expires_at: data.expires_at, | ||
| permissions: data.permissions, |
There was a problem hiding this comment.
Return the minted GitHub token under
token
provisionGitHubToken() in this file still reads installationToken.token for both the returned credential and the gh secret set instructions, but this helper now returns value instead. Even when GitHub successfully issues an installation token, github_deploy_token provisioning will hand back undefined to callers, so the new flow never produces a usable credential.
Useful? React with 👍 / 👎.
| body: JSON.stringify({ | ||
| repositories: [repository.split("/")[1]], | ||
| permissions: permissions || { contents: "read", metadata: "read" }, |
There was a problem hiding this comment.
Serialize GitHub permissions in the shape the API accepts
The default caller passes permissions as ['contents:write', 'actions:write'] (provisionGitHubToken() in the same file), but this request forwards that array unchanged. GitHub's installation-token endpoint expects a permission map such as { contents: 'write', actions: 'write' }, so the default github_deploy_token path will get a 422 instead of an access token.
Useful? React with 👍 / 👎.
| if (useCODEOWNERS && changedFiles.length > 0) { | ||
| try { | ||
| const owners = await getCodeOwners(token, owner, repo, changedFiles); |
There was a problem hiding this comment.
Don't enable CODEOWNERS lookup with the current matcher
This new call path turns getCodeOwners() on for every PR, but the parser below still turns entries like docs/ into ^docs/$ and keeps leading / characters, so common CODEOWNERS rules never match GitHub file paths such as docs/readme.md. In repositories that rely on directory-style or root-anchored patterns, reviewer assignment will silently miss the intended owners and fall back to the default team.
Useful? React with 👍 / 👎.
| const response = await fetch( | ||
| `https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}/files?per_page=100`, |
There was a problem hiding this comment.
Paginate the pull-request files fetch
This helper only makes a single per_page=100 request to the PR files API and never follows additional pages. On pull requests with more than 100 changed files, the later paths are dropped before autoLabelPullRequest() and requestReviewers() run, which makes label assignment and CODEOWNERS resolution incomplete for large PRs.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Pull request overview
Implements several previously-TODO automation and credential features across the GitHub event queue pipeline and credential provisioning, while migrating trust resolution to ChittyScore DRL (TY/VY/RY) and improving observability.
Changes:
- Implement scoped service-token derivation via ChittyAuth and GitHub App installation token generation.
- Fetch PR changed files and pass them into labeling + reviewer assignment; wire CODEOWNERS resolution into reviewer requests.
- Migrate trust resolver/test fixtures to DRL (ty/vy/ry) and log normalized MCP event metadata.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/mcp/tool-dispatcher.test.js | Updates trust resolver mocks to DRL response shape (ty/vy/ry). |
| tests/lib/trust-resolver.test.js | Updates trust resolver tests for DRL endpoint + derived trust level behavior. |
| src/services/credential-provisioner-enhanced.js | Implements scoped ChittyAuth derivative tokens and GitHub installation token creation. |
| src/middleware/oauth-provider.js | Adjusts OAuth actorId derivation during authorize flow. |
| src/lib/trust-resolver.js | Switches trust resolution to ChittyScore DRL and derives backward-compatible trust_level. |
| src/handlers/queue.js | Adds PR changed-files fetch; logs normalized MCP events; passes changedFiles into automations. |
| src/github/reviewers.js | Wires CODEOWNERS resolution into reviewer selection using changedFiles. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // @canon: chittycanon://gov/governance#core-types | ||
| const safeClientId = (oauthReqInfo.clientId || "anonymous").replace(/:/g, "-"); | ||
| const actorId = `mcp-client-${safeClientId}`; | ||
| const actorId = `mcp-client-${oauthReqInfo.clientId || "anonymous"}`; |
There was a problem hiding this comment.
actorId is derived directly from oauthReqInfo.clientId, but the comment above notes userId must not contain colons because OAuthProvider encodes/validates auth codes by splitting on :. A dynamically-registered clientId containing : will break the flow (and may enable ambiguous parsing). Reintroduce sanitization (e.g., replace : or otherwise encode clientId) before building actorId.
| const actorId = `mcp-client-${oauthReqInfo.clientId || "anonymous"}`; | |
| const rawClientId = oauthReqInfo.clientId || "anonymous"; | |
| // Sanitize clientId for use in userId: encode to ensure no ":" characters are present. | |
| const safeClientId = encodeURIComponent(rawClientId); | |
| const actorId = `mcp-client-${safeClientId}`; |
| const data = await response.json(); | ||
| return { | ||
| value: data.token, | ||
| expires_at: data.expires_at, | ||
| permissions: data.permissions, | ||
| repositories: data.repositories, | ||
| }; |
There was a problem hiding this comment.
createGitHubInstallationToken() returns { value: data.token, ... }, but provisionGitHubToken() reads installationToken.token when building the credential and usage instructions. This will make the provisioned GitHub token undefined. Align the return shape (e.g., return token instead of value, or update all call sites to use value).
| body: JSON.stringify({ | ||
| repositories: [repository.split("/")[1]], | ||
| permissions: permissions || { contents: "read", metadata: "read" }, | ||
| }), |
There was a problem hiding this comment.
permissions is passed into createGitHubInstallationToken() from provisionGitHubToken() as an array of strings (e.g., ["contents:write", "actions:write"]), but the GitHub API expects an object map (e.g., { contents: "write" }). As written, body.permissions will be an array and the token request will likely be rejected or ignored. Convert the input into the expected object form (or change the higher-level API to accept the object form) before POSTing to /access_tokens.
| const response = await fetch( | ||
| `https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}/files?per_page=100`, | ||
| { | ||
| headers: { | ||
| Authorization: `token ${token}`, | ||
| Accept: "application/vnd.github+json", | ||
| "User-Agent": "ChittyConnect/1.0", | ||
| }, | ||
| }, | ||
| ); | ||
|
|
||
| if (!response.ok) return []; | ||
|
|
||
| const files = await response.json(); | ||
| return files.map((f) => f.filename); |
There was a problem hiding this comment.
fetchChangedFiles() only requests the first page (per_page=100) and silently returns [] on any non-2xx response. For PRs with >100 files (or transient GitHub API issues), this will lead to missing labels/reviewers without any signal. Handle pagination via the Link header (or iterate page=) and log/propagate errors so automations can be retried/observed.
| const response = await fetch( | |
| `https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}/files?per_page=100`, | |
| { | |
| headers: { | |
| Authorization: `token ${token}`, | |
| Accept: "application/vnd.github+json", | |
| "User-Agent": "ChittyConnect/1.0", | |
| }, | |
| }, | |
| ); | |
| if (!response.ok) return []; | |
| const files = await response.json(); | |
| return files.map((f) => f.filename); | |
| const baseUrl = `https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}/files`; | |
| const perPage = 100; | |
| let page = 1; | |
| const filenames = []; | |
| // Paginate through all changed files to avoid missing files on large PRs | |
| while (true) { | |
| const url = `${baseUrl}?per_page=${perPage}&page=${page}`; | |
| const response = await fetch(url, { | |
| headers: { | |
| Authorization: `token ${token}`, | |
| Accept: "application/vnd.github+json", | |
| "User-Agent": "ChittyConnect/1.0", | |
| }, | |
| }); | |
| if (!response.ok) { | |
| let bodyText = ""; | |
| try { | |
| bodyText = await response.text(); | |
| } catch { | |
| // Ignore body read errors; we still log status information. | |
| } | |
| const truncatedBody = | |
| bodyText && bodyText.length > 500 | |
| ? `${bodyText.slice(0, 500)}...` | |
| : bodyText; | |
| console.error("Failed to fetch changed files from GitHub", { | |
| owner, | |
| repo, | |
| prNumber, | |
| page, | |
| status: response.status, | |
| statusText: response.statusText, | |
| body: truncatedBody, | |
| }); | |
| throw new Error( | |
| `GitHub API request for changed files failed with status ${response.status}`, | |
| ); | |
| } | |
| const files = await response.json(); | |
| if (!Array.isArray(files)) { | |
| console.error( | |
| "Unexpected GitHub API response format when fetching changed files", | |
| { | |
| owner, | |
| repo, | |
| prNumber, | |
| page, | |
| receivedType: typeof files, | |
| }, | |
| ); | |
| throw new Error("Unexpected GitHub API response format for changed files"); | |
| } | |
| filenames.push( | |
| ...files | |
| .map((f) => f && f.filename) | |
| .filter((name) => typeof name === "string" && name.length > 0), | |
| ); | |
| if (files.length < perPage) { | |
| // Last page reached. | |
| break; | |
| } | |
| page += 1; | |
| } | |
| return filenames; |
| // Resolve CODEOWNERS if we have changed files | ||
| if (useCODEOWNERS && changedFiles.length > 0) { | ||
| try { | ||
| const owners = await getCodeOwners(token, owner, repo, changedFiles); | ||
| defaultReviewers = [ | ||
| ...new Set([...defaultReviewers, ...owners.users]), | ||
| ]; | ||
| defaultTeams = [...new Set([...defaultTeams, ...owners.teams])]; |
There was a problem hiding this comment.
Now that requestReviewers() actively resolves CODEOWNERS, the current matching behavior can request too many reviewers because it accumulates owners from all matching rules. CODEOWNERS semantics are “last matching pattern wins” (with section handling, anchored patterns, etc.). Consider implementing last-match precedence (and more complete pattern rules) or using a proven CODEOWNERS parser to avoid incorrect reviewer assignment.
Summary
createScopedServiceToken— calls ChittyAuth derivative token APIcreateGitHubInstallationToken— uses GitHub App API for scoped installation tokensfetchChangedFiles— fetches PR changed files for label and reviewer automationrequestReviewers(parsing existed but was disconnected)Resolves all 5 TODOs identified in the brainstorm (Priority 2).
Test plan
🤖 Generated with Claude Code
Summary by CodeRabbit
Release Notes
New Features
Refactor