Skip to content

Latest commit

 

History

History
1425 lines (1139 loc) · 41.3 KB

File metadata and controls

1425 lines (1139 loc) · 41.3 KB

Visor Architecture

This document provides a comprehensive overview of Visor's internal architecture, explaining how the system works at a high level.

Table of Contents


System Overview

Visor is an AI-powered workflow orchestration tool that can run as a GitHub Action, CLI tool, Slack bot, or A2A agent server. The system uses a state machine-based execution engine to orchestrate checks (steps) with sophisticated dependency resolution, routing, and error handling.

High-Level Architecture Diagram

                                    +------------------+
                                    |   Entry Points   |
                                    +------------------+
                                    |                  |
                +------+------+-----+----+--------+---+----------+
                |             |          |        |              |
                v             v          v        v              v
         +------+------+ +---+----+ +---+---+ +--+-------+ +----+------+
         | GitHub      | |  CLI   | | Slack | | HTTP     | | A2A       |
         | Action      | |        | | Socket| | Webhook  | | Agent     |
         | (index.ts)  | |        | | Mode  | | Server   | | Server    |
         +------+------+ +---+----+ +---+---+ +--+-------+ +----+------+
                |             |          |        |              |
                +------+------+----------+--------+--------------+
                               |
                               v
                    +----------+-----------+
                    |  Configuration       |
                    |  Manager             |
                    |  (config.ts)         |
                    +----------+-----------+
                               |
                               v
                    +----------+-----------+
                    |  State Machine       |
                    |  Execution Engine    |
                    +----------+-----------+
                               |
          +--------------------+--------------------+
          |                    |                    |
          v                    v                    v
   +------+------+      +------+------+      +------+------+
   |  Dependency |      |   Journal   |      |  Provider   |
   |  Resolver   |      |   (Snapshot |      |  Registry   |
   |             |      |    Store)   |      |             |
   +-------------+      +-------------+      +------+------+
                                                    |
          +----------------+------------------------+
          |                |                |       |
          v                v                v       v
   +------+------+  +------+------+  +-----+----+ +--+--+
   | AI Provider |  | Command     |  | HTTP     | | ... |
   |             |  | Provider    |  | Provider | |     |
   +-------------+  +-------------+  +----------+ +-----+

Key Components

Component Description
Entry Points GitHub Action, CLI, Slack Socket Mode, HTTP webhooks, A2A Agent Protocol
Configuration Manager Loads and validates YAML configuration
State Machine Engine Orchestrates check execution with wave-based scheduling
Provider Registry Registry of pluggable check providers
Journal Execution state and output history
Dependency Resolver Builds execution graph from check dependencies

Entry Points

Visor supports multiple entry points for different integration scenarios.

GitHub Action Entry (src/index.ts)

The primary entry point for GitHub Actions:

// Simplified flow
async function run(): Promise<void> {
  const { octokit, authType } = await createAuthenticatedOctokit();
  const configManager = new ConfigManager();
  const config = await configManager.loadConfig(configPath);
  await handleEvent(octokit, inputs, eventName, context, config);
}

Responsibilities:

  • Authenticate with GitHub (token or App)
  • Load GitHub event context from GITHUB_EVENT_PATH
  • Map GitHub events to Visor event triggers
  • Execute checks via the state machine engine
  • Post results as PR comments

Supported GitHub Events:

  • pull_request (opened, synchronize, edited)
  • issue_comment (command-driven reviews)
  • issues (issue assistant workflows)
  • push (associated PR detection)

CLI Entry (src/cli-main.ts)

The command-line interface for local development and CI:

visor --config .visor.yaml --checks security,performance
visor review                  # Built-in code review workflow
visor test tests/             # Run YAML test suites
visor validate                # Validate configuration

CLI Features:

  • Configuration validation and loading
  • Check filtering by name or tags
  • Multiple output formats (table, json, markdown, sarif)
  • Debug visualizer integration
  • TUI (Terminal User Interface) mode
  • Telemetry/tracing support

Slack Socket Mode

For Slack bot integration:

visor --slack --config .visor.yaml

Uses Slack's Socket Mode for real-time event handling without requiring a public webhook endpoint.

HTTP Webhook Server

For receiving external webhooks:

http_server:
  enabled: true
  port: 8080
  endpoints:
    - path: /webhook
      transform: "{{ request.body | json }}"

A2A Agent Server (src/agent-protocol/a2a-frontend.ts)

