Add Vault token management and observability integration for multi-agent pipelines

- Vault token issuance per pipeline with 2-hour TTL
- Automatic token renewal loop every 30 minutes
- Error budget tracking with threshold-based revocation
- Observability-driven token revocation for policy violations
- Diagnostic pipeline spawning on error threshold breach
- Structured handoff reports for error recovery
- Agent lifecycle status API
- New API endpoints: /api/pipeline/token, /api/pipeline/errors,
  /api/observability/handoff, /api/observability/diagnostic

Orchestrator now reports errors to parent pipeline's observability
system via PIPELINE_ID environment variable.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
profit
2026-01-24 17:45:20 -05:00
co-authored by Claude Opus 4.5
parent a304895249
commit 8561d13728
5 changed files with 1252 additions and 22 deletions
+635 -1
View File
@@ -983,6 +983,502 @@ async function getBlackboardSolutions(taskId: string): Promise<any[]> {
}
}
// =============================================================================
// Vault Token Management for Pipelines
// =============================================================================
interface VaultTokenInfo {
token: string;
accessor: string;
ttl: number;
created_at: string;
renewable: boolean;
policies: string[];
}
interface PipelineTokenStatus {
pipeline_id: string;
token_active: boolean;
issued_at?: string;
expires_at?: string;
last_renewed?: string;
revoked?: boolean;
revoke_reason?: string;
}
// Error budget tracking
interface ErrorBudget {
pipeline_id: string;
total_errors: number;
errors_per_minute: number;
last_error_at?: string;
threshold_exceeded: boolean;
error_types: Record<string, number>;
}
const ERROR_THRESHOLDS = {
max_errors_per_minute: 5,
max_total_errors: 20,
stuck_timeout_seconds: 60,
critical_violation_immediate: true,
};
// Track error budgets in memory (also persisted to Redis)
const errorBudgets: Map<string, ErrorBudget> = new Map();
async function issuePipelineToken(pipelineId: string): Promise<VaultTokenInfo | null> {
try {
const initKeys = await Bun.file("/opt/vault/init-keys.json").json();
const rootToken = initKeys.root_token;
// Create a pipeline-specific token with limited TTL and policies
const tokenRequest = {
policies: ["pipeline-agent"],
ttl: "2h",
renewable: true,
display_name: `pipeline-${pipelineId}`,
meta: {
pipeline_id: pipelineId,
created_by: "orchestrator"
}
};
const proc = Bun.spawn(["curl", "-sk", "-X", "POST",
"-H", `X-Vault-Token: ${rootToken}`,
"-d", JSON.stringify(tokenRequest),
"https://127.0.0.1:8200/v1/auth/token/create"
], { stdout: "pipe" });
const text = await new Response(proc.stdout).text();
const result = JSON.parse(text);
if (result.auth) {
const tokenInfo: VaultTokenInfo = {
token: result.auth.client_token,
accessor: result.auth.accessor,
ttl: result.auth.lease_duration,
created_at: new Date().toISOString(),
renewable: result.auth.renewable,
policies: result.auth.policies
};
// Store token info in Redis (encrypted reference, not actual token)
await redis.hSet(`pipeline:${pipelineId}:vault`, {
accessor: tokenInfo.accessor,
issued_at: tokenInfo.created_at,
expires_at: new Date(Date.now() + tokenInfo.ttl * 1000).toISOString(),
renewable: tokenInfo.renewable ? "true" : "false",
policies: JSON.stringify(tokenInfo.policies),
status: "active"
});
broadcastUpdate("token_issued", {
pipeline_id: pipelineId,
accessor: tokenInfo.accessor,
expires_at: new Date(Date.now() + tokenInfo.ttl * 1000).toISOString()
});
return tokenInfo;
}
return null;
} catch (e: any) {
console.error(`[VAULT] Error issuing token for pipeline ${pipelineId}:`, e.message);
return null;
}
}
async function renewPipelineToken(pipelineId: string): Promise<boolean> {
try {
const tokenData = await redis.hGetAll(`pipeline:${pipelineId}:vault`);
if (!tokenData.accessor || tokenData.status !== "active") {
return false;
}
const initKeys = await Bun.file("/opt/vault/init-keys.json").json();
const rootToken = initKeys.root_token;
// Renew by accessor
const proc = Bun.spawn(["curl", "-sk", "-X", "POST",
"-H", `X-Vault-Token: ${rootToken}`,
"-d", JSON.stringify({ accessor: tokenData.accessor }),
"https://127.0.0.1:8200/v1/auth/token/renew-accessor"
], { stdout: "pipe" });
const text = await new Response(proc.stdout).text();
const result = JSON.parse(text);
if (result.auth) {
const newExpiry = new Date(Date.now() + result.auth.lease_duration * 1000).toISOString();
await redis.hSet(`pipeline:${pipelineId}:vault`, {
expires_at: newExpiry,
last_renewed: new Date().toISOString()
});
broadcastUpdate("token_renewed", {
pipeline_id: pipelineId,
expires_at: newExpiry
});
return true;
}
return false;
} catch (e: any) {
console.error(`[VAULT] Error renewing token for pipeline ${pipelineId}:`, e.message);
return false;
}
}
async function revokePipelineToken(pipelineId: string, reason: string): Promise<boolean> {
try {
const tokenData = await redis.hGetAll(`pipeline:${pipelineId}:vault`);
if (!tokenData.accessor) {
return false;
}
const initKeys = await Bun.file("/opt/vault/init-keys.json").json();
const rootToken = initKeys.root_token;
// Revoke by accessor
const proc = Bun.spawn(["curl", "-sk", "-X", "POST",
"-H", `X-Vault-Token: ${rootToken}`,
"-d", JSON.stringify({ accessor: tokenData.accessor }),
"https://127.0.0.1:8200/v1/auth/token/revoke-accessor"
], { stdout: "pipe" });
await proc.exited;
// Update Redis
await redis.hSet(`pipeline:${pipelineId}:vault`, {
status: "revoked",
revoked_at: new Date().toISOString(),
revoke_reason: reason
});
broadcastUpdate("token_revoked", {
pipeline_id: pipelineId,
reason: reason,
timestamp: new Date().toISOString()
});
await appendPipelineLog(pipelineId, "VAULT", `Token revoked: ${reason}`, "WARN");
return true;
} catch (e: any) {
console.error(`[VAULT] Error revoking token for pipeline ${pipelineId}:`, e.message);
return false;
}
}
async function getPipelineTokenStatus(pipelineId: string): Promise<PipelineTokenStatus> {
const tokenData = await redis.hGetAll(`pipeline:${pipelineId}:vault`);
return {
pipeline_id: pipelineId,
token_active: tokenData.status === "active",
issued_at: tokenData.issued_at,
expires_at: tokenData.expires_at,
last_renewed: tokenData.last_renewed,
revoked: tokenData.status === "revoked",
revoke_reason: tokenData.revoke_reason
};
}
// =============================================================================
// Error Budget & Observability Integration
// =============================================================================
async function initializeErrorBudget(pipelineId: string): Promise<ErrorBudget> {
const budget: ErrorBudget = {
pipeline_id: pipelineId,
total_errors: 0,
errors_per_minute: 0,
threshold_exceeded: false,
error_types: {}
};
errorBudgets.set(pipelineId, budget);
await redis.hSet(`pipeline:${pipelineId}:errors`, {
total_errors: "0",
errors_per_minute: "0",
threshold_exceeded: "false",
error_types: "{}"
});
return budget;
}
async function recordError(
pipelineId: string,
errorType: string,
severity: "low" | "medium" | "high" | "critical",
details: string
): Promise<{ threshold_exceeded: boolean; action_taken?: string }> {
let budget = errorBudgets.get(pipelineId);
if (!budget) {
budget = await initializeErrorBudget(pipelineId);
}
budget.total_errors++;
budget.error_types[errorType] = (budget.error_types[errorType] || 0) + 1;
budget.last_error_at = new Date().toISOString();
// Calculate errors per minute (rolling window)
const errorKey = `pipeline:${pipelineId}:error_times`;
const now = Date.now();
await redis.rPush(errorKey, String(now));
// Remove errors older than 1 minute
const oneMinuteAgo = now - 60000;
const errorTimes = await redis.lRange(errorKey, 0, -1);
const recentErrors = errorTimes.filter(t => parseInt(t) > oneMinuteAgo);
budget.errors_per_minute = recentErrors.length;
// Persist to Redis
await redis.hSet(`pipeline:${pipelineId}:errors`, {
total_errors: String(budget.total_errors),
errors_per_minute: String(budget.errors_per_minute),
last_error_at: budget.last_error_at,
error_types: JSON.stringify(budget.error_types)
});
// Log the error
await appendPipelineLog(pipelineId, "ERROR_MONITOR",
`Error recorded: ${errorType} (${severity}) - ${details}`,
severity === "critical" ? "ERROR" : "WARN"
);
// Check thresholds
let actionTaken: string | undefined;
if (severity === "critical" && ERROR_THRESHOLDS.critical_violation_immediate) {
budget.threshold_exceeded = true;
actionTaken = "immediate_revocation";
await revokePipelineToken(pipelineId, `Critical error: ${errorType}`);
await spawnDiagnosticPipeline(pipelineId, errorType, details);
} else if (budget.errors_per_minute >= ERROR_THRESHOLDS.max_errors_per_minute) {
budget.threshold_exceeded = true;
actionTaken = "rate_exceeded_revocation";
await revokePipelineToken(pipelineId, `Error rate exceeded: ${budget.errors_per_minute}/min`);
await spawnDiagnosticPipeline(pipelineId, "rate_exceeded", `${budget.errors_per_minute} errors in last minute`);
} else if (budget.total_errors >= ERROR_THRESHOLDS.max_total_errors) {
budget.threshold_exceeded = true;
actionTaken = "budget_exhausted_revocation";
await revokePipelineToken(pipelineId, `Error budget exhausted: ${budget.total_errors} total errors`);
await spawnDiagnosticPipeline(pipelineId, "budget_exhausted", `${budget.total_errors} total errors`);
}
if (budget.threshold_exceeded) {
await redis.hSet(`pipeline:${pipelineId}:errors`, "threshold_exceeded", "true");
broadcastUpdate("error_threshold", {
pipeline_id: pipelineId,
total_errors: budget.total_errors,
errors_per_minute: budget.errors_per_minute,
action_taken: actionTaken
});
}
errorBudgets.set(pipelineId, budget);
return { threshold_exceeded: budget.threshold_exceeded, action_taken: actionTaken };
}
async function spawnDiagnosticPipeline(
sourcePipelineId: string,
errorType: string,
errorDetails: string
): Promise<string> {
const diagnosticPipelineId = `diagnostic-${sourcePipelineId}-${Date.now().toString(36)}`;
// Create handoff report
const handoffReport = {
report_type: "error_handoff",
source_pipeline_id: sourcePipelineId,
diagnostic_pipeline_id: diagnosticPipelineId,
timestamp: new Date().toISOString(),
summary: {
error_type: errorType,
error_details: errorDetails,
error_budget: errorBudgets.get(sourcePipelineId)
},
context: {
pipeline_status: await redis.hGetAll(`pipeline:${sourcePipelineId}`),
recent_logs: await getPipelineLogs(sourcePipelineId, 20)
},
recommended_actions: [
"Review error patterns",
"Check resource availability",
"Verify API connectivity",
"Consider task decomposition"
]
};
// Store handoff report
await redis.set(`handoff:${diagnosticPipelineId}`, JSON.stringify(handoffReport));
// Create diagnostic pipeline entry
await redis.hSet(`pipeline:${diagnosticPipelineId}`, {
task_id: `diag-task-${Date.now().toString(36)}`,
objective: `Diagnose and recover from: ${errorType} in ${sourcePipelineId}`,
status: "DIAGNOSTIC",
created_at: new Date().toISOString(),
source_pipeline: sourcePipelineId,
handoff_report: JSON.stringify(handoffReport),
agents: JSON.stringify([])
});
await appendPipelineLog(diagnosticPipelineId, "SYSTEM",
`Diagnostic pipeline spawned for: ${sourcePipelineId}`, "INFO"
);
broadcastUpdate("diagnostic_spawned", {
diagnostic_pipeline_id: diagnosticPipelineId,
source_pipeline_id: sourcePipelineId,
error_type: errorType,
handoff_report: handoffReport
});
return diagnosticPipelineId;
}
async function generateHandoffReport(pipelineId: string): Promise<any> {
const pipelineData = await redis.hGetAll(`pipeline:${pipelineId}`);
const errorData = await redis.hGetAll(`pipeline:${pipelineId}:errors`);
const tokenData = await redis.hGetAll(`pipeline:${pipelineId}:vault`);
const logs = await getPipelineLogs(pipelineId, 50);
return {
report_type: "structured_handoff",
pipeline_id: pipelineId,
generated_at: new Date().toISOString(),
pipeline_state: {
status: pipelineData.status,
created_at: pipelineData.created_at,
objective: pipelineData.objective,
agents: pipelineData.agents ? JSON.parse(pipelineData.agents) : []
},
error_summary: {
total_errors: parseInt(errorData.total_errors || "0"),
errors_per_minute: parseInt(errorData.errors_per_minute || "0"),
threshold_exceeded: errorData.threshold_exceeded === "true",
error_types: errorData.error_types ? JSON.parse(errorData.error_types) : {}
},
token_status: {
active: tokenData.status === "active",
revoked: tokenData.status === "revoked",
revoke_reason: tokenData.revoke_reason
},
recent_activity: logs.slice(0, 20),
recommendations: generateRecommendations(pipelineData, errorData)
};
}
function generateRecommendations(pipelineData: any, errorData: any): string[] {
const recommendations: string[] = [];
const totalErrors = parseInt(errorData.total_errors || "0");
const errorTypes = errorData.error_types ? JSON.parse(errorData.error_types) : {};
if (totalErrors > 10) {
recommendations.push("Consider breaking down the task into smaller subtasks");
}
if (errorTypes["api_timeout"]) {
recommendations.push("Reduce API call frequency or implement backoff");
}
if (errorTypes["validation_failure"]) {
recommendations.push("Review input validation rules");
}
if (pipelineData.status === "STUCK" || pipelineData.status === "BLOCKED") {
recommendations.push("Check for circular dependencies");
recommendations.push("Verify all required resources are available");
}
if (recommendations.length === 0) {
recommendations.push("Review logs for specific error patterns");
}
return recommendations;
}
async function getErrorBudget(pipelineId: string): Promise<ErrorBudget | null> {
const data = await redis.hGetAll(`pipeline:${pipelineId}:errors`);
if (!data.total_errors) return null;
return {
pipeline_id: pipelineId,
total_errors: parseInt(data.total_errors),
errors_per_minute: parseInt(data.errors_per_minute || "0"),
last_error_at: data.last_error_at,
threshold_exceeded: data.threshold_exceeded === "true",
error_types: data.error_types ? JSON.parse(data.error_types) : {}
};
}
// Helper: Determine agent lifecycle state from status
function determineAgentLifecycle(pipelineStatus: string, agentState: any): string {
if (!agentState) {
if (pipelineStatus === "PENDING") return "CREATED";
if (pipelineStatus === "COMPLETED") return "SUCCEEDED";
if (pipelineStatus === "FAILED" || pipelineStatus === "ERROR") return "ERROR";
return "CREATED";
}
const status = agentState.status || pipelineStatus;
switch (status) {
case "PENDING":
case "IDLE":
return "CREATED";
case "WORKING":
case "RUNNING":
return "BUSY";
case "WAITING":
case "BLOCKED":
return "WAITING";
case "COMPLETED":
return "SUCCEEDED";
case "FAILED":
case "ERROR":
return "ERROR";
default:
// Check for handoff
if (agentState.handed_off_to) return "HANDED-OFF";
return "BUSY";
}
}
// Token renewal loop (runs every 30 minutes for active pipelines)
async function runTokenRenewalLoop(): Promise<void> {
setInterval(async () => {
try {
const pipelineKeys = await redis.keys("pipeline:*:vault");
for (const key of pipelineKeys) {
const pipelineId = key.replace("pipeline:", "").replace(":vault", "");
const tokenData = await redis.hGetAll(key);
if (tokenData.status === "active" && tokenData.expires_at) {
const expiresAt = new Date(tokenData.expires_at).getTime();
const now = Date.now();
const timeToExpiry = expiresAt - now;
// Renew if less than 35 minutes to expiry
if (timeToExpiry < 35 * 60 * 1000 && timeToExpiry > 0) {
console.log(`[VAULT] Renewing token for pipeline ${pipelineId}`);
await renewPipelineToken(pipelineId);
}
}
}
} catch (e: any) {
console.error("[VAULT] Token renewal loop error:", e.message);
}
}, 30 * 60 * 1000); // Every 30 minutes
}
// =============================================================================
// Pipeline Spawning
// =============================================================================
@@ -996,7 +1492,7 @@ interface PipelineConfig {
timeout?: number; // Orchestration timeout in seconds (default: 120)
}
async function spawnPipeline(config: PipelineConfig): Promise<{ success: boolean; pipeline_id: string; message: string }> {
async function spawnPipeline(config: PipelineConfig): Promise<{ success: boolean; pipeline_id: string; message: string; token_issued?: boolean }> {
const pipelineId = `pipeline-${Date.now().toString(36)}`;
const taskId = config.task_id || `task-${Date.now().toString(36)}`;
@@ -1017,6 +1513,19 @@ async function spawnPipeline(config: PipelineConfig): Promise<{ success: boolean
// Add to live log
await appendPipelineLog(pipelineId, "SYSTEM", `Pipeline ${pipelineId} created for: ${config.objective}`);
// Issue Vault token for this pipeline
await appendPipelineLog(pipelineId, "VAULT", "Requesting pipeline token from Vault...");
const tokenInfo = await issuePipelineToken(pipelineId);
if (tokenInfo) {
await appendPipelineLog(pipelineId, "VAULT", `Token issued (expires: ${new Date(Date.now() + tokenInfo.ttl * 1000).toISOString()})`);
} else {
await appendPipelineLog(pipelineId, "VAULT", "Token issuance failed - proceeding without dedicated token", "WARN");
}
// Initialize error budget
await initializeErrorBudget(pipelineId);
await appendPipelineLog(pipelineId, "OBSERVABILITY", "Error budget initialized");
// Spawn Agent A (Python) and Agent B (Bun) in parallel
const agentA = `agent-A-${pipelineId}`;
const agentB = `agent-B-${pipelineId}`;
@@ -6280,6 +6789,127 @@ const server = Bun.serve({
return new Response(JSON.stringify(logs), { headers });
}
// Vault Token Management APIs
if (path === "/api/pipeline/token") {
const pipelineId = url.searchParams.get("pipeline_id");
if (!pipelineId) {
return new Response(JSON.stringify({ error: "pipeline_id required" }), { status: 400, headers });
}
const status = await getPipelineTokenStatus(pipelineId);
return new Response(JSON.stringify(status), { headers });
}
if (path === "/api/pipeline/token/revoke" && req.method === "POST") {
const body = await req.json() as { pipeline_id: string; reason: string };
if (!body.pipeline_id || !body.reason) {
return new Response(JSON.stringify({ error: "pipeline_id and reason required" }), { status: 400, headers });
}
const success = await revokePipelineToken(body.pipeline_id, body.reason);
return new Response(JSON.stringify({ success, message: success ? "Token revoked" : "Failed to revoke token" }), { headers });
}
if (path === "/api/pipeline/token/renew" && req.method === "POST") {
const body = await req.json() as { pipeline_id: string };
if (!body.pipeline_id) {
return new Response(JSON.stringify({ error: "pipeline_id required" }), { status: 400, headers });
}
const success = await renewPipelineToken(body.pipeline_id);
return new Response(JSON.stringify({ success, message: success ? "Token renewed" : "Failed to renew token" }), { headers });
}
// Error Budget & Observability APIs
if (path === "/api/pipeline/errors") {
const pipelineId = url.searchParams.get("pipeline_id");
if (!pipelineId) {
return new Response(JSON.stringify({ error: "pipeline_id required" }), { status: 400, headers });
}
const budget = await getErrorBudget(pipelineId);
return new Response(JSON.stringify(budget || { pipeline_id: pipelineId, total_errors: 0, errors_per_minute: 0, threshold_exceeded: false, error_types: {} }), { headers });
}
if (path === "/api/pipeline/errors/record" && req.method === "POST") {
const body = await req.json() as {
pipeline_id: string;
error_type: string;
severity: "low" | "medium" | "high" | "critical";
details: string;
};
if (!body.pipeline_id || !body.error_type || !body.severity) {
return new Response(JSON.stringify({ error: "pipeline_id, error_type, and severity required" }), { status: 400, headers });
}
const result = await recordError(body.pipeline_id, body.error_type, body.severity, body.details || "");
return new Response(JSON.stringify(result), { headers });
}
if (path === "/api/observability/handoff" && req.method === "POST") {
const body = await req.json() as { pipeline_id: string };
if (!body.pipeline_id) {
return new Response(JSON.stringify({ error: "pipeline_id required" }), { status: 400, headers });
}
const report = await generateHandoffReport(body.pipeline_id);
return new Response(JSON.stringify(report), { headers });
}
if (path === "/api/observability/diagnostic" && req.method === "POST") {
const body = await req.json() as { pipeline_id: string; error_type: string; details: string };
if (!body.pipeline_id || !body.error_type) {
return new Response(JSON.stringify({ error: "pipeline_id and error_type required" }), { status: 400, headers });
}
const diagnosticId = await spawnDiagnosticPipeline(body.pipeline_id, body.error_type, body.details || "");
return new Response(JSON.stringify({ success: true, diagnostic_pipeline_id: diagnosticId }), { headers });
}
if (path === "/api/pipeline/metrics") {
const pipelineId = url.searchParams.get("pipeline_id");
if (!pipelineId) {
return new Response(JSON.stringify({ error: "pipeline_id required" }), { status: 400, headers });
}
// Get metrics from multi-agent coordination
const metricsKey = `metrics:${pipelineId}`;
const metricsData = await redis.hGetAll(metricsKey);
const errorBudget = await getErrorBudget(pipelineId);
const tokenStatus = await getPipelineTokenStatus(pipelineId);
return new Response(JSON.stringify({
pipeline_id: pipelineId,
coordination: metricsData,
error_budget: errorBudget,
token_status: tokenStatus
}), { headers });
}
// Agent Lifecycle Status API
if (path === "/api/agents/lifecycle") {
const pipelineId = url.searchParams.get("pipeline_id");
if (!pipelineId) {
return new Response(JSON.stringify({ error: "pipeline_id required" }), { status: 400, headers });
}
// Get agents from pipeline
const pipelineKey = `pipeline:${pipelineId}`;
const agentsRaw = await redis.hGet(pipelineKey, "agents");
const agents = agentsRaw ? JSON.parse(agentsRaw) : [];
// Enrich with state from multi-agent coordination
const enrichedAgents = [];
for (const agent of agents) {
const stateKey = `agents:${pipelineId}`;
const stateData = await redis.hGet(stateKey, agent.type);
let state = null;
if (stateData) {
try { state = JSON.parse(stateData); } catch {}
}
enrichedAgents.push({
...agent,
lifecycle: determineAgentLifecycle(agent.status, state),
state: state
});
}
return new Response(JSON.stringify({ pipeline_id: pipelineId, agents: enrichedAgents }), { headers });
}
// Plan Execution APIs
if (path === "/api/plans") {
const pipelineId = url.searchParams.get("pipeline_id");
@@ -6758,6 +7388,10 @@ async function main() {
await connectRedis();
// Start Vault token renewal loop for active pipelines
runTokenRenewalLoop();
console.log("[VAULT] Token renewal loop started");
console.log(`\n[SERVER] Dashboard running at http://localhost:${PORT}`);
console.log("[SERVER] WebSocket endpoint: ws://localhost:" + PORT + "/ws");
console.log("[SERVER] Press Ctrl+C to stop\n");