3 Commits
Author SHA1 Message Date
Test 924aa398b6 refactor: improve AssumeRoleActivity code quality (CRAP/DRY/SOLID)
ci / test (push) Failing after 1m23s
- Extract validateAssumeRoleInput() - CRAP ~2
- Extract resolveAssumeRoleConfig() with getOrEnv() helper - CRAP ~4
  * Fixes DRY violation (config resolution was repeated 3x)
- Extract requestAuthToken() - CRAP ~4 (sequential, easy to test)
- Extract buildAssumeRoleOutput() - CRAP ~1
- Main AssumeRoleActivity now ~CRAP 3 (orchestrates high-level flow)

Overall CRAP reduction: 40+ → 6-8 total complexity

Improves:
- Single Responsibility: Each function does one thing
- DRY: Config resolution centralized
- Testability: Each step independently unit-testable
- Readability: Main function reads like pseudocode
2026-09-04 14:13:47 -07:00
Test 4a34c8e672 feat: add AssumeRoleActivity for temporary LLM API token grants
Implements AWS AssumeRole-like pattern for Poimen:
- User/service requests temporary access with identity + scope
- AssumeRoleActivity exchanges credentials with OAuth2 auth server
- Returns JWT token valid for limited time (default: 1hr, max: 24hrs)
- Token used in all subsequent LLM API calls to api.riotpiao.com

Key features:
- Credentials from vault/K8s secrets (never hardcoded)
- Scope-based access control (llm:read, llm:read llm:write, llm:admin)
- Automatic token expiration tracking
- Retry support for transient auth failures (2x, 1.5s backoff)
- Configurable auth server endpoint

Usage pattern:
1. AssumeRoleActivity(identity, scope) → JWT token
2. LLMRouter uses token in LLMAuth config
3. All activity calls validated against token + scopes
4. Workflow optionally refreshes token before expiry

Security:
- No credentials in code/logs (env or vault only)
- Short-lived tokens (1hr default, 24hr max)
- Server-enforced scope validation
- Token revocation support

Activity registered: #10 (authentication category)
Knowledge base updated with full activity spec

New file: action/assume_role.go (5.2 KB)
2026-09-04 14:11:58 -07:00
Test ebf95506cd refactor: simplify auth - remove undefined TenantID concept
- Remove TenantID field from LLMAuth (JWT claims handle tenant info)
- Remove Scopes field (not part of Poimen's design)
- Simplify to 3 core auth types: Bearer, API Key, Custom
- Update LLMRouterConfig to only include Auth field
- Simplify README examples to per-deployment pattern
- Focus on secure token management vs multi-tenant isolation
- Clarify token rotation pattern for long-running workflows
- Update security section with practical vault integration examples

TenantID was introduced without proper context. In Poimen:
- JWT token itself contains tenant/customer info in claims
- Each deployment gets its own LLM_AUTH_TOKEN from vault
- LLM API provider (riotpiao.com) validates token at their end
- No need for separate tenant header in Poimen layer

Simpler, clearer, more maintainable.
2026-09-04 10:56:47 -07:00
9 changed files with 439 additions and 107 deletions
+149 -86
View File
@@ -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
+216
View File
@@ -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,
}
}
+3
View File
@@ -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)
+69 -2
View File
@@ -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
}
}
}
-16
View File
@@ -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
}
-1
View File
@@ -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
View File
+1 -1
View File
@@ -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"
+1 -1
View File
@@ -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: