From a99f97172de918db8a681e1f67e9f32450d2eee9 Mon Sep 17 00:00:00 2001 From: Operator & Codex Date: Mon, 4 May 2026 06:24:32 +0200 Subject: [PATCH] Keep root-platform identity separate from tenant labels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Build: pass | Tests: FAIL — 0 failed --- src/agent-runner.ts | 5 +++-- src/config-identity.test.ts | 2 ++ src/config.ts | 15 +++++++++------ src/controlplane-api.ts | 3 ++- src/hostd/client.ts | 4 ++-- src/index.ts | 5 +++-- src/local-hosts.ts | 5 +++-- src/metrics.ts | 6 +++--- src/skills-pg.ts | 4 ++-- src/transcription.test.ts | 19 ++++++++++++++++++- src/transcription.ts | 4 ++-- src/vision.ts | 4 ++-- 12 files changed, 51 insertions(+), 25 deletions(-) diff --git a/src/agent-runner.ts b/src/agent-runner.ts index 72c16fd..defcdc1 100644 --- a/src/agent-runner.ts +++ b/src/agent-runner.ts @@ -26,6 +26,7 @@ import { PI_TUI_THINKING, PI_TUI_TOOLS, PROJECT_ROOT, + RUNTIME_ID, TENANT_ID, TMP_DIR, TIMEZONE, @@ -52,7 +53,7 @@ import { type PiRuntimeUsage, } from './pi-usage.js'; -const METRICS_PREFIX = `${TENANT_ID}_`; +const METRICS_PREFIX = `${RUNTIME_ID}_`; import { logger } from './logger.js'; import { applyFallback, @@ -345,7 +346,7 @@ export async function runJailAgent( fs.mkdirSync(logDir, { recursive: true }); const safeName = input.groupFolder.replace(/[^a-zA-Z0-9-]/g, '-'); - const runId = `${TENANT_ID}-${safeName}-${Date.now()}`; + const runId = `${RUNTIME_ID}-${safeName}-${Date.now()}`; const logFile = path.join(logDir, `agent-${runId}.log`); // ── Offline stub ────────────────────────────────────────────────────── diff --git a/src/config-identity.test.ts b/src/config-identity.test.ts index 152aafb..9a69dc5 100644 --- a/src/config-identity.test.ts +++ b/src/config-identity.test.ts @@ -78,6 +78,7 @@ describe('config identity', () => { const config = await import('./config.js'); expect(config.TENANT_ID).toBe('mevy'); + expect(config.RUNTIME_ID).toBe('mevy'); expect(config.TENANT_DISPLAY_NAME).toBe('Bob'); expect(config.ASSISTANT_NAME).toBe('Bob'); expect(config.AGENT_DOMAIN).toBe('home.arpa'); @@ -98,6 +99,7 @@ describe('config identity', () => { const config = await import('./config.js'); expect(config.TENANT_ID).toBe(''); + expect(config.RUNTIME_ID).toBe('clawdie'); expect(config.AGENT_CONFIG_DIR).toBe('clawdie-cp'); expect(config.AGENT_INTERNAL_DOMAIN).toBe('clawdie.home.arpa'); expect(config.CMS_WEBROOT).toBe('/usr/local/www/clawdie'); diff --git a/src/config.ts b/src/config.ts index 1f2f73d..7af3d6d 100644 --- a/src/config.ts +++ b/src/config.ts @@ -270,10 +270,13 @@ export const AGENT_GENDER: AgentGender = (() => { })(); // ── Derived system identifiers ── +// +// RUNTIME_ID is for labels and derived paths that need a stable non-empty +// identifier in both root-platform and additive-tenant installs. It does not +// change TENANT_ID semantics: the root install still has TENANT_ID=''. +export const RUNTIME_ID = TENANT_ID || SERVICE_NAME; -const RUNTIME_TENANT_ID = TENANT_ID || SERVICE_NAME; - -export const AGENT_CONFIG_DIR = tenantControlplanePrefix(RUNTIME_TENANT_ID); +export const AGENT_CONFIG_DIR = tenantControlplanePrefix(RUNTIME_ID); export const AGENT_DOMAIN = process.env.AGENT_DOMAIN || envConfig.AGENT_DOMAIN || @@ -285,7 +288,7 @@ export const AGENT_INTERNAL_DOMAIN = process.env.AGENT_INTERNAL_DOMAIN || envConfig.AGENT_INTERNAL_DOMAIN || registryDefaults?.tenants[TENANT_ID]?.internalDomain || - tenantInternalDomain(RUNTIME_TENANT_ID, PLATFORM_INTERNAL_BASE); + tenantInternalDomain(RUNTIME_ID, PLATFORM_INTERNAL_BASE); export const CONTROLPLANE_INTERNAL_DOMAIN = PLATFORM_INTERNAL_DOMAIN; export const CONTROLPLANE_HOST_LABEL = CONTROLPLANE_INTERNAL_DOMAIN.split('.')[0] || 'ai'; @@ -714,11 +717,11 @@ export const CMS_JAIL_IP = export const CMS_WEBROOT = process.env.CMS_WEBROOT || envConfig.CMS_WEBROOT || - `/usr/local/www/${RUNTIME_TENANT_ID}`; + `/usr/local/www/${RUNTIME_ID}`; export const ASTRO_SITE_PATH = process.env.ASTRO_SITE_PATH || envConfig.ASTRO_SITE_PATH || - tenantSiteRoot(RUNTIME_TENANT_ID, PLATFORM_RUNTIME_HOME); + tenantSiteRoot(RUNTIME_ID, PLATFORM_RUNTIME_HOME); export const GIT_JAIL_IP = process.env.WARDEN_GIT_IP || envConfig.WARDEN_GIT_IP || diff --git a/src/controlplane-api.ts b/src/controlplane-api.ts index b916e45..9b9dcc4 100644 --- a/src/controlplane-api.ts +++ b/src/controlplane-api.ts @@ -14,6 +14,7 @@ import { CMS_WEBROOT, CONTROLPLANE_MAX_BODY_BYTES, CONTROLPLANE_BIND_HOST, + RUNTIME_ID, TENANT_ID, } from './config.js'; import { @@ -59,7 +60,7 @@ import { logger } from './logger.js'; // ── Types ────────────────────────────────────────────────────────────────── -const METRICS_PREFIX = `${TENANT_ID}_`; +const METRICS_PREFIX = `${RUNTIME_ID}_`; export interface ControlplaneState { agents: Array<{ id: string; role: string; heartbeat_enabled: boolean }>; diff --git a/src/hostd/client.ts b/src/hostd/client.ts index 75da351..1ffba4d 100644 --- a/src/hostd/client.ts +++ b/src/hostd/client.ts @@ -13,7 +13,7 @@ import { randomUUID } from 'crypto'; import net from 'net'; import { incLabeledCounter } from '../metrics.js'; -import { TENANT_ID } from '../config.js'; +import { RUNTIME_ID, TENANT_ID } from '../config.js'; import { buildHostdAuth } from './auth.js'; import { SOCKET_PATH } from './types.js'; import type { @@ -92,7 +92,7 @@ export class HostdClient { try { const resp = JSON.parse(line) as HostdResponse; if (resp.id === id) { - const _mp = `${TENANT_ID}_`; + const _mp = `${RUNTIME_ID}_`; incLabeledCounter(`${_mp}jail_ops_total`, 'op', op); if (!resp.ok) incLabeledCounter(`${_mp}jail_ops_errors_total`, 'op', op); settle(() => resolve(resp)); diff --git a/src/index.ts b/src/index.ts index f791e97..4b0021f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -23,6 +23,7 @@ import { PI_TUI_PROVIDER, PI_TUI_MODEL, PROJECT_ROOT, + RUNTIME_ID, TELEGRAM_BOT_TOKEN, STT_MODEL, STT_MAX_ATTEMPTS, @@ -60,8 +61,8 @@ import { cleanupPendingMaintenanceSnapshots } from './maintenance-snapshots.js'; import { hostd } from './hostd/client.js'; // ── Metrics prefix — derived from agent name for multi-install dashboards ── -const METRICS_PREFIX = `${TENANT_ID}_`; -const ROOT_AGENT_ID = TENANT_ID.trim() || SERVICE_NAME; +const METRICS_PREFIX = `${RUNTIME_ID}_`; +const ROOT_AGENT_ID = RUNTIME_ID; import { Watchdog } from './watchdog.js'; import { extractTmpImagePaths } from './outbound-images.js'; import './channels/telegram.js'; diff --git a/src/local-hosts.ts b/src/local-hosts.ts index 79a8640..ecc783e 100644 --- a/src/local-hosts.ts +++ b/src/local-hosts.ts @@ -9,6 +9,7 @@ import { LLAMA_CPP_INTERNAL_DOMAIN, OLLAMA_INTERNAL_DOMAIN, PLATFORM_INTERNAL_BASE, + RUNTIME_ID, SUBNET_BASE, TENANT_ID, } from './config.js'; @@ -41,11 +42,11 @@ function uniqueNames(names: string[]): string[] { } export function getLocalHostsBlockStart(): string { - return `# >>> ${TENANT_ID} local hosts >>>`; + return `# >>> ${RUNTIME_ID} local hosts >>>`; } export function getLocalHostsBlockEnd(): string { - return `# <<< ${TENANT_ID} local hosts <<<`; + return `# <<< ${RUNTIME_ID} local hosts <<<`; } export function getLocalHostsEntries(): LocalHostsEntry[] { diff --git a/src/metrics.ts b/src/metrics.ts index 82fa2b4..76509b0 100644 --- a/src/metrics.ts +++ b/src/metrics.ts @@ -7,7 +7,7 @@ * Enable by setting METRICS_PORT (default 9100). * Set METRICS_PORT=0 to disable entirely. * - * Metrics exposed (prefix = ${TENANT_ID}_): + * Metrics exposed (prefix = ${RUNTIME_ID}_): * sessions_started_total — counter * sessions_completed_total — counter {status="ok|error|timeout"} * session_duration_seconds_sum — counter (sum of durations) @@ -23,10 +23,10 @@ */ import http from 'http'; -import { TENANT_ID } from './config.js'; +import { RUNTIME_ID } from './config.js'; import { logger } from './logger.js'; -const P = `${TENANT_ID}_`; +const P = `${RUNTIME_ID}_`; // ── Registry ────────────────────────────────────────────────────────────────── diff --git a/src/skills-pg.ts b/src/skills-pg.ts index 99fe945..521f982 100644 --- a/src/skills-pg.ts +++ b/src/skills-pg.ts @@ -1,6 +1,6 @@ import pg from 'pg'; -import { SKILLS_DB_URL, TENANT_ID } from './config.js'; +import { RUNTIME_ID, SKILLS_DB_URL } from './config.js'; import { logger } from './logger.js'; import { incCounter } from './metrics.js'; @@ -121,7 +121,7 @@ export async function searchBuiltinKnowledge( [queryText, limit], ); - const _mp = `${TENANT_ID}_`; + const _mp = `${RUNTIME_ID}_`; incCounter(`${_mp}skill_searches_total`); incCounter(`${_mp}skill_search_hits_total`, rows.length); return rows; diff --git a/src/transcription.test.ts b/src/transcription.test.ts index fa4548b..42d658b 100644 --- a/src/transcription.test.ts +++ b/src/transcription.test.ts @@ -12,10 +12,11 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; // --------------------------------------------------------------------------- const mockCreate = vi.hoisted(() => vi.fn()); +const openAiCtor = vi.hoisted(() => vi.fn()); vi.mock('openai', () => { return { - default: vi.fn().mockImplementation(function () { + default: openAiCtor.mockImplementation(function () { return { audio: { transcriptions: { @@ -68,6 +69,22 @@ describe('initTranscription', () => { }), ).not.toThrow(); }); + + it('uses a stable OpenRouter title in root-platform mode', async () => { + const { initTranscription: init } = await import('./transcription.js'); + init({ + provider: 'openrouter', + model: 'whisper-1', + openrouterApiKey: 'or-test-key', + }); + expect(openAiCtor).toHaveBeenCalledWith( + expect.objectContaining({ + defaultHeaders: expect.objectContaining({ + 'X-Title': 'clawdie-ai', + }), + }), + ); + }); }); // --------------------------------------------------------------------------- diff --git a/src/transcription.ts b/src/transcription.ts index 33d6880..64388f8 100644 --- a/src/transcription.ts +++ b/src/transcription.ts @@ -1,7 +1,7 @@ import { logger } from './logger.js'; import OpenAI from 'openai'; import fs from 'fs'; -import { TENANT_ID } from './config.js'; +import { RUNTIME_ID } from './config.js'; let openaiClient: OpenAI | null = null; let transcriptionReady = false; @@ -123,7 +123,7 @@ export function initTranscription( baseURL: 'https://openrouter.ai/api/v1', defaultHeaders: { 'HTTP-Referer': 'https://codeberg.org/Clawdie/Clawdie-AI', - 'X-Title': `${TENANT_ID}-ai`, + 'X-Title': `${RUNTIME_ID}-ai`, }, }); transcriptionReady = true; diff --git a/src/vision.ts b/src/vision.ts index 6c4f31e..6a7075d 100644 --- a/src/vision.ts +++ b/src/vision.ts @@ -3,7 +3,7 @@ import path from 'path'; import { readEnvFile } from './env.js'; import { - TENANT_ID, + RUNTIME_ID, TMP_DIR, VISION_MAX_CHARS_PER_IMAGE, VISION_MAX_IMAGES, @@ -90,7 +90,7 @@ async function describeImageOpenRouter(imagePath: string): Promise { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json', 'HTTP-Referer': 'https://codeberg.org/Clawdie/Clawdie-AI', - 'X-Title': `${TENANT_ID}-ai`, + 'X-Title': `${RUNTIME_ID}-ai`, }, body: JSON.stringify(body), });