updated sdk manager routes - #1
Merged
Merged
Conversation
There was a problem hiding this comment.
Pull request overview
This PR migrates the former standalone “sdk-manager” functionality into the main AuthSec API by adding new SDK manager services/controllers and registering their HTTP routes under /authsec/sdkmgr/* (plus a backward-compat /sdkmgr/* alias).
Changes:
- Adds SDK manager service layer: MCP OAuth/PKCE auth + session storage, playground conversation/server management, SPIRE proxying, and a voice auth client.
- Adds SDK manager controllers and route registration for all new endpoints under
/authsec/sdkmgr/*and/sdkmgr/*. - Introduces new DB tables/migrations for OAuth sessions and playground artifacts, plus new config knobs for OAuth + Azure OpenAI.
Reviewed changes
Copilot reviewed 31 out of 32 changed files in this pull request and generated 17 comments.
Show a summary per file
| File | Description |
|---|---|
| services/sdkmgr/voice_client_service.go | Voice assistant service integrating Azure OpenAI chat/TTS with CIBA/TOTP flows |
| services/sdkmgr/spire_proxy_service.go | SPIRE agent proxy service (tenant mapping + socket probe/placeholder SVID) |
| services/sdkmgr/services_service.go | Session-based lookup service for external service credentials and user details |
| services/sdkmgr/retry.go | Generic retry helper with exponential backoff + jitter |
| services/sdkmgr/pkce.go | PKCE verifier/challenge/state generation utilities |
| services/sdkmgr/oauth_session_store.go | OAuth session persistence across master + tenant DBs (migration + lookup helpers) |
| services/sdkmgr/mcp_tools_manager.go | Generates MCP tool schemas and enforces session_id requirements in schemas |
| services/sdkmgr/mcp_playground_service.go | Playground conversation CRUD, chat completion, MCP server CRUD (stubs) |
| services/sdkmgr/mcp_oauth_service.go | OAuth discovery + authorization + callback HTML handling for MCP servers |
| services/sdkmgr/mcp_auth_service.go | Core MCP OAuth flow + RBAC tool protection and tool list handling |
| services/sdkmgr/jwt_decode.go | JWT payload decode helper (no signature verification) |
| services/sdkmgr/dev_server_service.go | Runs user-provided MCP server code as a Python subprocess (stdio JSON-RPC) |
| services/sdkmgr/dashboard_service.go | Dashboard analytics queries for oauth_sessions/users |
| services/sdkmgr/client_id.go | Normalization/candidate generation for client_id variants |
| services/sdkmgr/circuit_breaker.go | Named circuit breaker registry using gobreaker |
| routes/routes.go | Registers sdkmgr routes under /authsec/sdkmgr/* and alias /sdkmgr/* |
| models/sdkmgr/playground.go | GORM models for playground conversations/messages/servers |
| models/sdkmgr/oauth_session.go | GORM model for oauth_sessions + helpers for token/scopes/tools |
| migrations/tenant/012_create_playground_tables.sql | Tenant DB migration: playground tables |
| migrations/tenant/011_create_oauth_sessions.sql | Tenant DB migration: oauth_sessions table |
| migrations/master/003_create_oauth_sessions.sql | Master DB migration: oauth_sessions table |
| go.mod | Adds gobreaker dependency (currently marked indirect) |
| go.sum | Records gobreaker checksums |
| controllers/sdkmgr/voice_controller.go | HTTP handlers for voice interact/poll/tts |
| controllers/sdkmgr/spire_controller.go | HTTP handlers for SPIRE workload endpoints |
| controllers/sdkmgr/services_controller.go | HTTP handlers for service credentials/user-details |
| controllers/sdkmgr/mcp_playground_controller.go | HTTP handlers for playground CRUD/chat/server mgmt |
| controllers/sdkmgr/mcp_oauth_controller.go | HTTP handlers for MCP OAuth check/authorize/callback/refresh |
| controllers/sdkmgr/mcp_auth_controller.go | HTTP handlers for MCP auth flow/session/tools/protection |
| controllers/sdkmgr/dev_server_controller.go | HTTP handlers for dev MCP server start/stop/status |
| controllers/sdkmgr/dashboard_controller.go | HTTP handlers for dashboard statistics/admin-users |
| config/config.go | Adds sdkmgr + Azure OpenAI config fields and env loading |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+69
to
+80
| body, _ := json.Marshal(payload) | ||
| resp, err := http.Post(endpoint, "application/json", bytes.NewReader(body)) //nolint:gosec | ||
| if err != nil { | ||
| logrus.Errorf("CIBA Initiate error: %v", err) | ||
| return map[string]interface{}{"error": err.Error(), "auth_req_id": nil} | ||
| } | ||
| defer resp.Body.Close() | ||
|
|
||
| var result map[string]interface{} | ||
| if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { | ||
| return map[string]interface{}{"error": "decode error", "auth_req_id": nil} | ||
| } |
Comment on lines
+109
to
+122
| body, _ := json.Marshal(payload) | ||
| resp, err := http.Post(endpoint, "application/json", bytes.NewReader(body)) //nolint:gosec | ||
| if err != nil { | ||
| a.mu.Lock() | ||
| a.retryCounts[email]++ | ||
| remaining := 3 - a.retryCounts[email] | ||
| a.mu.Unlock() | ||
| return map[string]interface{}{"success": false, "error": err.Error(), "remaining": remaining} | ||
| } | ||
| defer resp.Body.Close() | ||
|
|
||
| var resData map[string]interface{} | ||
| json.NewDecoder(resp.Body).Decode(&resData) | ||
|
|
Comment on lines
+398
to
+399
| aiMsg := chatResp["choices"].([]interface{})[0].(map[string]interface{})["message"].(map[string]interface{}) | ||
| history = append(history, aiMsg) |
Comment on lines
+375
to
+386
| // Select SDK based on client_id | ||
| sdk := s.authSDK | ||
| if clientID != "" && clientID != s.authSDK.clientID { | ||
| sdk = newAuthSecSDK(clientID) | ||
| } | ||
|
|
||
| history := s.getHistory(email) | ||
| history = append(history, map[string]interface{}{"role": "user", "content": userInput}) | ||
| history = trimHistory(history) | ||
| // Update system prompt with latest retry count | ||
| history[0]["content"] = s.systemPrompt(email) | ||
|
|
Comment on lines
+250
to
+254
| azureOpenAIKey := getEnv("AZURE_OPENAI_API_KEY", "") | ||
| azureOpenAIEndpoint := getEnv("AZURE_OPENAI_ENDPOINT", "") | ||
| azureOpenAIDeployment := getEnv("AZURE_OPENAI_DEPLOYMENT", "") | ||
| azureOpenAIVersion := getEnv("AZURE_OPENAI_VERSION", "2024-02-15-preview") | ||
| azureOpenAITTSDeployment := getEnv("AZURE_OPENAI_TTS_DEPLOYMENT", "tts") |
Comment on lines
+74
to
+101
| cfg := config.AppConfig | ||
| if cfg != nil && cfg.PKCEChallenge != "" { | ||
| challenge = cfg.PKCEChallenge | ||
| } | ||
|
|
||
| session.PKCEVerifier = &verifier | ||
| session.PKCEChallenge = &challenge | ||
| session.OAuthState = &state | ||
|
|
||
| if err := s.SessionStore.SaveSession(session); err != nil { | ||
| return nil, fmt.Errorf("failed to save session: %w", err) | ||
| } | ||
|
|
||
| // Build authorization URL. | ||
| redirectURI := s.resolveRedirectURI(resolvedClientID) | ||
|
|
||
| params := url.Values{ | ||
| "response_type": {"code"}, | ||
| "client_id": {resolvedClientID}, | ||
| "redirect_uri": {redirectURI}, | ||
| "scope": {"openid profile email"}, | ||
| "state": {state}, | ||
| "code_challenge": {challenge}, | ||
| "code_challenge_method": {"S256"}, | ||
| } | ||
|
|
||
| authURL := cfg.OAuthAuthURL + "?" + params.Encode() | ||
|
|
Comment on lines
+61
to
+132
| // getServiceID looks up the service ID from the tenant database by name. | ||
| func (s *ServicesService) getServiceID(serviceName, tenantID string) (string, error) { | ||
| db, err := config.GetTenantGORMDB(tenantID) | ||
| if err != nil { | ||
| return "", fmt.Errorf("failed to connect to tenant DB: %w", err) | ||
| } | ||
|
|
||
| var result struct { | ||
| ID string | ||
| } | ||
| err = db.Table("services"). | ||
| Where("created_by = ? AND name = ?", tenantID, serviceName). | ||
| Order("created_at DESC"). | ||
| Select("id"). | ||
| First(&result).Error | ||
| if err != nil { | ||
| if err == gorm.ErrRecordNotFound { | ||
| return "", fmt.Errorf("service '%s' not found for tenant %s", serviceName, tenantID) | ||
| } | ||
| return "", fmt.Errorf("failed to query service: %w", err) | ||
| } | ||
| return result.ID, nil | ||
| } | ||
|
|
||
| // GetServiceCredentials validates a session and retrieves external service credentials. | ||
| // In-process replacement for the Python HTTP call to /exsvc/services/{id}/credentials. | ||
| func (s *ServicesService) GetServiceCredentials(sessionID, serviceName string) (map[string]interface{}, error) { | ||
| info, err := s.getSessionInfo(sessionID) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| if info.TenantID == "" { | ||
| return nil, fmt.Errorf("no tenant_id in session") | ||
| } | ||
| if info.AccessToken == "" { | ||
| return nil, fmt.Errorf("no access token in session") | ||
| } | ||
|
|
||
| serviceID, err := s.getServiceID(serviceName, info.TenantID) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| return s.fetchCredentials(serviceID, info.TenantID) | ||
| } | ||
|
|
||
| // fetchCredentials retrieves credentials for a service from the tenant DB. | ||
| func (s *ServicesService) fetchCredentials(serviceID, tenantID string) (map[string]interface{}, error) { | ||
| db, err := config.GetTenantGORMDB(tenantID) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to connect to tenant DB: %w", err) | ||
| } | ||
|
|
||
| var svc struct { | ||
| ID string `gorm:"column:id"` | ||
| Name string `gorm:"column:name"` | ||
| ServiceType string `gorm:"column:service_type"` | ||
| AuthType string `gorm:"column:auth_type"` | ||
| URL *string `gorm:"column:url"` | ||
| Credentials *string `gorm:"column:credentials"` | ||
| Metadata *string `gorm:"column:metadata"` | ||
| } | ||
|
|
||
| err = db.Table("external_services"). | ||
| Where("id = ?", serviceID). | ||
| First(&svc).Error | ||
| if err != nil { | ||
| if err == gorm.ErrRecordNotFound { | ||
| return nil, fmt.Errorf("external service %s not found", serviceID) | ||
| } | ||
| return nil, fmt.Errorf("failed to query external service: %w", err) | ||
| } |
Comment on lines
+264
to
+287
| type readResult struct { | ||
| line string | ||
| err error | ||
| } | ||
| ch := make(chan readResult, 1) | ||
| go func() { | ||
| line, err := info.stdout.ReadString('\n') | ||
| ch <- readResult{line, err} | ||
| }() | ||
|
|
||
| select { | ||
| case r := <-ch: | ||
| if r.err != nil { | ||
| return nil, r.err | ||
| } | ||
| var resp map[string]interface{} | ||
| if err := json.Unmarshal([]byte(r.line), &resp); err != nil { | ||
| return nil, fmt.Errorf("json: %w", err) | ||
| } | ||
| info.LastActivity = time.Now() | ||
| return resp, nil | ||
| case <-time.After(timeout): | ||
| return nil, fmt.Errorf("timeout") | ||
| } |
| OAuthUserInfoURL string // OAuth userinfo endpoint | ||
| PKCEChallenge string // Pre-computed PKCE challenge (if static) | ||
| OAuthRedirectURI string // Default OAuth redirect URI | ||
| OAuthRedirectURITemplate string // Redirect URI template with {tenant_id} |
Comment on lines
108
to
112
| github.com/quic-go/qpack v0.6.0 // indirect | ||
| github.com/quic-go/quic-go v0.59.0 // indirect | ||
| github.com/ryanuber/go-glob v1.0.0 // indirect | ||
| github.com/sony/gobreaker/v2 v2.4.0 // indirect | ||
| github.com/stretchr/objx v0.5.2 // indirect |
ritamAN77
pushed a commit
that referenced
this pull request
Jul 9, 2026
#1 One-transaction grant: GrantAssignment now creates the connector assignment + broker-RS registration (approved) + connector-executor role binding on the client's service account in a single transaction (repo.GrantAssignmentTx). RevokeAssignment tears down the registration + binding only when it's the client's last assignment. Replaces the manual 4-table / raw-SQL enablement with one API call. #2 last_seen_at: service_accounts.last_seen_at is now updated on M2M token issuance and on a successful broker action (was NULL forever). #3 Agent activity: GET /uflow/admin/agents/:id/activity returns the agent lens over connector_action_audit (matched by actor_client_id / subject_id / actor_spiffe_id) for Agent 360. Build/vet/gofmt clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ritamAN77
pushed a commit
that referenced
this pull request
Jul 10, 2026
…ng, refresh-lock, F8 audit F7 (owner always): owner_email required at service-account creation (threaded through CreateServiceAccount; implicit-create sites default to the acting admin). Audit now records actor ALWAYS (act.client_id, else authenticating client — fixes direct-M2M no-actor) and stamps the accountable owner into every connector_action_audit row. D6: reject a foreign-workspace client_id at grant time with a clear error (resolveClientPrincipals now returns the SA workspace; GrantAssignmentTx guards it). Cross-workspace stays the deferred A2A/XAA case. #2a schema hardening — connector_connections: workspace_id + composite FK, scope->binding_type, auth_type->auth_method, subject_user_id->uuid, binding/ status/auth CHECK constraints, NULL-safe partial unique indexes (a plain UNIQUE let duplicate workspace rows through), and lifecycle cols (version, external_account_*, refresh_expires_at, last_used_at, revoked_at). #2b refresh-under-lock — Refresh takes a non-blocking pg advisory lock on the connection id; a loser re-reads the rotated token instead of racing, the winner does a version CAS and increments version. Kills the thundering-herd refresh-token rotation race. #2c F8 audit triad — split the overloaded outcome/http_status into authz_outcome / broker_status / provider_status / action_outcome, so an authorized-but-provider-failed call is no longer logged as success. Schema change requires wipe+rebootstrap on deploy. Build/vet/gofmt clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ritamAN77
pushed a commit
that referenced
this pull request
Jul 10, 2026
The broker now gates WHICH TEAM an agent may act FOR, not just which agent
calls — enforced inside the broker chain, not at the agent's own front door.
- connectors.allowed_subject_groups (uuid[]): group ids the on-behalf-of user
must belong to; empty = no restriction.
- Gate 4 in runAction: for a delegated (XAA) call, the token subject must be a
member of an allowed group (SubjectInAnyGroup over user_groups). A connector
with a group policy but no human subject (M2M) is denied — the policy is
meaningless without a subject.
- PUT /authsec/connectors/:id/subject-groups {group_ids} (connector:assign).
Closes Track A (design-review D5): #1 owner/D6, #2 schema-harden+refresh-lock+
F8, #3 GitHub App (F1), #4 input constraints (F3), #5 user consent (R4), #6
this. Additive column; rides the existing rebootstrap window. Build/vet/gofmt
clean; routes conflict-free.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.