Add P2 SaaS CI, P4 scheduled discovery, P6 agent bridge (Redis pub/sub), Caddyfile
- P2: Gitea Actions CI for SaaS backend (separate from Go agent CI) - P4: ScheduledDiscovery with Redis distributed lock to prevent concurrent scans - P6: AgentBridge — Redis pub/sub for SaaS↔agent communication (approvals + step results) - Caddyfile: self-hosted reverse proxy with auto-TLS for all 6 products
This commit is contained in:
103
products/06-runbook-automation/saas/src/bridge/agent-bridge.ts
Normal file
103
products/06-runbook-automation/saas/src/bridge/agent-bridge.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import pino from 'pino';
|
||||
import Redis from 'ioredis';
|
||||
|
||||
const logger = pino({ name: 'agent-ws' });
|
||||
|
||||
/**
|
||||
* Agent communication via Redis pub/sub.
|
||||
* SaaS publishes approval decisions, agent subscribes.
|
||||
* Agent publishes step results, SaaS subscribes.
|
||||
*
|
||||
* Channel pattern: dd0c:run:{tenantId}:{executionId}:{direction}
|
||||
* - direction: 'to_agent' | 'from_agent'
|
||||
*/
|
||||
export class AgentBridge {
|
||||
private pub: Redis;
|
||||
private sub: Redis;
|
||||
|
||||
constructor(redisUrl: string) {
|
||||
this.pub = new Redis(redisUrl);
|
||||
this.sub = new Redis(redisUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send approval decision to agent.
|
||||
*/
|
||||
async sendApproval(tenantId: string, executionId: string, stepId: string, decision: 'approve' | 'reject'): Promise<void> {
|
||||
const channel = `dd0c:run:${tenantId}:${executionId}:to_agent`;
|
||||
const message = JSON.stringify({ type: 'approval', stepId, decision, timestamp: Date.now() });
|
||||
await this.pub.publish(channel, message);
|
||||
logger.info({ tenantId, executionId, stepId, decision }, 'Approval sent to agent');
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to step results from agent.
|
||||
*/
|
||||
async onStepResult(
|
||||
tenantId: string,
|
||||
executionId: string,
|
||||
callback: (result: StepResultMessage) => void,
|
||||
): Promise<void> {
|
||||
const channel = `dd0c:run:${tenantId}:${executionId}:from_agent`;
|
||||
await this.sub.subscribe(channel);
|
||||
|
||||
this.sub.on('message', (ch, message) => {
|
||||
if (ch !== channel) return;
|
||||
try {
|
||||
const parsed = JSON.parse(message) as StepResultMessage;
|
||||
callback(parsed);
|
||||
} catch (err) {
|
||||
logger.warn({ error: (err as Error).message }, 'Invalid message from agent');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish step result from agent side.
|
||||
*/
|
||||
async publishStepResult(tenantId: string, executionId: string, result: StepResultMessage): Promise<void> {
|
||||
const channel = `dd0c:run:${tenantId}:${executionId}:from_agent`;
|
||||
await this.pub.publish(channel, JSON.stringify(result));
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to approval decisions from SaaS side (agent-side).
|
||||
*/
|
||||
async onApproval(
|
||||
tenantId: string,
|
||||
executionId: string,
|
||||
callback: (decision: { stepId: string; decision: 'approve' | 'reject' }) => void,
|
||||
): Promise<void> {
|
||||
const channel = `dd0c:run:${tenantId}:${executionId}:to_agent`;
|
||||
await this.sub.subscribe(channel);
|
||||
|
||||
this.sub.on('message', (ch, message) => {
|
||||
if (ch !== channel) return;
|
||||
try {
|
||||
const parsed = JSON.parse(message);
|
||||
if (parsed.type === 'approval') {
|
||||
callback({ stepId: parsed.stepId, decision: parsed.decision });
|
||||
}
|
||||
} catch (err) {
|
||||
logger.warn({ error: (err as Error).message }, 'Invalid approval message');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
await this.sub.quit();
|
||||
await this.pub.quit();
|
||||
}
|
||||
}
|
||||
|
||||
export interface StepResultMessage {
|
||||
type: 'step_result';
|
||||
stepIndex: number;
|
||||
command: string;
|
||||
exitCode: number;
|
||||
status: 'success' | 'failed' | 'timed_out' | 'rejected' | 'skipped';
|
||||
durationMs: number;
|
||||
stdoutHash?: string;
|
||||
stderrHash?: string;
|
||||
timestamp: number;
|
||||
}
|
||||
Reference in New Issue
Block a user