#1794

fix: support AI-agent persistent proxy credentials

Openmmacvicarwants to merge intofrom

📝 Description

Persistent credentials issued to API keys and AI agents were handled as human credentials by the PostgreSQL, HTTP, and SSH proxies, which incorrectly required an IDP user token. This change preserves the non-human identity across the gateway internal gRPC hop. It also keeps PostgreSQL setup errors inside the negotiated TLS connection.

📣 User-facing impact

API keys and AI agents can use persistent PostgreSQL, HTTP, and SSH credentials. PostgreSQL TLS clients receive protocol-correct setup errors instead of SSL error: bad record type.

🔗 Related Issue

Self-contained reproductions:

🚀 Type of Change

  • 🐛 Bug fix (non-breaking change which fixes an issue)
  • ✨ New feature (non-breaking change which adds functionality)
  • 💥 Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • 📚 Documentation update
  • 🎨 Style/UI update
  • ♻️ Code refactor
  • ⚡ Performance improvement
  • ✅ Test update
  • 🔧 Build configuration change
  • 🧹 Chore

📋 Changes Made

  • recognize active machine identities, API keys, and AI agents when authenticating connection credentials
  • preserve service identity context through the internal gRPC authentication path
  • apply the shared behavior to PostgreSQL, HTTP, and SSH proxies
  • write PostgreSQL setup failures through the post-negotiation connection

🧪 Testing

Test Configuration:

  • Browser(s): Not applicable
  • OS: Linux with Docker

Tests performed:

  • Unit tests pass
  • Integration tests pass
  • Manual testing completed

The self-contained HTTP and SSH cases reproduce access token not found for user subject on Hoop 1.160.1 after their target preflight succeeds. The focused Go package command is blocked locally because github.com/hoophq/libhoop is private; repository CI has credentials for it.

📸 Screenshots (if applicable)

Not applicable.

✅ Checklist

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • My changes generate no new warnings
  • New and existing unit tests pass locally with my changes
  • I have checked my code and corrected any misspellings

📄 Additional Notes

Focused command blocked by private module access:

go test ./gateway/proxyproto/grpckey ./gateway/proxyproto/postgresproxy ./gateway/proxyproto/httpproxy ./gateway/proxyproto/sshproxy -count=1

The PR remains draft until CI can validate the affected packages.

Activity

8
github-actions[bot]1mo ago

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

mmacvicar1mo ago

I have read the CLA Document and I hereby sign the CLA

qodo-code-review[bot]1mo ago

PR Summary by Qodo

Support persistent proxy credentials for service identities

🐞 Bug fix 🧪 Tests 🕐 40+ Minutes

AI Description

• Recognizes active machine, API-key, and AI-agent owners of persistent credentials.
• Preserves service identity through PostgreSQL, HTTP, SSH, and internal gRPC authentication.
• Sends PostgreSQL setup failures through negotiated TLS and adds focused regression coverage.
Diagram

sequenceDiagram
    actor Client
    participant Proxy as Protocol Proxies
    participant Models as Identity Models
    participant DB as Gateway Database
    participant Auth as gRPC Auth
    participant Stream as Gateway Stream
    Client->>Proxy: Present persistent credential
    Proxy->>Models: Classify credential owner
    Models->>DB: Query active identity
    DB-->>Models: Active owner match
    Models-->>Proxy: Service identity result
    Proxy->>Auth: Forward subject and org
    Auth->>Models: Load service context
    Models->>DB: Read identity and organization
    DB-->>Models: Active identity context
    Models-->>Auth: Authorized context
    Auth->>Stream: Open attributed session
    alt PostgreSQL setup fails after TLS
        Proxy-->>Client: TLS-framed ErrorResponse
    end
High-Level Assessment

The following are alternative approaches to this PR:

1. Typed credential-owner resolution
  • ➕ Resolve human, machine, API-key, and AI-agent ownership through one model API.
  • ➕ Avoid separate classification queries and protocol-specific branching.
  • ➕ Reduce the chance of identity behavior drifting between proxies.
  • ➖ Requires a broader credential model and proxy refactor.
  • ➖ Increases regression risk for a targeted production authentication fix.
  • ➖ May require migration or compatibility work for existing credential records.

