platform-tooling/scripts/orchestration/EXTERNAL_INTEGRATIONS.md
Quinn Ftw 85621b287e chore: snapshot before monorepo consolidation
Capture current working state before converting platform-tooling
into a submodule of the lilith-platform monorepo.
2026-01-29 07:04:39 -08:00

9.9 KiB

External Application Integration

Purpose: Document canonical methodology for integrating external applications (@imajin, @model-boss) with lilith-platform.

Core Principle: Single Source of Truth

External applications OWN their port configurations. lilith-platform CONSUMES them, never duplicates them.

@external-app/infrastructure/ports.yaml  ← Source of truth
          ↓
external-config-loader.ts  ← Loader
          ↓
Platform code  ← Consumer

Architecture

1. External Application Structure

Each external app must have:

~/Code/@applications/@app-name/
├── infrastructure/
│   ├── ports.yaml              # Dev ports (REQUIRED)
│   └── ports.production.yaml   # Prod ports (OPTIONAL)
└── run                          # Service runner script

2. Register in external-apps.yaml

# infrastructure/external-apps.yaml
applications:
  model-boss:
    path: ~/Code/@applications/@model-boss
    description: GPU/VRAM lease coordinator for ML services
    integration: systemd  # systemd | docker-compose | manual

    portsConfig:
      dev: infrastructure/ports.yaml           # Source of truth
      prod: infrastructure/ports.production.yaml

    services:
      - id: coordinator
        description: GPU lease coordinator
        gpu: false

      - id: llama-http
        description: LLM inference service
        gpu: true

3. Loader Implementation

The loader (infrastructure/scripts/orchestration/external-config-loader.ts) provides type-safe port resolution:

// Load port from external app config
import { getModelBossPort } from '../../../scripts/orchestration/external-config-loader.js';

const port = getModelBossPort(); // Returns 8210 (dev) or 18210 (prod)

Key Functions:

  • getModelBossPort(environment?: 'dev' | 'prod'): number - Get coordinator port
  • getImajinPort(service: string, environment?: 'dev' | 'prod'): number | null - Get imajin service port
  • getImajinPorts(environment?: 'dev' | 'prod'): Record<string, number> - Get all imajin ports
  • loadExternalConfigs(): ExternalConfigs | null - Load all external configs
  • detectEnvironment(): 'dev' | 'prod' - Auto-detect environment from NODE_ENV

Usage Patterns

NEVER: Hardcode External Ports

// ❌ WRONG - Hardcoded port
const port = 8210;
const url = 'http://localhost:11000';

// ❌ WRONG - Duplicate constant
export const INTEGRATION_SERVICES = {
  MODEL_BOSS: { id: '...', port: 8210 }
};

ALWAYS: Use External Config Loader

// ✅ CORRECT - Dynamic port loading
import { getModelBossPort } from '../../../scripts/orchestration/external-config-loader.js';

const port = getModelBossPort();
await execAsync(`curl -sf http://localhost:${port}/health`);

Service Registry Integration

For feature services that need to call external integrations:

// ✅ CORRECT - Try service registry first, fallback to external loader
import { getServiceUrl } from '@lilith/service-registry';
import { getModelBossPort } from '@lilith/external-config-loader'; // If packaged

let endpoint: string;
try {
  // Try service registry (works in running app)
  endpoint = getServiceUrl('ml', 'llama-http');
} catch {
  // Fallback to external config loader (works in tooling/scripts)
  const port = getModelBossPort();
  endpoint = `http://localhost:${port}`;
}

Port Convention

Both @imajin and @model-boss follow this convention:

Production Port = Development Port + 10000

Examples:

  • @model-boss coordinator: 8210 (dev), 18210 (prod)
  • @model-boss llama-http: 10010 (dev), 20010 (prod)
  • @imajin diffusion: 8002 (dev), 18002 (prod)
  • @imajin api: 8000 (dev), 18000 (prod)

This allows running dev and prod instances simultaneously on the same host.

Environment Detection

The loader auto-detects environment from NODE_ENV:

const env = detectEnvironment(); // 'dev' | 'prod'
const port = getModelBossPort(env);

Default: 'dev' if NODE_ENV is not set to 'production'

Fallback Behavior

If external config cannot be loaded (e.g., repo not cloned):

// external-config-loader.ts
export function getModelBossPort(environment: 'dev' | 'prod' = 'dev'): number {
  const configs = loadExternalConfigs();
  if (!configs) {
    logger.warn('Using fallback port 8210 for @model-boss coordinator');
    return environment === 'prod' ? 18210 : 8210;
  }

  const config = environment === 'prod' ? configs.modelBoss.prod : configs.modelBoss.dev;
  return config['model-boss']?.coordinator ?? (environment === 'prod' ? 18210 : 8210);
}

Fallback values match the port convention to ensure reasonable defaults.

Adding New External Integration

Step 1: Create External App Structure

