diff --git a/docs/internal/MULTITENANT-ARCHITECTURE.md b/docs/internal/MULTITENANT-ARCHITECTURE.md index ae3ba01..5353623 100644 --- a/docs/internal/MULTITENANT-ARCHITECTURE.md +++ b/docs/internal/MULTITENANT-ARCHITECTURE.md @@ -298,3 +298,9 @@ Preflight for a future live apply should at minimum verify: - 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 + +The dry-run apply surface should also report a per-resource checklist: + +- `would-create` for tenant-owned resources a future live apply could create +- `exists-in-contract` for resources already satisfied declaratively +- `blocked` for anything a future live apply must not touch yet diff --git a/docs/internal/MULTITENANT-HANDOFF.md b/docs/internal/MULTITENANT-HANDOFF.md index 48fae6f..4d2999f 100644 --- a/docs/internal/MULTITENANT-HANDOFF.md +++ b/docs/internal/MULTITENANT-HANDOFF.md @@ -202,6 +202,8 @@ Also updated: 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 + - a per-resource checklist showing `would-create`, `exists-in-contract`, or + `blocked` status for databases, datasets, and worker jails ## Recommended first code tasks diff --git a/scripts/tenant-lifecycle.ts b/scripts/tenant-lifecycle.ts index 113a6c8..743b8cf 100644 --- a/scripts/tenant-lifecycle.ts +++ b/scripts/tenant-lifecycle.ts @@ -167,6 +167,16 @@ 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('Resource checklist:'); + for (const entry of plan.resourceChecklist.databases) { + console.log(`- [${entry.status}] database ${entry.name}: ${entry.detail}`); + } + for (const entry of plan.resourceChecklist.workerJails) { + console.log(`- [${entry.status}] worker-jail ${entry.name}: ${entry.detail}`); + } + for (const entry of plan.resourceChecklist.datasets) { + console.log(`- [${entry.status}] dataset ${entry.name}: ${entry.detail}`); + } console.log('Action policy:'); for (const action of plan.actionPolicy.automaticCandidates) { console.log(`- [future-auto] ${action.name}: ${action.resources.join(', ') || '(none)'}`); diff --git a/src/tenant-registry.test.ts b/src/tenant-registry.test.ts index 0e2ee49..802e844 100644 --- a/src/tenant-registry.test.ts +++ b/src/tenant-registry.test.ts @@ -115,6 +115,30 @@ describe('tenant-registry', () => { expect.objectContaining({ name: 'Shared platform resources' }), ]), ); + expect(plan.resourceChecklist.databases).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + name: 'mevy_brain', + status: 'would-create', + }), + ]), + ); + expect(plan.resourceChecklist.workerJails).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + name: 'mevy_ctrl_worker', + status: 'would-create', + }), + ]), + ); + expect(plan.resourceChecklist.datasets).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + name: 'zroot/mevy-ai', + status: 'would-create', + }), + ]), + ); expect(plan.manualSteps).toContain( 'No live resources are created by this workflow today.', ); diff --git a/src/tenant-registry.ts b/src/tenant-registry.ts index a5e2c9b..abbe753 100644 --- a/src/tenant-registry.ts +++ b/src/tenant-registry.ts @@ -139,6 +139,23 @@ export interface TenantApplyPlan { tenant: TenantRecord; registryPath: string; readyForAutomaticApply: boolean; + resourceChecklist: { + databases: Array<{ + name: string; + status: 'would-create' | 'exists-in-contract' | 'blocked'; + detail: string; + }>; + workerJails: Array<{ + name: string; + status: 'would-create' | 'exists-in-contract' | 'blocked'; + detail: string; + }>; + datasets: Array<{ + name: string; + status: 'would-create' | 'exists-in-contract' | 'blocked'; + detail: string; + }>; + }; actionPolicy: { automaticCandidates: Array<{ name: string; @@ -697,6 +714,26 @@ export function planTenantApply( tenant, registryPath, readyForAutomaticApply: false, + resourceChecklist: { + databases: Object.values(tenant.databases).map((name) => ({ + name, + status: 'would-create', + detail: + 'Future live apply could create this tenant-owned database, but no host mutation exists yet.', + })), + workerJails: tenant.workerJails.map((name) => ({ + name, + status: 'would-create', + detail: + 'Future live apply could provision this tenant-owned worker jail, subject to host-level review.', + })), + datasets: tenant.datasets.map((name) => ({ + name, + status: 'would-create', + detail: + 'Future live apply could create this tenant-owned dataset without touching shared platform datasets.', + })), + }, actionPolicy: { automaticCandidates: [ {