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)
This commit is contained in:
@@ -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 |
|
||||
@@ -820,6 +821,119 @@ The LLM provider (api.riotpiao.com) should:
|
||||
|
||||
---
|
||||
|
||||
## AssumeRoleActivity: Temporary LLM Token Grants
|
||||
|
||||
**Like AWS AssumeRole**, AssumeRoleActivity requests temporary credentials for accessing LLM APIs:
|
||||
|
||||
```go
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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)
|
||||
```
|
||||
|
||||
### Workflow Pattern: AssumeRole → LLM Router → Activities
|
||||
|
||||
```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)
|
||||
|
||||
// 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)
|
||||
```
|
||||
|
||||
### 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
|
||||
|
||||
- **[Routing Workflow Spec](./docs/ROUTING_WORKFLOW_SPEC.md)** — Complete spec format reference
|
||||
|
||||
Reference in New Issue
Block a user