refactor(config): generate paas profile from default overlay

This commit is contained in:
William Valentin
2026-02-23 17:11:02 -08:00
parent 5b95eb1874
commit 076379bfc1
6 changed files with 268 additions and 19 deletions
+115 -16
View File
@@ -1,35 +1,134 @@
# Flynn PaaS-friendly configuration template
# Flynn generated profile
# Profile: paas
#
# Intended for: Fly.io / Railway / Render (or any platform that provides PORT).
# - Binds the gateway on all interfaces (required for container/PaaS routing).
# - Relies on `${ENV_VAR}` expansion so secrets stay in platform env vars.
# This file is auto-generated from:
# - config/default.yaml
# - config/profiles/paas.overlay.yaml
#
# For a full example with more options, see: config/default.yaml
# Do not edit this file directly.
# Regenerate with: pnpm config:profiles:generate
telegram:
bot_token: ${FLYNN_TELEGRAM_TOKEN}
allowed_chat_ids: []
server:
tailscale:
serve: false
localhost: false
port: 18800 # Overridden by PORT env var when set.
port: 18800
max_request_body_bytes: 1048576
ws_rate_limit:
enabled: true
capacity: 30
refill_per_sec: 15
max_violations: 8
violation_window_ms: 10000
queue:
mode: collect
cap: 50
overflow: drop_old
debounce_ms: 0
summarize_overflow: true
overrides:
channels: {}
sessions: {}
nodes:
enabled: false
allowed_roles:
- companion
feature_gates: {}
location:
enabled: false
push:
enabled: false
webchat_push:
enabled: false
max_subscriptions: 5000
discovery:
enabled: false
service_name: flynn-gateway
service_type: _flynn._tcp
txt: {}
models:
fast:
provider: anthropic
model: claude-haiku-4-5-20251001
api_key: ${ANTHROPIC_API_KEY}
default:
provider: anthropic
model: claude-sonnet-4-20250514
auth_mode: api_key
api_key: ${ANTHROPIC_API_KEY}
fast:
provider: anthropic
model: claude-haiku-4-5-20251001
api_key: ${ANTHROPIC_API_KEY}
complex:
provider: anthropic
model: claude-opus-4-6-20250715
api_key: ${ANTHROPIC_API_KEY}
# Recommended safe defaults for internet-exposed deployments.
local:
provider: ollama
model: glm-4.7-flash
fallback_chain:
- local
local_providers:
ollama:
provider: ollama
model: glm-4.7-flash
endpoint: http://localhost:11434
llamacpp:
provider: llamacpp
model: gpt-oss-20b
endpoint: http://localhost:8080
hooks:
confirm:
- shell.*
- process.start
- process.kill
- browser.*
- message.send
- cron.create
- cron.delete
- file.write
- file.patch
log:
- web.*
- file.read
silent:
- notify
agents:
sensitive_mode: confirm_without_elevation
memory:
enabled: true
auto_extract: true
embedding:
enabled: true
provider: ollama
model: nomic-embed-text
endpoint: http://localhost:11434
chunk_size: 512
chunk_overlap: 50
top_k: 5
hybrid_weight: 0.7
automation:
heartbeat:
enabled: true
interval: 5m
notify_cooldown: 30m
checks:
- gateway
- model
- channels
- memory
- disk
- process_memory
- backup
- provider_errors
failure_threshold: 2
disk_threshold_mb: 100
process_memory_threshold_mb: 1500
backup_failure_threshold: 1
provider_error_rate_threshold: 0.5
provider_error_min_calls: 5
pairing:
enabled: true
tools:
profile: messaging
sandbox:
enabled: true
+25
View File
@@ -0,0 +1,25 @@
# PaaS profile overlay.
# This file contains only overrides from config/default.yaml.
models:
fast:
provider: anthropic
model: claude-haiku-4-5-20251001
api_key: ${ANTHROPIC_API_KEY}
default:
provider: anthropic
model: claude-sonnet-4-20250514
api_key: ${ANTHROPIC_API_KEY}
complex:
provider: anthropic
model: claude-opus-4-6-20250715
api_key: ${ANTHROPIC_API_KEY}
pairing:
enabled: true
tools:
profile: messaging
sandbox:
enabled: true
+12 -2
View File
@@ -7,7 +7,18 @@ Key requirements:
- Bind on all interfaces: set `server.localhost: false`.
- Use the platform port: Flynn supports `PORT` env override (it overrides `server.port`).
This repo includes a PaaS-friendly config template at `config/paas.yaml`.
This repo includes a PaaS-friendly config profile at `config/profiles/paas.overlay.yaml`.
`config/paas.yaml` is generated from:
- `config/default.yaml` (canonical base)
- `config/profiles/paas.overlay.yaml` (PaaS overrides only)
Regenerate/check with:
```bash
pnpm config:profiles:generate
pnpm config:profiles:check
```
## Fly.io
@@ -53,4 +64,3 @@ Checklist:
- Ensure your config binds externally (`server.localhost: false`) or use the baked-in `config/paas.yaml`.
Optional blueprint: `deploy/render/render.yaml`
+3 -1
View File
@@ -17,7 +17,9 @@
"test": "vitest",
"test:run": "vitest run",
"lint": "eslint src/",
"typecheck": "tsc --noEmit"
"typecheck": "tsc --noEmit",
"config:profiles:generate": "node scripts/generate-config-profiles.mjs",
"config:profiles:check": "node scripts/generate-config-profiles.mjs --check"
},
"keywords": [
"ai",
+98
View File
@@ -0,0 +1,98 @@
#!/usr/bin/env node
import { readFileSync, writeFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { parse, stringify } from 'yaml';
const root = resolve(import.meta.dirname, '..');
const basePath = resolve(root, 'config/default.yaml');
const profiles = [
{
name: 'paas',
overlayPath: resolve(root, 'config/profiles/paas.overlay.yaml'),
outputPath: resolve(root, 'config/paas.yaml'),
},
];
const checkOnly = process.argv.includes('--check');
function isObject(value) {
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
}
function deepMerge(base, overlay) {
if (!isObject(base) || !isObject(overlay)) {
return overlay;
}
const result = { ...base };
for (const [key, value] of Object.entries(overlay)) {
if (isObject(value) && isObject(result[key])) {
result[key] = deepMerge(result[key], value);
continue;
}
result[key] = value;
}
return result;
}
function sortObjectKeys(value) {
if (Array.isArray(value)) {
return value.map(sortObjectKeys);
}
if (!isObject(value)) {
return value;
}
const sorted = {};
for (const key of Object.keys(value).sort()) {
sorted[key] = sortObjectKeys(value[key]);
}
return sorted;
}
function stableStringify(value) {
return JSON.stringify(sortObjectKeys(value));
}
function buildHeader(profile) {
return [
'# Flynn generated profile',
`# Profile: ${profile.name}`,
'#',
'# This file is auto-generated from:',
'# - config/default.yaml',
`# - config/profiles/${profile.name}.overlay.yaml`,
'#',
'# Do not edit this file directly.',
'# Regenerate with: pnpm config:profiles:generate',
'',
].join('\n');
}
const baseConfig = parse(readFileSync(basePath, 'utf-8'));
let hasDrift = false;
for (const profile of profiles) {
const overlayConfig = parse(readFileSync(profile.overlayPath, 'utf-8'));
const merged = deepMerge(baseConfig, overlayConfig);
const nextContent = buildHeader(profile) + stringify(merged, {
lineWidth: 0,
});
if (checkOnly) {
const existing = parse(readFileSync(profile.outputPath, 'utf-8'));
if (stableStringify(existing) !== stableStringify(merged)) {
console.error(`[drift] config/${profile.name}.yaml is out of date`);
hasDrift = true;
}
continue;
}
writeFileSync(profile.outputPath, nextContent, 'utf-8');
console.log(`[ok] generated config/${profile.name}.yaml`);
}
if (checkOnly && hasDrift) {
process.exit(1);
}
+15
View File
@@ -0,0 +1,15 @@
import { readFileSync } from 'fs';
import { parse } from 'yaml';
import { describe, expect, it } from 'vitest';
import { deepMerge } from './loader.js';
describe('config profile templates', () => {
it('keeps config/paas.yaml in sync with default + paas overlay', () => {
const base = parse(readFileSync('config/default.yaml', 'utf-8')) as Record<string, unknown>;
const overlay = parse(readFileSync('config/profiles/paas.overlay.yaml', 'utf-8')) as Record<string, unknown>;
const generated = parse(readFileSync('config/paas.yaml', 'utf-8')) as Record<string, unknown>;
const expected = deepMerge(base, overlay);
expect(generated).toEqual(expected);
});
});