The A2A (Agent-to-Agent) entry point exposes Visor workflows as standards-compliant A2A agents:

visor --a2a --config .visor.yaml

Responsibilities:

  • Serve Agent Card at /.well-known/agent-card.json for discovery
  • Accept tasks via POST /message:send
  • Route incoming A2A skills to internal workflows via skill_routing
  • Manage task lifecycle (submitted → working → completed/failed)
  • Persist tasks in SQLite via TaskStore
  • Execute workflows through the state machine engine

Supported A2A Endpoints:

Method Path Auth Description
GET /.well-known/agent-card.json No Agent Card for discovery
POST /message:send Yes Submit a task
GET /tasks/{id} Yes Get task status
GET /tasks Yes List tasks
POST /tasks/{id}:cancel Yes Cancel a task

See A2A Provider for complete documentation.


Core Components

Configuration Loading (src/config.ts)

The ConfigManager handles all configuration operations:

+------------------+
| ConfigManager    |
+------------------+
| - loadConfig()   |  <-- Load from file path
| - findAndLoad()  |  <-- Auto-discover .visor.yaml
| - validateConfig |  <-- Schema validation (Ajv)
| - mergeDefaults  |  <-- Apply default values
+------------------+

Configuration Resolution Order:

  1. CLI --config parameter
  2. visor.yaml or .visor.yaml in project root
  3. visor.yaml or .visor.yaml in git repository root
  4. Bundled default configuration

Key Features:

  • YAML parsing with js-yaml
  • Schema validation with Ajv
  • Configuration inheritance via extends/include
  • Environment variable interpolation
  • Remote configuration loading (HTTP/HTTPS)

Check Execution Engine (src/state-machine-execution-engine.ts)

The main orchestration layer that coordinates check execution:

class StateMachineExecutionEngine {
  async executeGroupedChecks(
    prInfo: PRInfo,
    checks: string[],
    timeout?: number,
    config?: VisorConfig,
    // ... additional options
  ): Promise<ExecutionResult>
}

Responsibilities:

  • Build engine context from configuration
  • Initialize workspace isolation (if enabled)
  • Create and run the state machine runner
  • Manage frontends (GitHub, Slack integration)
  • Aggregate results into ExecutionResult

Provider System

Providers are pluggable components that implement specific check types:

+-------------------+
| CheckProvider     |  <-- Abstract base class
+-------------------+
| + getName()       |
| + execute()       |
| + validateConfig()|
| + isAvailable()   |
+-------------------+
         ^
         |
    +----+----+----+----+----+
    |    |    |    |    |    |
   AI  Cmd Script HTTP MCP  ...

See Provider Architecture for details.

Routing and Flow Control

Checks can define routing rules for success, failure, and completion:

checks:
  my-check:
    type: ai
    prompt: "..."
    on_fail:
      retry:
        max: 3
        backoff:
          mode: exponential
          delay_ms: 1000
      run: [remediation-step]
      goto: previous-step
    on_success:
      run: [post-process]
    on_finish:  # For forEach checks
      run: [aggregation-step]

Agent Protocol Layer

The A2A feature introduces a task-based execution layer that sits between external clients and the workflow engine. This is architecturally separate from the step-level state machine — it governs the lifecycle of inbound agent tasks.

External A2A Client
      │
      ▼
┌─────────────────────┐
│  A2A Frontend       │  HTTP server, auth, Agent Card
│  (a2a-frontend.ts)  │
└──────────┬──────────┘
           │
           ▼
┌─────────────────────┐     ┌─────────────────────┐
│  Task Store         │◄───►│  Task Queue          │
│  (SQLite)           │     │  (async execution)   │
└──────────┬──────────┘     └──────────┬──────────┘
           │                           │
           ▼                           ▼
┌──────────────────────────────────────────────────┐
│  State Machine Execution Engine                  │
│  (same engine used by CLI, GitHub Action, etc.)  │
└──────────────────────────────────────────────────┘
           │
           ▼
┌──────────────────────────────────────────────────┐
│  Provider Registry → [ai, a2a, command, ...]     │
└──────────────────────────────────────────────────┘

Task Store (src/agent-protocol/task-store.ts)

SQLite-backed persistence for A2A tasks:

