diff --git a/.claude/settings.json b/.claude/settings.json index 3c952dfc8..5c0e45090 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -29,6 +29,15 @@ "command": "bash scripts/claude-hooks/auto-format.sh" } ] + }, + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": "cd \"$(git rev-parse --show-toplevel)\" && bash scripts/hooks/coderabbit-review-gate.sh" + } + ] } ], "Stop": [ diff --git a/.github/workflows/coderabbit-smoke-test.yml b/.github/workflows/coderabbit-smoke-test.yml new file mode 100644 index 000000000..4db8f1cd8 --- /dev/null +++ b/.github/workflows/coderabbit-smoke-test.yml @@ -0,0 +1,129 @@ +name: CodeRabbit Integration Smoke Test + +# Validates the CodeRabbit integration works end-to-end: +# - CLI installs and authenticates +# - Can review files against the real CodeRabbit API +# - Config file (.coderabbit.yaml) is valid + +on: + pull_request: + branches: [main] + paths: + - '.coderabbit.yaml' + - 'components/backend/handlers/coderabbit_auth.go' + - 'components/backend/handlers/integration_validation.go' + - 'components/frontend/src/components/coderabbit-connection-card.tsx' + - 'components/runners/ambient-runner/ambient_runner/platform/auth.py' + - 'scripts/hooks/coderabbit-review-gate.sh' + - '.github/workflows/coderabbit-smoke-test.yml' + + workflow_dispatch: + + schedule: + - cron: '0 6 * * 1' # Weekly Monday 6am UTC + +permissions: + contents: read + +concurrency: + group: coderabbit-smoke-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + smoke-test: + name: CodeRabbit Smoke Test + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Set up Node.js + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 + with: + node-version: '20' + + - name: Install CodeRabbit CLI + run: npm install -g coderabbit + + - name: Verify CLI installed + run: | + coderabbit --version + echo "CLI binary: $(which coderabbit)" + + - name: Validate .coderabbit.yaml schema + run: | + echo "=== Validating .coderabbit.yaml ===" + python3 -c " + import yaml, sys + with open('.coderabbit.yaml') as f: + config = yaml.safe_load(f) + assert 'reviews' in config, 'Missing reviews section' + assert 'language' in config, 'Missing language field' + print(f'Config valid: {len(config)} top-level keys') + print(f'Reviews profile: {config[\"reviews\"].get(\"profile\", \"not set\")}') + print(f'Auto review: {config[\"reviews\"].get(\"auto_review\", {}).get(\"enabled\", False)}') + print(f'Tools configured: {len(config[\"reviews\"].get(\"tools\", {}))}') + " + echo "PASSED: .coderabbit.yaml is valid" + + - name: Run CodeRabbit review on config file + env: + CODERABBIT_API_KEY: ${{ secrets.CODERABBIT_API_KEY }} + run: | + echo "=== Running CodeRabbit review against real API ===" + + # Skip if no API key (fork PRs, missing secret) + if [ -z "$CODERABBIT_API_KEY" ]; then + echo "CODERABBIT_API_KEY not set - skipping live review" + echo "This is expected for fork PRs or when the secret is not configured" + exit 0 + fi + + # Review the config file itself using agent mode for structured output + EXIT_CODE=0 + OUTPUT=$(coderabbit review \ + --agent \ + --files .coderabbit.yaml \ + 2>&1) || EXIT_CODE=$? + + echo "$OUTPUT" + + # Auth errors are fatal + if echo "$OUTPUT" | grep -qiE "unauthorized|forbidden|invalid.*key"; then + echo "FAILED: CodeRabbit API key appears invalid" + exit 1 + fi + + # Non-zero exit from CLI is a real failure + if [ "$EXIT_CODE" -ne 0 ]; then + echo "FAILED: coderabbit review exited $EXIT_CODE" + exit 1 + fi + + echo "PASSED: CodeRabbit API responded successfully" + + - name: Verify review gate runs in standalone mode + env: + CODERABBIT_API_KEY: ${{ secrets.CODERABBIT_API_KEY }} + run: | + echo "=== Testing review gate (standalone / CI mode) ===" + chmod +x scripts/hooks/coderabbit-review-gate.sh + + # Run without CLAUDE_TOOL_INPUT — triggers standalone mode + # which runs coderabbit review --agent --base main directly. + EXIT_CODE=0 + OUTPUT=$(bash scripts/hooks/coderabbit-review-gate.sh 2>&1) || EXIT_CODE=$? + + echo "$OUTPUT" + + # Exit 0 = review passed, exit 2 = findings or CLI missing + if [ "$EXIT_CODE" -eq 0 ]; then + echo "PASSED: Review gate completed successfully" + elif [ "$EXIT_CODE" -eq 2 ]; then + echo "PASSED: Review gate blocked (expected in CI — findings or rate limit)" + else + echo "FAILED: Unexpected exit code $EXIT_CODE" + exit 1 + fi diff --git a/BOOKMARKS.md b/BOOKMARKS.md index a35b26239..ed91ee466 100644 --- a/BOOKMARKS.md +++ b/BOOKMARKS.md @@ -74,6 +74,7 @@ Convention documentation for each component. Loaded by review agents on demand. | [API Server Guide](components/ambient-api-server/CLAUDE.md) | rh-trex-ai REST API, plugin system, code generation | | [SDK Guide](components/ambient-sdk/CLAUDE.md) | Go + Python client libraries for the public API | | [CLI README](components/ambient-cli/README.md) | acpctl CLI for managing agentic sessions | +| [CodeRabbit Integration](docs/src/content/docs/features/coderabbit.md) | Setup, review gate, session credentials, `.coderabbit.yaml` config | ## Development Environment diff --git a/components/ambient-api-server/pkg/api/openapi/README.md b/components/ambient-api-server/pkg/api/openapi/README.md index 9ec2643b1..1a3ee70e5 100644 --- a/components/ambient-api-server/pkg/api/openapi/README.md +++ b/components/ambient-api-server/pkg/api/openapi/README.md @@ -209,4 +209,3 @@ Each of these functions takes a value of the given basic type and returns a poin ## Author ambient-code@redhat.com - diff --git a/components/ambient-api-server/pkg/api/openapi/docs/Agent.md b/components/ambient-api-server/pkg/api/openapi/docs/Agent.md index 3f53a91d9..b77e7810a 100644 --- a/components/ambient-api-server/pkg/api/openapi/docs/Agent.md +++ b/components/ambient-api-server/pkg/api/openapi/docs/Agent.md @@ -4,17 +4,17 @@ Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- -**Id** | Pointer to **string** | | [optional] -**Kind** | Pointer to **string** | | [optional] -**Href** | Pointer to **string** | | [optional] -**CreatedAt** | Pointer to **time.Time** | | [optional] -**UpdatedAt** | Pointer to **time.Time** | | [optional] -**ProjectId** | **string** | The project this agent belongs to | -**Name** | **string** | Human-readable identifier; unique within the project | -**Prompt** | Pointer to **string** | Defines who this agent is. Mutable via PATCH. Access controlled by RBAC. | [optional] -**CurrentSessionId** | Pointer to **string** | Denormalized for fast reads — the active session, if any | [optional] [readonly] -**Labels** | Pointer to **string** | | [optional] -**Annotations** | Pointer to **string** | | [optional] +**Id** | Pointer to **string** | | [optional] +**Kind** | Pointer to **string** | | [optional] +**Href** | Pointer to **string** | | [optional] +**CreatedAt** | Pointer to **time.Time** | | [optional] +**UpdatedAt** | Pointer to **time.Time** | | [optional] +**ProjectId** | **string** | The project this agent belongs to | +**Name** | **string** | Human-readable identifier; unique within the project | +**Prompt** | Pointer to **string** | Defines who this agent is. Mutable via PATCH. Access controlled by RBAC. | [optional] +**CurrentSessionId** | Pointer to **string** | Denormalized for fast reads — the active session, if any | [optional] [readonly] +**Labels** | Pointer to **string** | | [optional] +**Annotations** | Pointer to **string** | | [optional] ## Methods @@ -302,5 +302,3 @@ HasAnnotations returns a boolean if a field has been set. [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) - - diff --git a/components/ambient-api-server/pkg/api/openapi/docs/AgentPatchRequest.md b/components/ambient-api-server/pkg/api/openapi/docs/AgentPatchRequest.md index ec9075386..39ce75574 100644 --- a/components/ambient-api-server/pkg/api/openapi/docs/AgentPatchRequest.md +++ b/components/ambient-api-server/pkg/api/openapi/docs/AgentPatchRequest.md @@ -4,10 +4,10 @@ Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- -**Name** | Pointer to **string** | | [optional] -**Prompt** | Pointer to **string** | Update agent prompt (access controlled by RBAC) | [optional] -**Labels** | Pointer to **string** | | [optional] -**Annotations** | Pointer to **string** | | [optional] +**Name** | Pointer to **string** | | [optional] +**Prompt** | Pointer to **string** | Update agent prompt (access controlled by RBAC) | [optional] +**Labels** | Pointer to **string** | | [optional] +**Annotations** | Pointer to **string** | | [optional] ## Methods @@ -130,5 +130,3 @@ HasAnnotations returns a boolean if a field has been set. [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) - - diff --git a/components/ambient-api-server/pkg/api/openapi/docs/AgentSessionList.md b/components/ambient-api-server/pkg/api/openapi/docs/AgentSessionList.md index 61e32e0e6..81ea6408a 100644 --- a/components/ambient-api-server/pkg/api/openapi/docs/AgentSessionList.md +++ b/components/ambient-api-server/pkg/api/openapi/docs/AgentSessionList.md @@ -4,11 +4,11 @@ Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- -**Kind** | **string** | | -**Page** | **int32** | | -**Size** | **int32** | | -**Total** | **int32** | | -**Items** | [**[]Session**](Session.md) | | +**Kind** | **string** | | +**Page** | **int32** | | +**Size** | **int32** | | +**Total** | **int32** | | +**Items** | [**[]Session**](Session.md) | | ## Methods @@ -131,5 +131,3 @@ SetItems sets Items field to given value. [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) - - diff --git a/components/ambient-api-server/pkg/api/openapi/docs/Credential.md b/components/ambient-api-server/pkg/api/openapi/docs/Credential.md index 7ee37d7cc..55918dd28 100644 --- a/components/ambient-api-server/pkg/api/openapi/docs/Credential.md +++ b/components/ambient-api-server/pkg/api/openapi/docs/Credential.md @@ -4,20 +4,20 @@ Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- -**Id** | Pointer to **string** | | [optional] -**Kind** | Pointer to **string** | | [optional] -**Href** | Pointer to **string** | | [optional] -**CreatedAt** | Pointer to **time.Time** | | [optional] -**UpdatedAt** | Pointer to **time.Time** | | [optional] -**ProjectId** | **string** | ID of the project this credential belongs to | -**Name** | **string** | | -**Description** | Pointer to **string** | | [optional] -**Provider** | **string** | | -**Token** | Pointer to **string** | Credential token value; write-only, never returned in GET/LIST responses | [optional] -**Url** | Pointer to **string** | | [optional] -**Email** | Pointer to **string** | | [optional] -**Labels** | Pointer to **string** | | [optional] -**Annotations** | Pointer to **string** | | [optional] +**Id** | Pointer to **string** | | [optional] +**Kind** | Pointer to **string** | | [optional] +**Href** | Pointer to **string** | | [optional] +**CreatedAt** | Pointer to **time.Time** | | [optional] +**UpdatedAt** | Pointer to **time.Time** | | [optional] +**ProjectId** | **string** | ID of the project this credential belongs to | +**Name** | **string** | | +**Description** | Pointer to **string** | | [optional] +**Provider** | **string** | | +**Token** | Pointer to **string** | Credential token value; write-only, never returned in GET/LIST responses | [optional] +**Url** | Pointer to **string** | | [optional] +**Email** | Pointer to **string** | | [optional] +**Labels** | Pointer to **string** | | [optional] +**Annotations** | Pointer to **string** | | [optional] ## Methods @@ -375,5 +375,3 @@ HasAnnotations returns a boolean if a field has been set. [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) - - diff --git a/components/ambient-api-server/pkg/api/openapi/docs/CredentialList.md b/components/ambient-api-server/pkg/api/openapi/docs/CredentialList.md index ead48d971..37b3a95de 100644 --- a/components/ambient-api-server/pkg/api/openapi/docs/CredentialList.md +++ b/components/ambient-api-server/pkg/api/openapi/docs/CredentialList.md @@ -4,11 +4,11 @@ Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- -**Kind** | **string** | | -**Page** | **int32** | | -**Size** | **int32** | | -**Total** | **int32** | | -**Items** | [**[]Credential**](Credential.md) | | +**Kind** | **string** | | +**Page** | **int32** | | +**Size** | **int32** | | +**Total** | **int32** | | +**Items** | [**[]Credential**](Credential.md) | | ## Methods @@ -131,5 +131,3 @@ SetItems sets Items field to given value. [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) - - diff --git a/components/ambient-api-server/pkg/api/openapi/docs/CredentialPatchRequest.md b/components/ambient-api-server/pkg/api/openapi/docs/CredentialPatchRequest.md index b06cdbfac..f8935c4f4 100644 --- a/components/ambient-api-server/pkg/api/openapi/docs/CredentialPatchRequest.md +++ b/components/ambient-api-server/pkg/api/openapi/docs/CredentialPatchRequest.md @@ -4,14 +4,14 @@ Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- -**Name** | Pointer to **string** | | [optional] -**Description** | Pointer to **string** | | [optional] -**Provider** | Pointer to **string** | | [optional] -**Token** | Pointer to **string** | Credential token value; write-only, never returned in GET/LIST responses | [optional] -**Url** | Pointer to **string** | | [optional] -**Email** | Pointer to **string** | | [optional] -**Labels** | Pointer to **string** | | [optional] -**Annotations** | Pointer to **string** | | [optional] +**Name** | Pointer to **string** | | [optional] +**Description** | Pointer to **string** | | [optional] +**Provider** | Pointer to **string** | | [optional] +**Token** | Pointer to **string** | Credential token value; write-only, never returned in GET/LIST responses | [optional] +**Url** | Pointer to **string** | | [optional] +**Email** | Pointer to **string** | | [optional] +**Labels** | Pointer to **string** | | [optional] +**Annotations** | Pointer to **string** | | [optional] ## Methods @@ -234,5 +234,3 @@ HasAnnotations returns a boolean if a field has been set. [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) - - diff --git a/components/ambient-api-server/pkg/api/openapi/docs/CredentialTokenResponse.md b/components/ambient-api-server/pkg/api/openapi/docs/CredentialTokenResponse.md index ba3fff738..cf2d598c9 100644 --- a/components/ambient-api-server/pkg/api/openapi/docs/CredentialTokenResponse.md +++ b/components/ambient-api-server/pkg/api/openapi/docs/CredentialTokenResponse.md @@ -4,9 +4,9 @@ Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- -**CredentialId** | **string** | ID of the credential | -**Provider** | **string** | Provider type for this credential | -**Token** | **string** | Decrypted token value | +**CredentialId** | **string** | ID of the credential | +**Provider** | **string** | Provider type for this credential | +**Token** | **string** | Decrypted token value | ## Methods @@ -89,5 +89,3 @@ SetToken sets Token field to given value. [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) - - diff --git a/components/ambient-api-server/pkg/api/openapi/docs/DefaultAPI.md b/components/ambient-api-server/pkg/api/openapi/docs/DefaultAPI.md index f445b5cde..39838aee9 100644 --- a/components/ambient-api-server/pkg/api/openapi/docs/DefaultAPI.md +++ b/components/ambient-api-server/pkg/api/openapi/docs/DefaultAPI.md @@ -108,9 +108,9 @@ Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- **page** | **int32** | Page number of record list when record list exceeds specified page size | [default to 1] **size** | **int32** | Maximum number of records to return | [default to 100] - **search** | **string** | Specifies the search criteria | - **orderBy** | **string** | Specifies the order by criteria | - **fields** | **string** | Supplies a comma-separated list of fields to be returned | + **search** | **string** | Specifies the search criteria | + **orderBy** | **string** | Specifies the order by criteria | + **fields** | **string** | Supplies a comma-separated list of fields to be returned | ### Return type @@ -167,7 +167,7 @@ func main() { Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- **ctx** | **context.Context** | context for authentication, logging, cancellation, deadlines, tracing, etc. -**id** | **string** | The id of record | +**id** | **string** | The id of record | ### Other Parameters @@ -235,7 +235,7 @@ func main() { Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- **ctx** | **context.Context** | context for authentication, logging, cancellation, deadlines, tracing, etc. -**id** | **string** | The id of record | +**id** | **string** | The id of record | ### Other Parameters @@ -304,7 +304,7 @@ func main() { Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- **ctx** | **context.Context** | context for authentication, logging, cancellation, deadlines, tracing, etc. -**id** | **string** | The id of record | +**id** | **string** | The id of record | ### Other Parameters @@ -314,7 +314,7 @@ Other parameters are passed through a pointer to a apiApiAmbientV1ProjectSetting Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- - **projectSettingsPatchRequest** | [**ProjectSettingsPatchRequest**](ProjectSettingsPatchRequest.md) | Updated project settings data | + **projectSettingsPatchRequest** | [**ProjectSettingsPatchRequest**](ProjectSettingsPatchRequest.md) | Updated project settings data | ### Return type @@ -378,7 +378,7 @@ Other parameters are passed through a pointer to a apiApiAmbientV1ProjectSetting Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- - **projectSettings** | [**ProjectSettings**](ProjectSettings.md) | Project settings data | + **projectSettings** | [**ProjectSettings**](ProjectSettings.md) | Project settings data | ### Return type @@ -448,9 +448,9 @@ Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- **page** | **int32** | Page number of record list when record list exceeds specified page size | [default to 1] **size** | **int32** | Maximum number of records to return | [default to 100] - **search** | **string** | Specifies the search criteria | - **orderBy** | **string** | Specifies the order by criteria | - **fields** | **string** | Supplies a comma-separated list of fields to be returned | + **search** | **string** | Specifies the search criteria | + **orderBy** | **string** | Specifies the order by criteria | + **fields** | **string** | Supplies a comma-separated list of fields to be returned | ### Return type @@ -508,8 +508,8 @@ func main() { Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- **ctx** | **context.Context** | context for authentication, logging, cancellation, deadlines, tracing, etc. -**id** | **string** | The id of record | -**agentId** | **string** | The id of the agent | +**id** | **string** | The id of record | +**agentId** | **string** | The id of the agent | ### Other Parameters @@ -579,8 +579,8 @@ func main() { Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- **ctx** | **context.Context** | context for authentication, logging, cancellation, deadlines, tracing, etc. -**id** | **string** | The id of record | -**agentId** | **string** | The id of the agent | +**id** | **string** | The id of record | +**agentId** | **string** | The id of the agent | ### Other Parameters @@ -650,8 +650,8 @@ func main() { Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- **ctx** | **context.Context** | context for authentication, logging, cancellation, deadlines, tracing, etc. -**id** | **string** | The id of record | -**agentId** | **string** | The id of the agent | +**id** | **string** | The id of record | +**agentId** | **string** | The id of the agent | ### Other Parameters @@ -723,8 +723,8 @@ func main() { Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- **ctx** | **context.Context** | context for authentication, logging, cancellation, deadlines, tracing, etc. -**id** | **string** | The id of record | -**agentId** | **string** | The id of the agent | +**id** | **string** | The id of record | +**agentId** | **string** | The id of the agent | ### Other Parameters @@ -795,9 +795,9 @@ func main() { Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- **ctx** | **context.Context** | context for authentication, logging, cancellation, deadlines, tracing, etc. -**id** | **string** | The id of record | -**agentId** | **string** | The id of the agent | -**msgId** | **string** | The id of the inbox message | +**id** | **string** | The id of record | +**agentId** | **string** | The id of the agent | +**msgId** | **string** | The id of the inbox message | ### Other Parameters @@ -870,9 +870,9 @@ func main() { Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- **ctx** | **context.Context** | context for authentication, logging, cancellation, deadlines, tracing, etc. -**id** | **string** | The id of record | -**agentId** | **string** | The id of the agent | -**msgId** | **string** | The id of the inbox message | +**id** | **string** | The id of record | +**agentId** | **string** | The id of the agent | +**msgId** | **string** | The id of the inbox message | ### Other Parameters @@ -884,7 +884,7 @@ Name | Type | Description | Notes - **inboxMessagePatchRequest** | [**InboxMessagePatchRequest**](InboxMessagePatchRequest.md) | Inbox message patch | + **inboxMessagePatchRequest** | [**InboxMessagePatchRequest**](InboxMessagePatchRequest.md) | Inbox message patch | ### Return type @@ -945,8 +945,8 @@ func main() { Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- **ctx** | **context.Context** | context for authentication, logging, cancellation, deadlines, tracing, etc. -**id** | **string** | The id of record | -**agentId** | **string** | The id of the agent | +**id** | **string** | The id of record | +**agentId** | **string** | The id of the agent | ### Other Parameters @@ -957,7 +957,7 @@ Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- - **inboxMessage** | [**InboxMessage**](InboxMessage.md) | Inbox message to send | + **inboxMessage** | [**InboxMessage**](InboxMessage.md) | Inbox message to send | ### Return type @@ -1018,8 +1018,8 @@ func main() { Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- **ctx** | **context.Context** | context for authentication, logging, cancellation, deadlines, tracing, etc. -**id** | **string** | The id of record | -**agentId** | **string** | The id of the agent | +**id** | **string** | The id of record | +**agentId** | **string** | The id of the agent | ### Other Parameters @@ -1030,7 +1030,7 @@ Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- - **agentPatchRequest** | [**AgentPatchRequest**](AgentPatchRequest.md) | Updated agent data | + **agentPatchRequest** | [**AgentPatchRequest**](AgentPatchRequest.md) | Updated agent data | ### Return type @@ -1092,8 +1092,8 @@ func main() { Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- **ctx** | **context.Context** | context for authentication, logging, cancellation, deadlines, tracing, etc. -**id** | **string** | The id of record | -**agentId** | **string** | The id of the agent | +**id** | **string** | The id of record | +**agentId** | **string** | The id of the agent | ### Other Parameters @@ -1168,8 +1168,8 @@ func main() { Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- **ctx** | **context.Context** | context for authentication, logging, cancellation, deadlines, tracing, etc. -**id** | **string** | The id of record | -**agentId** | **string** | The id of the agent | +**id** | **string** | The id of record | +**agentId** | **string** | The id of the agent | ### Other Parameters @@ -1180,7 +1180,7 @@ Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- - **startRequest** | [**StartRequest**](StartRequest.md) | Optional start parameters | + **startRequest** | [**StartRequest**](StartRequest.md) | Optional start parameters | ### Return type @@ -1244,7 +1244,7 @@ func main() { Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- **ctx** | **context.Context** | context for authentication, logging, cancellation, deadlines, tracing, etc. -**id** | **string** | The id of record | +**id** | **string** | The id of record | ### Other Parameters @@ -1256,9 +1256,9 @@ Name | Type | Description | Notes **page** | **int32** | Page number of record list when record list exceeds specified page size | [default to 1] **size** | **int32** | Maximum number of records to return | [default to 100] - **search** | **string** | Specifies the search criteria | - **orderBy** | **string** | Specifies the order by criteria | - **fields** | **string** | Supplies a comma-separated list of fields to be returned | + **search** | **string** | Specifies the search criteria | + **orderBy** | **string** | Specifies the order by criteria | + **fields** | **string** | Supplies a comma-separated list of fields to be returned | ### Return type @@ -1318,7 +1318,7 @@ func main() { Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- **ctx** | **context.Context** | context for authentication, logging, cancellation, deadlines, tracing, etc. -**id** | **string** | The id of record | +**id** | **string** | The id of record | ### Other Parameters @@ -1328,7 +1328,7 @@ Other parameters are passed through a pointer to a apiApiAmbientV1ProjectsIdAgen Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- - **agent** | [**Agent**](Agent.md) | Agent data | + **agent** | [**Agent**](Agent.md) | Agent data | ### Return type @@ -1386,8 +1386,8 @@ func main() { Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- **ctx** | **context.Context** | context for authentication, logging, cancellation, deadlines, tracing, etc. -**id** | **string** | The id of record | -**credId** | **string** | The id of the credential | +**id** | **string** | The id of record | +**credId** | **string** | The id of the credential | ### Other Parameters @@ -1457,8 +1457,8 @@ func main() { Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- **ctx** | **context.Context** | context for authentication, logging, cancellation, deadlines, tracing, etc. -**id** | **string** | The id of record | -**credId** | **string** | The id of the credential | +**id** | **string** | The id of record | +**credId** | **string** | The id of the credential | ### Other Parameters @@ -1529,8 +1529,8 @@ func main() { Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- **ctx** | **context.Context** | context for authentication, logging, cancellation, deadlines, tracing, etc. -**id** | **string** | The id of record | -**credId** | **string** | The id of the credential | +**id** | **string** | The id of record | +**credId** | **string** | The id of the credential | ### Other Parameters @@ -1541,7 +1541,7 @@ Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- - **credentialPatchRequest** | [**CredentialPatchRequest**](CredentialPatchRequest.md) | Updated credential data | + **credentialPatchRequest** | [**CredentialPatchRequest**](CredentialPatchRequest.md) | Updated credential data | ### Return type @@ -1603,8 +1603,8 @@ func main() { Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- **ctx** | **context.Context** | context for authentication, logging, cancellation, deadlines, tracing, etc. -**id** | **string** | The id of record | -**credId** | **string** | The id of the credential | +**id** | **string** | The id of record | +**credId** | **string** | The id of the credential | ### Other Parameters @@ -1679,7 +1679,7 @@ func main() { Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- **ctx** | **context.Context** | context for authentication, logging, cancellation, deadlines, tracing, etc. -**id** | **string** | The id of record | +**id** | **string** | The id of record | ### Other Parameters @@ -1691,10 +1691,10 @@ Name | Type | Description | Notes **page** | **int32** | Page number of record list when record list exceeds specified page size | [default to 1] **size** | **int32** | Maximum number of records to return | [default to 100] - **search** | **string** | Specifies the search criteria | - **orderBy** | **string** | Specifies the order by criteria | - **fields** | **string** | Supplies a comma-separated list of fields to be returned | - **provider** | **string** | Filter credentials by provider | + **search** | **string** | Specifies the search criteria | + **orderBy** | **string** | Specifies the order by criteria | + **fields** | **string** | Supplies a comma-separated list of fields to be returned | + **provider** | **string** | Filter credentials by provider | ### Return type @@ -1754,7 +1754,7 @@ func main() { Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- **ctx** | **context.Context** | context for authentication, logging, cancellation, deadlines, tracing, etc. -**id** | **string** | The id of record | +**id** | **string** | The id of record | ### Other Parameters @@ -1764,7 +1764,7 @@ Other parameters are passed through a pointer to a apiApiAmbientV1ProjectsIdCred Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- - **credential** | [**Credential**](Credential.md) | Credential data | + **credential** | [**Credential**](Credential.md) | Credential data | ### Return type @@ -1821,7 +1821,7 @@ func main() { Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- **ctx** | **context.Context** | context for authentication, logging, cancellation, deadlines, tracing, etc. -**id** | **string** | The id of record | +**id** | **string** | The id of record | ### Other Parameters @@ -1889,7 +1889,7 @@ func main() { Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- **ctx** | **context.Context** | context for authentication, logging, cancellation, deadlines, tracing, etc. -**id** | **string** | The id of record | +**id** | **string** | The id of record | ### Other Parameters @@ -1957,7 +1957,7 @@ func main() { Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- **ctx** | **context.Context** | context for authentication, logging, cancellation, deadlines, tracing, etc. -**id** | **string** | The id of record | +**id** | **string** | The id of record | ### Other Parameters @@ -2026,7 +2026,7 @@ func main() { Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- **ctx** | **context.Context** | context for authentication, logging, cancellation, deadlines, tracing, etc. -**id** | **string** | The id of record | +**id** | **string** | The id of record | ### Other Parameters @@ -2036,7 +2036,7 @@ Other parameters are passed through a pointer to a apiApiAmbientV1ProjectsIdPatc Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- - **projectPatchRequest** | [**ProjectPatchRequest**](ProjectPatchRequest.md) | Updated project data | + **projectPatchRequest** | [**ProjectPatchRequest**](ProjectPatchRequest.md) | Updated project data | ### Return type @@ -2100,7 +2100,7 @@ Other parameters are passed through a pointer to a apiApiAmbientV1ProjectsPostRe Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- - **project** | [**Project**](Project.md) | Project data | + **project** | [**Project**](Project.md) | Project data | ### Return type @@ -2170,9 +2170,9 @@ Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- **page** | **int32** | Page number of record list when record list exceeds specified page size | [default to 1] **size** | **int32** | Maximum number of records to return | [default to 100] - **search** | **string** | Specifies the search criteria | - **orderBy** | **string** | Specifies the order by criteria | - **fields** | **string** | Supplies a comma-separated list of fields to be returned | + **search** | **string** | Specifies the search criteria | + **orderBy** | **string** | Specifies the order by criteria | + **fields** | **string** | Supplies a comma-separated list of fields to be returned | ### Return type @@ -2231,7 +2231,7 @@ func main() { Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- **ctx** | **context.Context** | context for authentication, logging, cancellation, deadlines, tracing, etc. -**id** | **string** | The id of record | +**id** | **string** | The id of record | ### Other Parameters @@ -2300,7 +2300,7 @@ func main() { Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- **ctx** | **context.Context** | context for authentication, logging, cancellation, deadlines, tracing, etc. -**id** | **string** | The id of record | +**id** | **string** | The id of record | ### Other Parameters @@ -2310,7 +2310,7 @@ Other parameters are passed through a pointer to a apiApiAmbientV1RoleBindingsId Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- - **roleBindingPatchRequest** | [**RoleBindingPatchRequest**](RoleBindingPatchRequest.md) | Updated roleBinding data | + **roleBindingPatchRequest** | [**RoleBindingPatchRequest**](RoleBindingPatchRequest.md) | Updated roleBinding data | ### Return type @@ -2374,7 +2374,7 @@ Other parameters are passed through a pointer to a apiApiAmbientV1RoleBindingsPo Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- - **roleBinding** | [**RoleBinding**](RoleBinding.md) | RoleBinding data | + **roleBinding** | [**RoleBinding**](RoleBinding.md) | RoleBinding data | ### Return type @@ -2444,9 +2444,9 @@ Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- **page** | **int32** | Page number of record list when record list exceeds specified page size | [default to 1] **size** | **int32** | Maximum number of records to return | [default to 100] - **search** | **string** | Specifies the search criteria | - **orderBy** | **string** | Specifies the order by criteria | - **fields** | **string** | Supplies a comma-separated list of fields to be returned | + **search** | **string** | Specifies the search criteria | + **orderBy** | **string** | Specifies the order by criteria | + **fields** | **string** | Supplies a comma-separated list of fields to be returned | ### Return type @@ -2505,7 +2505,7 @@ func main() { Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- **ctx** | **context.Context** | context for authentication, logging, cancellation, deadlines, tracing, etc. -**id** | **string** | The id of record | +**id** | **string** | The id of record | ### Other Parameters @@ -2574,7 +2574,7 @@ func main() { Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- **ctx** | **context.Context** | context for authentication, logging, cancellation, deadlines, tracing, etc. -**id** | **string** | The id of record | +**id** | **string** | The id of record | ### Other Parameters @@ -2584,7 +2584,7 @@ Other parameters are passed through a pointer to a apiApiAmbientV1RolesIdPatchRe Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- - **rolePatchRequest** | [**RolePatchRequest**](RolePatchRequest.md) | Updated role data | + **rolePatchRequest** | [**RolePatchRequest**](RolePatchRequest.md) | Updated role data | ### Return type @@ -2648,7 +2648,7 @@ Other parameters are passed through a pointer to a apiApiAmbientV1RolesPostReque Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- - **role** | [**Role**](Role.md) | Role data | + **role** | [**Role**](Role.md) | Role data | ### Return type @@ -2718,9 +2718,9 @@ Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- **page** | **int32** | Page number of record list when record list exceeds specified page size | [default to 1] **size** | **int32** | Maximum number of records to return | [default to 100] - **search** | **string** | Specifies the search criteria | - **orderBy** | **string** | Specifies the order by criteria | - **fields** | **string** | Supplies a comma-separated list of fields to be returned | + **search** | **string** | Specifies the search criteria | + **orderBy** | **string** | Specifies the order by criteria | + **fields** | **string** | Supplies a comma-separated list of fields to be returned | ### Return type @@ -2777,7 +2777,7 @@ func main() { Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- **ctx** | **context.Context** | context for authentication, logging, cancellation, deadlines, tracing, etc. -**id** | **string** | The id of record | +**id** | **string** | The id of record | ### Other Parameters @@ -2845,7 +2845,7 @@ func main() { Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- **ctx** | **context.Context** | context for authentication, logging, cancellation, deadlines, tracing, etc. -**id** | **string** | The id of record | +**id** | **string** | The id of record | ### Other Parameters @@ -2916,7 +2916,7 @@ func main() { Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- **ctx** | **context.Context** | context for authentication, logging, cancellation, deadlines, tracing, etc. -**id** | **string** | The id of record | +**id** | **string** | The id of record | ### Other Parameters @@ -2968,7 +2968,7 @@ import ( func main() { id := "id_example" // string | The id of record - sessionMessagePushRequest := *openapiclient.NewSessionMessagePushRequest() // SessionMessagePushRequest | + sessionMessagePushRequest := *openapiclient.NewSessionMessagePushRequest() // SessionMessagePushRequest | configuration := openapiclient.NewConfiguration() apiClient := openapiclient.NewAPIClient(configuration) @@ -2988,7 +2988,7 @@ func main() { Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- **ctx** | **context.Context** | context for authentication, logging, cancellation, deadlines, tracing, etc. -**id** | **string** | The id of record | +**id** | **string** | The id of record | ### Other Parameters @@ -2998,7 +2998,7 @@ Other parameters are passed through a pointer to a apiApiAmbientV1SessionsIdMess Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- - **sessionMessagePushRequest** | [**SessionMessagePushRequest**](SessionMessagePushRequest.md) | | + **sessionMessagePushRequest** | [**SessionMessagePushRequest**](SessionMessagePushRequest.md) | | ### Return type @@ -3058,7 +3058,7 @@ func main() { Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- **ctx** | **context.Context** | context for authentication, logging, cancellation, deadlines, tracing, etc. -**id** | **string** | The id of record | +**id** | **string** | The id of record | ### Other Parameters @@ -3068,7 +3068,7 @@ Other parameters are passed through a pointer to a apiApiAmbientV1SessionsIdPatc Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- - **sessionPatchRequest** | [**SessionPatchRequest**](SessionPatchRequest.md) | Updated session data | + **sessionPatchRequest** | [**SessionPatchRequest**](SessionPatchRequest.md) | Updated session data | ### Return type @@ -3129,7 +3129,7 @@ func main() { Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- **ctx** | **context.Context** | context for authentication, logging, cancellation, deadlines, tracing, etc. -**id** | **string** | The id of record | +**id** | **string** | The id of record | ### Other Parameters @@ -3200,7 +3200,7 @@ func main() { Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- **ctx** | **context.Context** | context for authentication, logging, cancellation, deadlines, tracing, etc. -**id** | **string** | The id of record | +**id** | **string** | The id of record | ### Other Parameters @@ -3210,7 +3210,7 @@ Other parameters are passed through a pointer to a apiApiAmbientV1SessionsIdStat Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- - **sessionStatusPatchRequest** | [**SessionStatusPatchRequest**](SessionStatusPatchRequest.md) | Session status fields to update | + **sessionStatusPatchRequest** | [**SessionStatusPatchRequest**](SessionStatusPatchRequest.md) | Session status fields to update | ### Return type @@ -3271,7 +3271,7 @@ func main() { Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- **ctx** | **context.Context** | context for authentication, logging, cancellation, deadlines, tracing, etc. -**id** | **string** | The id of record | +**id** | **string** | The id of record | ### Other Parameters @@ -3344,7 +3344,7 @@ Other parameters are passed through a pointer to a apiApiAmbientV1SessionsPostRe Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- - **session** | [**Session**](Session.md) | Session data | + **session** | [**Session**](Session.md) | Session data | ### Return type @@ -3414,9 +3414,9 @@ Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- **page** | **int32** | Page number of record list when record list exceeds specified page size | [default to 1] **size** | **int32** | Maximum number of records to return | [default to 100] - **search** | **string** | Specifies the search criteria | - **orderBy** | **string** | Specifies the order by criteria | - **fields** | **string** | Supplies a comma-separated list of fields to be returned | + **search** | **string** | Specifies the search criteria | + **orderBy** | **string** | Specifies the order by criteria | + **fields** | **string** | Supplies a comma-separated list of fields to be returned | ### Return type @@ -3475,7 +3475,7 @@ func main() { Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- **ctx** | **context.Context** | context for authentication, logging, cancellation, deadlines, tracing, etc. -**id** | **string** | The id of record | +**id** | **string** | The id of record | ### Other Parameters @@ -3544,7 +3544,7 @@ func main() { Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- **ctx** | **context.Context** | context for authentication, logging, cancellation, deadlines, tracing, etc. -**id** | **string** | The id of record | +**id** | **string** | The id of record | ### Other Parameters @@ -3554,7 +3554,7 @@ Other parameters are passed through a pointer to a apiApiAmbientV1UsersIdPatchRe Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- - **userPatchRequest** | [**UserPatchRequest**](UserPatchRequest.md) | Updated user data | + **userPatchRequest** | [**UserPatchRequest**](UserPatchRequest.md) | Updated user data | ### Return type @@ -3618,7 +3618,7 @@ Other parameters are passed through a pointer to a apiApiAmbientV1UsersPostReque Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- - **user** | [**User**](User.md) | User data | + **user** | [**User**](User.md) | User data | ### Return type @@ -3636,4 +3636,3 @@ Name | Type | Description | Notes [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - diff --git a/components/ambient-api-server/pkg/api/openapi/docs/InboxMessage.md b/components/ambient-api-server/pkg/api/openapi/docs/InboxMessage.md index 7641ca197..ce5d8ed8a 100644 --- a/components/ambient-api-server/pkg/api/openapi/docs/InboxMessage.md +++ b/components/ambient-api-server/pkg/api/openapi/docs/InboxMessage.md @@ -4,16 +4,16 @@ Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- -**Id** | Pointer to **string** | | [optional] -**Kind** | Pointer to **string** | | [optional] -**Href** | Pointer to **string** | | [optional] -**CreatedAt** | Pointer to **time.Time** | | [optional] -**UpdatedAt** | Pointer to **time.Time** | | [optional] -**AgentId** | **string** | Recipient — the agent address | -**FromAgentId** | Pointer to **string** | Sender Agent id — null if sent by a human | [optional] -**FromName** | Pointer to **string** | Denormalized sender display name | [optional] -**Body** | **string** | | -**Read** | Pointer to **bool** | false = unread; drained at session start | [optional] [readonly] +**Id** | Pointer to **string** | | [optional] +**Kind** | Pointer to **string** | | [optional] +**Href** | Pointer to **string** | | [optional] +**CreatedAt** | Pointer to **time.Time** | | [optional] +**UpdatedAt** | Pointer to **time.Time** | | [optional] +**AgentId** | **string** | Recipient — the agent address | +**FromAgentId** | Pointer to **string** | Sender Agent id — null if sent by a human | [optional] +**FromName** | Pointer to **string** | Denormalized sender display name | [optional] +**Body** | **string** | | +**Read** | Pointer to **bool** | false = unread; drained at session start | [optional] [readonly] ## Methods @@ -276,5 +276,3 @@ HasRead returns a boolean if a field has been set. [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) - - diff --git a/components/ambient-api-server/pkg/api/openapi/docs/InboxMessageList.md b/components/ambient-api-server/pkg/api/openapi/docs/InboxMessageList.md index 068519400..d6f011e43 100644 --- a/components/ambient-api-server/pkg/api/openapi/docs/InboxMessageList.md +++ b/components/ambient-api-server/pkg/api/openapi/docs/InboxMessageList.md @@ -4,11 +4,11 @@ Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- -**Kind** | **string** | | -**Page** | **int32** | | -**Size** | **int32** | | -**Total** | **int32** | | -**Items** | [**[]InboxMessage**](InboxMessage.md) | | +**Kind** | **string** | | +**Page** | **int32** | | +**Size** | **int32** | | +**Total** | **int32** | | +**Items** | [**[]InboxMessage**](InboxMessage.md) | | ## Methods @@ -131,5 +131,3 @@ SetItems sets Items field to given value. [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) - - diff --git a/components/ambient-api-server/pkg/api/openapi/docs/InboxMessagePatchRequest.md b/components/ambient-api-server/pkg/api/openapi/docs/InboxMessagePatchRequest.md index 335fd3527..948d5a56c 100644 --- a/components/ambient-api-server/pkg/api/openapi/docs/InboxMessagePatchRequest.md +++ b/components/ambient-api-server/pkg/api/openapi/docs/InboxMessagePatchRequest.md @@ -4,7 +4,7 @@ Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- -**Read** | Pointer to **bool** | | [optional] +**Read** | Pointer to **bool** | | [optional] ## Methods @@ -52,5 +52,3 @@ HasRead returns a boolean if a field has been set. [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) - - diff --git a/components/ambient-api-server/pkg/api/openapi/docs/Project.md b/components/ambient-api-server/pkg/api/openapi/docs/Project.md index 8c5f24fe0..71882603d 100644 --- a/components/ambient-api-server/pkg/api/openapi/docs/Project.md +++ b/components/ambient-api-server/pkg/api/openapi/docs/Project.md @@ -4,17 +4,17 @@ Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- -**Id** | Pointer to **string** | | [optional] -**Kind** | Pointer to **string** | | [optional] -**Href** | Pointer to **string** | | [optional] -**CreatedAt** | Pointer to **time.Time** | | [optional] -**UpdatedAt** | Pointer to **time.Time** | | [optional] -**Name** | **string** | | -**Description** | Pointer to **string** | | [optional] -**Labels** | Pointer to **string** | | [optional] -**Annotations** | Pointer to **string** | | [optional] -**Prompt** | Pointer to **string** | Workspace-level context injected into every agent start in this project | [optional] -**Status** | Pointer to **string** | | [optional] +**Id** | Pointer to **string** | | [optional] +**Kind** | Pointer to **string** | | [optional] +**Href** | Pointer to **string** | | [optional] +**CreatedAt** | Pointer to **time.Time** | | [optional] +**UpdatedAt** | Pointer to **time.Time** | | [optional] +**Name** | **string** | | +**Description** | Pointer to **string** | | [optional] +**Labels** | Pointer to **string** | | [optional] +**Annotations** | Pointer to **string** | | [optional] +**Prompt** | Pointer to **string** | Workspace-level context injected into every agent start in this project | [optional] +**Status** | Pointer to **string** | | [optional] ## Methods @@ -307,5 +307,3 @@ HasStatus returns a boolean if a field has been set. [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) - - diff --git a/components/ambient-api-server/pkg/api/openapi/docs/ProjectHome.md b/components/ambient-api-server/pkg/api/openapi/docs/ProjectHome.md index c214ea1b5..a9cb00875 100644 --- a/components/ambient-api-server/pkg/api/openapi/docs/ProjectHome.md +++ b/components/ambient-api-server/pkg/api/openapi/docs/ProjectHome.md @@ -4,8 +4,8 @@ Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- -**ProjectId** | Pointer to **string** | | [optional] -**Agents** | Pointer to [**[]ProjectHomeAgent**](ProjectHomeAgent.md) | | [optional] +**ProjectId** | Pointer to **string** | | [optional] +**Agents** | Pointer to [**[]ProjectHomeAgent**](ProjectHomeAgent.md) | | [optional] ## Methods @@ -78,5 +78,3 @@ HasAgents returns a boolean if a field has been set. [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) - - diff --git a/components/ambient-api-server/pkg/api/openapi/docs/ProjectHomeAgent.md b/components/ambient-api-server/pkg/api/openapi/docs/ProjectHomeAgent.md index f1a3533e7..5e0e7b1af 100644 --- a/components/ambient-api-server/pkg/api/openapi/docs/ProjectHomeAgent.md +++ b/components/ambient-api-server/pkg/api/openapi/docs/ProjectHomeAgent.md @@ -4,11 +4,11 @@ Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- -**AgentId** | Pointer to **string** | | [optional] -**AgentName** | Pointer to **string** | | [optional] -**SessionPhase** | Pointer to **string** | | [optional] -**InboxUnreadCount** | Pointer to **int32** | | [optional] -**Summary** | Pointer to **string** | | [optional] +**AgentId** | Pointer to **string** | | [optional] +**AgentName** | Pointer to **string** | | [optional] +**SessionPhase** | Pointer to **string** | | [optional] +**InboxUnreadCount** | Pointer to **int32** | | [optional] +**Summary** | Pointer to **string** | | [optional] ## Methods @@ -156,5 +156,3 @@ HasSummary returns a boolean if a field has been set. [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) - - diff --git a/components/ambient-api-server/pkg/api/openapi/docs/ProjectPatchRequest.md b/components/ambient-api-server/pkg/api/openapi/docs/ProjectPatchRequest.md index 3f2d8f69d..3613cf59d 100644 --- a/components/ambient-api-server/pkg/api/openapi/docs/ProjectPatchRequest.md +++ b/components/ambient-api-server/pkg/api/openapi/docs/ProjectPatchRequest.md @@ -4,12 +4,12 @@ Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- -**Name** | Pointer to **string** | | [optional] -**Description** | Pointer to **string** | | [optional] -**Labels** | Pointer to **string** | | [optional] -**Annotations** | Pointer to **string** | | [optional] -**Prompt** | Pointer to **string** | | [optional] -**Status** | Pointer to **string** | | [optional] +**Name** | Pointer to **string** | | [optional] +**Description** | Pointer to **string** | | [optional] +**Labels** | Pointer to **string** | | [optional] +**Annotations** | Pointer to **string** | | [optional] +**Prompt** | Pointer to **string** | | [optional] +**Status** | Pointer to **string** | | [optional] ## Methods @@ -182,5 +182,3 @@ HasStatus returns a boolean if a field has been set. [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) - - diff --git a/components/ambient-api-server/pkg/api/openapi/docs/Session.md b/components/ambient-api-server/pkg/api/openapi/docs/Session.md index eba93527b..3c78b1449 100644 --- a/components/ambient-api-server/pkg/api/openapi/docs/Session.md +++ b/components/ambient-api-server/pkg/api/openapi/docs/Session.md @@ -4,42 +4,42 @@ Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- -**Id** | Pointer to **string** | | [optional] -**Kind** | Pointer to **string** | | [optional] -**Href** | Pointer to **string** | | [optional] -**CreatedAt** | Pointer to **time.Time** | | [optional] -**UpdatedAt** | Pointer to **time.Time** | | [optional] -**Name** | **string** | | -**RepoUrl** | Pointer to **string** | | [optional] -**Prompt** | Pointer to **string** | | [optional] -**CreatedByUserId** | Pointer to **string** | Set from authentication token. Cannot be set or modified via API. | [optional] [readonly] -**AssignedUserId** | Pointer to **string** | | [optional] -**WorkflowId** | Pointer to **string** | | [optional] -**Repos** | Pointer to **string** | | [optional] -**Timeout** | Pointer to **int32** | | [optional] -**LlmModel** | Pointer to **string** | | [optional] -**LlmTemperature** | Pointer to **float64** | | [optional] -**LlmMaxTokens** | Pointer to **int32** | | [optional] -**ParentSessionId** | Pointer to **string** | | [optional] -**BotAccountName** | Pointer to **string** | | [optional] -**ResourceOverrides** | Pointer to **string** | | [optional] -**EnvironmentVariables** | Pointer to **string** | | [optional] -**Labels** | Pointer to **string** | | [optional] -**Annotations** | Pointer to **string** | | [optional] -**AgentId** | Pointer to **string** | The Agent that owns this session. Immutable after creation. | [optional] -**TriggeredByUserId** | Pointer to **string** | User who started the agent | [optional] [readonly] -**ProjectId** | Pointer to **string** | Immutable after creation. Set at creation time only. | [optional] -**Phase** | Pointer to **string** | | [optional] [readonly] -**StartTime** | Pointer to **time.Time** | | [optional] [readonly] -**CompletionTime** | Pointer to **time.Time** | | [optional] [readonly] -**SdkSessionId** | Pointer to **string** | | [optional] [readonly] -**SdkRestartCount** | Pointer to **int32** | | [optional] [readonly] -**Conditions** | Pointer to **string** | | [optional] [readonly] -**ReconciledRepos** | Pointer to **string** | | [optional] [readonly] -**ReconciledWorkflow** | Pointer to **string** | | [optional] [readonly] -**KubeCrName** | Pointer to **string** | | [optional] [readonly] -**KubeCrUid** | Pointer to **string** | | [optional] [readonly] -**KubeNamespace** | Pointer to **string** | | [optional] [readonly] +**Id** | Pointer to **string** | | [optional] +**Kind** | Pointer to **string** | | [optional] +**Href** | Pointer to **string** | | [optional] +**CreatedAt** | Pointer to **time.Time** | | [optional] +**UpdatedAt** | Pointer to **time.Time** | | [optional] +**Name** | **string** | | +**RepoUrl** | Pointer to **string** | | [optional] +**Prompt** | Pointer to **string** | | [optional] +**CreatedByUserId** | Pointer to **string** | Set from authentication token. Cannot be set or modified via API. | [optional] [readonly] +**AssignedUserId** | Pointer to **string** | | [optional] +**WorkflowId** | Pointer to **string** | | [optional] +**Repos** | Pointer to **string** | | [optional] +**Timeout** | Pointer to **int32** | | [optional] +**LlmModel** | Pointer to **string** | | [optional] +**LlmTemperature** | Pointer to **float64** | | [optional] +**LlmMaxTokens** | Pointer to **int32** | | [optional] +**ParentSessionId** | Pointer to **string** | | [optional] +**BotAccountName** | Pointer to **string** | | [optional] +**ResourceOverrides** | Pointer to **string** | | [optional] +**EnvironmentVariables** | Pointer to **string** | | [optional] +**Labels** | Pointer to **string** | | [optional] +**Annotations** | Pointer to **string** | | [optional] +**AgentId** | Pointer to **string** | The Agent that owns this session. Immutable after creation. | [optional] +**TriggeredByUserId** | Pointer to **string** | User who started the agent | [optional] [readonly] +**ProjectId** | Pointer to **string** | Immutable after creation. Set at creation time only. | [optional] +**Phase** | Pointer to **string** | | [optional] [readonly] +**StartTime** | Pointer to **time.Time** | | [optional] [readonly] +**CompletionTime** | Pointer to **time.Time** | | [optional] [readonly] +**SdkSessionId** | Pointer to **string** | | [optional] [readonly] +**SdkRestartCount** | Pointer to **int32** | | [optional] [readonly] +**Conditions** | Pointer to **string** | | [optional] [readonly] +**ReconciledRepos** | Pointer to **string** | | [optional] [readonly] +**ReconciledWorkflow** | Pointer to **string** | | [optional] [readonly] +**KubeCrName** | Pointer to **string** | | [optional] [readonly] +**KubeCrUid** | Pointer to **string** | | [optional] [readonly] +**KubeNamespace** | Pointer to **string** | | [optional] [readonly] ## Methods @@ -957,5 +957,3 @@ HasKubeNamespace returns a boolean if a field has been set. [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) - - diff --git a/components/ambient-api-server/pkg/api/openapi/docs/StartRequest.md b/components/ambient-api-server/pkg/api/openapi/docs/StartRequest.md index a24db2c04..fc1758a20 100644 --- a/components/ambient-api-server/pkg/api/openapi/docs/StartRequest.md +++ b/components/ambient-api-server/pkg/api/openapi/docs/StartRequest.md @@ -4,7 +4,7 @@ Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- -**Prompt** | Pointer to **string** | Task scope for this specific run (Session.prompt) | [optional] +**Prompt** | Pointer to **string** | Task scope for this specific run (Session.prompt) | [optional] ## Methods @@ -52,5 +52,3 @@ HasPrompt returns a boolean if a field has been set. [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) - - diff --git a/components/ambient-api-server/pkg/api/openapi/docs/StartResponse.md b/components/ambient-api-server/pkg/api/openapi/docs/StartResponse.md index bac9ee0f9..e41e5a794 100644 --- a/components/ambient-api-server/pkg/api/openapi/docs/StartResponse.md +++ b/components/ambient-api-server/pkg/api/openapi/docs/StartResponse.md @@ -4,8 +4,8 @@ Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- -**Session** | Pointer to [**Session**](Session.md) | | [optional] -**StartPrompt** | Pointer to **string** | Assembled start prompt — Agent.prompt + Inbox + Session.prompt + peer roster | [optional] +**Session** | Pointer to [**Session**](Session.md) | | [optional] +**StartPrompt** | Pointer to **string** | Assembled start prompt — Agent.prompt + Inbox + Session.prompt + peer roster | [optional] ## Methods @@ -78,5 +78,3 @@ HasStartPrompt returns a boolean if a field has been set. [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) - - diff --git a/components/ambient-api-server/plugins/agents/migration.go b/components/ambient-api-server/plugins/agents/migration.go index 45c8d3ce0..1f22cb4c4 100644 --- a/components/ambient-api-server/plugins/agents/migration.go +++ b/components/ambient-api-server/plugins/agents/migration.go @@ -45,12 +45,12 @@ func agentSchemaExpansionMigration() *gormigrate.Migration { type Agent struct { db.Model ProjectId string - ParentAgentId *string `gorm:"index"` + ParentAgentId *string `gorm:"index"` OwnerUserId *string Name string DisplayName *string Description *string - Prompt *string `gorm:"type:text"` + Prompt *string `gorm:"type:text"` RepoUrl *string WorkflowId *string LlmModel *string diff --git a/components/ambient-sdk/generator/parser.go b/components/ambient-sdk/generator/parser.go index 7eff71255..e924deae2 100644 --- a/components/ambient-sdk/generator/parser.go +++ b/components/ambient-sdk/generator/parser.go @@ -123,16 +123,6 @@ func inferParentPath(pathSegment string) string { return strings.Join(parts[:len(parts)-1], "/") } -func leafSegment(pathSegment string) string { - parts := strings.Split(pathSegment, "/") - for i := len(parts) - 1; i >= 0; i-- { - if !strings.Contains(parts[i], "{") { - return parts[i] - } - } - return pathSegment -} - type openAPIDoc struct { Paths map[string]interface{} `yaml:"paths"` Components struct { diff --git a/components/ambient-sdk/ts-sdk/example/css/styles.css b/components/ambient-sdk/ts-sdk/example/css/styles.css index 2ac9d2452..8d0402db5 100644 --- a/components/ambient-sdk/ts-sdk/example/css/styles.css +++ b/components/ambient-sdk/ts-sdk/example/css/styles.css @@ -11240,4 +11240,4 @@ body { width: 20px !important; font-size: 0.75rem; border-radius: 0.375rem !important; -} \ No newline at end of file +} diff --git a/components/backend/handlers/coderabbit_auth.go b/components/backend/handlers/coderabbit_auth.go new file mode 100644 index 000000000..816690099 --- /dev/null +++ b/components/backend/handlers/coderabbit_auth.go @@ -0,0 +1,298 @@ +package handlers + +import ( + "context" + "encoding/json" + "fmt" + "log" + "net/http" + "time" + + "github.com/gin-gonic/gin" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// CodeRabbitCredentials represents cluster-level CodeRabbit credentials for a user +type CodeRabbitCredentials struct { + UserID string `json:"userId"` + APIKey string `json:"apiKey"` + UpdatedAt time.Time `json:"updatedAt"` +} + +// ValidateCodeRabbitAPIKey is a package-level var for test mockability. +// Signature matches ValidateGitHubToken, ValidateGitLabToken, etc. +var ValidateCodeRabbitAPIKey = validateCodeRabbitAPIKeyImpl + +func validateCodeRabbitAPIKeyImpl(ctx context.Context, apiKey string) (bool, error) { + if apiKey == "" { + return false, fmt.Errorf("API key is empty") + } + + client := &http.Client{Timeout: 10 * time.Second} + req, err := http.NewRequestWithContext(ctx, "GET", "https://api.coderabbit.ai/api/v1/health", nil) + if err != nil { + return false, fmt.Errorf("failed to create request") + } + + req.Header.Set("Authorization", "Bearer "+apiKey) + + resp, err := client.Do(req) + if err != nil { + return false, fmt.Errorf("request failed: %w", networkError(err)) + } + defer resp.Body.Close() + + switch resp.StatusCode { + case http.StatusOK: + return true, nil + case http.StatusUnauthorized, http.StatusForbidden: + return false, nil + default: + return false, fmt.Errorf("upstream error: status %d", resp.StatusCode) + } +} + +// ConnectCodeRabbit handles POST /api/auth/coderabbit/connect +// Saves user's CodeRabbit credentials at cluster level +func ConnectCodeRabbit(c *gin.Context) { + // Verify user has valid K8s token (follows RBAC pattern) + reqK8s, _ := GetK8sClientsForRequest(c) + if reqK8s == nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid or missing token"}) + return + } + + // Verify user is authenticated and userID is valid + userID := c.GetString("userID") + if userID == "" { + c.JSON(http.StatusUnauthorized, gin.H{"error": "User authentication required"}) + return + } + if !isValidUserID(userID) { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user identifier"}) + return + } + + var req struct { + APIKey string `json:"apiKey" binding:"required"` + } + + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // Validate against CodeRabbit health API + valid, err := ValidateCodeRabbitAPIKey(c.Request.Context(), req.APIKey) + if err != nil { + log.Printf("Failed to validate CodeRabbit API key for user %s: %v", userID, err) + c.JSON(http.StatusBadGateway, gin.H{"error": "Failed to validate API key with CodeRabbit"}) + return + } + if !valid { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid CodeRabbit API key"}) + return + } + + // Store credentials + creds := &CodeRabbitCredentials{ + UserID: userID, + APIKey: req.APIKey, + UpdatedAt: time.Now(), + } + + if err := storeCodeRabbitCredentials(c.Request.Context(), creds); err != nil { + log.Printf("Failed to store CodeRabbit credentials for user %s: %v", userID, err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save CodeRabbit credentials"}) + return + } + + log.Printf("Stored CodeRabbit credentials for user %s", userID) + c.JSON(http.StatusOK, gin.H{ + "message": "CodeRabbit connected successfully", + }) +} + +// GetCodeRabbitStatus handles GET /api/auth/coderabbit/status +// Returns connection status for the authenticated user +func GetCodeRabbitStatus(c *gin.Context) { + // Verify user has valid K8s token + reqK8s, _ := GetK8sClientsForRequest(c) + if reqK8s == nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid or missing token"}) + return + } + + userID := c.GetString("userID") + if userID == "" { + c.JSON(http.StatusUnauthorized, gin.H{"error": "User authentication required"}) + return + } + + creds, err := GetCodeRabbitCredentials(c.Request.Context(), userID) + if err != nil { + if errors.IsNotFound(err) { + c.JSON(http.StatusOK, gin.H{"connected": false}) + return + } + log.Printf("Failed to get CodeRabbit credentials for user %s: %v", userID, err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to check CodeRabbit status"}) + return + } + + if creds == nil { + c.JSON(http.StatusOK, gin.H{"connected": false}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "connected": true, + "updatedAt": creds.UpdatedAt.Format(time.RFC3339), + }) +} + +// DisconnectCodeRabbit handles DELETE /api/auth/coderabbit/disconnect +// Removes user's CodeRabbit credentials +func DisconnectCodeRabbit(c *gin.Context) { + // Verify user has valid K8s token + reqK8s, _ := GetK8sClientsForRequest(c) + if reqK8s == nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid or missing token"}) + return + } + + userID := c.GetString("userID") + if userID == "" { + c.JSON(http.StatusUnauthorized, gin.H{"error": "User authentication required"}) + return + } + + if err := DeleteCodeRabbitCredentials(c.Request.Context(), userID); err != nil { + log.Printf("Failed to delete CodeRabbit credentials for user %s: %v", userID, err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to disconnect CodeRabbit"}) + return + } + + log.Printf("Deleted CodeRabbit credentials for user %s", userID) + c.JSON(http.StatusOK, gin.H{"message": "CodeRabbit disconnected successfully"}) +} + +// storeCodeRabbitCredentials stores CodeRabbit credentials in cluster-level Secret +func storeCodeRabbitCredentials(ctx context.Context, creds *CodeRabbitCredentials) error { + if creds == nil || creds.UserID == "" { + return fmt.Errorf("invalid credentials payload") + } + + const secretName = "coderabbit-credentials" + + for i := 0; i < 3; i++ { // retry on conflict + secret, err := K8sClient.CoreV1().Secrets(Namespace).Get(ctx, secretName, v1.GetOptions{}) + if err != nil { + if errors.IsNotFound(err) { + // Create Secret + secret = &corev1.Secret{ + ObjectMeta: v1.ObjectMeta{ + Name: secretName, + Namespace: Namespace, + Labels: map[string]string{ + "app": "ambient-code", + "ambient-code.io/provider": "coderabbit", + }, + }, + Type: corev1.SecretTypeOpaque, + Data: map[string][]byte{}, + } + if _, cerr := K8sClient.CoreV1().Secrets(Namespace).Create(ctx, secret, v1.CreateOptions{}); cerr != nil && !errors.IsAlreadyExists(cerr) { + return fmt.Errorf("failed to create Secret: %w", cerr) + } + // Fetch again to get resourceVersion + secret, err = K8sClient.CoreV1().Secrets(Namespace).Get(ctx, secretName, v1.GetOptions{}) + if err != nil { + return fmt.Errorf("failed to fetch Secret after create: %w", err) + } + } else { + return fmt.Errorf("failed to get Secret: %w", err) + } + } + + if secret.Data == nil { + secret.Data = map[string][]byte{} + } + + b, err := json.Marshal(creds) + if err != nil { + return fmt.Errorf("failed to marshal credentials: %w", err) + } + secret.Data[creds.UserID] = b + + if _, uerr := K8sClient.CoreV1().Secrets(Namespace).Update(ctx, secret, v1.UpdateOptions{}); uerr != nil { + if errors.IsConflict(uerr) { + continue // retry + } + return fmt.Errorf("failed to update Secret: %w", uerr) + } + return nil + } + return fmt.Errorf("failed to update Secret after retries") +} + +// GetCodeRabbitCredentials retrieves cluster-level CodeRabbit credentials for a user +func GetCodeRabbitCredentials(ctx context.Context, userID string) (*CodeRabbitCredentials, error) { + if userID == "" { + return nil, fmt.Errorf("userID is required") + } + + const secretName = "coderabbit-credentials" + + secret, err := K8sClient.CoreV1().Secrets(Namespace).Get(ctx, secretName, v1.GetOptions{}) + if err != nil { + return nil, err + } + + if secret.Data == nil || len(secret.Data[userID]) == 0 { + return nil, nil // User hasn't connected CodeRabbit + } + + var creds CodeRabbitCredentials + if err := json.Unmarshal(secret.Data[userID], &creds); err != nil { + return nil, fmt.Errorf("failed to parse credentials: %w", err) + } + + return &creds, nil +} + +// DeleteCodeRabbitCredentials removes CodeRabbit credentials for a user +func DeleteCodeRabbitCredentials(ctx context.Context, userID string) error { + if userID == "" { + return fmt.Errorf("userID is required") + } + + const secretName = "coderabbit-credentials" + + for i := 0; i < 3; i++ { // retry on conflict + secret, err := K8sClient.CoreV1().Secrets(Namespace).Get(ctx, secretName, v1.GetOptions{}) + if err != nil { + if errors.IsNotFound(err) { + return nil // Secret doesn't exist, nothing to delete + } + return fmt.Errorf("failed to get Secret: %w", err) + } + + if secret.Data == nil || len(secret.Data[userID]) == 0 { + return nil // User's credentials don't exist + } + + delete(secret.Data, userID) + + if _, uerr := K8sClient.CoreV1().Secrets(Namespace).Update(ctx, secret, v1.UpdateOptions{}); uerr != nil { + if errors.IsConflict(uerr) { + continue // retry + } + return fmt.Errorf("failed to update Secret: %w", uerr) + } + return nil + } + return fmt.Errorf("failed to update Secret after retries") +} diff --git a/components/backend/handlers/coderabbit_auth_test.go b/components/backend/handlers/coderabbit_auth_test.go new file mode 100644 index 000000000..e7ffef25d --- /dev/null +++ b/components/backend/handlers/coderabbit_auth_test.go @@ -0,0 +1,313 @@ +//go:build test + +package handlers + +import ( + "ambient-code-backend/tests/config" + test_constants "ambient-code-backend/tests/constants" + "context" + "net/http" + + "ambient-code-backend/tests/logger" + "ambient-code-backend/tests/test_utils" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var _ = Describe("CodeRabbit Auth Handler", Label(test_constants.LabelUnit, test_constants.LabelHandlers, test_constants.LabelCodeRabbitAuth), func() { + var ( + httpUtils *test_utils.HTTPTestUtils + k8sUtils *test_utils.K8sTestUtils + originalNamespace string + originalValidateCodeRabbitAPIKey func(context.Context, string) (bool, error) + testToken string + ) + + BeforeEach(func() { + logger.Log("Setting up CodeRabbit Auth Handler test") + + originalNamespace = Namespace + originalValidateCodeRabbitAPIKey = ValidateCodeRabbitAPIKey + ValidateCodeRabbitAPIKey = func(_ context.Context, _ string) (bool, error) { return true, nil } + + // Use centralized handler dependencies setup + k8sUtils = test_utils.NewK8sTestUtils(false, *config.TestNamespace) + SetupHandlerDependencies(k8sUtils) + + // coderabbit_auth.go uses Namespace (backend namespace) for some secret operations + Namespace = *config.TestNamespace + + httpUtils = test_utils.NewHTTPTestUtils() + + // Create namespace + role and mint a valid test token for this suite + ctx := context.Background() + _, err := k8sUtils.K8sClient.CoreV1().Namespaces().Create(ctx, &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{Name: *config.TestNamespace}, + }, metav1.CreateOptions{}) + if err != nil && !errors.IsAlreadyExists(err) { + Expect(err).NotTo(HaveOccurred()) + } + _, err = k8sUtils.CreateTestRole(ctx, *config.TestNamespace, "test-full-access-role", []string{"get", "list", "create", "update", "delete", "patch"}, "*", "") + Expect(err).NotTo(HaveOccurred()) + + token, _, err := httpUtils.SetValidTestToken( + k8sUtils, + *config.TestNamespace, + []string{"get", "list", "create", "update", "delete", "patch"}, + "*", + "", + "test-full-access-role", + ) + Expect(err).NotTo(HaveOccurred()) + testToken = token + }) + + AfterEach(func() { + Namespace = originalNamespace + ValidateCodeRabbitAPIKey = originalValidateCodeRabbitAPIKey + + // Clean up created namespace (best-effort) + if k8sUtils != nil { + _ = k8sUtils.K8sClient.CoreV1().Namespaces().Delete(context.Background(), *config.TestNamespace, metav1.DeleteOptions{}) + } + }) + + Context("Connection Management", func() { + Describe("ConnectCodeRabbit", func() { + It("Should require authentication", func() { + requestBody := map[string]interface{}{ + "apiKey": "cr_test_key_1234567890", + } + + context := httpUtils.CreateTestGinContext("POST", "/api/auth/coderabbit/connect", requestBody) + // Don't set auth header + httpUtils.SetUserContext("test-user", "Test User", "test@example.com") + + ConnectCodeRabbit(context) + + httpUtils.AssertHTTPStatus(http.StatusUnauthorized) + httpUtils.AssertErrorMessage("Invalid or missing token") + }) + + It("Should require user authentication", func() { + requestBody := map[string]interface{}{ + "apiKey": "cr_test_key_1234567890", + } + + context := httpUtils.CreateTestGinContext("POST", "/api/auth/coderabbit/connect", requestBody) + // Don't set auth header or user context + + ConnectCodeRabbit(context) + + httpUtils.AssertHTTPStatus(http.StatusUnauthorized) + httpUtils.AssertJSONContains(map[string]interface{}{ + "error": "Invalid or missing token", + }) + }) + + It("Should reject empty API key", func() { + requestBody := map[string]interface{}{ + "apiKey": "", + } + + context := httpUtils.CreateTestGinContext("POST", "/api/auth/coderabbit/connect", requestBody) + httpUtils.SetAuthHeader(testToken) + httpUtils.SetUserContext("test-user", "Test User", "test@example.com") + + ConnectCodeRabbit(context) + + httpUtils.AssertHTTPStatus(http.StatusBadRequest) + }) + + It("Should reject missing API key field", func() { + requestBody := map[string]interface{}{ + // apiKey missing + } + + context := httpUtils.CreateTestGinContext("POST", "/api/auth/coderabbit/connect", requestBody) + httpUtils.SetAuthHeader(testToken) + httpUtils.SetUserContext("test-user", "Test User", "test@example.com") + + ConnectCodeRabbit(context) + + // Gin binding returns detailed validation error message + httpUtils.AssertHTTPStatus(http.StatusBadRequest) + }) + + It("Should reject invalid API key", func() { + // Mock validation to return false + ValidateCodeRabbitAPIKey = func(_ context.Context, _ string) (bool, error) { return false, nil } + + requestBody := map[string]interface{}{ + "apiKey": "invalid_key_123", + } + + context := httpUtils.CreateTestGinContext("POST", "/api/auth/coderabbit/connect", requestBody) + httpUtils.SetAuthHeader(testToken) + httpUtils.SetUserContext("test-user", "Test User", "test@example.com") + + ConnectCodeRabbit(context) + + httpUtils.AssertHTTPStatus(http.StatusBadRequest) + httpUtils.AssertJSONContains(map[string]interface{}{ + "error": "Invalid CodeRabbit API key", + }) + }) + + It("Should store credentials successfully with valid API key", func() { + // Mock validation to return true + ValidateCodeRabbitAPIKey = func(_ context.Context, _ string) (bool, error) { return true, nil } + + requestBody := map[string]interface{}{ + "apiKey": "cr_valid_key_1234567890", + } + + context := httpUtils.CreateTestGinContext("POST", "/api/auth/coderabbit/connect", requestBody) + httpUtils.SetAuthHeader(testToken) + httpUtils.SetUserContext("test-user", "Test User", "test@example.com") + + ConnectCodeRabbit(context) + + httpUtils.AssertHTTPStatus(http.StatusOK) + httpUtils.AssertJSONContains(map[string]interface{}{ + "message": "CodeRabbit connected successfully", + }) + }) + + It("Should handle invalid user ID type", func() { + requestBody := map[string]interface{}{ + "apiKey": "cr_test_key_1234567890", + } + + context := httpUtils.CreateTestGinContext("POST", "/api/auth/coderabbit/connect", requestBody) + httpUtils.SetAuthHeader(testToken) + context.Set("userID", 123) // Invalid type (should be string) + + ConnectCodeRabbit(context) + + // GetString returns empty string for non-string types, triggers auth required error + httpUtils.AssertHTTPStatus(http.StatusUnauthorized) + httpUtils.AssertErrorMessage("User authentication required") + }) + + It("Should require valid JSON body", func() { + context := httpUtils.CreateTestGinContext("POST", "/api/auth/coderabbit/connect", "invalid-json") + httpUtils.SetAuthHeader(testToken) + httpUtils.SetUserContext("test-user", "Test User", "test@example.com") + + ConnectCodeRabbit(context) + + // Gin binding returns detailed validation error + httpUtils.AssertHTTPStatus(http.StatusBadRequest) + }) + }) + + Describe("GetCodeRabbitStatus", func() { + It("Should require authentication", func() { + context := httpUtils.CreateTestGinContext("GET", "/api/auth/coderabbit/status", nil) + // Don't set auth header + httpUtils.SetUserContext("test-user", "Test User", "test@example.com") + + GetCodeRabbitStatus(context) + + httpUtils.AssertHTTPStatus(http.StatusUnauthorized) + httpUtils.AssertErrorMessage("Invalid or missing token") + }) + + It("Should require user authentication", func() { + context := httpUtils.CreateTestGinContext("GET", "/api/auth/coderabbit/status", nil) + // Don't set auth header or user context + + GetCodeRabbitStatus(context) + + httpUtils.AssertHTTPStatus(http.StatusUnauthorized) + httpUtils.AssertJSONContains(map[string]interface{}{ + "error": "Invalid or missing token", + }) + }) + + It("Should return connected:false when no credentials stored", func() { + context := httpUtils.CreateTestGinContext("GET", "/api/auth/coderabbit/status", nil) + httpUtils.SetAuthHeader(testToken) + httpUtils.SetUserContext("test-user", "Test User", "test@example.com") + + GetCodeRabbitStatus(context) + + httpUtils.AssertHTTPStatus(http.StatusOK) + httpUtils.AssertJSONContains(map[string]interface{}{ + "connected": false, + }) + }) + + It("Should handle invalid user ID type", func() { + context := httpUtils.CreateTestGinContext("GET", "/api/auth/coderabbit/status", nil) + httpUtils.SetAuthHeader(testToken) + context.Set("userID", 123) // Invalid type + + GetCodeRabbitStatus(context) + + // GetString returns empty string for non-string types, triggers auth required error + httpUtils.AssertHTTPStatus(http.StatusUnauthorized) + httpUtils.AssertErrorMessage("User authentication required") + }) + }) + + Describe("DisconnectCodeRabbit", func() { + It("Should require authentication", func() { + context := httpUtils.CreateTestGinContext("DELETE", "/api/auth/coderabbit/disconnect", nil) + // Don't set auth header + httpUtils.SetUserContext("test-user", "Test User", "test@example.com") + + DisconnectCodeRabbit(context) + + httpUtils.AssertHTTPStatus(http.StatusUnauthorized) + httpUtils.AssertErrorMessage("Invalid or missing token") + }) + + It("Should require user authentication", func() { + context := httpUtils.CreateTestGinContext("DELETE", "/api/auth/coderabbit/disconnect", nil) + // Don't set auth header or user context + + DisconnectCodeRabbit(context) + + httpUtils.AssertHTTPStatus(http.StatusUnauthorized) + httpUtils.AssertJSONContains(map[string]interface{}{ + "error": "Invalid or missing token", + }) + }) + + It("Should succeed idempotently when no credentials exist", func() { + context := httpUtils.CreateTestGinContext("DELETE", "/api/auth/coderabbit/disconnect", nil) + httpUtils.SetAuthHeader(testToken) + httpUtils.SetUserContext("test-user", "Test User", "test@example.com") + + DisconnectCodeRabbit(context) + + httpUtils.AssertHTTPStatus(http.StatusOK) + httpUtils.AssertJSONContains(map[string]interface{}{ + "message": "CodeRabbit disconnected successfully", + }) + }) + }) + }) + + Context("Data Structure Validation", func() { + Describe("Request and Response Types", func() { + It("Should validate CodeRabbitCredentials structure", func() { + creds := CodeRabbitCredentials{ + UserID: "user123", + APIKey: "cr_test_key_1234567890", + UpdatedAt: metav1.Now().Time, + } + + Expect(creds.UserID).To(Equal("user123")) + Expect(creds.APIKey).To(Equal("cr_test_key_1234567890")) + Expect(creds.UpdatedAt).NotTo(BeZero()) + }) + }) + }) +}) diff --git a/components/backend/handlers/integration_validation.go b/components/backend/handlers/integration_validation.go index deb4f9e57..e137aebbb 100755 --- a/components/backend/handlers/integration_validation.go +++ b/components/backend/handlers/integration_validation.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "log" "net/http" "net/url" "time" @@ -227,3 +228,29 @@ func TestGitLabConnection(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"valid": true, "message": "GitLab connection successful"}) } + +// TestCodeRabbitConnection handles POST /api/auth/coderabbit/test +func TestCodeRabbitConnection(c *gin.Context) { + var req struct { + APIKey string `json:"apiKey" binding:"required"` + } + + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + valid, err := ValidateCodeRabbitAPIKey(c.Request.Context(), req.APIKey) + if err != nil { + log.Printf("CodeRabbit API key validation failed: %v", err) + c.JSON(http.StatusOK, gin.H{"valid": false, "error": "Failed to validate API key with CodeRabbit"}) + return + } + + if !valid { + c.JSON(http.StatusOK, gin.H{"valid": false, "error": "Invalid API key"}) + return + } + + c.JSON(http.StatusOK, gin.H{"valid": true, "message": "CodeRabbit connection successful"}) +} diff --git a/components/backend/handlers/integrations_status.go b/components/backend/handlers/integrations_status.go index 36e992c59..327f021d6 100644 --- a/components/backend/handlers/integrations_status.go +++ b/components/backend/handlers/integrations_status.go @@ -39,6 +39,9 @@ func GetIntegrationsStatus(c *gin.Context) { // GitLab status response["gitlab"] = getGitLabStatusForUser(ctx, userID) + // CodeRabbit status + response["coderabbit"] = getCodeRabbitStatusForUser(ctx, userID) + // MCP server credentials status response["mcpServers"] = getMCPServerStatusForUser(ctx, userID) @@ -144,3 +147,16 @@ func getGitLabStatusForUser(ctx context.Context, userID string) gin.H { "valid": true, } } + +func getCodeRabbitStatusForUser(ctx context.Context, userID string) gin.H { + creds, err := GetCodeRabbitCredentials(ctx, userID) + if err != nil || creds == nil { + return gin.H{"connected": false} + } + + return gin.H{ + "connected": true, + "updatedAt": creds.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"), + "valid": true, + } +} diff --git a/components/backend/handlers/runtime_credentials.go b/components/backend/handlers/runtime_credentials.go index 6cf4ea460..475cab477 100755 --- a/components/backend/handlers/runtime_credentials.go +++ b/components/backend/handlers/runtime_credentials.go @@ -343,6 +343,43 @@ func GetGitLabTokenForSession(c *gin.Context) { }) } +// GetCodeRabbitCredentialsForSession handles GET /api/projects/:project/agentic-sessions/:session/credentials/coderabbit +func GetCodeRabbitCredentialsForSession(c *gin.Context) { + project := c.Param("projectName") + session := c.Param("sessionName") + + reqK8s, reqDyn := GetK8sClientsForRequest(c) + if reqK8s == nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid or missing token"}) + return + } + + effectiveUserID, ok := enforceCredentialRBAC(c, reqK8s, reqDyn, project, session) + if !ok { + return + } + + creds, err := GetCodeRabbitCredentials(c.Request.Context(), effectiveUserID) + if err != nil { + if errors.IsNotFound(err) { + c.JSON(http.StatusNotFound, gin.H{"error": "CodeRabbit credentials not configured"}) + return + } + log.Printf("Failed to get CodeRabbit credentials for user %s: %v", effectiveUserID, err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get CodeRabbit credentials"}) + return + } + + if creds == nil { + c.JSON(http.StatusNotFound, gin.H{"error": "CodeRabbit credentials not configured"}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "apiKey": creds.APIKey, + }) +} + // refreshGoogleAccessToken refreshes a Google OAuth access token using the refresh token func refreshGoogleAccessToken(ctx context.Context, oldCreds *GoogleOAuthCredentials) (*GoogleOAuthCredentials, error) { if oldCreds.RefreshToken == "" { diff --git a/components/backend/routes.go b/components/backend/routes.go index 23abad9de..be84d589c 100644 --- a/components/backend/routes.go +++ b/components/backend/routes.go @@ -99,6 +99,7 @@ func registerRoutes(r *gin.Engine) { projectGroup.GET("/agentic-sessions/:sessionName/credentials/google", handlers.GetGoogleCredentialsForSession) projectGroup.GET("/agentic-sessions/:sessionName/credentials/jira", handlers.GetJiraCredentialsForSession) projectGroup.GET("/agentic-sessions/:sessionName/credentials/gitlab", handlers.GetGitLabTokenForSession) + projectGroup.GET("/agentic-sessions/:sessionName/credentials/coderabbit", handlers.GetCodeRabbitCredentialsForSession) projectGroup.GET("/agentic-sessions/:sessionName/credentials/mcp/:serverName", handlers.GetMCPCredentialsForSession) // Session export @@ -179,6 +180,12 @@ func registerRoutes(r *gin.Engine) { api.DELETE("/auth/gitlab/disconnect", handlers.DisconnectGitLabGlobal) api.POST("/auth/gitlab/test", handlers.TestGitLabConnection) + // Cluster-level CodeRabbit (user-scoped) + api.POST("/auth/coderabbit/connect", handlers.ConnectCodeRabbit) + api.GET("/auth/coderabbit/status", handlers.GetCodeRabbitStatus) + api.DELETE("/auth/coderabbit/disconnect", handlers.DisconnectCodeRabbit) + api.POST("/auth/coderabbit/test", handlers.TestCodeRabbitConnection) + // Generic MCP server credentials (user-scoped) api.POST("/auth/mcp/:serverName/connect", handlers.ConnectMCPServer) api.GET("/auth/mcp/:serverName/status", handlers.GetMCPServerStatus) diff --git a/components/backend/tests/constants/labels.go b/components/backend/tests/constants/labels.go index bdc7a5d57..be6971815 100644 --- a/components/backend/tests/constants/labels.go +++ b/components/backend/tests/constants/labels.go @@ -12,20 +12,21 @@ const ( LabelTypes = "types" // Specific component labels for handlers - LabelRepo = "repo" - LabelRepoSeed = "repo_seed" - LabelSecrets = "secrets" - LabelRepository = "repository" - LabelMiddleware = "middleware" - LabelPermissions = "permissions" - LabelProjects = "projects" - LabelGitHubAuth = "github-auth" - LabelGitLabAuth = "gitlab-auth" - LabelSessions = "sessions" - LabelContent = "content" - LabelFeatureFlags = "feature-flags" - LabelDisplayName = "display-name" - LabelHealth = "health" + LabelRepo = "repo" + LabelRepoSeed = "repo_seed" + LabelSecrets = "secrets" + LabelRepository = "repository" + LabelMiddleware = "middleware" + LabelPermissions = "permissions" + LabelProjects = "projects" + LabelGitHubAuth = "github-auth" + LabelGitLabAuth = "gitlab-auth" + LabelCodeRabbitAuth = "coderabbit-auth" + LabelSessions = "sessions" + LabelContent = "content" + LabelFeatureFlags = "feature-flags" + LabelDisplayName = "display-name" + LabelHealth = "health" // Specific component labels for other areas LabelOperations = "operations" // for git operations diff --git a/components/frontend/src/app/api/auth/coderabbit/connect/route.ts b/components/frontend/src/app/api/auth/coderabbit/connect/route.ts new file mode 100644 index 000000000..3732239e9 --- /dev/null +++ b/components/frontend/src/app/api/auth/coderabbit/connect/route.ts @@ -0,0 +1,23 @@ +import { BACKEND_URL } from '@/lib/config' +import { buildForwardHeadersAsync } from '@/lib/auth' + +export async function POST(request: Request) { + try { + const headers = await buildForwardHeadersAsync(request) + const body = await request.text() + + const resp = await fetch(`${BACKEND_URL}/auth/coderabbit/connect`, { + method: 'POST', + headers, + body, + }) + + const data = await resp.text() + return new Response(data, { + status: resp.status, + headers: { 'Content-Type': resp.headers.get('content-type') ?? 'application/json' }, + }) + } catch { + return Response.json({ error: 'CodeRabbit upstream unavailable' }, { status: 502 }) + } +} diff --git a/components/frontend/src/app/api/auth/coderabbit/disconnect/route.ts b/components/frontend/src/app/api/auth/coderabbit/disconnect/route.ts new file mode 100644 index 000000000..1faf40d30 --- /dev/null +++ b/components/frontend/src/app/api/auth/coderabbit/disconnect/route.ts @@ -0,0 +1,21 @@ +import { BACKEND_URL } from '@/lib/config' +import { buildForwardHeadersAsync } from '@/lib/auth' + +export async function DELETE(request: Request) { + try { + const headers = await buildForwardHeadersAsync(request) + + const resp = await fetch(`${BACKEND_URL}/auth/coderabbit/disconnect`, { + method: 'DELETE', + headers, + }) + + const data = await resp.text() + return new Response(data, { + status: resp.status, + headers: { 'Content-Type': resp.headers.get('content-type') ?? 'application/json' }, + }) + } catch { + return Response.json({ error: 'CodeRabbit upstream unavailable' }, { status: 502 }) + } +} diff --git a/components/frontend/src/app/api/auth/coderabbit/status/route.ts b/components/frontend/src/app/api/auth/coderabbit/status/route.ts new file mode 100644 index 000000000..906e89232 --- /dev/null +++ b/components/frontend/src/app/api/auth/coderabbit/status/route.ts @@ -0,0 +1,21 @@ +import { BACKEND_URL } from '@/lib/config' +import { buildForwardHeadersAsync } from '@/lib/auth' + +export async function GET(request: Request) { + try { + const headers = await buildForwardHeadersAsync(request) + + const resp = await fetch(`${BACKEND_URL}/auth/coderabbit/status`, { + method: 'GET', + headers, + }) + + const data = await resp.text() + return new Response(data, { + status: resp.status, + headers: { 'Content-Type': resp.headers.get('content-type') ?? 'application/json' }, + }) + } catch { + return Response.json({ error: 'CodeRabbit upstream unavailable' }, { status: 502 }) + } +} diff --git a/components/frontend/src/app/api/auth/coderabbit/test/route.ts b/components/frontend/src/app/api/auth/coderabbit/test/route.ts new file mode 100644 index 000000000..83dd12c31 --- /dev/null +++ b/components/frontend/src/app/api/auth/coderabbit/test/route.ts @@ -0,0 +1,23 @@ +import { BACKEND_URL } from '@/lib/config' +import { buildForwardHeadersAsync } from '@/lib/auth' + +export async function POST(request: Request) { + try { + const headers = await buildForwardHeadersAsync(request) + const body = await request.text() + + const resp = await fetch(`${BACKEND_URL}/auth/coderabbit/test`, { + method: 'POST', + headers, + body, + }) + + const data = await resp.text() + return new Response(data, { + status: resp.status, + headers: { 'Content-Type': resp.headers.get('content-type') ?? 'application/json' }, + }) + } catch { + return Response.json({ error: 'CodeRabbit upstream unavailable' }, { status: 502 }) + } +} diff --git a/components/frontend/src/app/integrations/IntegrationsClient.tsx b/components/frontend/src/app/integrations/IntegrationsClient.tsx index 47ffc0602..ca195b32f 100644 --- a/components/frontend/src/app/integrations/IntegrationsClient.tsx +++ b/components/frontend/src/app/integrations/IntegrationsClient.tsx @@ -4,6 +4,7 @@ import { GitHubConnectionCard } from '@/components/github-connection-card' import { GoogleDriveConnectionCard } from '@/components/google-drive-connection-card' import { GitLabConnectionCard } from '@/components/gitlab-connection-card' import { JiraConnectionCard } from '@/components/jira-connection-card' +import { CodeRabbitConnectionCard } from '@/components/coderabbit-connection-card' import { PageHeader } from '@/components/page-header' import { useIntegrationsStatus } from '@/services/queries/use-integrations' import { Loader2 } from 'lucide-react' @@ -53,6 +54,10 @@ export default function IntegrationsClient({ appSlug }: Props) { status={integrations?.jira} onRefresh={refetch} /> + )} diff --git a/components/frontend/src/app/projects/[name]/sessions/[sessionName]/components/settings/integrations-panel.tsx b/components/frontend/src/app/projects/[name]/sessions/[sessionName]/components/settings/integrations-panel.tsx index 0ce6ae906..6a61c78fe 100644 --- a/components/frontend/src/app/projects/[name]/sessions/[sessionName]/components/settings/integrations-panel.tsx +++ b/components/frontend/src/app/projects/[name]/sessions/[sessionName]/components/settings/integrations-panel.tsx @@ -18,6 +18,7 @@ export function IntegrationsPanel() { const githubConfigured = integrationsStatus?.github?.active != null; const gitlabConfigured = integrationsStatus?.gitlab?.connected ?? false; const jiraConfigured = integrationsStatus?.jira?.connected ?? false; + const coderabbitConfigured = integrationsStatus?.coderabbit?.connected ?? false; const googleConfigured = integrationsStatus?.google?.connected ?? false; const integrations = [ @@ -48,6 +49,15 @@ export function IntegrationsPanel() { configured: jiraConfigured, configuredMessage: "Authenticated. Issue and project access enabled.", }, + { + key: "coderabbit", + name: "CodeRabbit", + configured: true, + configuredMessage: + coderabbitConfigured + ? "Active. API key configured for private repos." + : "Active for public repositories. No configuration needed.", + }, ].sort((a, b) => a.name.localeCompare(b.name)); const configuredCount = integrations.filter((i) => i.configured).length; @@ -132,4 +142,3 @@ function IntegrationCard({ ); } - diff --git a/components/frontend/src/components/coderabbit-connection-card.tsx b/components/frontend/src/components/coderabbit-connection-card.tsx new file mode 100644 index 000000000..82b660732 --- /dev/null +++ b/components/frontend/src/components/coderabbit-connection-card.tsx @@ -0,0 +1,238 @@ +'use client' + +import React, { useState } from 'react' +import { Button } from '@/components/ui/button' +import { Card } from '@/components/ui/card' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { Loader2, Eye, EyeOff, ChevronDown, ChevronRight, TriangleAlert } from 'lucide-react' +import { toast } from 'sonner' +import { useConnectCodeRabbit, useDisconnectCodeRabbit } from '@/services/queries/use-coderabbit' + +type Props = { + status?: { + connected: boolean + updatedAt?: string + valid?: boolean + } + onRefresh?: () => void +} + +export function CodeRabbitConnectionCard({ status, onRefresh }: Props) { + const connectMutation = useConnectCodeRabbit() + const disconnectMutation = useDisconnectCodeRabbit() + const isLoading = !status + + const [showAdvanced, setShowAdvanced] = useState(status?.connected ?? false) + const [showForm, setShowForm] = useState(false) + const [apiKey, setApiKey] = useState('') + const [showKey, setShowKey] = useState(false) + + const handleConnect = async () => { + if (!apiKey) { + toast.error('Please enter an API key') + return + } + + connectMutation.mutate( + { apiKey }, + { + onSuccess: () => { + toast.success('API key saved for private repository access') + setShowForm(false) + setApiKey('') + onRefresh?.() + }, + onError: (error) => { + toast.error(error instanceof Error ? error.message : 'Failed to save API key') + }, + } + ) + } + + const handleDisconnect = async () => { + disconnectMutation.mutate(undefined, { + onSuccess: () => { + toast.success('API key removed') + onRefresh?.() + }, + onError: (error) => { + toast.error(error instanceof Error ? error.message : 'Failed to remove API key') + }, + }) + } + + return ( + +
+ {/* Header */} +
+
+ +
+
+

CodeRabbit

+

AI-powered code review

+
+
+ + {/* Default status — public repos are free */} +
+
+ + + Active for public repositories + +
+

+ Code review is free for public repositories via the{' '} + + CodeRabbit GitHub App + + . No configuration needed. +

+
+ + {/* Private repo access — collapsed by default */} +
+ + + {showAdvanced && ( +
+ {/* Billing warning */} +
+ +

+ Only needed for private repositories. Using an API key on public repos will incur + charges for reviews that are otherwise free. +

+
+ + {status?.connected ? ( + <> +

+ API key configured for private repository reviews. +

+
+ + +
+ + ) : ( +

+ Add an API key to enable CLI reviews for private repositories in sessions. +

+ )} + + {/* API key form */} + {(showForm || (!status?.connected && showAdvanced)) && ( +
+
+ +
+ setApiKey(e.target.value)} + disabled={connectMutation.isPending} + /> + +
+

+ Log in with GitHub at{' '} + + CodeRabbit API Keys + + {' '}to generate a key. +

+
+
+ + {status?.connected && ( + + )} +
+
+ )} +
+ )} +
+
+
+ ) +} diff --git a/components/frontend/src/services/api/coderabbit-auth.ts b/components/frontend/src/services/api/coderabbit-auth.ts new file mode 100644 index 000000000..d42f5f4ba --- /dev/null +++ b/components/frontend/src/services/api/coderabbit-auth.ts @@ -0,0 +1,31 @@ +import { apiClient } from './client' + +export type CodeRabbitStatus = { + connected: boolean + updatedAt?: string +} + +export type CodeRabbitConnectRequest = { + apiKey: string +} + +/** + * Get CodeRabbit connection status for the authenticated user + */ +export async function getCodeRabbitStatus(): Promise { + return apiClient.get('/auth/coderabbit/status') +} + +/** + * Connect CodeRabbit account for the authenticated user + */ +export async function connectCodeRabbit(data: CodeRabbitConnectRequest): Promise { + await apiClient.post('/auth/coderabbit/connect', data) +} + +/** + * Disconnect CodeRabbit account for the authenticated user + */ +export async function disconnectCodeRabbit(): Promise { + await apiClient.delete('/auth/coderabbit/disconnect') +} diff --git a/components/frontend/src/services/api/integrations.ts b/components/frontend/src/services/api/integrations.ts index 60d6addd2..8b1d07b4b 100644 --- a/components/frontend/src/services/api/integrations.ts +++ b/components/frontend/src/services/api/integrations.ts @@ -34,6 +34,11 @@ export type IntegrationsStatus = { updatedAt?: string valid?: boolean } + coderabbit: { + connected: boolean + updatedAt?: string + valid?: boolean + } mcpServers?: Record } diff --git a/components/frontend/src/services/queries/use-coderabbit.ts b/components/frontend/src/services/queries/use-coderabbit.ts new file mode 100644 index 000000000..6dc8ad5db --- /dev/null +++ b/components/frontend/src/services/queries/use-coderabbit.ts @@ -0,0 +1,26 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query' +import * as codeRabbitAuthApi from '../api/coderabbit-auth' + +export function useConnectCodeRabbit() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: codeRabbitAuthApi.connectCodeRabbit, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['coderabbit', 'status'] }) + queryClient.invalidateQueries({ queryKey: ['integrations', 'status'] }) + }, + }) +} + +export function useDisconnectCodeRabbit() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: codeRabbitAuthApi.disconnectCodeRabbit, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['coderabbit', 'status'] }) + queryClient.invalidateQueries({ queryKey: ['integrations', 'status'] }) + }, + }) +} diff --git a/components/runners/ambient-runner/ag_ui_gemini_cli/adapter.py b/components/runners/ambient-runner/ag_ui_gemini_cli/adapter.py index 315c62b4b..4b6b182b9 100755 --- a/components/runners/ambient-runner/ag_ui_gemini_cli/adapter.py +++ b/components/runners/ambient-runner/ag_ui_gemini_cli/adapter.py @@ -446,10 +446,14 @@ async def run( if reasoning_open and reasoning_message_id: try: yield ReasoningMessageEndEvent( - threadId=thread_id, runId=run_id, messageId=reasoning_message_id, + threadId=thread_id, + runId=run_id, + messageId=reasoning_message_id, ) yield ReasoningEndEvent( - threadId=thread_id, runId=run_id, messageId=reasoning_message_id, + threadId=thread_id, + runId=run_id, + messageId=reasoning_message_id, ) except Exception: pass @@ -477,10 +481,14 @@ async def run( if reasoning_open and reasoning_message_id: try: yield ReasoningMessageEndEvent( - threadId=thread_id, runId=run_id, messageId=reasoning_message_id, + threadId=thread_id, + runId=run_id, + messageId=reasoning_message_id, ) yield ReasoningEndEvent( - threadId=thread_id, runId=run_id, messageId=reasoning_message_id, + threadId=thread_id, + runId=run_id, + messageId=reasoning_message_id, ) except Exception: pass diff --git a/components/runners/ambient-runner/ag_ui_gemini_cli/types.py b/components/runners/ambient-runner/ag_ui_gemini_cli/types.py index b0132f12b..24920aee5 100755 --- a/components/runners/ambient-runner/ag_ui_gemini_cli/types.py +++ b/components/runners/ambient-runner/ag_ui_gemini_cli/types.py @@ -94,7 +94,16 @@ class ThinkingEvent: def parse_event( line: str, -) -> InitEvent | MessageEvent | ToolUseEvent | ToolResultEvent | ErrorEvent | ResultEvent | ThinkingEvent | None: +) -> ( + InitEvent + | MessageEvent + | ToolUseEvent + | ToolResultEvent + | ErrorEvent + | ResultEvent + | ThinkingEvent + | None +): """Parse a JSON line into the appropriate event dataclass. Returns ``None`` when the line cannot be parsed or has an unknown type. diff --git a/components/runners/ambient-runner/ambient_runner/bridges/claude/tools.py b/components/runners/ambient-runner/ambient_runner/bridges/claude/tools.py index 1838bd6f7..6f829758a 100755 --- a/components/runners/ambient-runner/ambient_runner/bridges/claude/tools.py +++ b/components/runners/ambient-runner/ambient_runner/bridges/claude/tools.py @@ -100,8 +100,7 @@ async def refresh_credentials_tool(args: dict) -> dict: diagnostics = _check_mcp_auth_after_refresh() parts = [ - f"Credentials refreshed successfully. " - f"Active integrations: {summary}.", + f"Credentials refreshed successfully. Active integrations: {summary}.", ] if diagnostics: parts.append(f"MCP diagnostics: {diagnostics}") diff --git a/components/runners/ambient-runner/ambient_runner/platform/auth.py b/components/runners/ambient-runner/ambient_runner/platform/auth.py index e29dda48f..fd21b9e98 100755 --- a/components/runners/ambient-runner/ambient_runner/platform/auth.py +++ b/components/runners/ambient-runner/ambient_runner/platform/auth.py @@ -284,6 +284,17 @@ async def fetch_gitlab_token(context: RunnerContext) -> str: return data.get("token", "") +async def fetch_coderabbit_credentials(context: RunnerContext) -> dict: + """Fetch CodeRabbit credentials from backend API. + + Returns dict with: apiKey + """ + data = await _fetch_credential(context, "coderabbit") + if data.get("apiKey"): + logger.info("Using CodeRabbit credentials from backend") + return data + + async def fetch_token_for_url(context: RunnerContext, url: str) -> str: """Fetch appropriate token based on repository URL host.""" try: @@ -306,11 +317,18 @@ async def populate_runtime_credentials(context: RunnerContext) -> None: logger.info("Fetching fresh credentials from backend API...") # Fetch all credentials concurrently - google_creds, jira_creds, gitlab_creds, github_creds = await asyncio.gather( + ( + google_creds, + jira_creds, + gitlab_creds, + github_creds, + coderabbit_creds, + ) = await asyncio.gather( fetch_google_credentials(context), fetch_jira_credentials(context), fetch_gitlab_credentials(context), fetch_github_credentials(context), + fetch_coderabbit_credentials(context), return_exceptions=True, ) @@ -404,6 +422,15 @@ async def populate_runtime_credentials(context: RunnerContext) -> None: if github_creds.get("email"): git_user_email = github_creds["email"] + # CodeRabbit credentials + if isinstance(coderabbit_creds, Exception): + logger.warning(f"Failed to refresh CodeRabbit credentials: {coderabbit_creds}") + if isinstance(coderabbit_creds, PermissionError): + auth_failures.append(str(coderabbit_creds)) + elif coderabbit_creds.get("apiKey"): + os.environ["CODERABBIT_API_KEY"] = coderabbit_creds["apiKey"] + logger.info("Updated CodeRabbit API key in environment") + # Configure git identity, credential helper, and gh CLI wrapper await configure_git_identity(git_user_name, git_user_email) install_git_credential_helper() @@ -432,6 +459,7 @@ def clear_runtime_credentials() -> None: "JIRA_URL", "JIRA_EMAIL", "USER_GOOGLE_EMAIL", + "CODERABBIT_API_KEY", ]: if os.environ.pop(key, None) is not None: cleared.append(key) @@ -529,15 +557,15 @@ async def populate_mcp_server_credentials(context: RunnerContext) -> None: logger.warning(f"Failed to fetch MCP credentials for {server_name}: {e}") -_GH_WRAPPER_DIR = "/tmp/bin" -_GH_WRAPPER_PATH = "/tmp/bin/gh" +_GH_WRAPPER_DIR = "" # Set at first install via tempfile.mkdtemp +_GH_WRAPPER_PATH = "" # Set at first install # Wrapper script for the gh CLI. The `gh` CLI reads GITHUB_TOKEN from the # process environment, but the CLI subprocess's env is fixed at spawn time. # This wrapper reads the latest token from the token file (updated on every # credential refresh) and exports GH_TOKEN before calling the real `gh`, # ensuring mid-run refreshes are picked up. -_GH_WRAPPER_SCRIPT = """\ +_GH_WRAPPER_SCRIPT_TEMPLATE = """\ #!/bin/sh # Ambient gh CLI wrapper — reads fresh GitHub token from file. token="" @@ -562,7 +590,7 @@ async def populate_mcp_server_credentials(context: RunnerContext) -> None: exit 1 fi exec "$real_gh" "$@" -""".format(wrapper_dir=_GH_WRAPPER_DIR) +""" _gh_wrapper_installed = False # reset on every new process / deployment @@ -669,26 +697,28 @@ def install_gh_wrapper() -> None: credential refresh. This wrapper reads from the token file (updated on every refresh) and exports ``GH_TOKEN`` before exec-ing the real ``gh``. """ - global _gh_wrapper_installed + global _gh_wrapper_installed, _GH_WRAPPER_DIR, _GH_WRAPPER_PATH if _gh_wrapper_installed: return import stat + import tempfile try: - wrapper_dir = Path(_GH_WRAPPER_DIR) - wrapper_dir.mkdir(parents=True, exist_ok=True) + wrapper_dir = tempfile.mkdtemp(prefix="ambient-gh-") + os.chmod(wrapper_dir, 0o700) + _GH_WRAPPER_DIR = wrapper_dir + _GH_WRAPPER_PATH = f"{wrapper_dir}/gh" wrapper_path = Path(_GH_WRAPPER_PATH) - wrapper_path.write_text(_GH_WRAPPER_SCRIPT) - wrapper_path.chmod( - stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH - ) # 755 + wrapper_path.write_text( + _GH_WRAPPER_SCRIPT_TEMPLATE.format(wrapper_dir=_GH_WRAPPER_DIR) + ) + wrapper_path.chmod(stat.S_IRWXU) # 700 # Prepend wrapper dir to PATH so it is found before the real gh. current_path = os.environ.get("PATH", "") - if _GH_WRAPPER_DIR not in current_path.split(":"): - os.environ["PATH"] = f"{_GH_WRAPPER_DIR}:{current_path}" + os.environ["PATH"] = f"{_GH_WRAPPER_DIR}:{current_path}" _gh_wrapper_installed = True logger.info("Installed gh CLI wrapper at %s", _GH_WRAPPER_PATH) diff --git a/components/runners/ambient-runner/tests/test_shared_session_credentials.py b/components/runners/ambient-runner/tests/test_shared_session_credentials.py index 11da4196e..0ed1f85d4 100755 --- a/components/runners/ambient-runner/tests/test_shared_session_credentials.py +++ b/components/runners/ambient-runner/tests/test_shared_session_credentials.py @@ -16,7 +16,6 @@ _GH_WRAPPER_PATH, _GITHUB_TOKEN_FILE, _GITLAB_TOKEN_FILE, - _GOOGLE_WORKSPACE_CREDS_FILE, _fetch_credential, clear_runtime_credentials, install_gh_wrapper, diff --git a/docs/pr-1307-impact-analysis.md b/docs/pr-1307-impact-analysis.md new file mode 100644 index 000000000..79388f59d --- /dev/null +++ b/docs/pr-1307-impact-analysis.md @@ -0,0 +1,34 @@ +# PR #1307 Impact Analysis: CodeRabbit Integration as Reference Implementation + +This document demonstrates the impact of PR #1307 (Claude Code automation overhaul) by showing how each artifact it introduced shaped the CodeRabbit integration implementation. + +## Impact Summary + +| #1307 Artifact | What It Provides | How It Shaped CodeRabbit Output | +|---|---|---| +| **scaffold/SKILL.md** | Integration file structure template + post-scaffold checklist | Defined the exact 10 new + 10 modified file list. Every file path in the plan came from this template. Without it, we'd have been reverse-engineering the pattern from existing code. | +| **backend/DEVELOPMENT.md** | Go handler conventions (K8s client selection, error handling, pre-commit checklist) | `ConnectCodeRabbit` uses `GetK8sClientsForRequest` for auth, `K8sClient` for Secret writes. The `/simplify` review caught a signature mismatch because this doc establishes the `func(context.Context, ...) (bool, error)` convention. | +| **backend/K8S_CLIENT_PATTERNS.md** | Decision tree: user-scoped vs service account clients | `GetCodeRabbitCredentialsForSession` uses `enforceCredentialRBAC` (user-scoped) then reads credentials via service account — exactly the pattern this doc prescribes. | +| **backend/ERROR_PATTERNS.md** | HTTP status code conventions (404 vs 400 vs 502) | After the signature fix, `ConnectCodeRabbit` now returns 502 on network errors vs 400 on invalid keys — a distinction this doc requires. | +| **frontend/DEVELOPMENT.md** | Zero-tolerance rules (no `any`, Shadcn only, React Query for all data) | Connection card uses only `Card`, `Button`, `Input`, `Label` from `@/components/ui/*`. API client is pure functions in `services/api/`, hooks in `services/queries/`. No manual `fetch()` in components. | +| **frontend/REACT_QUERY_PATTERNS.md** | Query/mutation hook structure, cache invalidation | `useConnectCodeRabbit` and `useDisconnectCodeRabbit` invalidate both `['coderabbit', 'status']` and `['integrations', 'status']` on success — the dual-invalidation pattern from this doc. | +| **docs/security-standards.md** | Token handling, RBAC enforcement, input validation | API keys never logged (only `len(token)` pattern initially, then simplified). `validateCodeRabbitAPIKeyImpl` uses `networkError()` to strip URLs from error messages — preventing credential leakage. | +| **settings.json hooks** | Real-time enforcement during edits (Shadcn reminder, React Query reminder, K8s client reminder, no-panic reminder) | These hooks fired during subagent implementation, keeping each subagent on-pattern even without full codebase context. The Shadcn hook ensures no raw `