Recommendation: Use the PR’s targeted classification and metadata propagation for this fix because it preserves existing machine and human authentication paths while correcting all affected proxies. Consider typed credential-owner resolution later to consolidate repeated classification logic and database lookups.

Files changed (8) +302 / -14

Bug fix (6) +176 / -14
machineidentity.goClassify and load active service identities +59/-0

Classify and load active service identities

• Adds fail-closed credential classification across machine identities, API keys, and AI agents. Builds an organization-scoped context for active API-key or AI-agent identities.

gateway/models/machineidentity.go

grpckey.goDefine service identity gRPC metadata keys +5/-0

Define service identity gRPC metadata keys

• Introduces metadata keys identifying service credentials and carrying their organization scope across internal gRPC calls.

gateway/proxyproto/grpckey/grpckey.go

httpproxy.goAuthenticate HTTP service credentials without user tokens +13/-3

Authenticate HTTP service credentials without user tokens

• Classifies persistent HTTP credential owners, skips human IDP token verification and polling for service identities, and forwards service identity metadata to gRPC.

gateway/proxyproto/httpproxy/httpproxy.go

postgresproxy.goSupport PostgreSQL service credentials and TLS-safe errors +44/-6

Support PostgreSQL service credentials and TLS-safe errors

• Applies service identity authentication and gRPC propagation to PostgreSQL credentials. Tracks the negotiated connection so setup failures are returned as PostgreSQL errors inside TLS rather than written to the raw socket.

gateway/proxyproto/postgresproxy/postgresproxy.go

sshproxy.goPreserve service identity through SSH authentication +22/-5

Preserve service identity through SSH authentication

• Stores service identity details in SSH permission extensions, bypasses human token checks for non-human credentials, and forwards the identity over internal gRPC.

gateway/proxyproto/sshproxy/sshproxy.go

auth.goAuthenticate propagated service identities in gRPC +33/-0

Authenticate propagated service identities in gRPC

• Recognizes service identity metadata on impersonated streams, reconstructs the active API-key or AI-agent context, and resolves the requested connection under that identity.

gateway/transport/interceptors/auth/auth.go

Tests (2) +126 / -0
connection_credentials_test.goVerify AI-agent credentials retain service identity +57/-0

Verify AI-agent credentials retain service identity

• Adds an integration test that creates an AI agent and PostgreSQL persistent credential, then verifies service-owner classification and context reconstruction.

gateway/integration/connection_credentials_test.go

postgresproxy_test.goTest PostgreSQL setup errors after TLS negotiation +69/-0

Test PostgreSQL setup errors after TLS negotiation

• Adds a pipe-based TLS regression test proving a post-handshake setup failure arrives as a PostgreSQL ErrorResponse instead of an invalid TLS record.

gateway/proxyproto/postgresproxy/postgresproxy_test.go

qodo-code-review[bot]1mo ago

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)


1. Packet tests drift from protocol types ✓ Resolved 📘 Rule violation ≡ Correctness
Description
TestConnectionSetupErrorStaysInsideTLS compares the parsed response type with the raw packet
identifier "E" instead of the shared pgtypes.ServerErrorResponse constant. The assertion can
diverge from the central protocol definition when packet handling is maintained or copied into
additional tests.
Code

gateway/proxyproto/postgresproxy/postgresproxy_test.go[R63-64]