CREATE TABLE agent_tasks (
  id TEXT PRIMARY KEY,           -- UUID
  context_id TEXT NOT NULL,      -- Session grouping
  state TEXT NOT NULL,           -- Task state
  created_at TEXT NOT NULL,      -- ISO 8601
  updated_at TEXT NOT NULL,
  request_message TEXT NOT NULL, -- JSON: original message
  artifacts TEXT DEFAULT '[]',   -- JSON: AgentArtifact[]
  history TEXT DEFAULT '[]',     -- JSON: AgentMessage[]
  workflow_id TEXT,              -- Target workflow
  run_id TEXT,                   -- Engine run correlation
  claimed_by TEXT,               -- Worker ID (queue)
  claimed_at TEXT,               -- Claim timestamp
  ttl TEXT                       -- Expiration
);

The store provides CRUD operations with state transition enforcement — invalid transitions (e.g., completedworking) throw InvalidStateTransitionError.

Task Queue (src/agent-protocol/task-queue.ts)

Manages async task execution with configurable concurrency:

  • Polls the database at poll_interval for unclaimed submitted tasks
  • Claims tasks using a worker ID with stale_claim_timeout to recover from crashed workers
  • Feeds tasks to the state machine engine, limited by max_concurrent
  • Updates task state and artifacts on completion

Task State Machine (src/agent-protocol/state-transitions.ts)

Separate from the workflow engine state machine — this governs task lifecycle:

submitted ──→ working ──→ completed (terminal)
    │            ├──→ failed (terminal)
    │            ├──→ canceled (terminal)
    │            ├──→ input_required ──→ working (resumed)
    │            └──→ auth_required ──→ working (resumed)
    ├──→ canceled (terminal)
    └──→ rejected (terminal)

Terminal states: completed, failed, canceled, rejected.

Agent Card & Discovery

The Agent Card is a JSON document describing the agent's capabilities:

interface AgentCard {
  name: string;
  description?: string;
  version?: string;
  provider?: { organization: string; url?: string };
  skills?: AgentSkill[];        // Advertised capabilities
  supported_interfaces?: Array<{ url: string; protocol_binding?: string }>;
  capabilities?: { streaming?: boolean; push_notifications?: boolean };
}

Skills are mapped to internal workflows via skill_routing configuration. When a client sends a message with metadata.skill_id, the frontend resolves the target workflow and dispatches execution.


Provider Architecture

Base Provider Interface

All providers implement the CheckProvider abstract class:

abstract class CheckProvider {
  abstract getName(): string;
  abstract getDescription(): string;
  abstract validateConfig(config: unknown): Promise<boolean>;
  abstract execute(
    prInfo: PRInfo,
    config: CheckProviderConfig,
    dependencyResults?: Map<string, ReviewSummary>,
    context?: ExecutionContext
  ): Promise<ReviewSummary>;
  abstract getSupportedConfigKeys(): string[];
  abstract isAvailable(): Promise<boolean>;
  abstract getRequirements(): string[];
}

Provider Registry

The singleton CheckProviderRegistry manages provider registration:

class CheckProviderRegistry {
  static getInstance(): CheckProviderRegistry;
  register(provider: CheckProvider): void;
  getProvider(name: string): CheckProvider | undefined;
  getAvailableProviders(): string[];
}

Registration Flow:

  1. Registry instantiates on first access
  2. Default providers registered automatically
  3. Custom providers can be registered programmatically

Built-in Providers

Provider Type Description
a2a A2A Agent Calls external A2A-compatible agents
ai AI-powered Uses Gemini, Claude, OpenAI, or Bedrock for analysis
command Command Executes shell commands
script Script Executes JavaScript in a sandbox
http HTTP Output Sends results to webhooks
http_input HTTP Input Receives webhook data
http_client HTTP Client Makes HTTP requests
mcp MCP Connects to MCP tool servers
utcp UTCP Calls UTCP tools via native protocols
claude-code Claude Code Uses Claude Code SDK with MCP tools
memory Memory Persistent key-value storage
log Logger Debug logging output
noop No-op Placeholder for orchestration
human-input Human Input Collects user input interactively
workflow Workflow Invokes nested workflows
git-checkout Git Checkout Checks out git references
github GitHub Ops GitHub API operations (labels, comments)

Provider Lifecycle

+-------------+     +-------------+     +-------------+
| Instantiate | --> | Validate    | --> | Execute     |
| Provider    |     | Config      |     | Check       |
+-------------+     +-------------+     +-------------+
                          |                    |
                          v                    v
                    Config Error?        +-----+-----+
                          |              | Result    |
                          v              | (Summary) |
                    Skip Check           +-----------+

State Machine

