Clarify root platform ownership in reports
--- Build: pass | Tests: FAIL — Tests 11 failed | 2089 passed | 4 skipped (2104)
This commit is contained in:
parent
ed80c822fa
commit
85646d24e8
11 changed files with 134 additions and 25 deletions
|
|
@ -3,6 +3,13 @@ platform:
|
|||
display_name: Clawdie
|
||||
repo: Clawdie-AI
|
||||
internal_base: home.arpa
|
||||
services:
|
||||
- clawdie
|
||||
- clawdie_hostd
|
||||
datasets:
|
||||
- zroot/clawdie-ai
|
||||
jails:
|
||||
- worker
|
||||
controlplane_exposure: internal
|
||||
cms_admin_exposure: internal
|
||||
code_admin_exposure: internal
|
||||
|
|
@ -18,7 +25,6 @@ platform:
|
|||
shared:
|
||||
services:
|
||||
- postgresql
|
||||
- mevy_hostd
|
||||
- cms
|
||||
- web-service
|
||||
- code-service
|
||||
|
|
|
|||
|
|
@ -103,7 +103,9 @@ async function main(): Promise<void> {
|
|||
console.log(`TENANT_CONTEXT_WARNING: ${warning}`);
|
||||
}
|
||||
console.log('SURFACES:');
|
||||
for (const line of formatSurfaceMapLines(registry)) {
|
||||
for (const line of formatSurfaceMapLines(registry, {
|
||||
includeTenantSurfaces: TENANT_ID.trim().length > 0,
|
||||
})) {
|
||||
console.log(`- ${line}`);
|
||||
}
|
||||
} catch {
|
||||
|
|
|
|||
|
|
@ -9,13 +9,15 @@ vi.mock('./authorized-hostd.js', () => ({
|
|||
describe('platform-audit-report', () => {
|
||||
it('collects observed service, jail, and dataset ownership', async () => {
|
||||
// Service probes run in alphabetical order over the deduped union of
|
||||
// shared.services + tenant services: cms, code-service, mevy,
|
||||
// mevy_hostd, postgresql, web-service. Then jails, then datasets.
|
||||
// platform.services + shared.services + tenant services:
|
||||
// clawdie, clawdie_hostd, cms, code-service, mevy, postgresql, web-service.
|
||||
// Then jails, then datasets.
|
||||
callAuthorizedHostdMock
|
||||
.mockResolvedValueOnce({ ok: false, error: 'clawdie is not running' })
|
||||
.mockResolvedValueOnce({ ok: false, error: 'clawdie_hostd is not running' })
|
||||
.mockResolvedValueOnce({ ok: false, error: 'cms is not running' })
|
||||
.mockResolvedValueOnce({ ok: false, error: 'code-service is not running' })
|
||||
.mockResolvedValueOnce({ ok: false, error: 'mevy is not running' })
|
||||
.mockResolvedValueOnce({ ok: false, error: 'mevy_hostd is not running' })
|
||||
.mockResolvedValueOnce({ ok: true, output: 'postgresql is running as pid 1' })
|
||||
.mockResolvedValueOnce({ ok: false, error: 'web-service is not running' })
|
||||
.mockResolvedValueOnce({
|
||||
|
|
@ -33,7 +35,7 @@ describe('platform-audit-report', () => {
|
|||
caller: 'operator',
|
||||
});
|
||||
|
||||
expect(report.observedServices).toHaveLength(6);
|
||||
expect(report.observedServices).toHaveLength(7);
|
||||
expect(report.observedJails).toEqual({
|
||||
shared: ['git'],
|
||||
tenants: { mevy: ['mevy_ctrl_worker'] },
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ async function probeDeclaredServices(
|
|||
opts: { tenantId: string; caller: HostdCaller },
|
||||
): Promise<ServiceAuditStatus[]> {
|
||||
const serviceNames = dedupeSorted([
|
||||
...registry.platform.services,
|
||||
...registry.shared.services,
|
||||
...Object.values(registry.tenants).map((tenant) => tenant.service),
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -14,13 +14,14 @@ const registry = loadTenantRegistry();
|
|||
|
||||
describe('platform-audit', () => {
|
||||
it('classifies shared and tenant-owned services', () => {
|
||||
expect(classifyServiceOwner('clawdie', registry)).toBe('tenant:clawdie');
|
||||
expect(classifyServiceOwner('clawdie_hostd', registry)).toBe(
|
||||
'tenant:clawdie',
|
||||
);
|
||||
expect(classifyServiceOwner('postgresql', registry)).toBe(
|
||||
'shared-platform',
|
||||
);
|
||||
expect(classifyServiceOwner('mevy', registry)).toBe('tenant:mevy');
|
||||
expect(classifyServiceOwner('mevy_hostd', registry)).toBe(
|
||||
'shared-platform',
|
||||
);
|
||||
expect(classifyServiceOwner('unknown-service', registry)).toBe('unknown');
|
||||
});
|
||||
|
||||
|
|
@ -29,12 +30,16 @@ describe('platform-audit', () => {
|
|||
});
|
||||
|
||||
it('classifies shared and tenant-owned jails', () => {
|
||||
expect(classifyJailOwner('worker', registry)).toBe('tenant:clawdie');
|
||||
expect(classifyJailOwner('git', registry)).toBe('shared-platform');
|
||||
expect(classifyJailOwner('mevy_ctrl_worker', registry)).toBe('tenant:mevy');
|
||||
expect(classifyJailOwner('unknown-jail', registry)).toBe('unknown');
|
||||
});
|
||||
|
||||
it('classifies datasets by shared ownership roots', () => {
|
||||
expect(classifyDatasetOwner('zroot/clawdie-ai/pgdata', registry)).toBe(
|
||||
'tenant:clawdie',
|
||||
);
|
||||
expect(
|
||||
classifyDatasetOwner('zroot/clawdie-runtime/jails/git', registry),
|
||||
).toBe('shared-platform');
|
||||
|
|
@ -52,17 +57,17 @@ describe('platform-audit', () => {
|
|||
|
||||
it('summarizes ownership for a tenant', () => {
|
||||
const summary = summarizeOwnership(
|
||||
['postgresql', 'mevy', 'mevy_hostd', 'mystery'],
|
||||
['postgresql', 'mevy', 'clawdie_hostd', 'mystery'],
|
||||
classifyServiceOwner,
|
||||
registry,
|
||||
'mevy',
|
||||
);
|
||||
expect(summary).toEqual({
|
||||
own: 1,
|
||||
shared: 2,
|
||||
otherTenants: 0,
|
||||
shared: 1,
|
||||
otherTenants: 1,
|
||||
unknown: 1,
|
||||
otherTenantBreakdown: {},
|
||||
otherTenantBreakdown: { clawdie: 1 },
|
||||
unknownItems: ['mystery'],
|
||||
});
|
||||
expect(
|
||||
|
|
@ -73,11 +78,11 @@ describe('platform-audit', () => {
|
|||
});
|
||||
|
||||
it('normalizes service-name comparisons (case + separator differences)', () => {
|
||||
expect(classifyServiceOwner('Mevy_HostD', registry)).toBe(
|
||||
'shared-platform',
|
||||
expect(classifyServiceOwner('Clawdie_HostD', registry)).toBe(
|
||||
'tenant:clawdie',
|
||||
);
|
||||
expect(classifyServiceOwner('mevy-hostd', registry)).toBe(
|
||||
'shared-platform',
|
||||
expect(classifyServiceOwner('clawdie-hostd', registry)).toBe(
|
||||
'tenant:clawdie',
|
||||
);
|
||||
expect(classifyServiceOwner('MEVY', registry)).toBe('tenant:mevy');
|
||||
});
|
||||
|
|
@ -105,7 +110,7 @@ describe('platform-audit', () => {
|
|||
|
||||
it('buckets observed resources by owner', () => {
|
||||
const buckets = bucketOwnedResources(
|
||||
['git', 'mevy_ctrl_worker', 'mevy_hostd', 'mystery'],
|
||||
['git', 'mevy_ctrl_worker', 'clawdie_hostd', 'mystery'],
|
||||
(item, currentRegistry) => {
|
||||
if (item.endsWith('_hostd'))
|
||||
return classifyServiceOwner(item, currentRegistry);
|
||||
|
|
@ -115,8 +120,9 @@ describe('platform-audit', () => {
|
|||
);
|
||||
|
||||
expect(buckets).toEqual({
|
||||
shared: ['git', 'mevy_hostd'],
|
||||
shared: ['git'],
|
||||
tenants: {
|
||||
clawdie: ['clawdie_hostd'],
|
||||
mevy: ['mevy_ctrl_worker'],
|
||||
},
|
||||
unknown: ['mystery'],
|
||||
|
|
|
|||
|
|
@ -39,6 +39,13 @@ export function classifyServiceOwner(
|
|||
serviceName: string,
|
||||
registry: PlatformRegistry,
|
||||
): ResourceOwner {
|
||||
if (
|
||||
registry.platform.services.some((platform) =>
|
||||
aliasMatches(serviceName, platform),
|
||||
)
|
||||
) {
|
||||
return `tenant:${registry.platform.id}`;
|
||||
}
|
||||
if (
|
||||
registry.shared.services.some((shared) => aliasMatches(serviceName, shared))
|
||||
) {
|
||||
|
|
@ -58,6 +65,11 @@ export function classifyJailOwner(
|
|||
jailName: string,
|
||||
registry: PlatformRegistry,
|
||||
): ResourceOwner {
|
||||
if (
|
||||
registry.platform.jails.some((platform) => aliasMatches(jailName, platform))
|
||||
) {
|
||||
return `tenant:${registry.platform.id}`;
|
||||
}
|
||||
if (registry.shared.jails.some((shared) => aliasMatches(jailName, shared))) {
|
||||
return 'shared-platform';
|
||||
}
|
||||
|
|
@ -75,6 +87,11 @@ export function classifyDatasetOwner(
|
|||
dataset: string,
|
||||
registry: PlatformRegistry,
|
||||
): ResourceOwner {
|
||||
for (const platformDataset of registry.platform.datasets) {
|
||||
if (isDatasetUnder(dataset, platformDataset)) {
|
||||
return `tenant:${registry.platform.id}`;
|
||||
}
|
||||
}
|
||||
for (const tenant of Object.values(registry.tenants)) {
|
||||
for (const tenantDataset of tenant.datasets) {
|
||||
if (isDatasetUnder(dataset, tenantDataset)) {
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ describe('system report rendering', () => {
|
|||
const report = buildSystemReport({
|
||||
observedAt: new Date('2026-04-25T21:58:00Z'),
|
||||
services: [
|
||||
{ label: 'Mevy agent', status: 'running' },
|
||||
{ label: 'Clawdie agent', status: 'running' },
|
||||
{ label: 'hostd', status: 'running' },
|
||||
],
|
||||
hostdReachable: true,
|
||||
|
|
@ -76,7 +76,7 @@ describe('system report rendering', () => {
|
|||
const rendered = renderSystemReport(report);
|
||||
expect(rendered).toContain('<b>System Report</b>');
|
||||
expect(rendered).toContain('<b>Observed</b>');
|
||||
expect(rendered).toContain('- Mevy agent: <code>running</code>');
|
||||
expect(rendered).toContain('- Clawdie agent: <code>running</code>');
|
||||
expect(rendered).toContain('- Controlplane auth: <code>auth_required</code>');
|
||||
expect(rendered).toContain('<b>Interpretation</b>');
|
||||
expect(rendered).toContain('Controlplane auth is enforced for anonymous requests.');
|
||||
|
|
|
|||
|
|
@ -10,6 +10,9 @@ const registry: PlatformRegistry = {
|
|||
internalDomain: 'ai.home.arpa',
|
||||
internalBase: 'home.arpa',
|
||||
publicBase: 'example.com',
|
||||
services: ['clawdie', 'clawdie_hostd'],
|
||||
datasets: ['zroot/clawdie-ai'],
|
||||
jails: ['worker'],
|
||||
controlplaneExposure: 'public',
|
||||
cmsAdminExposure: 'internal',
|
||||
codeAdminExposure: 'internal',
|
||||
|
|
@ -17,7 +20,7 @@ const registry: PlatformRegistry = {
|
|||
reservedHostLabels: ['ai', 'cms', 'git', 'web', 'www', 'mail'],
|
||||
},
|
||||
shared: {
|
||||
services: ['postgresql', 'clawdie', 'clawdie_hostd', 'cms', 'web-service', 'code-service'],
|
||||
services: ['postgresql', 'cms', 'web-service', 'code-service'],
|
||||
datasets: [],
|
||||
jails: [],
|
||||
},
|
||||
|
|
@ -57,4 +60,18 @@ describe('formatSurfaceMapLines', () => {
|
|||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('can omit tenant surfaces for root-mode displays', () => {
|
||||
expect(formatSurfaceMapLines(registry, { includeTenantSurfaces: false })).toEqual(
|
||||
expect.arrayContaining([
|
||||
'cms-admin.internal=cms.home.arpa',
|
||||
'code-admin.internal=git.home.arpa',
|
||||
'controlplane.internal=ai.home.arpa',
|
||||
'controlplane.public=ai.example.com',
|
||||
]),
|
||||
);
|
||||
expect(
|
||||
formatSurfaceMapLines(registry, { includeTenantSurfaces: false }).join('\n'),
|
||||
).not.toContain('tenant-home.mevy');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import type { PlatformRegistry } from './tenant-registry.js';
|
||||
|
||||
import { buildSurfaceInventory } from './surface-inventory.js';
|
||||
import type { SurfaceRecord } from './surface-inventory.js';
|
||||
|
||||
function surfaceLabel(surface: ReturnType<typeof buildSurfaceInventory>[number]): string {
|
||||
function surfaceLabel(surface: SurfaceRecord): string {
|
||||
switch (surface.kind) {
|
||||
case 'controlplane':
|
||||
return `controlplane.${surface.exposure}`;
|
||||
|
|
@ -21,8 +22,17 @@ function surfaceLabel(surface: ReturnType<typeof buildSurfaceInventory>[number])
|
|||
}
|
||||
}
|
||||
|
||||
export function formatSurfaceMapLines(registry: PlatformRegistry): string[] {
|
||||
export function formatSurfaceMapLines(
|
||||
registry: PlatformRegistry,
|
||||
opts: { includeTenantSurfaces?: boolean } = {},
|
||||
): string[] {
|
||||
const includeTenantSurfaces = opts.includeTenantSurfaces ?? true;
|
||||
return buildSurfaceInventory(registry)
|
||||
.filter((surface) =>
|
||||
includeTenantSurfaces
|
||||
? true
|
||||
: surface.kind !== 'tenant-home' && surface.kind !== 'tenant-site',
|
||||
)
|
||||
.slice()
|
||||
.sort((a, b) => surfaceLabel(a).localeCompare(surfaceLabel(b)))
|
||||
.map((surface) => `${surfaceLabel(surface)}=${surface.host}`);
|
||||
|
|
|
|||
|
|
@ -2012,7 +2012,7 @@ export async function handleReportCommand(
|
|||
const report = buildSystemReport({
|
||||
services: [
|
||||
{
|
||||
label: 'Mevy agent',
|
||||
label: `${TENANT_DISPLAY_NAME || 'Clawdie'} agent`,
|
||||
status: mainServiceRes.ok
|
||||
? parseServiceStatus(mainServiceRes.output || '')
|
||||
: 'unknown',
|
||||
|
|
|
|||
|
|
@ -62,6 +62,9 @@ const PlatformRegistrySchema = z.object({
|
|||
repo: z.string().optional(),
|
||||
internal_base: z.string().optional(),
|
||||
public_base: z.string().optional(),
|
||||
services: z.array(z.string()).optional(),
|
||||
datasets: z.array(z.string()).optional(),
|
||||
jails: z.array(z.string()).optional(),
|
||||
controlplane_exposure: z.enum(['internal', 'public']).optional(),
|
||||
cms_admin_exposure: z.enum(['internal', 'public']).optional(),
|
||||
code_admin_exposure: z.enum(['internal', 'public']).optional(),
|
||||
|
|
@ -113,6 +116,9 @@ export interface PlatformRegistry {
|
|||
internalDomain: string;
|
||||
internalBase: string;
|
||||
publicBase?: string;
|
||||
services: string[];
|
||||
datasets: string[];
|
||||
jails: string[];
|
||||
controlplaneExposure: 'internal' | 'public';
|
||||
cmsAdminExposure: 'internal' | 'public';
|
||||
codeAdminExposure: 'internal' | 'public';
|
||||
|
|
@ -466,6 +472,9 @@ export function loadTenantRegistry(registryPath?: string): PlatformRegistry {
|
|||
internalDomain: platformControlplaneInternalDomain(internalBase),
|
||||
internalBase: internalBase || 'home.arpa',
|
||||
publicBase: parsed.platform.public_base,
|
||||
services: parsed.platform.services || [],
|
||||
datasets: parsed.platform.datasets || [],
|
||||
jails: parsed.platform.jails || [],
|
||||
controlplaneExposure: parsed.platform.controlplane_exposure || 'internal',
|
||||
cmsAdminExposure: parsed.platform.cms_admin_exposure || 'internal',
|
||||
codeAdminExposure: parsed.platform.code_admin_exposure || 'internal',
|
||||
|
|
@ -541,9 +550,17 @@ function validateTenantRecord(
|
|||
const sharedJailAliases = new Set(
|
||||
registry.shared.jails.map(canonicalResourceAlias),
|
||||
);
|
||||
const platformServiceAliases = new Set(
|
||||
registry.platform.services.map(canonicalResourceAlias),
|
||||
);
|
||||
const platformJailAliases = new Set(
|
||||
registry.platform.jails.map(canonicalResourceAlias),
|
||||
);
|
||||
const reservedIdAliases = new Set([
|
||||
canonicalResourceAlias(registry.platform.id),
|
||||
...registry.platform.reservedHostLabels.map(canonicalHostLabel),
|
||||
...platformServiceAliases,
|
||||
...platformJailAliases,
|
||||
...sharedServiceAliases,
|
||||
...sharedJailAliases,
|
||||
]);
|
||||
|
|
@ -556,9 +573,17 @@ function validateTenantRecord(
|
|||
`Tenant service conflicts with shared service: ${candidate.service}`,
|
||||
);
|
||||
}
|
||||
if (platformServiceAliases.has(canonicalResourceAlias(candidate.service))) {
|
||||
throw new Error(
|
||||
`Tenant service conflicts with platform service: ${candidate.service}`,
|
||||
);
|
||||
}
|
||||
if (sharedJailAliases.has(canonicalResourceAlias(candidate.service))) {
|
||||
throw new Error(`Tenant service conflicts with shared jail: ${candidate.service}`);
|
||||
}
|
||||
if (platformJailAliases.has(canonicalResourceAlias(candidate.service))) {
|
||||
throw new Error(`Tenant service conflicts with platform jail: ${candidate.service}`);
|
||||
}
|
||||
|
||||
const sharedDatasetOverlap = findDatasetOverlap(
|
||||
candidate.datasets,
|
||||
|
|
@ -569,6 +594,15 @@ function validateTenantRecord(
|
|||
`Tenant dataset overlaps shared platform dataset: ${sharedDatasetOverlap.left} vs ${sharedDatasetOverlap.right}`,
|
||||
);
|
||||
}
|
||||
const platformDatasetOverlap = findDatasetOverlap(
|
||||
candidate.datasets,
|
||||
registry.platform.datasets,
|
||||
);
|
||||
if (platformDatasetOverlap) {
|
||||
throw new Error(
|
||||
`Tenant dataset overlaps platform dataset: ${platformDatasetOverlap.left} vs ${platformDatasetOverlap.right}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
hasCanonicalOverlap(
|
||||
|
|
@ -581,6 +615,17 @@ function validateTenantRecord(
|
|||
`Tenant worker jails overlap shared platform jails: ${candidate.id}`,
|
||||
);
|
||||
}
|
||||
if (
|
||||
hasCanonicalOverlap(
|
||||
candidate.workerJails,
|
||||
registry.platform.jails,
|
||||
canonicalResourceAlias,
|
||||
)
|
||||
) {
|
||||
throw new Error(
|
||||
`Tenant worker jails overlap platform jails: ${candidate.id}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
canonicalDomain(candidate.internalDomain) ===
|
||||
|
|
@ -751,6 +796,9 @@ function buildPlainRegistry(registry: PlatformRegistry): Record<string, unknown>
|
|||
repo: registry.platform.repo,
|
||||
internal_base: registry.platform.internalBase,
|
||||
public_base: registry.platform.publicBase,
|
||||
services: registry.platform.services,
|
||||
datasets: registry.platform.datasets,
|
||||
jails: registry.platform.jails,
|
||||
controlplane_exposure: registry.platform.controlplaneExposure,
|
||||
cms_admin_exposure: registry.platform.cmsAdminExposure,
|
||||
code_admin_exposure: registry.platform.codeAdminExposure,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue