Define tenant apply preflight policy

Turn tenant-apply into a structured preflight contract that marks what already passes in the declarative model, what remains manual, and what still blocks any future automatic host mutation.

---
Build: pass | Tests: pass — 31 passed (1 file)
This commit is contained in:
Mevy Assistant 2026-04-24 09:31:52 +02:00
parent e1c969e3d0
commit 2d3f2253c9
5 changed files with 90 additions and 0 deletions

View file

@ -82,6 +82,19 @@ Each tenant owns:
- startup reports
- tenant-specific worker execution
Tenant ownership in V2 is still declarative first. A declared tenant means:
- named tenant-owned databases
- named tenant-owned worker jails
- named tenant-owned datasets
It does not mean:
- a per-tenant hostd
- a per-tenant Unix user
- a per-tenant repo checkout
- automatic live provisioning on declaration alone
Examples:
- `mevy`
@ -258,3 +271,20 @@ V2 must discover or declare ownership instead of guessing from one name.
4. Refactor setup and diagnostics to use the registry
5. Make hostd authorization tenant-aware
6. Add degraded-mode boundaries between chat, memory, and ops
## Future tenant apply policy
Any future live `tenant-apply` must satisfy these rules:
- create only tenant-owned resources
- never create, mutate, or delete shared platform resources
- remain non-destructive by default
- require explicit operator review before host mutation
- keep per-tenant hostd, Unix users, and repo workspaces out of scope
Preflight for a future live apply should at minimum verify:
- tenant exists in the registry
- tenant resources are still disjoint from shared platform resources
- tenant resources are still disjoint from other tenants
- no blocked manual step is being silently skipped

View file

@ -198,6 +198,8 @@ Also updated:
- what a future live apply would be allowed to touch
- prerequisites before any host mutation could exist
- what remains manual or explicitly out of scope
- a structured preflight checklist showing what already passes in the
declarative model and what still blocks any automatic apply
## Recommended first code tasks

View file

@ -161,6 +161,9 @@ function printApplyPlan(
): void {
console.log(`Apply plan for tenant ${plan.tenant.id}:`);
console.log(`Display: ${plan.tenant.displayName}`);
console.log(
`Automatic apply: ${plan.readyForAutomaticApply ? 'ready' : 'not ready'}`,
);
console.log(`Allowed databases: ${plan.allowedResources.databases.join(', ')}`);
console.log(`Allowed worker jails: ${plan.allowedResources.workerJails.join(', ')}`);
console.log(`Allowed datasets: ${plan.allowedResources.datasets.join(', ') || '(none)'}`);
@ -170,6 +173,10 @@ function printApplyPlan(
console.log(`- ${blocker}`);
}
}
console.log('Preflight:');
for (const check of plan.preflightChecks) {
console.log(`- [${check.status}] ${check.name}: ${check.detail}`);
}
console.log('Prerequisites:');
for (const item of plan.prerequisites) {
console.log(`- ${item}`);

View file

@ -82,8 +82,21 @@ describe('tenant-registry', () => {
const registryPath = makeTempRegistry();
const plan = planTenantApply('mevy', registryPath);
expect(plan.tenant.id).toBe('mevy');
expect(plan.readyForAutomaticApply).toBe(false);
expect(plan.allowedResources.databases).toContain('mevy_brain');
expect(plan.allowedResources.workerJails).toContain('mevy_ctrl_worker');
expect(plan.preflightChecks).toEqual(
expect.arrayContaining([
expect.objectContaining({
name: 'Registry declaration',
status: 'pass',
}),
expect.objectContaining({
name: 'Live apply implementation',
status: 'blocked',
}),
]),
);
expect(plan.manualSteps).toContain(
'No live resources are created by this workflow today.',
);

View file

@ -138,11 +138,17 @@ export interface TenantProvisioningPlan {
export interface TenantApplyPlan {
tenant: TenantRecord;
registryPath: string;
readyForAutomaticApply: boolean;
allowedResources: {
databases: string[];
workerJails: string[];
datasets: string[];
};
preflightChecks: Array<{
name: string;
status: 'pass' | 'manual' | 'blocked';
detail: string;
}>;
prerequisites: string[];
manualSteps: string[];
blockers: string[];
@ -675,11 +681,43 @@ export function planTenantApply(
return {
tenant,
registryPath,
readyForAutomaticApply: false,
allowedResources: {
databases: Object.values(tenant.databases),
workerJails: tenant.workerJails,
datasets: tenant.datasets,
},
preflightChecks: [
{
name: 'Registry declaration',
status: 'pass',
detail: 'Tenant already exists in the registry.',
},
{
name: 'Ownership disjointness',
status: 'pass',
detail:
'Tenant resources remain disjoint from shared platform resources and other tenants by registry validation.',
},
{
name: 'Shared-resource exclusion',
status: 'pass',
detail:
'Platform-owned shared services, datasets, and jails stay outside tenant apply.',
},
{
name: 'Live apply implementation',
status: 'blocked',
detail:
'No host-mutating tenant apply workflow exists yet; this command is planning only.',
},
{
name: 'Operator confirmation',
status: 'manual',
detail:
'Any future live apply should require explicit operator review before touching host state.',
},
],
prerequisites: [
'Tenant must already exist in the registry.',
'Platform-owned shared services must already remain outside tenant apply.',