Visor uses a state machine-based execution engine for orchestrating checks. This provides deterministic execution, observability, and support for complex workflows.

State Machine States

+-------+     +-----------+     +-------------+
| Init  | --> | PlanReady | --> | WavePlanning|
+-------+     +-----------+     +-------------+
                                       |
                                       v
+-------------+     +---------------+  |
| Completed   | <-- | LevelDispatch | <+
+-------------+     +---------------+
      ^                    |
      |                    v
+-------+          +-------------+
| Error |          | CheckRunning|
+-------+          +-------------+
                          |
                          v
                   +-------------+
                   |   Routing   |
                   +-------------+

State Descriptions

State Description
Init Initialize context, load configuration, build dependency graph
PlanReady Execution plan is ready, dependency graph built
WavePlanning Plan the next execution wave based on dependency levels
LevelDispatch Dispatch checks at the current dependency level
CheckRunning Execute dispatched checks (parallel execution)
Routing Evaluate routing rules (on_success, on_fail, on_finish)
Completed All checks completed, aggregate results
Error Fatal error occurred, cleanup and exit

Wave-Based Execution

Checks are executed in waves based on their dependency levels:

Wave 0: [check-a, check-b]     # No dependencies
           |
           v
Wave 1: [check-c]              # depends_on: [check-a]
           |
           v
Wave 2: [check-d, check-e]     # depends_on: [check-c]

Wave Planning Rules:

  1. Checks with no unmet dependencies are scheduled
  2. Checks run in parallel within a wave (respecting max_parallelism)
  3. A new wave starts when all checks in the current wave complete
  4. Routing can trigger additional waves (on_fail.goto, on_success.run)

Dependency Resolution

The DependencyResolver builds an execution graph:

interface DependencyGraph {
  levels: ExecutionGroup[];  // Checks grouped by dependency level
  checkDeps: Map<string, string[]>;  // Check -> its dependencies
  checkDependents: Map<string, string[]>;  // Check -> checks that depend on it
}

Dependency Features:

  • Linear dependencies: depends_on: [step-a]
  • OR dependencies: depends_on: "step-a | step-b" (either can satisfy)
  • Cycle detection and error reporting
  • Skip propagation (failed dependencies skip dependents)

State Management

Runtime state is tracked in RunState:

interface RunState {
  currentState: EngineState;
  wave: number;
  levelQueue: ExecutionGroup[];
  eventQueue: EngineEvent[];
  activeDispatches: Map<string, DispatchRecord>;
  completedChecks: Set<string>;
  stats: Map<string, CheckExecutionStats>;
  historyLog: EngineEvent[];
  // ... additional tracking fields
}

Output History

The ExecutionJournal stores all check outputs for cross-check access:

// In Liquid templates
{{ outputs["previous-check"].result }}
{{ outputs["data-fetch"] | json }}

// In JavaScript expressions (fail_if, transform_js)
outputs["check-name"].issues.length > 0

Event Sources

GitHub Webhook Integration

GitHub events are processed by the action entry point:

GitHub Event --> GITHUB_EVENT_PATH --> Event Parser --> Event Trigger

Event Mapping:

GitHub Event Action Visor Trigger
pull_request opened pr_opened
pull_request synchronize pr_updated
issue_comment created issue_comment
issues opened issue_opened

Slack Socket Mode

Real-time Slack integration without public webhooks:

class SlackSocketRunner {
  async start(): Promise<void> {
    // Connect to Slack Socket Mode
    // Handle app_mention and message events
    // Route to engine execution
  }
}

Event Flow:

  1. User mentions bot or sends message
  2. Slack Socket Mode delivers event
  3. Visor extracts thread context
  4. Engine executes configured checks
  5. Results posted back to thread

HTTP Input Provider

For custom webhook sources:

checks:
  webhook-handler:
    type: http_input
    endpoint: /api/webhook
    transform: |
      {% assign data = request.body | json %}
      {{ data.message }}

A2A Protocol

External agents and orchestrators send tasks via the A2A protocol:

  1. Client fetches Agent Card from GET /.well-known/agent-card.json
  2. Client sends POST /message:send with skill_id in metadata
  3. A2A Frontend authenticates the request (bearer/api_key)
  4. Task created in TaskStore (state: submitted)
  5. Skill routing maps skill_id → internal workflow name
  6. Engine executes the workflow (same engine as CLI/GitHub Action)
  7. Task updated with artifacts (state: completed)
  8. Client receives response (blocking) or polls GET /tasks/{id} (async)

