feat: channel-based agent authorization for Slack#2033
feat: channel-based agent authorization for Slack#2033nick-inkeep wants to merge 16 commits intomainfrom
Conversation
Enable linked Slack users in channels with configured default agents to execute those agents without explicit SpiceDB project membership. Changes: - Extend JWT schema with channel auth claims (authorized, authSource, channelId, authorizedProjectId) - Add SpiceDB bypass in trySlackUserJwtAuth when channel-authorized with project binding verification (D8) - Extend BaseExecutionContext metadata with slack auth context - Switch app-mention.ts from resolveChannelAgentConfig to resolveEffectiveAgent (D7) and pass channel auth params - Update slash command background exec to pass channel auth params - Add comprehensive tests for bypass logic, project mismatch rejection, and graceful fallback Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: c55e654 The changes in this PR will be included in the next version bump. This PR includes changesets to release 10 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tests were mocking the old resolveChannelAgentConfig from utils, but the implementation now imports resolveEffectiveAgent from agent-resolution. Updated all test cases to mock the correct module and include the required `source` field in mock return values. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
PR Review Summary
(4) Total Issues | Risk: Medium
🟠⚠️ Major (3) 🟠⚠️
🟠 1) packages/agents-core Missing changeset for published package
Issue: This PR modifies exported types (SlackAccessTokenPayloadSchema, SignSlackUserTokenParams, BaseExecutionContext) from the published @inkeep/agents-core package. Per CLAUDE.md changeset requirements: "Create a changeset for any user-facing change to a published package."
Why: Without a changeset, version bumps will not be triggered, and consumers will not see release notes for these changes. While the changes are additive and non-breaking, they still modify the public API surface of an npm-published package.
Fix:
pnpm bump patch --pkg agents-core "Add channel-based authorization claims to Slack JWT schema and BaseExecutionContext type"Refs:
Inline Comments:
- 🟠 Major:
slack-user-token.ts:42Schema allows incomplete authorization states —authorized: truewithout requiringauthorizedProjectId - 🟠 Major:
app-mention.test.ts:270Missing test verification of channel auth params passed tosignSlackUserToken
🟡 Minor (1) 🟡
Inline Comments:
- 🟡 Minor:
utility.ts:309-314Type allowsauthorized: falsebut runtime only populates when true
💭 Consider (3) 💭
💭 1) agents-api/src/middleware/runAuth.ts:247 Add debug log when bypass is skipped
Issue: When slackAuthorized evaluates to false, there's no log explaining why the bypass wasn't applied.
Why: Makes it harder to diagnose authorization issues when admins expect channel-based auth to work but it silently falls through to SpiceDB.
Fix: Add debug log with slackAuthorizedClaim, slackAuthorizedProjectId, requestedProjectId, and projectMatch fields.
💭 2) app-mention.ts:242 Consider stricter slackAuthorized condition
Issue: slackAuthorized is set to agentConfig != null, which means ANY resolved agent config triggers it, even if source === 'none'.
Fix: Consider slackAuthorized: agentConfig != null && agentConfig.source !== 'none' to ensure the bypass is only granted when there's true admin-configured channel or workspace default.
💭 3) api-key-auth.test.ts Missing test for authSource default fallback
Issue: The implementation defaults authSource to 'channel' when missing (line 313), but no test verifies this fallback behavior.
Fix: Add test case for authorized: true with missing authSource to document expected behavior.
🚫 REQUEST CHANGES
Summary: This PR implements a well-designed channel-based authorization bypass for Slack with appropriate security controls (project binding, JWT signature verification, short TTL, graceful fallback to SpiceDB). The security architecture is sound. However, there are 3 major issues to address before merging:
- Missing changeset — Required for any change to published packages
- Schema invariant enforcement — The JWT schema should enforce that
authorized: truerequiresauthorizedProjectId - Test coverage gap — The app-mention tests don't verify the critical channel auth params are passed correctly
Once these are addressed, this PR is ready to ship. 🚀
Discarded (12)
| Location | Issue | Reason Discarded |
|---|---|---|
runAuth.ts:243-280 |
Security design is sound | INFO — positive validation, not an issue |
runAuth.ts:282-296 |
Auth success logging follows patterns | INFO — confirms conventions |
slack-user-token.ts:38-41 |
JWT claim naming follows conventions | INFO — confirms conventions |
slack-user-token.ts:57-60 |
SignSlackUserTokenParams follows prefixing convention | INFO — confirms conventions |
utility.ts:309-314 |
metadata.slack follows established pattern | INFO — confirms conventions |
api-key-auth.test.ts:664-996 |
Test coverage follows established patterns | INFO — confirms conventions |
runAuth.ts:307-318 |
Authorization source metadata enables audit trails | INFO — positive validation |
runAuth.ts:243-280 |
Channel auth bypass is architecturally sound | INFO — positive validation |
relationTools.ts |
Sub-agent delegation inherits bypass without re-verification | Acceptable design per D9 — documented in PR description |
runAuth.ts:243-280 |
Establishes precedent for capability-based authorization | INFO — observation for future reference |
slack-user-token.test.ts |
Missing test for partial claims (authorized without authorizedProjectId) | Already covered by auth middleware tests — implicit coverage |
api-key-auth.test.ts |
Missing test for JWT verification throwing unexpectedly | Edge case caught by general error handling |
Reviewers (8)
| Reviewer | Returned | Main Findings | Consider | While You're Here | Inline Comments | Pending Recs | Discarded |
|---|---|---|---|---|---|---|---|
pr-review-security-iam |
3 | 0 | 1 | 0 | 1 | 0 | 1 |
pr-review-architecture |
5 | 0 | 0 | 0 | 0 | 0 | 5 |
pr-review-standards |
0 | 0 | 0 | 0 | 0 | 0 | 0 |
pr-review-consistency |
6 | 0 | 0 | 0 | 0 | 0 | 6 |
pr-review-breaking-changes |
5 | 1 | 0 | 0 | 0 | 0 | 4 |
pr-review-tests |
5 | 0 | 1 | 0 | 1 | 0 | 3 |
pr-review-types |
2 | 0 | 0 | 0 | 2 | 0 | 0 |
pr-review-errors |
2 | 0 | 1 | 0 | 0 | 0 | 1 |
| Total | 28 | 1 | 3 | 0 | 4 | 0 | 20 |
Note: Schema invariant finding merged from pr-review-types and pr-review-security-iam (same underlying issue).
Documents the new channel-based agent authorization feature from PR #2033. Users in channels with configured default agents are now automatically authorized to execute those agents without needing explicit project membership.
Review feedback addressed: Major: - Add .refine() to SlackAccessTokenPayloadSchema enforcing that when authorized=true, authorizedProjectId and authSource are required - Add changeset for agents-core (patch) - Add signSlackUserToken channel auth param verification in app-mention test Minor: - Change BaseExecutionContext.metadata.slack.authorized to literal `true` since the field is only populated when authorization is granted Consider: - Add debug log in runAuth.ts when channel auth bypass is not applied - Add test for authSource default fallback to 'channel' - Add schema invariant enforcement tests (authorized without authorizedProjectId, authorized without authSource) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Review Response (commit 39f6eef)Major — All addressed
Minor — Addressed
Consider — Assessed
|
There was a problem hiding this comment.
PR Review Summary
(0) Total Issues | Risk: Low
Re-Review Assessment
This is a re-review scoped to the 2 commits since the last automated review (786a3317, 39f6eef1). The delta addresses all 5 issues raised in the prior review:
| Prior Issue | Status | Verification |
|---|---|---|
🟠 Missing changeset for @inkeep/agents-core |
✅ RESOLVED | .changeset/christian-lime-piranha.md added with correct content |
| 🟠 Schema allows incomplete authorization states | ✅ RESOLVED | .refine() added to enforce authorized: true requires both authorizedProjectId and authSource |
| 🟠 Missing test verification of channel auth params | ✅ RESOLVED | app-mention.test.ts now verifies signSlackUserToken is called with correct params |
🟡 Type allows authorized: false but runtime only populates when true |
✅ RESOLVED | Changed to authorized: true literal type |
| 💭 Add debug log when bypass is skipped | ✅ RESOLVED | Debug log added with slackAuthorizedClaim, slackAuthorizedProjectId, requestedProjectId, and projectMatch fields |
Delta Quality Assessment
The delta changes are well-implemented:
- Schema invariant (
.refine()) is clean and follows Zod best practices - Debug logging includes all relevant fields for debugging authorization issues
- Type narrowing (
authorized: trueliteral) accurately reflects runtime behavior - Test coverage verifies the critical integration point between work app and JWT signing
- Two new tests for schema invariant enforcement (authorized without authorizedProjectId, authorized without authSource)
No New Issues
The delta introduces no new concerns. All changes are focused remediation of prior feedback.
✅ APPROVE
Summary: All prior review feedback has been addressed comprehensively. The implementation is well-designed with appropriate security controls (project binding, JWT signature verification, short TTL, graceful fallback to SpiceDB), proper schema invariants, and good observability. Ready to ship! 🚀
Reviewers (0)
| Reviewer | Returned | Main Findings | Consider | While You're Here | Inline Comments | Pending Recs | Discarded |
|---|---|---|---|---|---|---|---|
| (none dispatched — delta is remediation only) | — | — | — | — | — | — | — |
Note: Reviewer dispatch skipped because the delta consists entirely of remediation for prior review feedback. No new functional changes to review.
…+ OTel spans - Modal submissions (call sites 3-4): explicit slackAuthorized: false - /inkeep run and /inkeep list (call sites 6-7): explicit slackAuthorized: false - OTel: add AUTHORIZED and AUTH_SOURCE to SLACK_SPAN_KEYS - Set slack.authorized and slack.auth_source span attributes in app-mention and modal-submission handlers - Add modal-submission test file verifying slackAuthorized: false and span attributes - Add workspace auth source test to app-mention tests Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
PR Review Summary
(0) Total Issues | Risk: Low
Re-Review Assessment
This is a re-review scoped to 2 commits since the last automated review (83099d7e). The delta is a merge from main only — it brings in CI/infrastructure improvements but contains no changes to the PR's feature code.
| Delta Change | Source | Assessment |
|---|---|---|
.github/workflows/ci.yml |
main merge | ✅ Unrelated — Next.js build cache setup |
.github/workflows/cypress.yml |
main merge | ✅ Unrelated — Next.js build cache setup |
agents-manage-ui/next.config.ts |
main merge | ✅ Unrelated — Turbopack filesystem cache |
None of the PR's actual files have changed since the last review:
- JWT schema (
slack-user-token.ts) — unchanged - Auth middleware (
runAuth.ts) — unchanged - Agent resolution (
agent-resolution.ts) — unchanged - UI toggle (
channel-agent-cell.tsx) — unchanged - All tests — unchanged
- Documentation — unchanged
Prior Review Status
This PR has been reviewed 9 times by automated review, with all issues resolved:
| Review | Outcome | Notes |
|---|---|---|
| Initial | REQUEST CHANGES | 4 issues raised |
| Re-review 1 | ✅ APPROVE | All issues resolved |
| Re-review 2–8 | ✅ APPROVE | Clean deltas, 0 new issues |
| This review | ✅ APPROVE | Merge-only delta, no feature changes |
No New Issues
The delta introduces no functional changes to review. All PR files remain unchanged from the previously approved state.
✅ APPROVE
Summary: This is a routine merge of main into the feature branch bringing in CI/build improvements (Next.js build caching, Turbopack filesystem cache). The PR's channel-based Slack authorization feature is complete and unchanged from the prior approved state. All 9 prior review rounds have confirmed the implementation is well-designed with appropriate security controls (project binding, JWT signature verification, short TTL, graceful SpiceDB fallback), comprehensive test coverage (40+ tests), and proper documentation. Ship it! 🚀
Reviewers (0)
| Reviewer | Returned | Main Findings | Consider | While You're Here | Inline Comments | Pending Recs | Discarded |
|---|---|---|---|---|---|---|---|
| (none dispatched — delta contains no feature changes) | — | — | — | — | — | — | — |
Note: Reviewer dispatch skipped because the delta consists entirely of a merge from main with no changes to PR files.
There was a problem hiding this comment.
PR Review Summary
(0) Total Issues | Risk: Low
Re-Review Assessment
This is a re-review scoped to 1 commit (3b999da3e) since the last automated review (206b342fd). The delta is a merge from main only — it brings in CI/infrastructure improvements but contains no changes to the PR's feature code.
| Delta Change | Source | Assessment |
|---|---|---|
.changeset/*.md |
main merge | ✅ Unrelated — version packages cleanup |
.github/workflows/*.yml |
main merge | ✅ Unrelated — CI workflow improvements |
agents-api/src/__tests__/*.ts |
main merge | ✅ Unrelated — test infrastructure improvements |
agents-api/src/domains/manage/routes/signoz.ts |
main merge | ✅ Unrelated — SigNoz route additions |
agents-manage-ui/* |
main merge | ✅ Unrelated — UI improvements and bug fixes |
packages/agents-core/src/utils/mock-provider.ts |
main merge | ✅ Unrelated — mock provider for testing |
None of the PR's actual files have changed since the last review:
- JWT schema (
slack-user-token.ts) — unchanged - Auth middleware (
runAuth.ts) — unchanged - Agent resolution (
agent-resolution.ts) — unchanged - UI toggle (
channel-agent-cell.tsx) — unchanged - All tests — unchanged
- Documentation — unchanged
Prior Review Status
This PR has been reviewed 10 times by automated review, with all issues resolved:
| Review | Outcome | Notes |
|---|---|---|
| Initial | REQUEST CHANGES | 4 issues raised |
| Re-review 1 | ✅ APPROVE | All issues resolved |
| Re-review 2–9 | ✅ APPROVE | Clean deltas, 0 new issues |
| This review | ✅ APPROVE | Merge-only delta, no feature changes |
No New Issues
The delta introduces no functional changes to review. All PR files remain unchanged from the previously approved state.
✅ APPROVE
Summary: This is a routine merge of main into the feature branch bringing in CI/build improvements (mock provider for testing, SigNoz routes, workflow optimizations). The PR's channel-based Slack authorization feature is complete and unchanged from the prior approved state. All 10 prior review rounds have confirmed the implementation is well-designed with appropriate security controls (project binding, JWT signature verification, short TTL, graceful SpiceDB fallback), comprehensive test coverage (40+ tests), and proper documentation. Ship it! 🚀
Reviewers (0)
| Reviewer | Returned | Main Findings | Consider | While You're Here | Inline Comments | Pending Recs | Discarded |
|---|---|---|---|---|---|---|---|
| (none dispatched — delta contains no feature changes) | — | — | — | — | — | — | — |
Note: Reviewer dispatch skipped because the delta consists entirely of a merge from main with no changes to PR files.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Resolve conflicts in app-mention.ts and app-mention.test.ts: - Keep resolveEffectiveAgent from PR branch - Adopt timedOp pattern from main for consistent timing instrumentation - Keep separate agent-resolution mock in tests Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
PR Review Summary
(0) Total Issues | Risk: Low
Re-Review Assessment
This is a re-review scoped to 6 commits since the last automated review (3b999da3e). The delta contains:
| Delta Change | Source | Assessment |
|---|---|---|
.changeset/yabbering-teal-squirrel.md |
PR #2072 merge | ✅ Unrelated — changeset for Slack message formatting |
.github/workflows/ci.yml |
PR #2092 merge | ✅ Unrelated — CI build/test phase splitting |
agents-manage-ui/.../form.browser.test.tsx |
PR #2088 merge | ✅ Unrelated — Monaco waitFor timeout increase |
src/slack/services/client.ts |
PR #2072 merge | ✅ Compatible — new getSlackChannelInfo() helper |
src/slack/services/events/app-mention.ts |
PR #2072 merge | ✅ Compatible — channel/user context enrichment |
src/slack/services/events/utils.ts |
PR #2072 merge | ✅ Compatible — new helpers (timedOp, formatChannelContext) |
src/__tests__/slack/*.test.ts |
PR #2072 merge | ✅ Compatible — test updates for new message formats |
Authorization Path Verification
The channel authorization feature code is unchanged in this delta:
| Location | Code | Status |
|---|---|---|
app-mention.ts:167 |
span.setAttribute(SLACK_SPAN_KEYS.AUTHORIZED, agentConfig.grantAccessToMembers) |
✅ Unchanged |
app-mention.ts:250-254 |
JWT signing with slackAuthorized: agentConfig?.grantAccessToMembers ?? false |
✅ Unchanged |
app-mention.ts:352-356 |
JWT signing with channel auth context | ✅ Unchanged |
The merged changes from PR #2072 enhance the Slack message context (adding channel names, user display names, structured thread formatting) without modifying the authorization logic. The new helpers have proper error handling with graceful fallbacks:
getSlackChannelInfo()returnsnullon errorformatChannelContext()falls back to'Slack'when channel info unavailabletimedOp()logs slow operations without affecting execution
Prior Review Status
| Review | Outcome | Notes |
|---|---|---|
| Initial | REQUEST CHANGES | 4 issues raised |
| Re-review 1 | ✅ APPROVE | All issues resolved |
| Re-review 2–10 | ✅ APPROVE | Clean deltas, 0 new issues |
| This review | ✅ APPROVE | Merged improvements, no authorization changes |
No New Issues
The delta introduces no concerns affecting the channel-based authorization feature. All PR files related to authorization remain unchanged from the previously approved state.
✅ APPROVE
Summary: This delta merges improvements from main including enhanced Slack message context (PR #2072) and CI optimizations (PR #2088, #2092). The channel-based authorization feature is complete and unchanged — JWT schema with .refine() invariant, SpiceDB bypass logic, grantAccessToMembers toggle, and all 40+ tests remain intact. The merged changes are additive enhancements that improve message readability without affecting security controls. Ship it! 🚀
Reviewers (0)
| Reviewer | Returned | Main Findings | Consider | While You're Here | Inline Comments | Pending Recs | Discarded |
|---|---|---|---|---|---|---|---|
| (none dispatched — delta review) | — | — | — | — | — | — | — |
Note: No sub-reviewers dispatched for this delta re-review. The changes are merged improvements from main that don't touch the authorization feature code, so specialized review was not warranted.
Summary
Implements channel-based agent authorization for Slack, allowing linked Slack users in channels with configured default agents to execute those agents without explicit SpiceDB project membership. Includes a configurable
grantAccessToMemberstoggle so admins can control whether channel-based auth bypass is enabled per channel.Problem: When an admin assigns a default agent to a Slack channel, users must also be explicit project members in SpiceDB. This creates a high-friction onboarding barrier — admins configure the channel, but users get silently denied.
Solution: Extend the Slack JWT with channel authorization claims (
slack.authorized,slack.authorizedProjectId). When the work app resolves an agent via channel/workspace config andgrantAccessToMembersis enabled, it setsslackAuthorized: truein the JWT. The Run API'strySlackUserJwtAuththen bypasses SpiceDB when channel-authorized AND the authorized project matches the request.Spec: SPEC.md
Changes
JWT Schema (
agents-core)authorized,authSource,channelId,authorizedProjectIdtoSlackAccessTokenPayloadSchema.refine()constraint enforcingauthorizedProjectIdandauthSourceare required whenauthorized: trueSignSlackUserTokenParamssignSlackUserTokenclaims constructionAuth Bypass (
agents-api)canUseProjectStrictintrySlackUserJwtAuthwhenslack.authorized === trueANDauthorizedProjectIdmatchesx-inkeep-project-idheadermetadata.slackcontext for channel-authorized requestsExecution Context (
agents-core)metadata.slacktoBaseExecutionContexttype (authorized: trueliteral,authSource,channelId,teamId)Database Schema + Migration (
agents-core)grant_access_to_membersboolean column towork_app_slack_channel_agent_configstable (defaulttrue,NOT NULL)0014_odd_oracle.sql— singleALTER TABLE ADD COLUMNfindWorkAppSlackChannelAgentConfignow returnsgrantAccessToMembersfieldAgent Resolution (
agents-work-apps)resolveEffectiveAgentpropagatesgrantAccessToMembersfrom channel config (explicit value) or workspace config (defaults totrue)ResolvedAgentConfigtype extended withgrantAccessToMembers: booleanCall Sites — JWT Signing (
agents-work-apps)resolveChannelAgentConfigtoresolveEffectiveAgent+ pass channel auth params (2 call sites).slackAuthorizedgated byagentConfig.grantAccessToMembers(not hardcodedtrue)slackAuthorized: agentConfig.grantAccessToMembers./inkeep run nameand/inkeep listpassslackAuthorized: false(2 call sites)slackAuthorized: false(2 call sites) — correct because modal submissions bypass channel contextOTel Observability (
agents-work-apps)AUTHORIZEDandAUTH_SOURCEtoSLACK_SPAN_KEYSintracer.tsslack.authorizedspan attribute reflects actualgrantAccessToMembersvalue (not hardcoded)Manage UI —
grantAccessToMembersToggle (agents-manage-ui)channel-agent-cell.tsx: AddSwitchtoggle withShieldCheckicon and tooltip inside channel agent popover. Tooltip: "When enabled, channel members can use this agent without explicit project access." Toggle appears only when channel has a custom agent configchannel-defaults-section.tsx: ThreadonToggleGrantAccessprop toChannelAgentCellindex.tsx(AgentConfigurationCard):handleToggleGrantAccesshandler callsslackApi.setChannelDefaultAgentwith updatedgrantAccessToMembers. Bulk set explicitly passesgrantAccessToMembers: trueslack-api.ts:listChannelsreturn type includesgrantAccessToMembers?: booleanonagentConfigtypes.ts:Channel.agentConfigtype includesgrantAccessToMembers?: booleanChannel Config API Route (
agents-work-apps)PUT /workspaces/:teamId/channels/:channelId/settingsacceptsgrantAccessToMembersinagentConfigbodywork_app_slack_channel_agent_configstableGET /workspaces/:teamId/channelsresponseKey Design Decisions
resolveEffectiveAgent(has source field) instead ofresolveChannelAgentConfigauthorizedProjectIdmust matchx-inkeep-project-idheadertruefor backward compatibility — existing channel configs automatically grant access. Admins can opt out per channel via the Manage UI toggleTest plan
JWT Schema Tests (
agents-core)authorized: truerequiresauthorizedProjectIdandauthSourceAuth Bypass Tests (
agents-api)slack.authorized: true+ matching projectId)channelauthorized: falseAgent Resolution Tests (
agents-work-apps)grantAccessToMembers: true→ returned in resultgrantAccessToMembers: false→ returned in resultgrantAccessToMembers: truegrantAccessToMembers: false→ propagatedApp Mention Tests (
agents-work-apps)slack.authorized,slack.auth_source)slackAuthorized: falsewhengrantAccessToMembers: falseSlash Command Tests (
agents-work-apps)handleQuestionCommandpassesslackAuthorized: truewhengrantAccessToMembers: truehandleQuestionCommandpassesslackAuthorized: falsewhengrantAccessToMembers: falseModal Submission Tests (
agents-work-apps)slackAuthorized: falseslack.authorizedspan attribute set tofalseQuality Gates
pnpm test— 291 tests passingpnpm typecheck— cleanpnpm lint— cleanQA Checklist
Verified via automated tests only (no manual/browser testing)
grantAccessToMembersSwitch toggle inchannel-agent-cell.tsxwas NOT browser-tested. Rendering, layout, tooltip positioning, and click behavior verified only by code review — no Chrome extension or manual browser session was used.grantAccessToMembers: true: Logic verified by code review ofindex.tsx. No manual test of bulk assign + verify toggle state.handleToggleGrantAccess→ API call → re-fetch flow was not manually verified end-to-end in a running app.Not tested (requires deployed environment)
/inkeepcommand in a real Slack channel withgrantAccessToMemberstoggled on/off and verifying the JWT bypass activates/deactivates. This requires a deployed work-apps service + Run API + Slack workspace.Rationale
This feature touches backend auth middleware, JWT signing, and a DB schema change — none of which are testable via browser automation alone. The Manage UI toggle is the only browser-testable surface, and it was verified via code review + type safety (props thread correctly through 3 components). All authorization logic is covered by 40+ unit tests.
🤖 Generated with Claude Code