Classify tenant apply policy actions

Refine tenant-apply planning so future automatic candidates, manual-only steps, and permanent out-of-scope actions are reported explicitly instead of being implied by generic prose.

---
Build: pass | Tests: pass — 31 passed (1 file)
This commit is contained in:
Mevy Assistant 2026-04-24 09:36:09 +02:00
parent 0f81c69d28
commit 253cdcecb6
5 changed files with 111 additions and 0 deletions

View file

@ -282,6 +282,16 @@ Any future live `tenant-apply` must satisfy these rules:
- require explicit operator review before host mutation
- keep per-tenant hostd, Unix users, and repo workspaces out of scope
The intended policy buckets are:
- future automatic candidates:
tenant-owned databases, tenant-owned worker jails, tenant-owned datasets
- manual-only:
operator approval and any host-level execution step
- out of scope:
per-tenant hostd, per-tenant Unix users, per-tenant repo workspaces, any
shared platform resource mutation
Preflight for a future live apply should at minimum verify:
- tenant exists in the registry

View file

@ -200,6 +200,8 @@ Also updated:
- 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
- explicit policy buckets for future automatic candidates, manual-only
steps, and permanent out-of-scope actions
## Recommended first code tasks

View file

@ -167,6 +167,17 @@ function printApplyPlan(
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)'}`);
console.log('Action policy:');
for (const action of plan.actionPolicy.automaticCandidates) {
console.log(`- [future-auto] ${action.name}: ${action.resources.join(', ') || '(none)'}`);
console.log(` ${action.detail}`);
}
for (const action of plan.actionPolicy.manualOnly) {
console.log(`- [manual] ${action.name}: ${action.detail}`);
}
for (const action of plan.actionPolicy.outOfScope) {
console.log(`- [out-of-scope] ${action.name}: ${action.detail}`);
}
if (plan.blockers.length > 0) {
console.log('Blockers:');
for (const blocker of plan.blockers) {

View file

@ -97,6 +97,24 @@ describe('tenant-registry', () => {
}),
]),
);
expect(plan.actionPolicy.automaticCandidates).toEqual(
expect.arrayContaining([
expect.objectContaining({
name: 'Tenant databases',
resources: expect.arrayContaining(['mevy_brain']),
}),
expect.objectContaining({
name: 'Tenant worker jails',
resources: expect.arrayContaining(['mevy_ctrl_worker']),
}),
]),
);
expect(plan.actionPolicy.outOfScope).toEqual(
expect.arrayContaining([
expect.objectContaining({ name: 'Per-tenant hostd' }),
expect.objectContaining({ name: 'Shared platform resources' }),
]),
);
expect(plan.manualSteps).toContain(
'No live resources are created by this workflow today.',
);

View file

@ -139,6 +139,21 @@ export interface TenantApplyPlan {
tenant: TenantRecord;
registryPath: string;
readyForAutomaticApply: boolean;
actionPolicy: {
automaticCandidates: Array<{
name: string;
resources: string[];
detail: string;
}>;
manualOnly: Array<{
name: string;
detail: string;
}>;
outOfScope: Array<{
name: string;
detail: string;
}>;
};
allowedResources: {
databases: string[];
workerJails: string[];
@ -682,6 +697,61 @@ export function planTenantApply(
tenant,
registryPath,
readyForAutomaticApply: false,
actionPolicy: {
automaticCandidates: [
{
name: 'Tenant databases',
resources: Object.values(tenant.databases),
detail:
'A future live apply may create tenant-owned databases only, with no shared DB mutation.',
},
{
name: 'Tenant worker jails',
resources: tenant.workerJails,
detail:
'A future live apply may provision tenant-owned worker jails only, subject to host-level review.',
},
{
name: 'Tenant datasets',
resources: tenant.datasets,
detail:
'A future live apply may create tenant-owned datasets only, without touching shared platform datasets.',
},
],
manualOnly: [
{
name: 'Operator approval',
detail:
'Any future live apply must require explicit operator confirmation before host mutation.',
},
{
name: 'Host-level execution',
detail:
'Any future jail, dataset, or database creation must run as an explicit host-level step.',
},
],
outOfScope: [
{
name: 'Per-tenant hostd',
detail: 'Tenant apply must never create a separate hostd service.',
},
{
name: 'Per-tenant Unix user',
detail:
'Tenant apply must not create a tenant-scoped Unix user or home contract.',
},
{
name: 'Per-tenant repo workspace',
detail:
'Tenant apply must not create a tenant-specific repo checkout or runtime workspace.',
},
{
name: 'Shared platform resources',
detail:
'Tenant apply must never create, mutate, or delete shared platform services, datasets, or jails.',
},
],
},
allowedResources: {
databases: Object.values(tenant.databases),
workerJails: tenant.workerJails,