Data Flow

PR Review Request Flow

1. GitHub PR Event
        |
        v
2. Load Configuration
        |
        v
3. Build Dependency Graph
        |
        v
4. Wave Planning (Level 0)
        |
        v
5. Dispatch Checks -----> [AI Provider]
        |                      |
        |                      v
        |               6. AI Analysis
        |                      |
        v                      v
7. Collect Results <----- [ReviewSummary]
        |
        v
8. Route (on_success/on_fail)
        |
        v
9. Next Wave or Complete
        |
        v
10. Aggregate Results
        |
        v
11. Post PR Comment

Output Propagation Between Checks

checks:
  fetch-data:
    type: command
    exec: "curl -s https://api.example.com/data"

  analyze:
    type: ai
    depends_on: [fetch-data]
    prompt: |
      Analyze this data:
      {{ outputs["fetch-data"] | json }}

Output Access:

  • Liquid templates: {{ outputs["check-name"] }}
  • JavaScript: outputs["check-name"]
  • Transform: transform_js: "return outputs['fetch-data'].items"

Template Rendering with Liquid

Visor uses Liquid templates extensively:

checks:
  example:
    type: ai
    prompt: |
      Review this PR:
      Title: {{ pr.title }}
      Author: {{ pr.author }}
      Files changed: {{ pr.files | size }}

      {% for file in pr.files %}
      - {{ file.filename }} (+{{ file.additions }}/-{{ file.deletions }})
      {% endfor %}

Available Context:

Variable Description
pr PR information (title, body, author, files)
outputs Previous check outputs
env Environment variables
event GitHub event context
memory Memory store accessor

Extension Points

Adding New Providers

Create a new provider by extending CheckProvider:

import { CheckProvider, CheckProviderConfig } from './check-provider.interface';

export class CustomProvider extends CheckProvider {
  getName(): string { return 'custom'; }
  getDescription(): string { return 'My custom provider'; }

  async execute(
    prInfo: PRInfo,
    config: CheckProviderConfig,
    dependencyResults?: Map<string, ReviewSummary>,
    context?: ExecutionContext
  ): Promise<ReviewSummary> {
    // Implementation
  }

  // ... other required methods
}

// Register
registry.register(new CustomProvider());

Custom Tools

Define tools in configuration for use with MCP/AI checks:

tools:
  search-docs:
    description: "Search documentation"
    inputSchema:
      type: object
      properties:
        query: { type: string }
      required: [query]
    exec: "grep -r '{{ query }}' docs/"
    transform_js: |
      return { results: output.split('\n').filter(Boolean) }

Tools are exposed to AI providers via an ephemeral MCP server.

MCP Integration

Connect to external MCP servers:

checks:
  code-analysis:
    type: ai
    ai:
      mcpServers:
        probe:
          command: "npx"
          args: ["-y", "@anthropic/probe-mcp"]
    prompt: "Analyze the codebase"

Or use the MCP provider directly:

checks:
  direct-mcp:
    type: mcp
    transport: stdio
    exec: "npx @my-org/tool"
    method: analyze
    methodArgs:
      path: "{{ pr.files[0].filename }}"

Workflow Composition

Create reusable workflows:

# workflows/security-scan.yaml
id: security-scan
inputs:
  - name: severity_threshold
    default: warning
steps:
  scan:
    type: ai
    prompt: "Scan for security issues..."
outputs:
  - name: issues
    value: "{{ outputs['scan'].issues }}"

Use in main configuration:

checks:
  security:
    type: workflow
    workflow: workflows/security-scan.yaml
    args:
      severity_threshold: error

Frontend Integration

Create event-driven integrations:

class CustomFrontend implements Frontend {
  async start(ctx: FrontendContext): Promise<void> {
    ctx.eventBus.on('CheckCompleted', async (event) => {
      // Handle check completion
    });
  }
}

Frontends receive events via the event bus:

  • CheckScheduled
  • CheckCompleted
  • CheckErrored
  • HumanInputRequested
  • StateTransition

Telemetry and Observability

Visor includes comprehensive telemetry support for debugging, monitoring, and performance analysis.

OpenTelemetry Integration

Visor supports OpenTelemetry (OTEL) for distributed tracing:

# visor.yaml
telemetry:
  enabled: true
  sink: otlp  # or 'file', 'console'
  file:
    dir: ./output/traces
    ndjson: true
  tracing:
    auto_instrumentations: true
    trace_report:
      enabled: true