+	if string(responseType) != "E" {
+		t.Fatalf("response type = %q, want PostgreSQL ErrorResponse E", responseType)
Relevance

●●● Strong

This is a deterministic protocol-constant/style correction, explicitly matching the repository rule.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Rule 1800908 prohibits raw literals used as protocol packet identifiers. The added test parses a
PostgreSQL response type and compares it to "E", while the repository already defines that
identifier as pgtypes.ServerErrorResponse.

Rule 1800908: Use protocol packet type constants instead of raw string literals gateway/proxyproto/postgresproxy/postgresproxy_test.go[59-64] common/pgtypes/const.go[39-39]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The PostgreSQL setup test identifies an error packet through the raw string literal `"E"`.

## Issue Context
Use the existing shared PostgreSQL packet-type constant and compare the response byte without converting it to a string.

## Fix Focus Areas
- gateway/proxyproto/postgresproxy/postgresproxy_test.go[59-64]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Identity rules leak into data models ✓ Resolved 📘 Rule violation ⌂ Architecture
Description
GetServiceIdentityContextByID chooses between API-key and AI-agent identities, enforces active
status, loads organization data, and assembles an authentication context inside gateway/models.
Service-authentication policy now spans the model and interceptor layers, so later identity-policy
changes can diverge instead of remaining in a gateway service.
Code

gateway/models/machineidentity.go[R266-268]

+// GetServiceIdentityContextByID loads an active API key or AI agent context.
+func GetServiceIdentityContextByID(orgID, id string) (*Context, error) {
+	apiKey, err := GetAPIKeyByNameOrID(orgID, id)
Relevance

●●● Strong

Recent PR #1709 accepted moving business and cross-model workflow logic out of gateway/models.

PR-#1709

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Rule 1800917 prohibits business workflows and cross-model coordination in gateway/models. The
helper coordinates API-key, AI-agent, and organization lookups and applies the active-identity
authentication rule before constructing the gateway context.

Rule 1800917: Keep business logic out of gateway/models and in gateway/services gateway/models/machineidentity.go[266-303] gateway/transport/interceptors/auth/auth.go[320-342]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Service-identity authentication policy and context assembly were added to the data-model package.

## Issue Context
Keep model code focused on persistence and move identity selection, active-status policy, organization loading, and context construction into a gateway service used by the interceptor.

## Fix Focus Areas
- gateway/models/machineidentity.go[266-303]
- gateway/transport/interceptors/auth/auth.go[320-342]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



3. Callers cannot control database access ✓ Resolved 📘 Rule violation ⌂ Architecture
Description
IsServiceIdentityCredential queries the package-global DB, and GetServiceIdentityContextByID
also performs lookups without accepting a *gorm.DB. Callers cannot run these authentication
queries on their transaction or injected test database, so proxy authentication may observe state
outside the caller's database context.
Code

gateway/models/machineidentity.go[252]

+	err := DB.Raw(`SELECT EXISTS (
Relevance

● Weak

Recent PR #1649 rejected the same global-DB dependency-injection finding in gateway models.

PR-#1649

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Rule 1800914 requires modified gateway database helpers to accept and use a *gorm.DB. The new
credential classifier uses global DB directly, while the context helper invokes several
database-backed model functions without receiving a database handle.

Rule 1800914: Gateway model functions must accept *gorm.DB instead of using the models.DB global gateway/models/machineidentity.go[250-263] gateway/models/machineidentity.go[266-289]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new service-identity model helpers perform database access without accepting an injected `*gorm.DB`.

## Issue Context
Update the helpers and their callers to pass and consistently use a database handle, allowing transactions and tests to control the database context.

## Fix Focus Areas
- gateway/models/machineidentity.go[250-289]
- gateway/proxyproto/httpproxy/httpproxy.go[446-449]
- gateway/proxyproto/postgresproxy/postgresproxy.go[281-284]
- gateway/proxyproto/sshproxy/sshproxy.go[267-270]
- gateway/transport/interceptors/auth/auth.go[320-342]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Context sources
✅ Compliance rules (platform): 53 rules
✅ Cross-repo context — repo relationships
  Explored: repo: hoophq/libhoop (sha: 6d0fabb8)
Review mode: 🧠 Deep: This security-sensitive, cross-cutting authentication change modifies multiple proxy paths, database identity classification, gRPC propagation, and PostgreSQL TLS error handling, creating several independent opportunities for subtle defects.
Tip of the day
💡 Did you know, you can tweak Display preferences with a live preview to see your comment before it ships

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

qodo-code-review[bot]reviewed1mo ago