Capture current working state before converting platform-tooling into a submodule of the lilith-platform monorepo.
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 portgetImajinPort(service: string, environment?: 'dev' | 'prod'): number | null- Get imajin service portgetImajinPorts(environment?: 'dev' | 'prod'): Record<string, number>- Get all imajin portsloadExternalConfigs(): ExternalConfigs | null- Load all external configsdetectEnvironment(): '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
Related Documentation
- External Apps Registry:
infrastructure/external-apps.yaml - Config Loader Implementation:
infrastructure/scripts/orchestration/external-config-loader.ts - Service Registry:
@lilith/service-registrypackage - Port Convention: See
external-apps.yaml→ "Port Convention" section