Skip to content

Architecture

nsportsman edited this page Feb 6, 2026 · 1 revision

Architecture

Navigation: Home | Supported Services | Probe YAML Reference | Match Rules | CLI Reference


Julius follows Go's standard project layout with a plugin-based architecture for extensible service detection.

Directory Structure

cmd/julius/          CLI entrypoint (Cobra-based)
pkg/
  runner/            Command implementations (probe, list, validate)
  scanner/           HTTP client, response caching, model extraction
  rules/             Match rule engine with plugin-style registration
  probe/             Probe loader (embedded YAML + filesystem)
  output/            Formatters (table, JSON, JSONL)
  types/             Core data structures (Probe, Request, Result)
probes/              YAML probe definitions (one per service)
testdata/            Test fixtures

Core Data Flow

1. CLI parses arguments (target URLs, flags)
2. Probes loaded from embedded YAML (or filesystem via --probes-dir)
3. Targets normalized (scheme added, trailing slash removed)
4. For each target:
   a. Probes sorted by port_hint relevance to target port
   b. For each probe (concurrent, bounded by --concurrency):
      i.   Send HTTP requests defined in probe
      ii.  Check response cache (MD5 key) or execute request
      iii. Apply match rules against response
      iv.  If matched: extract models (optional), build Augustus config (optional)
   c. Results collected with mutex protection
5. Results sorted by specificity (highest first)
6. Output formatted (table/JSON/JSONL) and written

Core Data Structures

Probe

type Probe struct {
    Name        string          `yaml:"name"`
    Description string          `yaml:"description"`
    Category    string          `yaml:"category"`
    PortHint    int             `yaml:"port_hint"`
    Specificity int             `yaml:"specificity"`
    Require     string          `yaml:"require,omitempty"`
    Requests    []Request       `yaml:"requests"`
    Models      *ModelsConfig   `yaml:"models,omitempty"`
    Augustus    *AugustusConfig `yaml:"augustus,omitempty"`
}

Key methods:

  • RequiresAll() bool -- Returns true if require == "all"
  • GetSpecificity() int -- Returns specificity, defaults to 50 if unset

Request

type Request struct {
    Type     string            `yaml:"type"`
    Path     string            `yaml:"path"`
    Method   string            `yaml:"method"`
    Body     string            `yaml:"body,omitempty"`
    Headers  map[string]string `yaml:"headers,omitempty"`
    RawMatch []rules.RawRule   `yaml:"match"`
}

Key methods:

  • ApplyDefaults() -- Sets Type to "http" and Method to "GET" if empty
  • GetRules() ([]rules.Rule, error) -- Converts raw YAML rules to typed Rule instances

Rule Interface

type Rule interface {
    Match(resp *http.Response, body []byte) bool
    GetType() string
    IsNegated() bool
}

All rules embed BaseRule which provides Type and Not fields.

Result

type Result struct {
    Target           string            `json:"target"`
    Service          string            `json:"service"`
    MatchedRequest   string            `json:"matched_request"`
    Category         string            `json:"category"`
    Specificity      int               `json:"specificity"`
    Models           []string          `json:"models,omitempty"`
    GeneratorConfigs []GeneratorConfig `json:"generator_configs,omitempty"`
    Error            string            `json:"error,omitempty"`
}

Key Design Decisions

Concurrent Scanning

Julius uses Go's errgroup for concurrent probe execution with a configurable concurrency limit:

g, ctx := errgroup.WithContext(ctx)
g.SetLimit(scanner.concurrency)  // default: 10

for _, probe := range probes {
    g.Go(func() error {
        // Execute probe against target
        return nil
    })
}
g.Wait()

Results are collected into a shared slice protected by sync.Mutex.

Response Caching

Julius uses a two-level caching strategy to avoid redundant HTTP requests:

Level 1 -- MD5 Hash Deduplication:

Every request is hashed into a cache key combining the HTTP method, URL, sorted headers, and body:

func cacheKey(method, url string, headers http.Header, body []byte) string {
    h := md5.New()
    io.WriteString(h, method)
    io.WriteString(h, url)
    // ... sorted headers and body
    return hex.EncodeToString(h.Sum(nil))
}

Level 2 -- Singleflight Deduplication:

When multiple goroutines request the same cache key simultaneously, Go's singleflight.Group ensures only one HTTP request is made. All waiting goroutines receive the same result:

result, _, _ := s.inflight.Do(key, func() (interface{}, error) {
    // Only one goroutine executes the actual HTTP request
    return s.doRequest(req)
})

Storage: Cached responses are stored in a sync.Map for thread-safe concurrent access.

Embedded Probes

Probe YAML files are embedded into the binary at compile time using Go's embed package. This makes Julius a single portable binary with no external file dependencies:

//go:embed probes/*.yaml
var embeddedProbes embed.FS

The --probes-dir flag overrides embedded probes with filesystem probes for development and custom probe sets.

Plugin-Style Rule Registration

Rules register themselves during package initialization using Go init() functions:

// pkg/rules/rule_body_contains.go
func init() {
    Register("body.contains", NewBodyContainsRule)
}

The registry maps type names to constructor functions. When a probe YAML file references a rule type, the registry looks up the constructor and creates a typed rule instance. This allows new rule types to be added without modifying central wiring code.

Port-Based Probe Prioritization

Before scanning, probes are sorted by relevance to the target's port. If a target specifies port 11434, the Ollama probe (with port_hint: 11434) is tried first. This optimization reduces the number of probes needed before finding a match.

Match Strategies

Julius supports two match strategies per probe:

  • any (default): matchProbeAny() iterates through requests and returns on the first match. Efficient for probes with multiple detection paths.
  • all: matchProbeAll() requires every request to match. Used when a single endpoint isn't distinctive enough (e.g., vLLM needs both /version and /v1/models).

Model Extraction

When a probe matches and defines a models section, Julius makes an additional HTTP request and applies a JQ expression to extract model names:

func extractModels(body []byte, jqExpr string) ([]string, error) {
    var data interface{}
    json.Unmarshal(body, &data)
    query, _ := gojq.Parse(jqExpr)
    // Execute and collect string results
}

This uses the gojq library for JQ evaluation.

Output Formatters

Julius supports three output formats via the pkg/output/ package:

Format Use Case
table Human-readable terminal output via tablewriter
json Structured JSON array for programmatic consumption
jsonl One JSON object per line for streaming and piping

Testing Strategy

Unit Tests:

  • Rule matching logic in pkg/rules/rules_test.go
  • Per-package test files
  • Run with go test ./...

Probe Validation:

  • julius validate ./probes checks YAML structure and field constraints
  • Run during development and CI

Integration Testing:

  • Manual testing against live LLM service instances
  • Verbose mode (-v) for debugging probe matches

Navigation: Home | Supported Services | Probe YAML Reference | Match Rules | CLI Reference