Environment Variables:

Variable Description
VISOR_TELEMETRY_ENABLED Enable telemetry (true/false)
VISOR_TELEMETRY_SINK Sink type: otlp, file, console
VISOR_TRACE_DIR Directory for trace files
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT OTLP endpoint (e.g., http://localhost:4318/v1/traces)

Trace Structure

visor.run (root span)
  |
  +-- engine.state.init
  |     +-- dependency_resolver.build_graph
  |
  +-- engine.state.wave_planning
  |     +-- wave: 0, wave_kind: initial
  |
  +-- engine.state.level_dispatch
  |     +-- level_size: 3, level_checks_preview: [check-a, check-b, check-c]
  |
  +-- visor.check.check-a
  |     +-- visor.check.id: check-a
  |     +-- visor.check.type: ai
  |
  +-- visor.routing (events)
        +-- trigger: on_success, action: run, target: next-check

Debug Visualizer

Start the debug visualizer for interactive debugging:

visor --debug-server --debug-port 3456 --config .visor.yaml

The visualizer provides:

  • Real-time span streaming
  • State machine visualization
  • Execution timeline
  • Pause/resume/step controls

Error Handling

Visor implements comprehensive error handling at multiple levels.

Provider-Level Error Handling

Each provider handles errors and returns them in the ReviewSummary:

try {
  const result = await provider.execute(prInfo, config, deps, context);
  return result;
} catch (error) {
  return {
    issues: [{
      severity: 'error',
      ruleId: `${checkName}/execution-error`,
      message: error.message,
      file: '',
      line: 0
    }]
  };
}

Routing-Based Error Recovery

Checks can define error recovery strategies:

checks:
  risky-operation:
    type: command
    exec: "./risky-script.sh"
    on_fail:
      retry:
        max: 3
        backoff:
          mode: exponential
          delay_ms: 1000
          max_delay_ms: 30000
      run: [cleanup-step]
      goto: safe-fallback

  cleanup-step:
    type: command
    exec: "./cleanup.sh"

  safe-fallback:
    type: noop
    message: "Using fallback due to failure"

Fail-Fast Mode

Enable fail-fast to stop execution on first critical error:

# visor.yaml
fail_fast: true

# Or via CLI
visor --fail-fast --config .visor.yaml

Error Propagation

Errors propagate through the dependency graph:

check-a (fails)
    |
    v
check-b (depends_on: [check-a]) --> SKIPPED
    |
    v
check-c (depends_on: [check-b]) --> SKIPPED

Skipped checks are marked with severity: info and ruleId: checkName/__skipped.


Security Considerations

Workspace Isolation

For untrusted code execution, enable workspace isolation:

# visor.yaml
workspace:
  enabled: true
  base_path: /tmp/visor-workspaces
  cleanup: true  # Remove after execution

This creates isolated directories for each execution, preventing cross-contamination.

Process Sandbox Engines

Visor supports three sandbox engines for isolating command execution:

Engine Platform Isolation Model
Docker Linux, macOS, Windows Full container isolation
Bubblewrap Linux only Linux kernel namespaces (PID, mount, network)
Seatbelt macOS only macOS sandbox-exec with SBPL profiles

All three implement the SandboxInstance interface and are routed through SandboxManager. Configure via the sandboxes: block and sandbox: default in .visor.yaml.

See Sandbox Engines for complete documentation.

Environment Variable Handling

Sensitive values can be passed via environment:

checks:
  api-call:
    type: http_client
    url: "https://api.example.com"
    headers:
      Authorization: "Bearer {{ env.API_TOKEN }}"

Security practices:

  • Never commit secrets to configuration files
  • Use GitHub Secrets or environment variables
  • Visor does not log environment variable values

Command Execution Security

The command provider executes shell commands. Mitigate risks by:

  1. Using explicit paths: exec: "/usr/bin/grep"
  2. Avoiding shell expansion: Use args array instead of string interpolation
  3. Setting working directory: workingDirectory: /safe/path
  4. Limiting execution time: timeout: 30000

AI Provider Security

When using AI providers:

  • API keys are never logged or included in outputs
  • Prompts containing sensitive data should use redaction
  • Review AI outputs before automated actions (labels, comments)

HTTP Input Validation

For webhook endpoints, configure authentication:

http_server:
  enabled: true
  port: 8080
  auth:
    type: hmac
    secret_env: WEBHOOK_SECRET
    header: X-Signature-256

Memory and State Management

Memory Store

Visor provides a persistent key-value memory store:

checks:
  store-data:
    type: memory
    operation: set
    key: "session:{{ pr.number }}"
    value: "{{ outputs['fetch-data'] | json }}"

  retrieve-data:
    type: memory
    operation: get
    key: "session:{{ pr.number }}"
    depends_on: [store-data]

Memory persists across checks within a single execution but not across separate runs (unless using external persistence).

Execution Journal

The ExecutionJournal (snapshot store) maintains:

  • Output history per check
  • Scoped visibility for nested workflows
  • Snapshot points for rollback scenarios
// Read outputs visible to a check
const outputs = journal.readVisible(sessionId, snapshot, scope);

// Write check result
journal.write(sessionId, checkId, scope, result);

Session Registry

AI sessions are managed via SessionRegistry:

checks:
  first-analysis:
    type: ai
    prompt: "Initial analysis..."
    reuse_ai_session: self  # Create named session

  follow-up:
    type: ai
    prompt: "Follow-up question..."
    depends_on: [first-analysis]
    reuse_ai_session: first-analysis  # Reuse session
    session_mode: append  # or 'clone'

Performance Optimization

Parallel Execution

Control parallelism via configuration:

max_parallelism: 5  # Execute up to 5 checks concurrently

Or via CLI:

visor --max-parallelism 10 --config .visor.yaml

Dependency-Aware Scheduling

The wave-based scheduler automatically optimizes execution:

  1. Identifies independent checks (no dependencies)
  2. Schedules them in parallel
  3. Waits for wave completion before next level
  4. Respects max_parallelism limits

Caching Considerations

Currently, Visor does not cache AI responses between runs. For expensive operations:

  1. Use memory provider to store intermediate results
  2. Implement if conditions to skip unnecessary checks
  3. Consider external caching for HTTP responses

Related Documentation


Appendix A: File Structure

src/
  index.ts                    # GitHub Action entry point
  cli-main.ts                 # CLI entry point
  config.ts                   # Configuration loading
  state-machine-execution-engine.ts  # Main orchestration

  types/
    config.ts                 # Configuration types
    engine.ts                 # Engine state types
    execution.ts              # Execution result types
    cli.ts                    # CLI option types

  state-machine/
    runner.ts                 # State machine runner
    states/
      init.ts                 # Init state handler
      plan-ready.ts           # PlanReady state handler
      wave-planning.ts        # WavePlanning state handler
      level-dispatch.ts       # LevelDispatch state handler
      check-running.ts        # CheckRunning state handler
      routing.ts              # Routing logic
      completed.ts            # Completed state handler
      error.ts                # Error state handler

  providers/
    check-provider.interface.ts  # Provider base class
    check-provider-registry.ts   # Provider registry
    a2a-check-provider.ts        # A2A agent client provider
    ai-check-provider.ts         # AI provider (Gemini, Claude, OpenAI)
    claude-code-check-provider.ts # Claude Code SDK provider
    command-check-provider.ts    # Shell command provider
    script-check-provider.ts     # JavaScript sandbox provider
    http-check-provider.ts       # HTTP output provider
    http-input-provider.ts       # HTTP webhook input provider
    http-client-provider.ts      # HTTP client provider
    mcp-check-provider.ts        # MCP tool provider
    utcp-check-provider.ts       # UTCP tool provider
    mcp-tools.ts                 # MCP server management
    memory-check-provider.ts     # Key-value memory provider
    log-check-provider.ts        # Debug logging provider
    noop-check-provider.ts       # No-op orchestration provider
    human-input-check-provider.ts # Interactive input provider
    workflow-check-provider.ts   # Nested workflow provider
    git-checkout-provider.ts     # Git reference checkout provider
    github-ops-provider.ts       # GitHub API operations provider
    custom-tool-executor.ts      # Custom tool execution

  event-bus/
    event-bus.ts              # Event bus for frontends
    types.ts                  # Event envelope types

  agent-protocol/
    a2a-frontend.ts             # A2A HTTP server and endpoints
    types.ts                    # Protocol-agnostic types and error classes
    state-transitions.ts        # Task state machine validation
    task-store.ts               # SQLite-backed task persistence
    task-queue.ts               # Async task execution queue
    task-stream-manager.ts      # SSE streaming support
    push-notification-manager.ts # Push notification delivery
    tasks-cli-handler.ts        # CLI: visor tasks
    index.ts                    # Re-exports

  frontends/
    host.ts                   # Frontend manager
    github/                   # GitHub integration frontend
    slack/                    # Slack integration frontend

  sandbox/
    types.ts                  # SandboxInstance interface, SandboxConfig
    sandbox-manager.ts        # Lifecycle management, engine routing
    docker-image-sandbox.ts   # Docker image-based sandbox
    docker-compose-sandbox.ts # Docker Compose sandbox
    bubblewrap-sandbox.ts     # Linux namespace isolation (bwrap)
    seatbelt-sandbox.ts       # macOS sandbox-exec isolation
    check-runner.ts           # Check execution in sandboxes
    env-filter.ts             # Environment variable filtering
    cache-volume-manager.ts   # Docker cache volumes
    sandbox-telemetry.ts      # Telemetry for sandbox operations

  utils/
    config-loader.ts          # Remote config loading
    config-merger.ts          # Configuration merging
    workspace-manager.ts      # Workspace isolation
    sandbox.ts                # JavaScript sandbox
    liquid-helpers.ts         # Liquid template helpers

  telemetry/
    opentelemetry.ts          # OTEL initialization
    trace-helpers.ts          # Span creation helpers
    fallback-ndjson.ts        # File-based trace export

  debug-visualizer/
    ws-server.ts              # WebSocket server for debug UI

Appendix B: Configuration Schema Reference

The complete configuration schema is defined in src/types/config.ts. Key types:

interface VisorConfig {
  version: string;
  checks: Record<string, CheckConfig>;
  steps?: Record<string, CheckConfig>;  // Alias for checks

  // Global settings
  max_parallelism?: number;
  fail_fast?: boolean;
  fail_if?: string;

  // AI configuration
  ai_provider?: string;
  ai_model?: string;
  ai_mcp_servers?: Record<string, McpServerConfig>;

  // Output configuration
  output?: OutputConfig;

  // HTTP server
  http_server?: HttpServerConfig;

  // Agent Protocol (A2A)
  agent_protocol?: AgentProtocolConfig;

  // Workflow imports
  imports?: string[];

  // Frontends (GitHub, Slack)
  frontends?: FrontendConfig[];

  // Telemetry
  telemetry?: TelemetryConfig;
}

interface CheckConfig {
  type: ConfigCheckType;
  prompt?: string;
  exec?: string;
  url?: string;

  // Dependencies and routing
  depends_on?: string[];
  on_success?: RoutingConfig;
  on_fail?: RoutingConfig;
  on_finish?: RoutingConfig;

  // Execution control
  if?: string;
  forEach?: string | boolean;
  timeout?: number;

  // Output handling
  schema?: string | object;
  transform_js?: string;
  fail_if?: string;

  // Metadata
  tags?: string[];
  group?: string;
  on?: EventTrigger[];
}

Appendix C: Event Types

Events that flow through the state machine:

Event Description Fields
PlanBuilt Dependency graph constructed graph
WaveRequested New wave requested wave
LevelReady Execution level ready level, wave
LevelDepleted All checks in level complete level, wave
CheckScheduled Check dispatched for execution checkId, scope
CheckCompleted Check finished successfully checkId, scope, result
CheckErrored Check failed with error checkId, scope, error
ForwardRunRequested Routing triggered new check target, gotoEvent, scope, origin
WaveRetry Wave needs re-execution reason
StateTransition State machine transitioned from, to
Shutdown Engine shutting down error?

Appendix D: Glossary

Term Definition
Check A single unit of work (AI analysis, command, HTTP call, etc.)
Step Alias for check (used interchangeably)
Wave A batch of checks executed in parallel
Level Dependency level in the execution graph
Provider Implementation of a check type
Journal Execution history and output storage
Scope Hierarchical path for nested workflow execution
Frontend Integration point (GitHub, Slack)
Routing Control flow based on check results
A2A Agent-to-Agent protocol — Google's open standard for agent interoperability
Agent Card JSON metadata describing an agent's capabilities, skills, and endpoints
Task A unit of work in the A2A protocol with lifecycle state (submitted → working → completed)
Skill A named capability advertised in an Agent Card, mapped to a Visor workflow
Skill Routing Configuration mapping A2A skill IDs to internal workflow names
Task Store SQLite-backed persistence layer for A2A tasks
Task Queue Async execution queue for processing A2A tasks with concurrency control
MCP Model Context Protocol - standard for AI tool integration
UTCP Universal Tool Calling Protocol - client-side protocol for calling tools directly via native protocols