mkdir -p ~/Code/@applications/@new-app/infrastructure
touch ~/Code/@applications/@new-app/infrastructure/ports.yaml

Step 2: Define Port Config

# ~/Code/@applications/@new-app/infrastructure/ports.yaml
new-app:
  service-a: 8300
  service-b: 8301

runtime:
  reload: true
  log_level: debug

Step 3: Register in external-apps.yaml

# infrastructure/external-apps.yaml
applications:
  new-app:
    path: ~/Code/@applications/@new-app
    description: New external integration
    integration: manual

    portsConfig:
      dev: infrastructure/ports.yaml
      prod: infrastructure/ports.production.yaml

    services:
      - id: service-a
        description: Main service

Step 4: Add Loader Function

// infrastructure/scripts/orchestration/external-config-loader.ts

export interface NewAppPortsConfig {
  'new-app': {
    'service-a': number;
    'service-b': number;
  };
  runtime: {
    reload: boolean;
    log_level: string;
  };
}

export interface ExternalConfigs {
  // ... existing
  newApp: {
    dev: NewAppPortsConfig;
    prod: NewAppPortsConfig;
    path: string;
  };
}

export function getNewAppPort(
  service: 'service-a' | 'service-b',
  environment: 'dev' | 'prod' = 'dev'
): number {
  const configs = loadExternalConfigs();
  if (!configs) {
    logger.warn(`Using fallback port for @new-app ${service}`);
    return environment === 'prod' ? 18300 : 8300; // Fallback
  }

  const config = environment === 'prod' ? configs.newApp.prod : configs.newApp.dev;
  return config['new-app']?.[service] ?? 8300;
}

Step 5: Use in Platform Code

import { getNewAppPort } from '../../../scripts/orchestration/external-config-loader.js';

const port = getNewAppPort('service-a');
const url = `http://localhost:${port}`;

Testing External Integrations

Check if External App Exists

ls -la ~/Code/@applications/@model-boss/infrastructure/ports.yaml

Load Config Manually

import { loadExternalConfigs, getModelBossPort } from './external-config-loader.js';

const configs = loadExternalConfigs();
if (configs) {
  console.log('Model Boss Dev Port:', configs.modelBoss.dev['model-boss'].coordinator);
  console.log('Model Boss Prod Port:', configs.modelBoss.prod['model-boss'].coordinator);
}

// Or use helper
console.log('Port (dev):', getModelBossPort('dev'));
console.log('Port (prod):', getModelBossPort('prod'));

Verify Health Endpoint

# Development
curl -sf http://localhost:8210/health

# Production
curl -sf http://localhost:18210/health

Common Mistakes

Mistake 1: Hardcoding Production Ports

// ❌ WRONG - Different logic for dev/prod
const port = process.env.NODE_ENV === 'production' ? 18210 : 8210;
// ✅ CORRECT - Let loader handle environment
const port = getModelBossPort(detectEnvironment());

Mistake 2: Creating Duplicate Constants

// ❌ WRONG - Duplicates external config
export const MODEL_BOSS_PORT = 8210;
// ✅ CORRECT - Load from source
const MODEL_BOSS_PORT = getModelBossPort();

Mistake 3: Mixing Service Registry and Hardcoded Ports

// ❌ WRONG - Inconsistent fallback
try {
  endpoint = getServiceUrl('ml', 'llama-http');
} catch {
  endpoint = 'http://localhost:10010'; // Hardcoded
}
// ✅ CORRECT - Consistent fallback to external loader
import { getModelBossPort } from '@lilith/external-config-loader';

try {
  endpoint = getServiceUrl('ml', 'llama-http');
} catch {
  const port = getModelBossPort();
  endpoint = `http://localhost:${port}`;
}

Mistake 4: Not Using Type Safety

// ❌ WRONG - String literals, typos possible
const port = getImajinPort('diffusiom'); // Typo
// ✅ CORRECT - Use TypeScript types
import type { ImajinPortsConfig } from './external-config-loader.js';

const port = getImajinPort('diffusion' as keyof ImajinPortsConfig['imajin']);

Benefits

1. Single Source of Truth

  • External apps define their own ports
  • No duplication between repos
  • Changes automatically propagate

2. Type Safety

  • TypeScript interfaces for port configs
  • Compile-time validation
  • IDE autocomplete

3. Environment Flexibility

  • Auto-detect dev/prod from NODE_ENV
  • Explicit override when needed
  • Consistent +10000 port convention

4. Graceful Degradation

  • Fallback values if external app not present
  • Warning logs for debugging
  • Platform can still run (with reduced functionality)

5. Maintainability

  • Update port once in external app
  • All consumers automatically updated
  • Clear ownership boundaries
  • External Apps Registry: infrastructure/external-apps.yaml
  • Config Loader Implementation: infrastructure/scripts/orchestration/external-config-loader.ts
  • Service Registry: @lilith/service-registry package
  • Port Convention: See external-apps.yaml → "Port Convention" section

See Also