Compare commits
3
Commits
66c17e821f
...
924aa398b6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
924aa398b6 | ||
|
|
4a34c8e672 | ||
|
|
ebf95506cd |
@@ -120,6 +120,7 @@ Poimen embodies three core principles:
|
|||||||
|
|
||||||
| Activity | Purpose | Timeout | Retry |
|
| Activity | Purpose | Timeout | Retry |
|
||||||
|----------|---------|---------|-------|
|
|----------|---------|---------|-------|
|
||||||
|
| **AssumeRoleActivity** | Request temporary JWT token (like AWS AssumeRole) | 30s | 2x |
|
||||||
| **CloneRepo** | Clone git repository | 30s | 3x |
|
| **CloneRepo** | Clone git repository | 30s | 3x |
|
||||||
| **AnalyzeCode** | Static analysis (SAST) | 120s | 2x |
|
| **AnalyzeCode** | Static analysis (SAST) | 120s | 2x |
|
||||||
| **SecurityScan** | Dependency & vulnerability scan | 60s | 2x |
|
| **SecurityScan** | Dependency & vulnerability scan | 60s | 2x |
|
||||||
@@ -548,8 +549,6 @@ Poimen supports multiple authentication methods to federate LLM access across cu
|
|||||||
auth := &routing.LLMAuth{
|
auth := &routing.LLMAuth{
|
||||||
Type: routing.AuthTypeBearer,
|
Type: routing.AuthTypeBearer,
|
||||||
Token: "eyJhbGciOiJIUzI1NiIs...", // JWT token from your auth provider
|
Token: "eyJhbGciOiJIUzI1NiIs...", // JWT token from your auth provider
|
||||||
TenantID: "customer-123", // Optional: tenant isolation
|
|
||||||
Scopes: "llm:read llm:write", // Optional: OAuth2 scopes
|
|
||||||
}
|
}
|
||||||
client := routing.NewLLMClientWithAuth(auth)
|
client := routing.NewLLMClientWithAuth(auth)
|
||||||
```
|
```
|
||||||
@@ -559,7 +558,6 @@ client := routing.NewLLMClientWithAuth(auth)
|
|||||||
auth := &routing.LLMAuth{
|
auth := &routing.LLMAuth{
|
||||||
Type: routing.AuthTypeAPIKey,
|
Type: routing.AuthTypeAPIKey,
|
||||||
APIKey: "sk-xxx-yyy-zzz", // API key from provider
|
APIKey: "sk-xxx-yyy-zzz", // API key from provider
|
||||||
TenantID: "customer-456",
|
|
||||||
}
|
}
|
||||||
client := routing.NewLLMClientWithAuth(auth)
|
client := routing.NewLLMClientWithAuth(auth)
|
||||||
```
|
```
|
||||||
@@ -570,7 +568,6 @@ auth := &routing.LLMAuth{
|
|||||||
Type: routing.AuthTypeCustom,
|
Type: routing.AuthTypeCustom,
|
||||||
HeaderName: "X-Custom-Auth",
|
HeaderName: "X-Custom-Auth",
|
||||||
HeaderValue: "custom-token-value",
|
HeaderValue: "custom-token-value",
|
||||||
TenantID: "customer-789",
|
|
||||||
}
|
}
|
||||||
client := routing.NewLLMClientWithAuth(auth)
|
client := routing.NewLLMClientWithAuth(auth)
|
||||||
```
|
```
|
||||||
@@ -583,47 +580,39 @@ config := &routing.LLMRouterConfig{
|
|||||||
Auth: &routing.LLMAuth{
|
Auth: &routing.LLMAuth{
|
||||||
Type: routing.AuthTypeBearer,
|
Type: routing.AuthTypeBearer,
|
||||||
Token: jwtToken,
|
Token: jwtToken,
|
||||||
TenantID: customerID, // Tenant isolation in multi-tenant deployments
|
|
||||||
},
|
},
|
||||||
TenantID: customerID, // Additional tenant tracking
|
|
||||||
}
|
}
|
||||||
router := routing.NewLLMRouter(config)
|
router := routing.NewLLMRouter(config)
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Multi-Tenant Federated Access
|
#### Per-Deployment Auth
|
||||||
|
|
||||||
For multi-tenant deployments:
|
Each Poimen deployment gets its own LLM token:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// Per-customer isolated routers
|
// In K8s secret/vault
|
||||||
func CreateCustomerRouter(customerID, jwtToken string) (*routing.LLMRouter, error) {
|
LLM_AUTH_TOKEN="eyJhbGciOiJIUzI1NiIs..."
|
||||||
|
|
||||||
|
// In code
|
||||||
|
func InitializeRouter() (*routing.LLMRouter, error) {
|
||||||
|
token := os.Getenv("LLM_AUTH_TOKEN")
|
||||||
|
if token == "" {
|
||||||
|
return nil, fmt.Errorf("LLM_AUTH_TOKEN not set")
|
||||||
|
}
|
||||||
|
|
||||||
auth := &routing.LLMAuth{
|
auth := &routing.LLMAuth{
|
||||||
Type: routing.AuthTypeBearer,
|
Type: routing.AuthTypeBearer,
|
||||||
Token: jwtToken,
|
Token: token,
|
||||||
TenantID: customerID, // Passed to LLM API in X-Tenant-ID header
|
|
||||||
Scopes: "llm:read", // Restrict scopes per customer
|
|
||||||
}
|
}
|
||||||
|
|
||||||
config := &routing.LLMRouterConfig{
|
config := &routing.LLMRouterConfig{
|
||||||
Provider: &routing.LLMClient{},
|
Provider: &routing.LLMClient{},
|
||||||
KnowledgeBase: globalKB,
|
KnowledgeBase: kb,
|
||||||
Auth: auth,
|
Auth: auth,
|
||||||
TenantID: customerID,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return routing.NewLLMRouter(config)
|
return routing.NewLLMRouter(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Usage in activity
|
|
||||||
input := &routing.LLMRouterInput{
|
|
||||||
Message: "Analyze code",
|
|
||||||
MemoryContext: memCtx,
|
|
||||||
}
|
|
||||||
output, err := router.Route(ctx, input)
|
|
||||||
// Headers sent to LLM API:
|
|
||||||
// Authorization: Bearer <jwtToken>
|
|
||||||
// X-Tenant-ID: <customerID>
|
|
||||||
// X-OAuth-Scopes: llm:read
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Token Refresh & Rotation
|
#### Token Refresh & Rotation
|
||||||
@@ -649,10 +638,8 @@ result, err := client.Chat(ctx, systemPrompt, userMsg)
|
|||||||
|
|
||||||
| Header | Set When | Value | Purpose |
|
| Header | Set When | Value | Purpose |
|
||||||
|--------|----------|-------|---------|
|
|--------|----------|-------|---------|
|
||||||
| `Authorization` | Bearer auth | `Bearer {token}` | OAuth2/JWT authentication |
|
| `Authorization` | Bearer auth | `Bearer {token}` | JWT/OAuth2 authentication |
|
||||||
| `X-API-Key` | API Key auth | `{api-key}` | API key authentication |
|
| `X-API-Key` | API Key auth | `{api-key}` | API key authentication |
|
||||||
| `X-Tenant-ID` | Any auth type | `{tenantID}` | Tenant/customer isolation |
|
|
||||||
| `X-OAuth-Scopes` | Bearer with scopes | Space-separated scopes | OAuth2 scope enforcement |
|
|
||||||
| Custom header | Custom auth | `{headerValue}` | Custom authentication scheme |
|
| Custom header | Custom auth | `{headerValue}` | Custom authentication scheme |
|
||||||
|
|
||||||
### LLMRouter Configuration (Programmatic)
|
### LLMRouter Configuration (Programmatic)
|
||||||
@@ -780,95 +767,171 @@ kubectl apply -f k8s/worker-deployment.yaml
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Security & Multi-Tenancy
|
## Security & Token Management
|
||||||
|
|
||||||
### Token Management
|
### Never Hardcode Tokens
|
||||||
|
|
||||||
**Never hardcode tokens!** Use secure secret management:
|
**Use secure secret management:**
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// ❌ DON'T DO THIS
|
// ❌ DON'T DO THIS
|
||||||
auth := &routing.LLMAuth{
|
auth := &routing.LLMAuth{
|
||||||
|
Type: routing.AuthTypeBearer,
|
||||||
Token: "eyJhbGciOiJIUzI1NiIs...", // Hardcoded!
|
Token: "eyJhbGciOiJIUzI1NiIs...", // Hardcoded!
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ DO THIS
|
// ✅ DO THIS
|
||||||
tokenFromVault, _ := vaultClient.GetSecret("llm-token-" + customerID)
|
token := os.Getenv("LLM_AUTH_TOKEN")
|
||||||
auth := &routing.LLMAuth{
|
auth := &routing.LLMAuth{
|
||||||
Type: routing.AuthTypeBearer,
|
Type: routing.AuthTypeBearer,
|
||||||
Token: tokenFromVault,
|
Token: token,
|
||||||
TenantID: customerID,
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Recommended secret management:**
|
**Recommended secret management:**
|
||||||
- Kubernetes Secrets (development)
|
- **Kubernetes Secrets** (development) — stored in etcd
|
||||||
- HashiCorp Vault (production)
|
- **HashiCorp Vault** (production) — centralized secret management
|
||||||
- AWS Secrets Manager / GCP Secret Manager (cloud)
|
- **AWS Secrets Manager** (cloud) — managed service
|
||||||
- Sealed Secrets / Sealed Policies
|
- **GCP Secret Manager** (cloud) — managed service
|
||||||
|
- **Sealed Secrets** or **Sealed Policies** — encrypted in git
|
||||||
|
|
||||||
### Tenant Isolation
|
### Token Rotation & Refresh
|
||||||
|
|
||||||
The `TenantID` header enforces tenant isolation on the LLM API side:
|
For long-running Temporal workflows, refresh tokens before they expire:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// Customer A's workflow
|
client := routing.NewLLMClientWithAuth(auth)
|
||||||
authA := &routing.LLMAuth{
|
|
||||||
Type: routing.AuthTypeBearer,
|
|
||||||
Token: tokenA,
|
|
||||||
TenantID: "customer-A", // ← Isolates this customer
|
|
||||||
}
|
|
||||||
|
|
||||||
// Customer B's workflow
|
// Later: token expires
|
||||||
authB := &routing.LLMAuth{
|
newToken := os.Getenv("LLM_AUTH_TOKEN_REFRESHED")
|
||||||
|
newAuth := &routing.LLMAuth{
|
||||||
Type: routing.AuthTypeBearer,
|
Type: routing.AuthTypeBearer,
|
||||||
Token: tokenB,
|
Token: newToken,
|
||||||
TenantID: "customer-B", // ← Isolates this customer
|
|
||||||
}
|
}
|
||||||
|
client.UpdateAuth(newAuth)
|
||||||
```
|
```
|
||||||
|
|
||||||
The LLM API server should:
|
### Authorization: LLM API Side
|
||||||
- Validate tenant ownership of tokens
|
|
||||||
- Enforce data boundaries per tenant
|
|
||||||
- Log access per tenant ID
|
|
||||||
- Rate-limit per tenant
|
|
||||||
|
|
||||||
### Scope-Based Access Control
|
The LLM provider (api.riotpiao.com) should:
|
||||||
|
- Validate JWT signature & expiration
|
||||||
|
- Enforce API rate limits per token
|
||||||
|
- Log all requests with token identity
|
||||||
|
- Support token revocation / blacklisting
|
||||||
|
|
||||||
Use OAuth2 scopes to limit capabilities:
|
---
|
||||||
|
|
||||||
|
## AssumeRoleActivity: Temporary LLM Token Grants
|
||||||
|
|
||||||
|
**Like AWS AssumeRole**, AssumeRoleActivity requests temporary credentials for accessing LLM APIs:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// Analytics-only customer
|
// 1. User requests temporary token
|
||||||
analytics := &routing.LLMAuth{
|
assumeRoleInput := &routing.AssumeRoleInput{
|
||||||
Type: routing.AuthTypeBearer,
|
Identity: "[email protected]", // Who is accessing
|
||||||
Token: token,
|
Scope: "llm:read llm:write", // What permissions
|
||||||
Scopes: "llm:read", // Read-only
|
DurationSeconds: 1800, // 30 minutes
|
||||||
}
|
}
|
||||||
|
|
||||||
// Full-access customer
|
// 2. Activity exchanges with auth server → returns JWT
|
||||||
admin := &routing.LLMAuth{
|
output, err := temporalClient.ExecuteActivity(ctx,
|
||||||
|
routing.AssumeRoleActivity,
|
||||||
|
assumeRoleInput)
|
||||||
|
|
||||||
|
// 3. Extract token from result
|
||||||
|
var tokenOutput *routing.AssumeRoleOutput
|
||||||
|
output.Get(&tokenOutput)
|
||||||
|
|
||||||
|
// 4. Use token in LLM Router
|
||||||
|
auth := &routing.LLMAuth{
|
||||||
Type: routing.AuthTypeBearer,
|
Type: routing.AuthTypeBearer,
|
||||||
Token: token,
|
Token: tokenOutput.Token, // ← JWT valid for 30 minutes
|
||||||
Scopes: "llm:read llm:write llm:admin", // Full access
|
|
||||||
}
|
}
|
||||||
|
router := routing.NewLLMRouter(config)
|
||||||
```
|
```
|
||||||
|
|
||||||
The LLM API server should validate scopes before executing requests.
|
### Workflow Pattern: AssumeRole → LLM Router → Activities
|
||||||
|
|
||||||
### Audit & Compliance
|
```go
|
||||||
|
// Step 1: Get temporary credentials
|
||||||
|
assumeRoleResult := workflow.ExecuteActivity(ctx, routing.AssumeRoleActivity, &routing.AssumeRoleInput{
|
||||||
|
Identity: workflowInput.UserID,
|
||||||
|
Scope: "llm:read llm:write",
|
||||||
|
DurationSeconds: 1800,
|
||||||
|
})
|
||||||
|
var token *routing.AssumeRoleOutput
|
||||||
|
assumeRoleResult.Get(&token)
|
||||||
|
|
||||||
All requests include identifying headers for audit trails:
|
// Step 2: Use token for all LLM router calls
|
||||||
|
routerInput := &routing.LLMRouterInput{
|
||||||
|
Message: "Analyze code for security",
|
||||||
|
Context: map[string]interface{}{"repo": "myrepo"},
|
||||||
|
}
|
||||||
|
|
||||||
|
routerOutput := workflow.ExecuteActivity(ctx, routing.LLMRouterActivity, routerInput)
|
||||||
|
// LLMRouter automatically uses the token from LLMRouterConfig
|
||||||
|
|
||||||
|
// Step 3: Execute generated workflow with same token
|
||||||
|
// (token baked into all activity calls)
|
||||||
```
|
```
|
||||||
POST /v1/chat/completions
|
|
||||||
Authorization: Bearer eyJhbGc...
|
|
||||||
X-Tenant-ID: customer-A
|
|
||||||
X-OAuth-Scopes: llm:read
|
|
||||||
|
|
||||||
# Server logs:
|
### Configuration: Credentials from Vault
|
||||||
# timestamp=2024-01-15T10:30:00Z tenant=customer-A scope=llm:read status=200 tokens=1500
|
|
||||||
|
Never hardcode credentials. Use Kubernetes Secrets or Hashicorp Vault:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# In K8s secret
|
||||||
|
kubectl create secret generic llm-oauth-creds \
|
||||||
|
--from-literal=OAUTH_CLIENT_ID="client-xxx" \
|
||||||
|
--from-literal=OAUTH_CLIENT_SECRET="secret-yyy" \
|
||||||
|
--from-literal=AUTH_SERVER_URL="https://auth.company.com"
|
||||||
|
|
||||||
|
# Pod reads from secret
|
||||||
|
env:
|
||||||
|
- name: OAUTH_CLIENT_ID
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: llm-oauth-creds
|
||||||
|
key: OAUTH_CLIENT_ID
|
||||||
```
|
```
|
||||||
|
|
||||||
|
In code, AssumeRoleActivity reads from environment:
|
||||||
|
```go
|
||||||
|
input := &routing.AssumeRoleInput{
|
||||||
|
Identity: "[email protected]",
|
||||||
|
Scope: "llm:read",
|
||||||
|
// clientId, clientSecret, authServerUrl read from env automatically
|
||||||
|
}
|
||||||
|
result, _ := AssumeRoleActivity(ctx, input)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Token Lifecycle
|
||||||
|
|
||||||
|
| Stage | Duration | Action |
|
||||||
|
|-------|----------|--------|
|
||||||
|
| **Request** | T+0s | User calls AssumeRoleActivity with identity + scope |
|
||||||
|
| **Grant** | T+1s | Auth server validates, issues JWT (default: 1hr validity) |
|
||||||
|
| **Use** | T+1s to T+3600s | LLMRouter uses token for all api.riotpiao.com calls |
|
||||||
|
| **Refresh** | Before expiry | If workflow > 1hr, request new token via AssumeRole again |
|
||||||
|
| **Revoke** | On demand | Auth server can immediately revoke token if needed |
|
||||||
|
|
||||||
|
### Scopes & Access Control
|
||||||
|
|
||||||
|
Scopes define granular permissions:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Read-only access (safe for analytics)
|
||||||
|
asScope: "llm:read"
|
||||||
|
|
||||||
|
// Full access (for agent workflows)
|
||||||
|
scope: "llm:read llm:write"
|
||||||
|
|
||||||
|
// Admin access (for operator/setup)
|
||||||
|
scope: "llm:admin"
|
||||||
|
```
|
||||||
|
|
||||||
|
The LLM API validates scopes on every request. AssumeRoleActivity can't escalate privileges—scopes returned by auth server are trusted.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|||||||
@@ -0,0 +1,216 @@
|
|||||||
|
package action
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AssumeRoleInput is the input to AssumeRoleActivity
|
||||||
|
type AssumeRoleInput struct {
|
||||||
|
// Identity is the user/service identity requesting access
|
||||||
|
Identity string `json:"identity"`
|
||||||
|
|
||||||
|
// ClientID is the OAuth2/OIDC client ID (from vault or env)
|
||||||
|
ClientID string `json:"clientId,omitempty"`
|
||||||
|
|
||||||
|
// ClientSecret is the OAuth2/OIDC client secret (from vault or env)
|
||||||
|
ClientSecret string `json:"clientSecret,omitempty"`
|
||||||
|
|
||||||
|
// Scope defines what APIs this token can access (e.g., "llm:read llm:write")
|
||||||
|
Scope string `json:"scope"`
|
||||||
|
|
||||||
|
// DurationSeconds is how long the token is valid (default: 3600 = 1 hour)
|
||||||
|
DurationSeconds int `json:"durationSeconds,omitempty"`
|
||||||
|
|
||||||
|
// AuthServerURL is the auth server endpoint (from env if not provided)
|
||||||
|
AuthServerURL string `json:"authServerUrl,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AssumeRoleOutput is the output from AssumeRoleActivity
|
||||||
|
type AssumeRoleOutput struct {
|
||||||
|
// Token is the JWT token for calling api.riotpiao.com
|
||||||
|
Token string `json:"token"`
|
||||||
|
|
||||||
|
// ExpiresAt is when the token expires (Unix timestamp)
|
||||||
|
ExpiresAt int64 `json:"expiresAt"`
|
||||||
|
|
||||||
|
// ExpiresIn is the duration in seconds until expiration
|
||||||
|
ExpiresIn int `json:"expiresIn"`
|
||||||
|
|
||||||
|
// TokenType is typically "Bearer"
|
||||||
|
TokenType string `json:"tokenType"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// oauthTokenRequest is sent to the auth server
|
||||||
|
type oauthTokenRequest struct {
|
||||||
|
GrantType string `json:"grant_type"`
|
||||||
|
ClientID string `json:"client_id"`
|
||||||
|
ClientSecret string `json:"client_secret"`
|
||||||
|
Scope string `json:"scope"`
|
||||||
|
Subject string `json:"subject,omitempty"` // The identity being assumed
|
||||||
|
}
|
||||||
|
|
||||||
|
// oauthTokenResponse is returned from the auth server
|
||||||
|
type oauthTokenResponse struct {
|
||||||
|
AccessToken string `json:"access_token"`
|
||||||
|
TokenType string `json:"token_type"`
|
||||||
|
ExpiresIn int `json:"expires_in"`
|
||||||
|
Scope string `json:"scope"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AssumeRoleActivity requests a temporary JWT token for accessing LLM APIs
|
||||||
|
//
|
||||||
|
// This activity works like AWS AssumeRole:
|
||||||
|
// 1. User provides identity + scope of access needed
|
||||||
|
// 2. Activity exchanges credentials with auth server
|
||||||
|
// 3. Returns JWT token valid for a limited time
|
||||||
|
// 4. Caller uses token in subsequent LLM API calls
|
||||||
|
//
|
||||||
|
// Security: Credentials should come from vault/secrets, never hardcoded
|
||||||
|
func AssumeRoleActivity(ctx context.Context, input *AssumeRoleInput) (*AssumeRoleOutput, error) {
|
||||||
|
// Validate inputs
|
||||||
|
if err := validateAssumeRoleInput(input); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve configuration from input + environment
|
||||||
|
config, err := resolveAssumeRoleConfig(input)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request token from auth server
|
||||||
|
tokenResp, err := requestAuthToken(ctx, config, input)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build output
|
||||||
|
return buildAssumeRoleOutput(tokenResp), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateAssumeRoleInput checks required fields
|
||||||
|
func validateAssumeRoleInput(input *AssumeRoleInput) error {
|
||||||
|
if input.Identity == "" {
|
||||||
|
return fmt.Errorf("identity is required")
|
||||||
|
}
|
||||||
|
if input.Scope == "" {
|
||||||
|
return fmt.Errorf("scope is required (e.g., 'llm:read' or 'llm:read llm:write')")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// assumeRoleConfig holds resolved configuration
|
||||||
|
type assumeRoleConfig struct {
|
||||||
|
authServerURL string
|
||||||
|
clientID string
|
||||||
|
clientSecret string
|
||||||
|
duration int
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolveAssumeRoleConfig gets config from input or environment
|
||||||
|
func resolveAssumeRoleConfig(input *AssumeRoleInput) (*assumeRoleConfig, error) {
|
||||||
|
cfg := &assumeRoleConfig{}
|
||||||
|
|
||||||
|
// Helper function to avoid DRY violation
|
||||||
|
getOrEnv := func(val, envKey, fieldName string) (string, error) {
|
||||||
|
if val != "" {
|
||||||
|
return val, nil
|
||||||
|
}
|
||||||
|
if val = os.Getenv(envKey); val != "" {
|
||||||
|
return val, nil
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("%s not provided and %s not set", fieldName, envKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
if cfg.authServerURL, err = getOrEnv(input.AuthServerURL, "AUTH_SERVER_URL", "authServerUrl"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if cfg.clientID, err = getOrEnv(input.ClientID, "OAUTH_CLIENT_ID", "clientId"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if cfg.clientSecret, err = getOrEnv(input.ClientSecret, "OAUTH_CLIENT_SECRET", "clientSecret"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate and set duration
|
||||||
|
cfg.duration = input.DurationSeconds
|
||||||
|
if cfg.duration == 0 {
|
||||||
|
cfg.duration = 3600 // 1 hour default
|
||||||
|
}
|
||||||
|
if cfg.duration > 86400 {
|
||||||
|
cfg.duration = 86400 // Max 24 hours
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// requestAuthToken calls the auth server and returns the token response
|
||||||
|
func requestAuthToken(ctx context.Context, config *assumeRoleConfig, input *AssumeRoleInput) (*oauthTokenResponse, error) {
|
||||||
|
tokenReq := oauthTokenRequest{
|
||||||
|
GrantType: "client_credentials",
|
||||||
|
ClientID: config.clientID,
|
||||||
|
ClientSecret: config.clientSecret,
|
||||||
|
Scope: input.Scope,
|
||||||
|
Subject: input.Identity,
|
||||||
|
}
|
||||||
|
|
||||||
|
reqBody, err := json.Marshal(tokenReq)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to marshal token request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
httpReq, err := http.NewRequestWithContext(ctx, "POST",
|
||||||
|
fmt.Sprintf("%s/oauth/token", config.authServerURL),
|
||||||
|
bytes.NewReader(reqBody))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
httpReq.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
client := &http.Client{Timeout: 10 * time.Second}
|
||||||
|
resp, err := client.Do(httpReq)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to call auth server: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
respBody, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("auth server returned status %d: %s", resp.StatusCode, string(respBody))
|
||||||
|
}
|
||||||
|
|
||||||
|
var tokenResp oauthTokenResponse
|
||||||
|
if err := json.Unmarshal(respBody, &tokenResp); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to unmarshal token response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tokenResp.AccessToken == "" {
|
||||||
|
return nil, fmt.Errorf("auth server returned empty access token")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &tokenResp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildAssumeRoleOutput constructs the output from token response
|
||||||
|
func buildAssumeRoleOutput(tokenResp *oauthTokenResponse) *AssumeRoleOutput {
|
||||||
|
expiresAt := time.Now().Add(time.Duration(tokenResp.ExpiresIn) * time.Second).Unix()
|
||||||
|
return &AssumeRoleOutput{
|
||||||
|
Token: tokenResp.AccessToken,
|
||||||
|
ExpiresAt: expiresAt,
|
||||||
|
ExpiresIn: tokenResp.ExpiresIn,
|
||||||
|
TokenType: tokenResp.TokenType,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -85,6 +85,9 @@ func main() {
|
|||||||
w.RegisterActivity(action.DeploymentPreCheckActivity)
|
w.RegisterActivity(action.DeploymentPreCheckActivity)
|
||||||
w.RegisterActivity(action.ApproveWorkflowActivity)
|
w.RegisterActivity(action.ApproveWorkflowActivity)
|
||||||
|
|
||||||
|
// Authentication activities
|
||||||
|
w.RegisterActivity(action.AssumeRoleActivity)
|
||||||
|
|
||||||
// Memory activities
|
// Memory activities
|
||||||
w.RegisterActivity(action.RetrieveMemoryActivity)
|
w.RegisterActivity(action.RetrieveMemoryActivity)
|
||||||
|
|
||||||
|
|||||||
@@ -407,10 +407,76 @@
|
|||||||
"dependencies": [],
|
"dependencies": [],
|
||||||
"notes": "Network-dependent. First activity to run for context-aware routing. Fast timeout."
|
"notes": "Network-dependent. First activity to run for context-aware routing. Fast timeout."
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "AssumeRoleActivity",
|
||||||
|
"description": "Request temporary JWT token for accessing LLM APIs (like AWS AssumeRole)",
|
||||||
|
"category": "authentication",
|
||||||
|
"inputs": {
|
||||||
|
"identity": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "User/service identity requesting access",
|
||||||
|
"required": true,
|
||||||
|
"examples": ["[email protected]", "service:poimen-worker"]
|
||||||
|
},
|
||||||
|
"clientId": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "OAuth2 client ID (from vault if not provided)",
|
||||||
|
"required": false
|
||||||
|
},
|
||||||
|
"clientSecret": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "OAuth2 client secret (from vault if not provided)",
|
||||||
|
"required": false
|
||||||
|
},
|
||||||
|
"scope": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Scope of access (e.g., 'llm:read' or 'llm:read llm:write')",
|
||||||
|
"required": true,
|
||||||
|
"examples": ["llm:read", "llm:read llm:write", "llm:admin"]
|
||||||
|
},
|
||||||
|
"durationSeconds": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Token validity duration in seconds (default: 3600, max: 86400)",
|
||||||
|
"required": false,
|
||||||
|
"default": 3600
|
||||||
|
},
|
||||||
|
"authServerUrl": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Auth server URL (from AUTH_SERVER_URL env if not provided)",
|
||||||
|
"required": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"outputs": {
|
||||||
|
"token": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "JWT token for calling api.riotpiao.com"
|
||||||
|
},
|
||||||
|
"expiresAt": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Token expiration time (Unix timestamp)"
|
||||||
|
},
|
||||||
|
"expiresIn": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Seconds until token expires"
|
||||||
|
},
|
||||||
|
"tokenType": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Token type (typically 'Bearer')"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"constraints": {
|
||||||
|
"defaultTimeout": "30s",
|
||||||
|
"isFlaky": false,
|
||||||
|
"recommendedRetries": 2,
|
||||||
|
"retryBackoff": 1.5,
|
||||||
|
"dependencies": [],
|
||||||
|
"notes": "Must run before LLM Router to provide auth token. Call early in workflow."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"totalActivities": 9,
|
"totalActivities": 10,
|
||||||
"lastUpdated": "2025-08-31T00:00:00Z",
|
"lastUpdated": "2025-08-31T00:00:00Z",
|
||||||
"categories": {
|
"categories": {
|
||||||
"repository": 1,
|
"repository": 1,
|
||||||
@@ -421,7 +487,8 @@
|
|||||||
"notification": 1,
|
"notification": 1,
|
||||||
"approval": 1,
|
"approval": 1,
|
||||||
"storage": 1,
|
"storage": 1,
|
||||||
"memory": 1
|
"memory": 1,
|
||||||
|
"authentication": 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,12 +53,6 @@ type LLMAuth struct {
|
|||||||
|
|
||||||
// HeaderValue is the custom header value for Custom auth
|
// HeaderValue is the custom header value for Custom auth
|
||||||
HeaderValue string `json:"headerValue,omitempty"`
|
HeaderValue string `json:"headerValue,omitempty"`
|
||||||
|
|
||||||
// TenantID is the tenant/customer ID for multi-tenant federated access
|
|
||||||
TenantID string `json:"tenantId,omitempty"`
|
|
||||||
|
|
||||||
// Scopes are the OAuth2 scopes (space-separated)
|
|
||||||
Scopes string `json:"scopes,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// LLMClient is a simple LLM client for routing
|
// LLMClient is a simple LLM client for routing
|
||||||
@@ -216,16 +210,6 @@ func (c *LLMClient) applyAuth(req *http.Request) error {
|
|||||||
req.Header.Set(c.auth.HeaderName, c.auth.HeaderValue)
|
req.Header.Set(c.auth.HeaderName, c.auth.HeaderValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add tenant ID if specified (for multi-tenant federated access)
|
|
||||||
if c.auth.TenantID != "" {
|
|
||||||
req.Header.Set("X-Tenant-ID", c.auth.TenantID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add scopes if specified (for OAuth2 flows)
|
|
||||||
if c.auth.Scopes != "" {
|
|
||||||
req.Header.Set("X-OAuth-Scopes", c.auth.Scopes)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -69,7 +69,6 @@ type LLMRouterConfig struct {
|
|||||||
Validators []WorkflowValidator
|
Validators []WorkflowValidator
|
||||||
ParamBinder ParameterBinder
|
ParamBinder ParameterBinder
|
||||||
Auth *LLMAuth // Authentication config for LLM API
|
Auth *LLMAuth // Authentication config for LLM API
|
||||||
TenantID string // Tenant ID for multi-tenant isolation
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewLLMRouter creates a new LLM router with custom config
|
// NewLLMRouter creates a new LLM router with custom config
|
||||||
|
|||||||
+1
-1
@@ -9,6 +9,6 @@ metadata:
|
|||||||
app.kubernetes.io/name: poimen
|
app.kubernetes.io/name: poimen
|
||||||
app.kubernetes.io/component: orchestrator
|
app.kubernetes.io/component: orchestrator
|
||||||
data:
|
data:
|
||||||
GIT_COMMIT: "30644a8e" # Updated automatically by CI/CD
|
GIT_COMMIT: "cdb6efe2" # Updated automatically by CI/CD
|
||||||
GIT_BRANCH: "main"
|
GIT_BRANCH: "main"
|
||||||
DEPLOYMENT_DATE: "2026-09-04"
|
DEPLOYMENT_DATE: "2026-09-04"
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ spec:
|
|||||||
labels:
|
labels:
|
||||||
app: poimen-worker
|
app: poimen-worker
|
||||||
annotations:
|
annotations:
|
||||||
git-commit: "30644a8e" # ✅ Updated on each push, triggers rolling restart
|
git-commit: "cdb6efe2" # ✅ Updated on each push, triggers rolling restart
|
||||||
deployment-date: "2026-09-04"
|
deployment-date: "2026-09-04"
|
||||||
spec:
|
spec:
|
||||||
containers:
|
containers:
|
||||||
|
|||||||
Reference in New Issue
Block a user