Keep root-platform identity separate from tenant labels

---
Build: pass | Tests: FAIL — 0 failed
This commit is contained in:
Operator & Codex 2026-05-04 06:24:32 +02:00
parent d383e88b09
commit a99f97172d
12 changed files with 51 additions and 25 deletions

View file

@ -26,6 +26,7 @@ import {
PI_TUI_THINKING, PI_TUI_THINKING,
PI_TUI_TOOLS, PI_TUI_TOOLS,
PROJECT_ROOT, PROJECT_ROOT,
RUNTIME_ID,
TENANT_ID, TENANT_ID,
TMP_DIR, TMP_DIR,
TIMEZONE, TIMEZONE,
@ -52,7 +53,7 @@ import {
type PiRuntimeUsage, type PiRuntimeUsage,
} from './pi-usage.js'; } from './pi-usage.js';
const METRICS_PREFIX = `${TENANT_ID}_`; const METRICS_PREFIX = `${RUNTIME_ID}_`;
import { logger } from './logger.js'; import { logger } from './logger.js';
import { import {
applyFallback, applyFallback,
@ -345,7 +346,7 @@ export async function runJailAgent(
fs.mkdirSync(logDir, { recursive: true }); fs.mkdirSync(logDir, { recursive: true });
const safeName = input.groupFolder.replace(/[^a-zA-Z0-9-]/g, '-'); 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`); const logFile = path.join(logDir, `agent-${runId}.log`);
// ── Offline stub ────────────────────────────────────────────────────── // ── Offline stub ──────────────────────────────────────────────────────

View file

@ -78,6 +78,7 @@ describe('config identity', () => {
const config = await import('./config.js'); const config = await import('./config.js');
expect(config.TENANT_ID).toBe('mevy'); expect(config.TENANT_ID).toBe('mevy');
expect(config.RUNTIME_ID).toBe('mevy');
expect(config.TENANT_DISPLAY_NAME).toBe('Bob'); expect(config.TENANT_DISPLAY_NAME).toBe('Bob');
expect(config.ASSISTANT_NAME).toBe('Bob'); expect(config.ASSISTANT_NAME).toBe('Bob');
expect(config.AGENT_DOMAIN).toBe('home.arpa'); expect(config.AGENT_DOMAIN).toBe('home.arpa');
@ -98,6 +99,7 @@ describe('config identity', () => {
const config = await import('./config.js'); const config = await import('./config.js');
expect(config.TENANT_ID).toBe(''); expect(config.TENANT_ID).toBe('');
expect(config.RUNTIME_ID).toBe('clawdie');
expect(config.AGENT_CONFIG_DIR).toBe('clawdie-cp'); expect(config.AGENT_CONFIG_DIR).toBe('clawdie-cp');
expect(config.AGENT_INTERNAL_DOMAIN).toBe('clawdie.home.arpa'); expect(config.AGENT_INTERNAL_DOMAIN).toBe('clawdie.home.arpa');
expect(config.CMS_WEBROOT).toBe('/usr/local/www/clawdie'); expect(config.CMS_WEBROOT).toBe('/usr/local/www/clawdie');

View file

@ -270,10 +270,13 @@ export const AGENT_GENDER: AgentGender = (() => {
})(); })();
// ── Derived system identifiers ── // ── 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_ID);
export const AGENT_CONFIG_DIR = tenantControlplanePrefix(RUNTIME_TENANT_ID);
export const AGENT_DOMAIN = export const AGENT_DOMAIN =
process.env.AGENT_DOMAIN || process.env.AGENT_DOMAIN ||
envConfig.AGENT_DOMAIN || envConfig.AGENT_DOMAIN ||
@ -285,7 +288,7 @@ export const AGENT_INTERNAL_DOMAIN =
process.env.AGENT_INTERNAL_DOMAIN || process.env.AGENT_INTERNAL_DOMAIN ||
envConfig.AGENT_INTERNAL_DOMAIN || envConfig.AGENT_INTERNAL_DOMAIN ||
registryDefaults?.tenants[TENANT_ID]?.internalDomain || 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_INTERNAL_DOMAIN = PLATFORM_INTERNAL_DOMAIN;
export const CONTROLPLANE_HOST_LABEL = export const CONTROLPLANE_HOST_LABEL =
CONTROLPLANE_INTERNAL_DOMAIN.split('.')[0] || 'ai'; CONTROLPLANE_INTERNAL_DOMAIN.split('.')[0] || 'ai';
@ -714,11 +717,11 @@ export const CMS_JAIL_IP =
export const CMS_WEBROOT = export const CMS_WEBROOT =
process.env.CMS_WEBROOT || process.env.CMS_WEBROOT ||
envConfig.CMS_WEBROOT || envConfig.CMS_WEBROOT ||
`/usr/local/www/${RUNTIME_TENANT_ID}`; `/usr/local/www/${RUNTIME_ID}`;
export const ASTRO_SITE_PATH = export const ASTRO_SITE_PATH =
process.env.ASTRO_SITE_PATH || process.env.ASTRO_SITE_PATH ||
envConfig.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 = export const GIT_JAIL_IP =
process.env.WARDEN_GIT_IP || process.env.WARDEN_GIT_IP ||
envConfig.WARDEN_GIT_IP || envConfig.WARDEN_GIT_IP ||

View file

@ -14,6 +14,7 @@ import {
CMS_WEBROOT, CMS_WEBROOT,
CONTROLPLANE_MAX_BODY_BYTES, CONTROLPLANE_MAX_BODY_BYTES,
CONTROLPLANE_BIND_HOST, CONTROLPLANE_BIND_HOST,
RUNTIME_ID,
TENANT_ID, TENANT_ID,
} from './config.js'; } from './config.js';
import { import {
@ -59,7 +60,7 @@ import { logger } from './logger.js';
// ── Types ────────────────────────────────────────────────────────────────── // ── Types ──────────────────────────────────────────────────────────────────
const METRICS_PREFIX = `${TENANT_ID}_`; const METRICS_PREFIX = `${RUNTIME_ID}_`;
export interface ControlplaneState { export interface ControlplaneState {
agents: Array<{ id: string; role: string; heartbeat_enabled: boolean }>; agents: Array<{ id: string; role: string; heartbeat_enabled: boolean }>;

View file

@ -13,7 +13,7 @@ import { randomUUID } from 'crypto';
import net from 'net'; import net from 'net';
import { incLabeledCounter } from '../metrics.js'; 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 { buildHostdAuth } from './auth.js';
import { SOCKET_PATH } from './types.js'; import { SOCKET_PATH } from './types.js';
import type { import type {
@ -92,7 +92,7 @@ export class HostdClient {
try { try {
const resp = JSON.parse(line) as HostdResponse; const resp = JSON.parse(line) as HostdResponse;
if (resp.id === id) { if (resp.id === id) {
const _mp = `${TENANT_ID}_`; const _mp = `${RUNTIME_ID}_`;
incLabeledCounter(`${_mp}jail_ops_total`, 'op', op); incLabeledCounter(`${_mp}jail_ops_total`, 'op', op);
if (!resp.ok) incLabeledCounter(`${_mp}jail_ops_errors_total`, 'op', op); if (!resp.ok) incLabeledCounter(`${_mp}jail_ops_errors_total`, 'op', op);
settle(() => resolve(resp)); settle(() => resolve(resp));

View file

@ -23,6 +23,7 @@ import {
PI_TUI_PROVIDER, PI_TUI_PROVIDER,
PI_TUI_MODEL, PI_TUI_MODEL,
PROJECT_ROOT, PROJECT_ROOT,
RUNTIME_ID,
TELEGRAM_BOT_TOKEN, TELEGRAM_BOT_TOKEN,
STT_MODEL, STT_MODEL,
STT_MAX_ATTEMPTS, STT_MAX_ATTEMPTS,
@ -60,8 +61,8 @@ import { cleanupPendingMaintenanceSnapshots } from './maintenance-snapshots.js';
import { hostd } from './hostd/client.js'; import { hostd } from './hostd/client.js';
// ── Metrics prefix — derived from agent name for multi-install dashboards ── // ── Metrics prefix — derived from agent name for multi-install dashboards ──
const METRICS_PREFIX = `${TENANT_ID}_`; const METRICS_PREFIX = `${RUNTIME_ID}_`;
const ROOT_AGENT_ID = TENANT_ID.trim() || SERVICE_NAME; const ROOT_AGENT_ID = RUNTIME_ID;
import { Watchdog } from './watchdog.js'; import { Watchdog } from './watchdog.js';
import { extractTmpImagePaths } from './outbound-images.js'; import { extractTmpImagePaths } from './outbound-images.js';
import './channels/telegram.js'; import './channels/telegram.js';

View file

@ -9,6 +9,7 @@ import {
LLAMA_CPP_INTERNAL_DOMAIN, LLAMA_CPP_INTERNAL_DOMAIN,
OLLAMA_INTERNAL_DOMAIN, OLLAMA_INTERNAL_DOMAIN,
PLATFORM_INTERNAL_BASE, PLATFORM_INTERNAL_BASE,
RUNTIME_ID,
SUBNET_BASE, SUBNET_BASE,
TENANT_ID, TENANT_ID,
} from './config.js'; } from './config.js';
@ -41,11 +42,11 @@ function uniqueNames(names: string[]): string[] {
} }
export function getLocalHostsBlockStart(): string { export function getLocalHostsBlockStart(): string {
return `# >>> ${TENANT_ID} local hosts >>>`; return `# >>> ${RUNTIME_ID} local hosts >>>`;
} }
export function getLocalHostsBlockEnd(): string { export function getLocalHostsBlockEnd(): string {
return `# <<< ${TENANT_ID} local hosts <<<`; return `# <<< ${RUNTIME_ID} local hosts <<<`;
} }
export function getLocalHostsEntries(): LocalHostsEntry[] { export function getLocalHostsEntries(): LocalHostsEntry[] {

View file

@ -7,7 +7,7 @@
* Enable by setting METRICS_PORT (default 9100). * Enable by setting METRICS_PORT (default 9100).
* Set METRICS_PORT=0 to disable entirely. * Set METRICS_PORT=0 to disable entirely.
* *
* Metrics exposed (prefix = ${TENANT_ID}_): * Metrics exposed (prefix = ${RUNTIME_ID}_):
* sessions_started_total counter * sessions_started_total counter
* sessions_completed_total counter {status="ok|error|timeout"} * sessions_completed_total counter {status="ok|error|timeout"}
* session_duration_seconds_sum counter (sum of durations) * session_duration_seconds_sum counter (sum of durations)
@ -23,10 +23,10 @@
*/ */
import http from 'http'; import http from 'http';
import { TENANT_ID } from './config.js'; import { RUNTIME_ID } from './config.js';
import { logger } from './logger.js'; import { logger } from './logger.js';
const P = `${TENANT_ID}_`; const P = `${RUNTIME_ID}_`;
// ── Registry ────────────────────────────────────────────────────────────────── // ── Registry ──────────────────────────────────────────────────────────────────

View file

@ -1,6 +1,6 @@
import pg from 'pg'; 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 { logger } from './logger.js';
import { incCounter } from './metrics.js'; import { incCounter } from './metrics.js';
@ -121,7 +121,7 @@ export async function searchBuiltinKnowledge(
[queryText, limit], [queryText, limit],
); );
const _mp = `${TENANT_ID}_`; const _mp = `${RUNTIME_ID}_`;
incCounter(`${_mp}skill_searches_total`); incCounter(`${_mp}skill_searches_total`);
incCounter(`${_mp}skill_search_hits_total`, rows.length); incCounter(`${_mp}skill_search_hits_total`, rows.length);
return rows; return rows;

View file

@ -12,10 +12,11 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
const mockCreate = vi.hoisted(() => vi.fn()); const mockCreate = vi.hoisted(() => vi.fn());
const openAiCtor = vi.hoisted(() => vi.fn());
vi.mock('openai', () => { vi.mock('openai', () => {
return { return {
default: vi.fn().mockImplementation(function () { default: openAiCtor.mockImplementation(function () {
return { return {
audio: { audio: {
transcriptions: { transcriptions: {
@ -68,6 +69,22 @@ describe('initTranscription', () => {
}), }),
).not.toThrow(); ).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',
}),
}),
);
});
}); });
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View file

@ -1,7 +1,7 @@
import { logger } from './logger.js'; import { logger } from './logger.js';
import OpenAI from 'openai'; import OpenAI from 'openai';
import fs from 'fs'; import fs from 'fs';
import { TENANT_ID } from './config.js'; import { RUNTIME_ID } from './config.js';
let openaiClient: OpenAI | null = null; let openaiClient: OpenAI | null = null;
let transcriptionReady = false; let transcriptionReady = false;
@ -123,7 +123,7 @@ export function initTranscription(
baseURL: 'https://openrouter.ai/api/v1', baseURL: 'https://openrouter.ai/api/v1',
defaultHeaders: { defaultHeaders: {
'HTTP-Referer': 'https://codeberg.org/Clawdie/Clawdie-AI', 'HTTP-Referer': 'https://codeberg.org/Clawdie/Clawdie-AI',
'X-Title': `${TENANT_ID}-ai`, 'X-Title': `${RUNTIME_ID}-ai`,
}, },
}); });
transcriptionReady = true; transcriptionReady = true;

View file

@ -3,7 +3,7 @@ import path from 'path';
import { readEnvFile } from './env.js'; import { readEnvFile } from './env.js';
import { import {
TENANT_ID, RUNTIME_ID,
TMP_DIR, TMP_DIR,
VISION_MAX_CHARS_PER_IMAGE, VISION_MAX_CHARS_PER_IMAGE,
VISION_MAX_IMAGES, VISION_MAX_IMAGES,
@ -90,7 +90,7 @@ async function describeImageOpenRouter(imagePath: string): Promise<string> {
Authorization: `Bearer ${key}`, Authorization: `Bearer ${key}`,
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'HTTP-Referer': 'https://codeberg.org/Clawdie/Clawdie-AI', 'HTTP-Referer': 'https://codeberg.org/Clawdie/Clawdie-AI',
'X-Title': `${TENANT_ID}-ai`, 'X-Title': `${RUNTIME_ID}-ai`,
}, },
body: JSON.stringify(body), body: JSON.stringify(body),
}); });