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:
Test
2026-09-04 14:11:58 -07:00
parent ebf95506cd
commit 4a34c8e672
6 changed files with 366 additions and 4 deletions
+114
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 |
@@ -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