@celilo/cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1566 -0
- package/bin/celilo +16 -0
- package/drizzle/0000_complex_puma.sql +179 -0
- package/drizzle/0001_dizzy_wolfpack.sql +2 -0
- package/drizzle/0002_web_routes.sql +16 -0
- package/drizzle/0003_backup_storage.sql +32 -0
- package/drizzle/meta/0000_snapshot.json +1151 -0
- package/drizzle/meta/0001_snapshot.json +1167 -0
- package/drizzle/meta/0002_snapshot.json +1257 -0
- package/drizzle/meta/_journal.json +27 -0
- package/package.json +64 -0
- package/schemas/system_config.json +106 -0
- package/src/__integration__/container-services-cli.integration.test.ts +246 -0
- package/src/ansible/dependencies.test.ts +309 -0
- package/src/ansible/dependencies.ts +896 -0
- package/src/ansible/inventory.test.ts +463 -0
- package/src/ansible/inventory.ts +445 -0
- package/src/ansible/secrets.ts +222 -0
- package/src/ansible/validation.test.ts +92 -0
- package/src/ansible/validation.ts +272 -0
- package/src/api-clients/digitalocean.ts +94 -0
- package/src/api-clients/proxmox.ts +655 -0
- package/src/capabilities/logging-wrapper.test.ts +217 -0
- package/src/capabilities/lookup.test.ts +149 -0
- package/src/capabilities/lookup.ts +89 -0
- package/src/capabilities/public-web-helpers.test.ts +198 -0
- package/src/capabilities/public-web-publish.test.ts +458 -0
- package/src/capabilities/registration.test.ts +395 -0
- package/src/capabilities/registration.ts +200 -0
- package/src/capabilities/route-validation.test.ts +121 -0
- package/src/capabilities/route-validation.ts +96 -0
- package/src/capabilities/secret-ref.test.ts +313 -0
- package/src/capabilities/secret-validation.ts +157 -0
- package/src/capabilities/secrets.test.ts +750 -0
- package/src/capabilities/secrets.ts +244 -0
- package/src/capabilities/validation.test.ts +613 -0
- package/src/capabilities/validation.ts +160 -0
- package/src/capabilities/well-known.test.ts +238 -0
- package/src/capabilities/well-known.ts +222 -0
- package/src/cli/cli.test.ts +654 -0
- package/src/cli/command-registry.ts +742 -0
- package/src/cli/command-tree-parser.test.ts +180 -0
- package/src/cli/command-tree-parser.ts +193 -0
- package/src/cli/commands/backup-create.ts +137 -0
- package/src/cli/commands/backup-delete.ts +74 -0
- package/src/cli/commands/backup-import.ts +97 -0
- package/src/cli/commands/backup-list.ts +132 -0
- package/src/cli/commands/backup-name.ts +73 -0
- package/src/cli/commands/backup-prune.ts +98 -0
- package/src/cli/commands/backup-restore.ts +122 -0
- package/src/cli/commands/capability-info.ts +121 -0
- package/src/cli/commands/capability-list.ts +47 -0
- package/src/cli/commands/completion.ts +87 -0
- package/src/cli/commands/hook-run.ts +176 -0
- package/src/cli/commands/ipam.ts +607 -0
- package/src/cli/commands/machine-add.ts +235 -0
- package/src/cli/commands/machine-earmark.ts +82 -0
- package/src/cli/commands/machine-list.ts +77 -0
- package/src/cli/commands/machine-remove.ts +90 -0
- package/src/cli/commands/machine-status.ts +131 -0
- package/src/cli/commands/module-audit.ts +51 -0
- package/src/cli/commands/module-build.ts +60 -0
- package/src/cli/commands/module-config.ts +170 -0
- package/src/cli/commands/module-deploy.ts +71 -0
- package/src/cli/commands/module-generate.ts +236 -0
- package/src/cli/commands/module-health.ts +108 -0
- package/src/cli/commands/module-import.ts +80 -0
- package/src/cli/commands/module-list.ts +43 -0
- package/src/cli/commands/module-logs.ts +73 -0
- package/src/cli/commands/module-remove.ts +162 -0
- package/src/cli/commands/module-show.ts +208 -0
- package/src/cli/commands/module-status.ts +131 -0
- package/src/cli/commands/module-types.ts +189 -0
- package/src/cli/commands/module-upgrade.ts +192 -0
- package/src/cli/commands/package.ts +68 -0
- package/src/cli/commands/secret-list.ts +99 -0
- package/src/cli/commands/secret-set.ts +134 -0
- package/src/cli/commands/service-add-digitalocean.ts +133 -0
- package/src/cli/commands/service-add-proxmox.ts +342 -0
- package/src/cli/commands/service-config-get.ts +83 -0
- package/src/cli/commands/service-config-set.ts +145 -0
- package/src/cli/commands/service-list.ts +74 -0
- package/src/cli/commands/service-reconfigure.ts +230 -0
- package/src/cli/commands/service-remove.ts +103 -0
- package/src/cli/commands/service-verify.ts +240 -0
- package/src/cli/commands/status.ts +216 -0
- package/src/cli/commands/storage-add-local.ts +106 -0
- package/src/cli/commands/storage-add-s3.ts +114 -0
- package/src/cli/commands/storage-list.ts +72 -0
- package/src/cli/commands/storage-remove.ts +54 -0
- package/src/cli/commands/storage-set-default.ts +44 -0
- package/src/cli/commands/storage-verify.ts +54 -0
- package/src/cli/commands/system-config.ts +168 -0
- package/src/cli/commands/system-init.ts +314 -0
- package/src/cli/commands/system-secret-get.ts +98 -0
- package/src/cli/commands/system-secret-set.ts +76 -0
- package/src/cli/commands/system-vault-password.ts +34 -0
- package/src/cli/completion.test.ts +37 -0
- package/src/cli/completion.ts +482 -0
- package/src/cli/fuel-gauge.test.ts +208 -0
- package/src/cli/fuel-gauge.ts +405 -0
- package/src/cli/generate-zsh-completion.test.ts +95 -0
- package/src/cli/generate-zsh-completion.ts +497 -0
- package/src/cli/index.ts +1583 -0
- package/src/cli/interactive-config.test.ts +201 -0
- package/src/cli/interactive-config.ts +62 -0
- package/src/cli/parser.test.ts +227 -0
- package/src/cli/parser.ts +244 -0
- package/src/cli/prompts.test.ts +33 -0
- package/src/cli/prompts.ts +121 -0
- package/src/cli/types.ts +38 -0
- package/src/cli/validators.test.ts +235 -0
- package/src/cli/validators.ts +188 -0
- package/src/config/env.ts +41 -0
- package/src/config/paths.test.ts +172 -0
- package/src/config/paths.ts +108 -0
- package/src/db/client.ts +190 -0
- package/src/db/migrate.ts +30 -0
- package/src/db/schema.test.ts +221 -0
- package/src/db/schema.ts +434 -0
- package/src/hooks/capability-loader-firewall.test.ts +246 -0
- package/src/hooks/capability-loader.test.ts +100 -0
- package/src/hooks/capability-loader.ts +520 -0
- package/src/hooks/define-hook.test.ts +488 -0
- package/src/hooks/executor.test.ts +462 -0
- package/src/hooks/executor.ts +469 -0
- package/src/hooks/logger.test.ts +54 -0
- package/src/hooks/logger.ts +95 -0
- package/src/hooks/test-fixtures/failing-hook.ts +13 -0
- package/src/hooks/test-fixtures/no-default-hook.ts +6 -0
- package/src/hooks/test-fixtures/success-hook.ts +20 -0
- package/src/hooks/test-fixtures/unbranded-hook.ts +11 -0
- package/src/hooks/test-fixtures/void-hook.ts +13 -0
- package/src/hooks/types.ts +89 -0
- package/src/infrastructure/property-extractor.test.ts +194 -0
- package/src/infrastructure/property-extractor.ts +151 -0
- package/src/ipam/allocator.test.ts +442 -0
- package/src/ipam/allocator.ts +369 -0
- package/src/ipam/auto-allocator.test.ts +247 -0
- package/src/ipam/auto-allocator.ts +270 -0
- package/src/ipam/subnet-parser.test.ts +107 -0
- package/src/ipam/subnet-parser.ts +136 -0
- package/src/manifest/contracts/index.ts +61 -0
- package/src/manifest/contracts/v1.ts +118 -0
- package/src/manifest/json-schema-roundtrip.test.ts +99 -0
- package/src/manifest/schema.ts +367 -0
- package/src/manifest/template-validator.test.ts +231 -0
- package/src/manifest/template-validator.ts +322 -0
- package/src/manifest/validate.test.ts +1180 -0
- package/src/manifest/validate.ts +415 -0
- package/src/module/import.test.ts +355 -0
- package/src/module/import.ts +676 -0
- package/src/module/packaging/audit.ts +169 -0
- package/src/module/packaging/build.ts +228 -0
- package/src/module/packaging/checksum.ts +41 -0
- package/src/module/packaging/extract.ts +234 -0
- package/src/module/packaging/signature.ts +47 -0
- package/src/secrets/encryption.test.ts +284 -0
- package/src/secrets/encryption.ts +162 -0
- package/src/secrets/generators.test.ts +112 -0
- package/src/secrets/generators.ts +127 -0
- package/src/secrets/master-key.test.ts +159 -0
- package/src/secrets/master-key.ts +114 -0
- package/src/secrets/storage.test.ts +115 -0
- package/src/secrets/storage.ts +106 -0
- package/src/secrets/vault.test.ts +35 -0
- package/src/secrets/vault.ts +42 -0
- package/src/services/backup-create.ts +532 -0
- package/src/services/backup-metadata.ts +198 -0
- package/src/services/backup-restore.ts +229 -0
- package/src/services/backup-retention.ts +84 -0
- package/src/services/backup-storage.ts +281 -0
- package/src/services/build-stream.test.ts +122 -0
- package/src/services/build-stream.ts +201 -0
- package/src/services/config-interview.ts +694 -0
- package/src/services/container-service.test.ts +298 -0
- package/src/services/container-service.ts +401 -0
- package/src/services/cross-module-data-manager.test.ts +405 -0
- package/src/services/cross-module-data-manager.ts +412 -0
- package/src/services/deploy-ansible.ts +88 -0
- package/src/services/deploy-planner.ts +153 -0
- package/src/services/deploy-preflight.ts +274 -0
- package/src/services/deploy-ssh.ts +131 -0
- package/src/services/deploy-terraform.test.ts +55 -0
- package/src/services/deploy-terraform.ts +445 -0
- package/src/services/deploy-validation.ts +311 -0
- package/src/services/dns-auto-register.ts +211 -0
- package/src/services/health-runner.ts +184 -0
- package/src/services/infrastructure-selector.test.ts +485 -0
- package/src/services/infrastructure-selector.ts +245 -0
- package/src/services/infrastructure-variable-resolver.test.ts +751 -0
- package/src/services/infrastructure-variable-resolver.ts +234 -0
- package/src/services/machine-detector.ts +328 -0
- package/src/services/machine-pool.test.ts +405 -0
- package/src/services/machine-pool.ts +316 -0
- package/src/services/manifest-validation.ts +120 -0
- package/src/services/module-build.test.ts +290 -0
- package/src/services/module-build.ts +431 -0
- package/src/services/module-config.test.ts +237 -0
- package/src/services/module-config.ts +298 -0
- package/src/services/module-deploy.ts +862 -0
- package/src/services/module-types-drift.test.ts +73 -0
- package/src/services/module-types-generator.test.ts +288 -0
- package/src/services/module-types-generator.ts +189 -0
- package/src/services/proxmox-state-recovery.ts +140 -0
- package/src/services/schema-validation.ts +155 -0
- package/src/services/secret-schema-loader.test.ts +311 -0
- package/src/services/secret-schema-loader.ts +239 -0
- package/src/services/ssh-key-manager.test.ts +283 -0
- package/src/services/ssh-key-manager.ts +193 -0
- package/src/services/storage-providers/local.ts +105 -0
- package/src/services/storage-providers/s3.ts +182 -0
- package/src/services/storage-providers/types.ts +24 -0
- package/src/services/system-config-schema-types.ts +25 -0
- package/src/services/system-config-validator.test.ts +160 -0
- package/src/services/system-config-validator.ts +74 -0
- package/src/services/system-init.test.ts +153 -0
- package/src/services/system-init.ts +253 -0
- package/src/services/terraform-safety.ts +174 -0
- package/src/services/zone-detector.test.ts +110 -0
- package/src/services/zone-detector.ts +102 -0
- package/src/services/zone-policy.test.ts +97 -0
- package/src/services/zone-policy.ts +126 -0
- package/src/templates/generator.test.ts +645 -0
- package/src/templates/generator.ts +1119 -0
- package/src/templates/types.ts +62 -0
- package/src/test-utils/INTERACTIVE_PROMPTS.md +167 -0
- package/src/test-utils/cli-context-interactive.test.ts +152 -0
- package/src/test-utils/cli-context-server.test.ts +66 -0
- package/src/test-utils/cli-context.test.ts +273 -0
- package/src/test-utils/cli-context.ts +677 -0
- package/src/test-utils/cli-result.test.ts +282 -0
- package/src/test-utils/cli-result.ts +241 -0
- package/src/test-utils/cli.ts +55 -0
- package/src/test-utils/completion-harness.test.ts +126 -0
- package/src/test-utils/completion-harness.ts +82 -0
- package/src/test-utils/database.test.ts +182 -0
- package/src/test-utils/database.ts +126 -0
- package/src/test-utils/filesystem.test.ts +208 -0
- package/src/test-utils/filesystem.ts +142 -0
- package/src/test-utils/fixtures.test.ts +123 -0
- package/src/test-utils/fixtures.ts +160 -0
- package/src/test-utils/golden-diff.ts +197 -0
- package/src/test-utils/index.ts +77 -0
- package/src/test-utils/integration.ts +81 -0
- package/src/test-utils/module-fixtures.ts +468 -0
- package/src/test-utils/modules.test.ts +144 -0
- package/src/test-utils/modules.ts +183 -0
- package/src/test-utils/setup-test-db.ts +90 -0
- package/src/test-utils/value-extractor.test.ts +231 -0
- package/src/test-utils/value-extractor.ts +228 -0
- package/src/types/infrastructure.ts +157 -0
- package/src/utils/shell.test.ts +365 -0
- package/src/utils/shell.ts +159 -0
- package/src/validation/schemas.ts +166 -0
- package/src/variables/ansible-resolver.test.ts +142 -0
- package/src/variables/ansible-resolver.ts +69 -0
- package/src/variables/capability-self-ref.test.ts +220 -0
- package/src/variables/context.test.ts +1265 -0
- package/src/variables/context.ts +624 -0
- package/src/variables/declarative-derivation.test.ts +743 -0
- package/src/variables/declarative-derivation.ts +200 -0
- package/src/variables/parser.test.ts +231 -0
- package/src/variables/parser.ts +76 -0
- package/src/variables/resolver.test.ts +458 -0
- package/src/variables/resolver.ts +282 -0
- package/src/variables/types.ts +59 -0
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
import { KNOWN_CAPABILITY_NAMES } from '@celilo/capabilities';
|
|
2
|
+
import { parse as parseYaml } from 'yaml';
|
|
3
|
+
import type { ZodError } from 'zod';
|
|
4
|
+
import { validateModuleZoneRequirements } from '../services/zone-policy';
|
|
5
|
+
import { resolveContract, supportedContractVersions } from './contracts';
|
|
6
|
+
import { ModuleManifestSchema } from './schema';
|
|
7
|
+
import type { ModuleManifest } from './schema';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Validation result types
|
|
11
|
+
*/
|
|
12
|
+
export interface ValidationSuccess {
|
|
13
|
+
success: true;
|
|
14
|
+
data: ModuleManifest;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ValidationError {
|
|
18
|
+
success: false;
|
|
19
|
+
errors: Array<{
|
|
20
|
+
path: string;
|
|
21
|
+
message: string;
|
|
22
|
+
}>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type ValidationResult = ValidationSuccess | ValidationError;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Parse YAML string into unknown object
|
|
29
|
+
* This is the first stage - just parse the YAML
|
|
30
|
+
*/
|
|
31
|
+
function parseManifestYaml(yamlContent: string): unknown {
|
|
32
|
+
try {
|
|
33
|
+
return parseYaml(yamlContent);
|
|
34
|
+
} catch (error) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
`Failed to parse YAML: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Format Zod errors into readable validation errors
|
|
43
|
+
*/
|
|
44
|
+
function formatZodErrors(error: ZodError): ValidationError {
|
|
45
|
+
return {
|
|
46
|
+
success: false,
|
|
47
|
+
errors: error.errors.map((err) => ({
|
|
48
|
+
path: err.path.join('.'),
|
|
49
|
+
message: err.message,
|
|
50
|
+
})),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Validate module manifest
|
|
56
|
+
*
|
|
57
|
+
* Policy function (Rule 10.1) - validates input only
|
|
58
|
+
* Does NOT perform any side effects or business logic
|
|
59
|
+
*
|
|
60
|
+
* @param yamlContent - Raw YAML string from manifest.yml
|
|
61
|
+
* @returns Validation result with parsed manifest or errors
|
|
62
|
+
*/
|
|
63
|
+
export function validateManifest(yamlContent: string): ValidationResult {
|
|
64
|
+
if (!yamlContent || yamlContent.trim().length === 0) {
|
|
65
|
+
return {
|
|
66
|
+
success: false,
|
|
67
|
+
errors: [{ path: '', message: 'Manifest content cannot be empty' }],
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let parsed: unknown;
|
|
72
|
+
try {
|
|
73
|
+
parsed = parseManifestYaml(yamlContent);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
return {
|
|
76
|
+
success: false,
|
|
77
|
+
errors: [
|
|
78
|
+
{
|
|
79
|
+
path: '',
|
|
80
|
+
message: error instanceof Error ? error.message : 'Failed to parse YAML',
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const result = ModuleManifestSchema.safeParse(parsed);
|
|
87
|
+
|
|
88
|
+
if (!result.success) {
|
|
89
|
+
return formatZodErrors(result.error);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
success: true,
|
|
94
|
+
data: result.data,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Validate that required capabilities exist
|
|
100
|
+
*
|
|
101
|
+
* Policy function - checks capability requirements against available capabilities
|
|
102
|
+
* Does NOT perform database queries - caller provides capability list
|
|
103
|
+
*
|
|
104
|
+
* @param manifest - Validated manifest
|
|
105
|
+
* @param availableCapabilities - List of capability names available in the system
|
|
106
|
+
* @returns Validation errors if any required capabilities are missing
|
|
107
|
+
*/
|
|
108
|
+
export function validateCapabilityRequirements(
|
|
109
|
+
manifest: ModuleManifest,
|
|
110
|
+
availableCapabilities: string[],
|
|
111
|
+
): ValidationError | null {
|
|
112
|
+
const errors: Array<{ path: string; message: string }> = [];
|
|
113
|
+
|
|
114
|
+
for (const required of manifest.requires.capabilities) {
|
|
115
|
+
if (!availableCapabilities.includes(required.name)) {
|
|
116
|
+
errors.push({
|
|
117
|
+
path: `requires.capabilities.${required.name}`,
|
|
118
|
+
message: `Required capability '${required.name}' is not provided by any installed module`,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (errors.length > 0) {
|
|
124
|
+
return { success: false, errors };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Validate that every capability name listed in `requires.capabilities` and
|
|
132
|
+
* `optional.capabilities` is a known capability in the framework registry.
|
|
133
|
+
*
|
|
134
|
+
* Policy function (Rule 10.1) — pure validation, no side effects.
|
|
135
|
+
*
|
|
136
|
+
* Why (HOOK_API_V2 Phase 3 / D3): catches typos and stale references in the
|
|
137
|
+
* manifest before they become silent runtime no-ops. Without this check, a
|
|
138
|
+
* misspelled capability name (`requires: [{ name: dns_register }]`) would
|
|
139
|
+
* pass schema validation, fail to load any provider, and give the user a
|
|
140
|
+
* confusing "capability not provided" error far downstream. The known
|
|
141
|
+
* registry lives in `@celilo/capabilities` so the TS interface and the
|
|
142
|
+
* runtime list stay in sync.
|
|
143
|
+
*
|
|
144
|
+
* @param manifest - Validated manifest
|
|
145
|
+
* @returns Validation error if any name is unknown, null otherwise
|
|
146
|
+
*/
|
|
147
|
+
export function validateCapabilityNames(manifest: ModuleManifest): ValidationError | null {
|
|
148
|
+
const errors: Array<{ path: string; message: string }> = [];
|
|
149
|
+
const knownNames: readonly string[] = KNOWN_CAPABILITY_NAMES;
|
|
150
|
+
|
|
151
|
+
for (const required of manifest.requires.capabilities) {
|
|
152
|
+
if (!knownNames.includes(required.name)) {
|
|
153
|
+
errors.push({
|
|
154
|
+
path: `requires.capabilities.${required.name}`,
|
|
155
|
+
message: `Unknown capability '${required.name}'. Known capabilities: ${knownNames.join(', ')}.`,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
for (const opt of manifest.optional?.capabilities ?? []) {
|
|
161
|
+
if (!knownNames.includes(opt.name)) {
|
|
162
|
+
errors.push({
|
|
163
|
+
path: `optional.capabilities.${opt.name}`,
|
|
164
|
+
message: `Unknown capability '${opt.name}'. Known capabilities: ${knownNames.join(', ')}.`,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (errors.length > 0) {
|
|
170
|
+
return { success: false, errors };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Validate that variable sources are valid
|
|
178
|
+
*
|
|
179
|
+
* Policy function - checks that capability references in variables exist
|
|
180
|
+
*
|
|
181
|
+
* @param manifest - Validated manifest
|
|
182
|
+
* @returns Validation errors if any variable sources are invalid
|
|
183
|
+
*/
|
|
184
|
+
export function validateVariableSources(manifest: ModuleManifest): ValidationError | null {
|
|
185
|
+
const errors: Array<{ path: string; message: string }> = [];
|
|
186
|
+
|
|
187
|
+
// Variables can import from capabilities listed in requires OR optional —
|
|
188
|
+
// both are declared dependencies of this module.
|
|
189
|
+
const declaredCapabilityNames = new Set([
|
|
190
|
+
...manifest.requires.capabilities.map((c) => c.name),
|
|
191
|
+
...(manifest.optional?.capabilities ?? []).map((c) => c.name),
|
|
192
|
+
]);
|
|
193
|
+
|
|
194
|
+
for (const varImport of manifest.variables.imports) {
|
|
195
|
+
if (varImport.source === 'capability') {
|
|
196
|
+
const capabilityName = varImport.from.split('.')[0];
|
|
197
|
+
|
|
198
|
+
if (!capabilityName) {
|
|
199
|
+
errors.push({
|
|
200
|
+
path: `variables.imports.${varImport.name}`,
|
|
201
|
+
message: `Variable '${varImport.name}' has invalid capability reference: '${varImport.from}'`,
|
|
202
|
+
});
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (!declaredCapabilityNames.has(capabilityName)) {
|
|
207
|
+
errors.push({
|
|
208
|
+
path: `variables.imports.${varImport.name}`,
|
|
209
|
+
message: `Variable '${varImport.name}' imports capability '${capabilityName}' but module does not declare it in requires or optional`,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (errors.length > 0) {
|
|
216
|
+
return { success: false, errors };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Validate zone requirements for module capabilities
|
|
224
|
+
*
|
|
225
|
+
* Policy function - checks that module provides capabilities in correct zones
|
|
226
|
+
*
|
|
227
|
+
* Zone-based policy enforcement
|
|
228
|
+
* - Modules providing well-known capabilities must be deployed to specific zones
|
|
229
|
+
* - Example: public_web must be in 'dmz' zone (defense perimeter, internet-facing)
|
|
230
|
+
* - Example: public_web must be in 'dmz' zone (defense perimeter)
|
|
231
|
+
*
|
|
232
|
+
* @param manifest - Validated manifest
|
|
233
|
+
* @returns Validation errors if zone requirements are violated
|
|
234
|
+
*/
|
|
235
|
+
export function validateZoneRequirements(manifest: ModuleManifest): ValidationError | null {
|
|
236
|
+
const errors: Array<{ path: string; message: string }> = [];
|
|
237
|
+
|
|
238
|
+
// Check if module has infrastructure spec (requires.machine)
|
|
239
|
+
const hasInfrastructureSpec = manifest.requires?.machine;
|
|
240
|
+
|
|
241
|
+
// If module requires infrastructure, zone field is mandatory
|
|
242
|
+
if (hasInfrastructureSpec) {
|
|
243
|
+
const zone = manifest.requires?.machine?.zone;
|
|
244
|
+
|
|
245
|
+
if (!zone) {
|
|
246
|
+
errors.push({
|
|
247
|
+
path: 'requires.machine.zone',
|
|
248
|
+
message: 'Zone field is required for modules with infrastructure requirements',
|
|
249
|
+
});
|
|
250
|
+
// Can't validate zone requirements without a zone
|
|
251
|
+
return { success: false, errors };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Validate zone requirements for provided capabilities
|
|
255
|
+
if (manifest.provides.capabilities.length > 0) {
|
|
256
|
+
const capabilityNames = manifest.provides.capabilities.map((cap) => cap.name);
|
|
257
|
+
const zoneValidation = validateModuleZoneRequirements(capabilityNames, zone);
|
|
258
|
+
|
|
259
|
+
if (!zoneValidation.valid && zoneValidation.error) {
|
|
260
|
+
errors.push({
|
|
261
|
+
path: 'requires.machine.zone',
|
|
262
|
+
message: zoneValidation.error,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (errors.length > 0) {
|
|
269
|
+
return { success: false, errors };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Validate that the prefix of every variable's `derive_from` template
|
|
277
|
+
* matches its declared `source`.
|
|
278
|
+
*
|
|
279
|
+
* Why: a manifest like
|
|
280
|
+
* `source: capability, derive_from: "$system:primary_domain"`
|
|
281
|
+
* is incoherent — the source says the value comes from a capability but the
|
|
282
|
+
* derivation reads from system config. Zod can't catch this because both
|
|
283
|
+
* fields are individually valid; this validator enforces the coherence rule
|
|
284
|
+
* at runtime so the bug fails import instead of confusing a future reader.
|
|
285
|
+
*
|
|
286
|
+
* Rules:
|
|
287
|
+
* - `source: capability` → `derive_from` must only reference `$capability:`
|
|
288
|
+
* tokens (and may also include `{var}` placeholders).
|
|
289
|
+
* - `source: system` → `derive_from` must only reference `$system:` tokens
|
|
290
|
+
* (and `{var}` placeholders).
|
|
291
|
+
* - Other sources (`user`, `infrastructure`, `terraform`) — `derive_from` is
|
|
292
|
+
* optional and unconstrained; we don't check the prefix.
|
|
293
|
+
*/
|
|
294
|
+
export function validateDeriveFromSources(manifest: ModuleManifest): ValidationError | null {
|
|
295
|
+
const errors: Array<{ path: string; message: string }> = [];
|
|
296
|
+
|
|
297
|
+
// Match $foo:bar tokens (foo is the prefix, bar is the path)
|
|
298
|
+
const tokenPattern = /\$([a-z_]+):/g;
|
|
299
|
+
|
|
300
|
+
for (const variable of manifest.variables.owns) {
|
|
301
|
+
if (!variable.derive_from) continue;
|
|
302
|
+
|
|
303
|
+
let expected: string | null = null;
|
|
304
|
+
if (variable.source === 'capability') expected = 'capability';
|
|
305
|
+
else if (variable.source === 'system') expected = 'system';
|
|
306
|
+
if (!expected) continue;
|
|
307
|
+
|
|
308
|
+
const tokens = [...variable.derive_from.matchAll(tokenPattern)].map((m) => m[1]);
|
|
309
|
+
const offending = tokens.filter((t) => t !== expected);
|
|
310
|
+
if (offending.length > 0) {
|
|
311
|
+
const unique = Array.from(new Set(offending)).join(', ');
|
|
312
|
+
errors.push({
|
|
313
|
+
path: `variables.owns.${variable.name}.derive_from`,
|
|
314
|
+
message: `Variable '${variable.name}' has source: ${variable.source} but derive_from references $${unique}: tokens. Either change source to match or rewrite the derivation.`,
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (errors.length > 0) {
|
|
320
|
+
return { success: false, errors };
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Validate that every declared hook is part of the contract version the
|
|
328
|
+
* manifest targets.
|
|
329
|
+
*
|
|
330
|
+
* Why: the Zod schema's `.strict()` on the hooks block already prevents
|
|
331
|
+
* unknown hook names structurally, but the contract registry is the
|
|
332
|
+
* authoritative list of which hooks Celilo will actually invoke. This
|
|
333
|
+
* validator surfaces a clear error if someone declares a hook that the
|
|
334
|
+
* declared contract version doesn't promise to call.
|
|
335
|
+
*/
|
|
336
|
+
export function validateHookContract(manifest: ModuleManifest): ValidationError | null {
|
|
337
|
+
const errors: Array<{ path: string; message: string }> = [];
|
|
338
|
+
|
|
339
|
+
const contract = resolveContract(manifest.celilo_contract);
|
|
340
|
+
if (!contract) {
|
|
341
|
+
errors.push({
|
|
342
|
+
path: 'celilo_contract',
|
|
343
|
+
message: `Unsupported celilo_contract version '${manifest.celilo_contract}'. Supported: ${supportedContractVersions().join(', ')}`,
|
|
344
|
+
});
|
|
345
|
+
return { success: false, errors };
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (!manifest.hooks) {
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
for (const hookName of Object.keys(manifest.hooks)) {
|
|
353
|
+
if (!(hookName in contract.hooks)) {
|
|
354
|
+
errors.push({
|
|
355
|
+
path: `hooks.${hookName}`,
|
|
356
|
+
message: `Hook '${hookName}' is not part of celilo_contract ${manifest.celilo_contract}. Known hooks: ${Object.keys(contract.hooks).join(', ')}`,
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (errors.length > 0) {
|
|
362
|
+
return { success: false, errors };
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Validate that capability data templates in `provides.capabilities[].data`
|
|
370
|
+
* do not contain cross-capability references.
|
|
371
|
+
*
|
|
372
|
+
* Why (D9 firm rule): well-known and module-provided capability data
|
|
373
|
+
* templates must not embed `$capability:other_capability.x` references. If a
|
|
374
|
+
* capability value depends on another capability, the providing module
|
|
375
|
+
* derives it in its own variables and exposes the resolved value through
|
|
376
|
+
* `provides.capabilities[].data`. Cross-capability references in data
|
|
377
|
+
* templates create implicit ordering dependencies between capability
|
|
378
|
+
* providers, which the variable resolver isn't designed to handle and which
|
|
379
|
+
* make the dependency graph hard to reason about.
|
|
380
|
+
*/
|
|
381
|
+
export function validateProvidesNoCrossCapabilityRefs(
|
|
382
|
+
manifest: ModuleManifest,
|
|
383
|
+
): ValidationError | null {
|
|
384
|
+
const errors: Array<{ path: string; message: string }> = [];
|
|
385
|
+
|
|
386
|
+
function walk(node: unknown, path: string): void {
|
|
387
|
+
if (typeof node === 'string') {
|
|
388
|
+
if (node.includes('$capability:')) {
|
|
389
|
+
errors.push({
|
|
390
|
+
path,
|
|
391
|
+
message: `Capability data template at ${path} contains a $capability: reference. Per D9, capability data must not cross-reference other capabilities — derive the value in this module's variables instead and expose the resolved value here.`,
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
if (node === null || typeof node !== 'object') return;
|
|
397
|
+
if (Array.isArray(node)) {
|
|
398
|
+
node.forEach((item, idx) => walk(item, `${path}[${idx}]`));
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
for (const [key, value] of Object.entries(node as Record<string, unknown>)) {
|
|
402
|
+
walk(value, `${path}.${key}`);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
for (const cap of manifest.provides.capabilities) {
|
|
407
|
+
walk(cap.data, `provides.capabilities.${cap.name}.data`);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (errors.length > 0) {
|
|
411
|
+
return { success: false, errors };
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return null;
|
|
415
|
+
}
|