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 |
|
||||
|----------|---------|---------|-------|
|
||||
| **AssumeRoleActivity** | Request temporary JWT token (like AWS AssumeRole) | 30s | 2x |
|
||||
| **CloneRepo** | Clone git repository | 30s | 3x |
|
||||
| **AnalyzeCode** | Static analysis (SAST) | 120s | 2x |
|
||||
| **SecurityScan** | Dependency & vulnerability scan | 60s | 2x |
|
||||
@@ -546,10 +547,8 @@ Poimen supports multiple authentication methods to federate LLM access across cu
|
||||
#### 1. Bearer Token (JWT/OAuth2)
|
||||
```go
|
||||
auth := &routing.LLMAuth{
|
||||
Type: routing.AuthTypeBearer,
|
||||
Token: "eyJhbGciOiJIUzI1NiIs...", // JWT token from your auth provider
|
||||
TenantID: "customer-123", // Optional: tenant isolation
|
||||
Scopes: "llm:read llm:write", // Optional: OAuth2 scopes
|
||||
Type: routing.AuthTypeBearer,
|
||||
Token: "eyJhbGciOiJIUzI1NiIs...", // JWT token from your auth provider
|
||||
}
|
||||
client := routing.NewLLMClientWithAuth(auth)
|
||||
```
|
||||
@@ -557,9 +556,8 @@ client := routing.NewLLMClientWithAuth(auth)
|
||||
#### 2. API Key
|
||||
```go
|
||||
auth := &routing.LLMAuth{
|
||||
Type: routing.AuthTypeAPIKey,
|
||||
APIKey: "sk-xxx-yyy-zzz", // API key from provider
|
||||
TenantID: "customer-456",
|
||||
Type: routing.AuthTypeAPIKey,
|
||||
APIKey: "sk-xxx-yyy-zzz", // API key from provider
|
||||
}
|
||||
client := routing.NewLLMClientWithAuth(auth)
|
||||
```
|
||||
@@ -570,7 +568,6 @@ auth := &routing.LLMAuth{
|
||||
Type: routing.AuthTypeCustom,
|
||||
HeaderName: "X-Custom-Auth",
|
||||
HeaderValue: "custom-token-value",
|
||||
TenantID: "customer-789",
|
||||
}
|
||||
client := routing.NewLLMClientWithAuth(auth)
|
||||
```
|
||||
@@ -581,49 +578,41 @@ config := &routing.LLMRouterConfig{
|
||||
Provider: openaiProvider,
|
||||
KnowledgeBase: kb,
|
||||
Auth: &routing.LLMAuth{
|
||||
Type: routing.AuthTypeBearer,
|
||||
Token: jwtToken,
|
||||
TenantID: customerID, // Tenant isolation in multi-tenant deployments
|
||||
Type: routing.AuthTypeBearer,
|
||||
Token: jwtToken,
|
||||
},
|
||||
TenantID: customerID, // Additional tenant tracking
|
||||
}
|
||||
router := routing.NewLLMRouter(config)
|
||||
```
|
||||
|
||||
#### Multi-Tenant Federated Access
|
||||
#### Per-Deployment Auth
|
||||
|
||||
For multi-tenant deployments:
|
||||
Each Poimen deployment gets its own LLM token:
|
||||
|
||||
```go
|
||||
// Per-customer isolated routers
|
||||
func CreateCustomerRouter(customerID, jwtToken string) (*routing.LLMRouter, error) {
|
||||
// In K8s secret/vault
|
||||
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{
|
||||
Type: routing.AuthTypeBearer,
|
||||
Token: jwtToken,
|
||||
TenantID: customerID, // Passed to LLM API in X-Tenant-ID header
|
||||
Scopes: "llm:read", // Restrict scopes per customer
|
||||
Type: routing.AuthTypeBearer,
|
||||
Token: token,
|
||||
}
|
||||
|
||||
config := &routing.LLMRouterConfig{
|
||||
Provider: &routing.LLMClient{},
|
||||
KnowledgeBase: globalKB,
|
||||
KnowledgeBase: kb,
|
||||
Auth: auth,
|
||||
TenantID: customerID,
|
||||
}
|
||||
|
||||
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
|
||||
@@ -649,10 +638,8 @@ result, err := client.Chat(ctx, systemPrompt, userMsg)
|
||||
|
||||
| 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-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 |
|
||||
|
||||
### 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
|
||||
// ❌ DON'T DO THIS
|
||||
auth := &routing.LLMAuth{
|
||||
Type: routing.AuthTypeBearer,
|
||||
Token: "eyJhbGciOiJIUzI1NiIs...", // Hardcoded!
|
||||
}
|
||||
|
||||
// ✅ DO THIS
|
||||
tokenFromVault, _ := vaultClient.GetSecret("llm-token-" + customerID)
|
||||
token := os.Getenv("LLM_AUTH_TOKEN")
|
||||
auth := &routing.LLMAuth{
|
||||
Type: routing.AuthTypeBearer,
|
||||
Token: tokenFromVault,
|
||||
TenantID: customerID,
|
||||
Type: routing.AuthTypeBearer,
|
||||
Token: token,
|
||||
}
|
||||
```
|
||||
|
||||
**Recommended secret management:**
|
||||
- Kubernetes Secrets (development)
|
||||
- HashiCorp Vault (production)
|
||||
- AWS Secrets Manager / GCP Secret Manager (cloud)
|
||||
- Sealed Secrets / Sealed Policies
|
||||
- **Kubernetes Secrets** (development) — stored in etcd
|
||||
- **HashiCorp Vault** (production) — centralized secret management
|
||||
- **AWS Secrets Manager** (cloud) — managed service
|
||||
- **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
|
||||
// Customer A's workflow
|
||||
authA := &routing.LLMAuth{
|
||||
Type: routing.AuthTypeBearer,
|
||||
Token: tokenA,
|
||||
TenantID: "customer-A", // ← Isolates this customer
|
||||
}
|
||||
client := routing.NewLLMClientWithAuth(auth)
|
||||
|
||||
// Customer B's workflow
|
||||
authB := &routing.LLMAuth{
|
||||
Type: routing.AuthTypeBearer,
|
||||
Token: tokenB,
|
||||
TenantID: "customer-B", // ← Isolates this customer
|
||||
// Later: token expires
|
||||
newToken := os.Getenv("LLM_AUTH_TOKEN_REFRESHED")
|
||||
newAuth := &routing.LLMAuth{
|
||||
Type: routing.AuthTypeBearer,
|
||||
Token: newToken,
|
||||
}
|
||||
client.UpdateAuth(newAuth)
|
||||
```
|
||||
|
||||
The LLM API server should:
|
||||
- Validate tenant ownership of tokens
|
||||
- Enforce data boundaries per tenant
|
||||
- Log access per tenant ID
|
||||
- Rate-limit per tenant
|
||||
### Authorization: LLM API Side
|
||||
|
||||
### 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
|
||||
// Analytics-only customer
|
||||
analytics := &routing.LLMAuth{
|
||||
Type: routing.AuthTypeBearer,
|
||||
Token: token,
|
||||
Scopes: "llm:read", // Read-only
|
||||
// 1. User requests temporary token
|
||||
assumeRoleInput := &routing.AssumeRoleInput{
|
||||
Identity: "[email protected]", // Who is accessing
|
||||
Scope: "llm:read llm:write", // What permissions
|
||||
DurationSeconds: 1800, // 30 minutes
|
||||
}
|
||||
|
||||
// Full-access customer
|
||||
admin := &routing.LLMAuth{
|
||||
Type: routing.AuthTypeBearer,
|
||||
Token: token,
|
||||
Scopes: "llm:read llm:write llm:admin", // Full access
|
||||
// 2. Activity exchanges with auth server → returns JWT
|
||||
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,
|
||||
Token: tokenOutput.Token, // ← JWT valid for 30 minutes
|
||||
}
|
||||
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:
|
||||
# timestamp=2024-01-15T10:30:00Z tenant=customer-A scope=llm:read status=200 tokens=1500
|
||||
### Configuration: Credentials from Vault
|
||||
|
||||
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
|
||||
|
||||
@@ -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.ApproveWorkflowActivity)
|
||||
|
||||
// Authentication activities
|
||||
w.RegisterActivity(action.AssumeRoleActivity)
|
||||
|
||||
// Memory activities
|
||||
w.RegisterActivity(action.RetrieveMemoryActivity)
|
||||
|
||||
|
||||
@@ -407,10 +407,76 @@
|
||||
"dependencies": [],
|
||||
"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": {
|
||||
"totalActivities": 9,
|
||||
"totalActivities": 10,
|
||||
"lastUpdated": "2025-08-31T00:00:00Z",
|
||||
"categories": {
|
||||
"repository": 1,
|
||||
@@ -421,7 +487,8 @@
|
||||
"notification": 1,
|
||||
"approval": 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 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
|
||||
@@ -216,16 +210,6 @@ func (c *LLMClient) applyAuth(req *http.Request) error {
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -69,7 +69,6 @@ type LLMRouterConfig struct {
|
||||
Validators []WorkflowValidator
|
||||
ParamBinder ParameterBinder
|
||||
Auth *LLMAuth // Authentication config for LLM API
|
||||
TenantID string // Tenant ID for multi-tenant isolation
|
||||
}
|
||||
|
||||
// NewLLMRouter creates a new LLM router with custom config
|
||||
|
||||
+1
-1
@@ -9,6 +9,6 @@ metadata:
|
||||
app.kubernetes.io/name: poimen
|
||||
app.kubernetes.io/component: orchestrator
|
||||
data:
|
||||
GIT_COMMIT: "30644a8e" # Updated automatically by CI/CD
|
||||
GIT_COMMIT: "cdb6efe2" # Updated automatically by CI/CD
|
||||
GIT_BRANCH: "main"
|
||||
DEPLOYMENT_DATE: "2026-09-04"
|
||||
|
||||
@@ -13,7 +13,7 @@ spec:
|
||||
labels:
|
||||
app: poimen-worker
|
||||
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"
|
||||
spec:
|
||||
containers:
|
||||
|
||||
Reference in New Issue
Block a user