@celilo/cli 4.0.0 → 5.0.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/AGENTS.md +2 -2
- package/CELILO_SUBSYSTEMS.md +1 -0
- package/drizzle/0034_private_routes.sql +41 -0
- package/drizzle/meta/_journal.json +8 -1
- package/package.json +5 -4
- package/src/api/sessions.test.ts +1 -1
- package/src/capabilities/registration.test.ts +11 -11
- package/src/capabilities/secrets.test.ts +18 -18
- package/src/capabilities/validation.test.ts +24 -24
- package/src/cli/cli.test.ts +3 -3
- package/src/cli/command-tree-parser.test.ts +18 -0
- package/src/cli/command-tree-parser.ts +12 -0
- package/src/cli/commands/api.ts +67 -14
- package/src/cli/commands/events.ts +5 -2
- package/src/cli/commands/machine-reclassify.ts +79 -0
- package/src/cli/commands/module-changeset.test.ts +1 -1
- package/src/cli/commands/module-generate.test.ts +2 -2
- package/src/cli/commands/module-import-aspect.test.ts +2 -2
- package/src/cli/commands/module-update.test.ts +14 -14
- package/src/cli/commands/module-upgrade-gate.test.ts +26 -1
- package/src/cli/commands/module-upgrade.test.ts +3 -3
- package/src/cli/commands/module-upgrade.ts +12 -0
- package/src/cli/commands/module-version.test.ts +1 -1
- package/src/cli/commands/system-audit.ts +22 -0
- package/src/cli/commands/system-reboot.ts +126 -0
- package/src/cli/commands/system-update.ts +5 -0
- package/src/cli/completion.ts +3 -2
- package/src/cli/index.ts +41 -2
- package/src/cli/restore-migration-failure.test.ts +1 -1
- package/src/cli/stdout-pipe-flush.test.ts +35 -0
- package/src/cli/tui/audit-state.ts +4 -0
- package/src/db/private-routes.test.ts +159 -0
- package/src/db/schema.ts +49 -0
- package/src/hooks/broker.ts +31 -0
- package/src/hooks/capability-loader.ts +185 -12
- package/src/hooks/executor.test.ts +13 -13
- package/src/hooks/executor.ts +20 -2
- package/src/hooks/hook-state-dir.test.ts +2 -2
- package/src/hooks/test-fixtures/slow-capability-hook.ts +28 -0
- package/src/hooks/test-fixtures/store-backed.ts +5 -3
- package/src/hooks/timeout-names-provider.test.ts +87 -0
- package/src/manifest/contracts/index.ts +28 -7
- package/src/manifest/contracts/v2.ts +32 -0
- package/src/manifest/icon-schema.test.ts +1 -1
- package/src/manifest/schema.ts +13 -2
- package/src/manifest/template-validator.test.ts +1 -1
- package/src/manifest/validate-privileged.test.ts +1 -1
- package/src/manifest/validate.test.ts +68 -29
- package/src/manifest/validate.ts +14 -1
- package/src/module/import.test.ts +7 -7
- package/src/module/packaging/build.test.ts +1 -1
- package/src/packaging/nfpm-depends.test.ts +42 -0
- package/src/policy/capability-shape-baseline.ts +7 -1
- package/src/policy/fixture-capability-coverage.test.ts +5 -5
- package/src/policy/module-business-baseline.ts +33 -4
- package/src/services/api-access.test.ts +44 -0
- package/src/services/aspect-approvals.test.ts +2 -2
- package/src/services/aspect-reconcile.test.ts +2 -2
- package/src/services/aspect-runner.test.ts +4 -4
- package/src/services/audit/backups.test.ts +1 -1
- package/src/services/audit/capability-abi.test.ts +1 -1
- package/src/services/audit/index.test.ts +7 -0
- package/src/services/audit/index.ts +15 -0
- package/src/services/audit/machine-interface-zones.test.ts +30 -0
- package/src/services/audit/machine-interface-zones.ts +43 -0
- package/src/services/audit/module-configs.test.ts +1 -1
- package/src/services/audit/reboot-pending.test.ts +63 -0
- package/src/services/audit/reboot-pending.ts +85 -0
- package/src/services/audit/recurrence-gate.test.ts +21 -0
- package/src/services/audit/server-bun-pin.test.ts +44 -0
- package/src/services/audit/server-bun-pin.ts +95 -0
- package/src/services/audit/types.ts +2 -0
- package/src/services/build-bus/hook-dispatch-executor.test.ts +3 -3
- package/src/services/bus-interview-park.test.ts +1 -1
- package/src/services/bus-secret-flow.test.ts +1 -1
- package/src/services/capability-compat.test.ts +22 -0
- package/src/services/capability-compat.ts +15 -2
- package/src/services/capability-table-rows.test.ts +20 -2
- package/src/services/celilo-events.test.ts +119 -6
- package/src/services/celilo-events.ts +156 -16
- package/src/services/celilo-mgmt-hooks.test.ts +6 -3
- package/src/services/cross-module-read.test.ts +2 -2
- package/src/services/deploy-preflight.ts +12 -3
- package/src/services/deploy-validation.test.ts +86 -7
- package/src/services/deploy-validation.ts +19 -0
- package/src/services/dns-provider-backfill.test.ts +2 -2
- package/src/services/dns-registrations.test.ts +1 -1
- package/src/services/fleet-checks.test.ts +2 -2
- package/src/services/fleet-checks.ts +40 -10
- package/src/services/health-runner.test.ts +1 -1
- package/src/services/infrastructure-variable-resolver.test.ts +11 -11
- package/src/services/instance-ops.test.ts +2 -2
- package/src/services/machine-interface-zones.test.ts +73 -0
- package/src/services/machine-interface-zones.ts +101 -0
- package/src/services/module-deploy.dns-repoint.test.ts +1 -1
- package/src/services/module-deploy.stale-provider-guard.test.ts +98 -0
- package/src/services/module-deploy.ts +31 -10
- package/src/services/module-pause-quiescence.test.ts +1 -1
- package/src/services/module-pause.test.ts +1 -1
- package/src/services/module-subscriptions.test.ts +2 -2
- package/src/services/module-types-generator.test.ts +1 -1
- package/src/services/module-validator/contract-version.test.ts +3 -3
- package/src/services/network-ensure.test.ts +1 -1
- package/src/services/provider-arrival.test.ts +62 -46
- package/src/services/provider-arrival.ts +41 -10
- package/src/services/proxmox-reconcile.test.ts +2 -2
- package/src/services/remove-guard.test.ts +1 -1
- package/src/services/system-reboot.test.ts +231 -0
- package/src/services/system-reboot.ts +357 -0
- package/src/services/update/dep-graph.test.ts +1 -1
- package/src/services/update/orchestrator.test.ts +165 -1
- package/src/services/update/orchestrator.ts +43 -12
- package/src/services/update/types.ts +10 -1
- package/src/templates/generator.test.ts +15 -4
- package/src/templates/generator.ts +12 -4
- package/src/variables/capability-self-ref.test.ts +2 -2
- package/src/variables/computed/computed-integration.test.ts +1 -1
- package/src/variables/context.ts +14 -1
- package/src/variables/declarative-derivation.test.ts +19 -19
package/src/manifest/schema.ts
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
MONITOR_INTERVAL_FLOOR_MINUTES,
|
|
7
7
|
cadenceSchema,
|
|
8
8
|
} from '../services/cadence';
|
|
9
|
-
import {
|
|
9
|
+
import { KNOWN_CONTRACT_VERSIONS } from './contracts';
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Variable declaration sources
|
|
@@ -610,7 +610,18 @@ export const ModuleManifestSchema = z
|
|
|
610
610
|
* here so a future v2 contract can change requirements without breaking
|
|
611
611
|
* v1 modules.
|
|
612
612
|
*/
|
|
613
|
-
|
|
613
|
+
/**
|
|
614
|
+
* The enum accepts RETIRED versions as well as supported ones. Refusing a
|
|
615
|
+
* retired version is a POLICY about importing a manifest, and it lives in
|
|
616
|
+
* `validateManifest`, not here: this schema is also what re-parses the
|
|
617
|
+
* manifest celilo already STORED for an installed module
|
|
618
|
+
* (`module-pause.ts`, `module-remove.ts`, `provider-arrival.ts`), and each
|
|
619
|
+
* of those `continue`s past a parse failure. Refusing here would make
|
|
620
|
+
* every still-unmigrated installed module silently vanish from the
|
|
621
|
+
* dependent-removal check and the provider backfill — a narrowing with no
|
|
622
|
+
* error, which is the exact failure mode this change exists to remove.
|
|
623
|
+
*/
|
|
624
|
+
celilo_contract: z.enum(KNOWN_CONTRACT_VERSIONS),
|
|
614
625
|
|
|
615
626
|
id: z
|
|
616
627
|
.string()
|
|
@@ -10,7 +10,7 @@ import { validatePrivilegedCapabilities } from './validate';
|
|
|
10
10
|
|
|
11
11
|
function buildManifest(overrides: Partial<ModuleManifest>): ModuleManifest {
|
|
12
12
|
return {
|
|
13
|
-
celilo_contract: '
|
|
13
|
+
celilo_contract: '2.0',
|
|
14
14
|
id: 'some-module',
|
|
15
15
|
name: 'some module',
|
|
16
16
|
version: '0.0.1',
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test';
|
|
2
|
-
import { getSingularSystemSpec } from './schema';
|
|
2
|
+
import { ModuleManifestSchema, getSingularSystemSpec } from './schema';
|
|
3
3
|
import {
|
|
4
4
|
validateCapabilityNames,
|
|
5
5
|
validateCapabilityRequirements,
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
validateVariableSources,
|
|
13
13
|
} from './validate';
|
|
14
14
|
|
|
15
|
-
const CONTRACT_LINE = 'celilo_contract: "
|
|
15
|
+
const CONTRACT_LINE = 'celilo_contract: "2.0"';
|
|
16
16
|
|
|
17
17
|
describe('validateManifest', () => {
|
|
18
18
|
test('should validate minimal valid manifest', () => {
|
|
@@ -251,6 +251,45 @@ version: 1.0.0
|
|
|
251
251
|
expect(result.success).toBe(false);
|
|
252
252
|
});
|
|
253
253
|
|
|
254
|
+
test('a stored manifest at a retired version still PARSES — only importing one is refused', () => {
|
|
255
|
+
// module-pause.ts, module-remove.ts and provider-arrival.ts re-parse the
|
|
256
|
+
// manifest celilo already stored, and each `continue`s past a failure. If
|
|
257
|
+
// the retirement moved onto the schema, every installed module not yet
|
|
258
|
+
// migrated would silently vanish from the dependent-removal check and the
|
|
259
|
+
// provider backfill — a narrowing with no error, which is the failure mode
|
|
260
|
+
// this whole change exists to remove.
|
|
261
|
+
const stored = ModuleManifestSchema.safeParse({
|
|
262
|
+
celilo_contract: '1.0',
|
|
263
|
+
id: 'legacy-module',
|
|
264
|
+
name: 'Legacy',
|
|
265
|
+
version: '1.0.0',
|
|
266
|
+
});
|
|
267
|
+
expect(stored.success).toBe(true);
|
|
268
|
+
|
|
269
|
+
const imported = validateManifest(
|
|
270
|
+
'celilo_contract: "1.0"\nid: legacy-module\nname: Legacy\nversion: 1.0.0\n',
|
|
271
|
+
);
|
|
272
|
+
expect(imported.success).toBe(false);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test('should reject a retired celilo_contract version, naming the migration', () => {
|
|
276
|
+
// The whole point of retiring 1.0 rather than deleting it: a 1.0 module
|
|
277
|
+
// deploys fine and silently stores nothing, so the refusal has to say why.
|
|
278
|
+
const yaml = `
|
|
279
|
+
celilo_contract: "1.0"
|
|
280
|
+
id: test-module
|
|
281
|
+
name: Test
|
|
282
|
+
version: 1.0.0
|
|
283
|
+
`;
|
|
284
|
+
const result = validateManifest(yaml);
|
|
285
|
+
expect(result.success).toBe(false);
|
|
286
|
+
if (!result.success) {
|
|
287
|
+
const message = result.errors.find((e) => e.path === 'celilo_contract')?.message ?? '';
|
|
288
|
+
expect(message).toContain('retired');
|
|
289
|
+
expect(message).toContain('context.secrets.set');
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
|
|
254
293
|
test('should reject manifest with unknown top-level key', () => {
|
|
255
294
|
const yaml = `
|
|
256
295
|
${CONTRACT_LINE}
|
|
@@ -653,7 +692,7 @@ version: 1.0.0
|
|
|
653
692
|
describe('validateCapabilityNames', () => {
|
|
654
693
|
test('accepts known capability names in requires', () => {
|
|
655
694
|
const manifest = {
|
|
656
|
-
celilo_contract: '
|
|
695
|
+
celilo_contract: '2.0' as const,
|
|
657
696
|
id: 'lunacycle',
|
|
658
697
|
name: 'LunaCycle',
|
|
659
698
|
version: '1.0.0',
|
|
@@ -672,7 +711,7 @@ describe('validateCapabilityNames', () => {
|
|
|
672
711
|
|
|
673
712
|
test('accepts known capability names in optional', () => {
|
|
674
713
|
const manifest = {
|
|
675
|
-
celilo_contract: '
|
|
714
|
+
celilo_contract: '2.0' as const,
|
|
676
715
|
id: 'caddy',
|
|
677
716
|
name: 'Caddy',
|
|
678
717
|
version: '1.0.0',
|
|
@@ -689,7 +728,7 @@ describe('validateCapabilityNames', () => {
|
|
|
689
728
|
|
|
690
729
|
test('rejects unknown capability name in requires', () => {
|
|
691
730
|
const manifest = {
|
|
692
|
-
celilo_contract: '
|
|
731
|
+
celilo_contract: '2.0' as const,
|
|
693
732
|
id: 'broken',
|
|
694
733
|
name: 'Broken',
|
|
695
734
|
version: '1.0.0',
|
|
@@ -712,7 +751,7 @@ describe('validateCapabilityNames', () => {
|
|
|
712
751
|
|
|
713
752
|
test('rejects unknown capability name in optional', () => {
|
|
714
753
|
const manifest = {
|
|
715
|
-
celilo_contract: '
|
|
754
|
+
celilo_contract: '2.0' as const,
|
|
716
755
|
id: 'broken',
|
|
717
756
|
name: 'Broken',
|
|
718
757
|
version: '1.0.0',
|
|
@@ -734,7 +773,7 @@ describe('validateCapabilityNames', () => {
|
|
|
734
773
|
|
|
735
774
|
test('reports both required and optional unknowns in one pass', () => {
|
|
736
775
|
const manifest = {
|
|
737
|
-
celilo_contract: '
|
|
776
|
+
celilo_contract: '2.0' as const,
|
|
738
777
|
id: 'broken',
|
|
739
778
|
name: 'Broken',
|
|
740
779
|
version: '1.0.0',
|
|
@@ -757,7 +796,7 @@ describe('validateCapabilityNames', () => {
|
|
|
757
796
|
|
|
758
797
|
test('passes when no capabilities declared', () => {
|
|
759
798
|
const manifest = {
|
|
760
|
-
celilo_contract: '
|
|
799
|
+
celilo_contract: '2.0' as const,
|
|
761
800
|
id: 'standalone',
|
|
762
801
|
name: 'Standalone',
|
|
763
802
|
version: '1.0.0',
|
|
@@ -773,7 +812,7 @@ describe('validateCapabilityNames', () => {
|
|
|
773
812
|
describe('validateCapabilityRequirements', () => {
|
|
774
813
|
test('should pass when all required capabilities are available', () => {
|
|
775
814
|
const manifest = {
|
|
776
|
-
celilo_contract: '
|
|
815
|
+
celilo_contract: '2.0' as const,
|
|
777
816
|
id: 'caddy',
|
|
778
817
|
name: 'Caddy',
|
|
779
818
|
version: '1.0.0',
|
|
@@ -791,7 +830,7 @@ describe('validateCapabilityRequirements', () => {
|
|
|
791
830
|
|
|
792
831
|
test('should fail when required capability is missing', () => {
|
|
793
832
|
const manifest = {
|
|
794
|
-
celilo_contract: '
|
|
833
|
+
celilo_contract: '2.0' as const,
|
|
795
834
|
id: 'caddy',
|
|
796
835
|
name: 'Caddy',
|
|
797
836
|
version: '1.0.0',
|
|
@@ -816,7 +855,7 @@ describe('validateCapabilityRequirements', () => {
|
|
|
816
855
|
|
|
817
856
|
test('should pass when no capabilities required', () => {
|
|
818
857
|
const manifest = {
|
|
819
|
-
celilo_contract: '
|
|
858
|
+
celilo_contract: '2.0' as const,
|
|
820
859
|
id: 'homebridge',
|
|
821
860
|
name: 'Homebridge',
|
|
822
861
|
version: '1.0.0',
|
|
@@ -834,7 +873,7 @@ describe('validateCapabilityRequirements', () => {
|
|
|
834
873
|
describe('validateVariableSources', () => {
|
|
835
874
|
test('should pass when imported capability variables reference required capabilities', () => {
|
|
836
875
|
const manifest = {
|
|
837
|
-
celilo_contract: '
|
|
876
|
+
celilo_contract: '2.0' as const,
|
|
838
877
|
id: 'caddy',
|
|
839
878
|
name: 'Caddy',
|
|
840
879
|
version: '1.0.0',
|
|
@@ -862,7 +901,7 @@ describe('validateVariableSources', () => {
|
|
|
862
901
|
|
|
863
902
|
test('should fail when imported capability variable references unrequired capability', () => {
|
|
864
903
|
const manifest = {
|
|
865
|
-
celilo_contract: '
|
|
904
|
+
celilo_contract: '2.0' as const,
|
|
866
905
|
id: 'caddy',
|
|
867
906
|
name: 'Caddy',
|
|
868
907
|
version: '1.0.0',
|
|
@@ -891,7 +930,7 @@ describe('validateVariableSources', () => {
|
|
|
891
930
|
|
|
892
931
|
test('should pass when imported capability variable references an optional capability', () => {
|
|
893
932
|
const manifest = {
|
|
894
|
-
celilo_contract: '
|
|
933
|
+
celilo_contract: '2.0' as const,
|
|
895
934
|
id: 'caddy',
|
|
896
935
|
name: 'Caddy',
|
|
897
936
|
version: '1.0.0',
|
|
@@ -919,7 +958,7 @@ describe('validateVariableSources', () => {
|
|
|
919
958
|
|
|
920
959
|
test('should fail when imported capability variable has invalid reference format', () => {
|
|
921
960
|
const manifest = {
|
|
922
|
-
celilo_contract: '
|
|
961
|
+
celilo_contract: '2.0' as const,
|
|
923
962
|
id: 'test',
|
|
924
963
|
name: 'Test',
|
|
925
964
|
version: '1.0.0',
|
|
@@ -941,7 +980,7 @@ describe('validateVariableSources', () => {
|
|
|
941
980
|
|
|
942
981
|
test('should pass when no variables import capabilities', () => {
|
|
943
982
|
const manifest = {
|
|
944
|
-
celilo_contract: '
|
|
983
|
+
celilo_contract: '2.0' as const,
|
|
945
984
|
id: 'homebridge',
|
|
946
985
|
name: 'Homebridge',
|
|
947
986
|
version: '1.0.0',
|
|
@@ -969,7 +1008,7 @@ describe('validateVariableSources', () => {
|
|
|
969
1008
|
describe('validateDeriveFromSources', () => {
|
|
970
1009
|
test('should pass when capability source uses $capability: prefix', () => {
|
|
971
1010
|
const manifest = {
|
|
972
|
-
celilo_contract: '
|
|
1011
|
+
celilo_contract: '2.0' as const,
|
|
973
1012
|
id: 'lunacycle',
|
|
974
1013
|
name: 'LunaCycle',
|
|
975
1014
|
version: '1.0.0',
|
|
@@ -996,7 +1035,7 @@ describe('validateDeriveFromSources', () => {
|
|
|
996
1035
|
test('should reject capability source with $system: prefix in derive_from (regression)', () => {
|
|
997
1036
|
// This is the original lunacycle bug that motivated D7.
|
|
998
1037
|
const manifest = {
|
|
999
|
-
celilo_contract: '
|
|
1038
|
+
celilo_contract: '2.0' as const,
|
|
1000
1039
|
id: 'lunacycle',
|
|
1001
1040
|
name: 'LunaCycle',
|
|
1002
1041
|
version: '1.0.0',
|
|
@@ -1026,7 +1065,7 @@ describe('validateDeriveFromSources', () => {
|
|
|
1026
1065
|
|
|
1027
1066
|
test('should pass for variables without derive_from', () => {
|
|
1028
1067
|
const manifest = {
|
|
1029
|
-
celilo_contract: '
|
|
1068
|
+
celilo_contract: '2.0' as const,
|
|
1030
1069
|
id: 'test',
|
|
1031
1070
|
name: 'Test',
|
|
1032
1071
|
version: '1.0.0',
|
|
@@ -1055,7 +1094,7 @@ describe('validateDeriveFromSources', () => {
|
|
|
1055
1094
|
// doesn't introduce the cross-context mismatch the validator exists
|
|
1056
1095
|
// to catch (e.g. authentik's auth_url derives from $self:domain).
|
|
1057
1096
|
const manifest = {
|
|
1058
|
-
celilo_contract: '
|
|
1097
|
+
celilo_contract: '2.0' as const,
|
|
1059
1098
|
id: 'authentik',
|
|
1060
1099
|
name: 'Authentik',
|
|
1061
1100
|
version: '1.0.0',
|
|
@@ -1090,7 +1129,7 @@ describe('validateDeriveFromSources', () => {
|
|
|
1090
1129
|
// mismatch error. This guards against accidentally green-lighting
|
|
1091
1130
|
// "$self:foo and $system:bar" combos.
|
|
1092
1131
|
const manifest = {
|
|
1093
|
-
celilo_contract: '
|
|
1132
|
+
celilo_contract: '2.0' as const,
|
|
1094
1133
|
id: 'test',
|
|
1095
1134
|
name: 'Test',
|
|
1096
1135
|
version: '1.0.0',
|
|
@@ -1122,7 +1161,7 @@ describe('validateDeriveFromSources', () => {
|
|
|
1122
1161
|
describe('validateHookContract', () => {
|
|
1123
1162
|
test('should pass when all declared hooks are in v1 contract', () => {
|
|
1124
1163
|
const manifest = {
|
|
1125
|
-
celilo_contract: '
|
|
1164
|
+
celilo_contract: '2.0' as const,
|
|
1126
1165
|
id: 'test',
|
|
1127
1166
|
name: 'Test',
|
|
1128
1167
|
version: '1.0.0',
|
|
@@ -1141,7 +1180,7 @@ describe('validateHookContract', () => {
|
|
|
1141
1180
|
|
|
1142
1181
|
test('should pass for manifests with no hooks block', () => {
|
|
1143
1182
|
const manifest = {
|
|
1144
|
-
celilo_contract: '
|
|
1183
|
+
celilo_contract: '2.0' as const,
|
|
1145
1184
|
id: 'test',
|
|
1146
1185
|
name: 'Test',
|
|
1147
1186
|
version: '1.0.0',
|
|
@@ -1158,7 +1197,7 @@ describe('validateHookContract', () => {
|
|
|
1158
1197
|
describe('validateProvidesNoCrossCapabilityRefs', () => {
|
|
1159
1198
|
test('should pass when capability data has no cross-capability references', () => {
|
|
1160
1199
|
const manifest = {
|
|
1161
|
-
celilo_contract: '
|
|
1200
|
+
celilo_contract: '2.0' as const,
|
|
1162
1201
|
id: 'authentik',
|
|
1163
1202
|
name: 'Authentik',
|
|
1164
1203
|
version: '1.0.0',
|
|
@@ -1184,7 +1223,7 @@ describe('validateProvidesNoCrossCapabilityRefs', () => {
|
|
|
1184
1223
|
|
|
1185
1224
|
test('should reject capability data containing $capability: reference (D9 firm rule)', () => {
|
|
1186
1225
|
const manifest = {
|
|
1187
|
-
celilo_contract: '
|
|
1226
|
+
celilo_contract: '2.0' as const,
|
|
1188
1227
|
id: 'authentik',
|
|
1189
1228
|
name: 'Authentik',
|
|
1190
1229
|
version: '1.0.0',
|
|
@@ -1536,7 +1575,7 @@ base_module_aspect:
|
|
|
1536
1575
|
|
|
1537
1576
|
describe('submodules (openspec/changes/submodules D1, D9)', () => {
|
|
1538
1577
|
const parent = (over: Record<string, unknown> = {}) => ({
|
|
1539
|
-
celilo_contract: '
|
|
1578
|
+
celilo_contract: '2.0' as const,
|
|
1540
1579
|
id: 'forgejo',
|
|
1541
1580
|
name: 'Forgejo',
|
|
1542
1581
|
version: '1.0.0',
|
|
@@ -1547,7 +1586,7 @@ describe('submodules (openspec/changes/submodules D1, D9)', () => {
|
|
|
1547
1586
|
});
|
|
1548
1587
|
|
|
1549
1588
|
const submodule = (over: Record<string, unknown> = {}) => ({
|
|
1550
|
-
celilo_contract: '
|
|
1589
|
+
celilo_contract: '2.0' as const,
|
|
1551
1590
|
id: 'runner',
|
|
1552
1591
|
name: 'Forgejo Runner',
|
|
1553
1592
|
version: '1.0.0',
|
|
@@ -1642,7 +1681,7 @@ describe('submodules (openspec/changes/submodules D1, D9)', () => {
|
|
|
1642
1681
|
describe('schema', () => {
|
|
1643
1682
|
test('parses a manifest declaring submodules', () => {
|
|
1644
1683
|
const yaml = `
|
|
1645
|
-
celilo_contract: "
|
|
1684
|
+
celilo_contract: "2.0"
|
|
1646
1685
|
id: forgejo
|
|
1647
1686
|
name: Forgejo
|
|
1648
1687
|
version: 1.0.0
|
|
@@ -1658,7 +1697,7 @@ requires:
|
|
|
1658
1697
|
|
|
1659
1698
|
test('rejects a non-kebab-case submodule name', () => {
|
|
1660
1699
|
const yaml = `
|
|
1661
|
-
celilo_contract: "
|
|
1700
|
+
celilo_contract: "2.0"
|
|
1662
1701
|
id: forgejo
|
|
1663
1702
|
name: Forgejo
|
|
1664
1703
|
version: 1.0.0
|
package/src/manifest/validate.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { parse as parseYaml } from 'yaml';
|
|
|
3
3
|
import type { ZodError } from 'zod';
|
|
4
4
|
import { SUBMODULES_DIR } from '../module/packaging/package-rules';
|
|
5
5
|
import { validateModuleZoneRequirements } from '../services/zone-policy';
|
|
6
|
-
import { resolveContract, supportedContractVersions } from './contracts';
|
|
6
|
+
import { RETIRED_CONTRACT_VERSIONS, resolveContract, supportedContractVersions } from './contracts';
|
|
7
7
|
import { ModuleManifestSchema, getSingularSystemSpec } from './schema';
|
|
8
8
|
import type { ModuleManifest } from './schema';
|
|
9
9
|
|
|
@@ -90,6 +90,19 @@ export function validateManifest(yamlContent: string): ValidationResult {
|
|
|
90
90
|
return formatZodErrors(result.error);
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
const retired = RETIRED_CONTRACT_VERSIONS[result.data.celilo_contract];
|
|
94
|
+
if (retired) {
|
|
95
|
+
return {
|
|
96
|
+
success: false,
|
|
97
|
+
errors: [
|
|
98
|
+
{
|
|
99
|
+
path: 'celilo_contract',
|
|
100
|
+
message: `celilo_contract '${result.data.celilo_contract}' is retired: ${retired}`,
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
93
106
|
return {
|
|
94
107
|
success: true,
|
|
95
108
|
data: result.data,
|
|
@@ -120,7 +120,7 @@ describe('Module Import', () => {
|
|
|
120
120
|
await writeFile(
|
|
121
121
|
join(moduleDir, 'manifest.yml'),
|
|
122
122
|
`
|
|
123
|
-
celilo_contract: "
|
|
123
|
+
celilo_contract: "2.0"
|
|
124
124
|
id: homebridge
|
|
125
125
|
name: Homebridge
|
|
126
126
|
version: 1.0.0
|
|
@@ -173,7 +173,7 @@ version: 1.0.0
|
|
|
173
173
|
|
|
174
174
|
describe('submodules (openspec/changes/submodules D1)', () => {
|
|
175
175
|
const PARENT = `
|
|
176
|
-
celilo_contract: "
|
|
176
|
+
celilo_contract: "2.0"
|
|
177
177
|
id: forgejo
|
|
178
178
|
name: Forgejo
|
|
179
179
|
version: 1.0.0
|
|
@@ -181,7 +181,7 @@ submodules:
|
|
|
181
181
|
- runner
|
|
182
182
|
`;
|
|
183
183
|
const SUBMODULE = `
|
|
184
|
-
celilo_contract: "
|
|
184
|
+
celilo_contract: "2.0"
|
|
185
185
|
id: runner
|
|
186
186
|
name: Forgejo Runner
|
|
187
187
|
version: 1.0.0
|
|
@@ -250,7 +250,7 @@ provides:
|
|
|
250
250
|
const dir = await writeParent(
|
|
251
251
|
'plain-module',
|
|
252
252
|
`
|
|
253
|
-
celilo_contract: "
|
|
253
|
+
celilo_contract: "2.0"
|
|
254
254
|
id: plain
|
|
255
255
|
name: Plain
|
|
256
256
|
version: 1.0.0
|
|
@@ -322,7 +322,7 @@ version: 1.0.0
|
|
|
322
322
|
describe('insertModuleToDb', () => {
|
|
323
323
|
test('should insert module record into database', async () => {
|
|
324
324
|
const manifest: ModuleManifest = {
|
|
325
|
-
celilo_contract: '
|
|
325
|
+
celilo_contract: '2.0',
|
|
326
326
|
id: 'test-module',
|
|
327
327
|
name: 'Test Module',
|
|
328
328
|
version: '1.0.0',
|
|
@@ -375,7 +375,7 @@ version: 1.0.0
|
|
|
375
375
|
await writeFile(
|
|
376
376
|
join(moduleDir, 'manifest.yml'),
|
|
377
377
|
`
|
|
378
|
-
celilo_contract: "
|
|
378
|
+
celilo_contract: "2.0"
|
|
379
379
|
id: homebridge
|
|
380
380
|
name: Homebridge
|
|
381
381
|
version: 1.0.0
|
|
@@ -453,7 +453,7 @@ version: 1.0.0
|
|
|
453
453
|
await mkdir(moduleDir, { recursive: true });
|
|
454
454
|
await writeFile(
|
|
455
455
|
join(moduleDir, 'manifest.yml'),
|
|
456
|
-
'celilo_contract: "
|
|
456
|
+
'celilo_contract: "2.0"\nid: existing\nname: Existing\nversion: 2.0.0',
|
|
457
457
|
);
|
|
458
458
|
|
|
459
459
|
const result = await importModule({
|
|
@@ -102,7 +102,7 @@ describe('buildModule — a root package.json must not strip the hook runtime',
|
|
|
102
102
|
source = mkdtempSync(join(tmpdir(), 'celilo-pack-root-pkg-'));
|
|
103
103
|
out = join(mkdtempSync(join(tmpdir(), 'celilo-pack-out-')), 'm.netapp');
|
|
104
104
|
|
|
105
|
-
write('manifest.yml', 'celilo_contract: "
|
|
105
|
+
write('manifest.yml', 'celilo_contract: "2.0"\nid: root-pkg-module\nname: M\nversion: 0.1.0\n');
|
|
106
106
|
// The shape that triggers it: a module that is also a bun project. `files`
|
|
107
107
|
// deliberately names scripts/ — proving the loss is npm-pack semantics and
|
|
108
108
|
// not a missing entry.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { parse } from 'yaml';
|
|
5
|
+
|
|
6
|
+
// structural
|
|
7
|
+
//
|
|
8
|
+
// bubblewrap is the hook jail's Linux backend, and it must NOT be in the .deb's
|
|
9
|
+
// `depends` while the module conversions of
|
|
10
|
+
// openspec/changes/module-orchestrator-primitives are in flight (design.md D9).
|
|
11
|
+
// Shipping the backend before the modules survive it means a jail that arrives
|
|
12
|
+
// with enforcement in the same apt upgrade — it would break deploys rather than
|
|
13
|
+
// fail open. The jail keeps being exercised meanwhile because the e2e management
|
|
14
|
+
// image installs bubblewrap in its own Dockerfile
|
|
15
|
+
// (packages/e2e/docker/Dockerfile.management).
|
|
16
|
+
//
|
|
17
|
+
// REVERSAL CONDITION: slice 8 of module-orchestrator-primitives — the last
|
|
18
|
+
// commit of that change — puts `bubblewrap` back in `depends`. Delete this test
|
|
19
|
+
// in that same commit. Do not re-add the line for any other reason; the old
|
|
20
|
+
// nfpm comment argued for it from the AppArmor profile alone and predates the
|
|
21
|
+
// migration.
|
|
22
|
+
const NFPM_PATH = join(import.meta.dir, '../../../../packaging/celilo/nfpm.yaml');
|
|
23
|
+
|
|
24
|
+
function nfpmDepends(): string[] {
|
|
25
|
+
const parsed = parse(readFileSync(NFPM_PATH, 'utf8')) as { depends?: string[] };
|
|
26
|
+
return parsed.depends ?? [];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('nfpm depends (structural)', () => {
|
|
30
|
+
test('parses a non-empty depends list, so an absence assertion means something', () => {
|
|
31
|
+
// Guards the reach of the assertion below: a typo'd path or a renamed key
|
|
32
|
+
// would otherwise give an empty list, in which nothing is ever found.
|
|
33
|
+
const depends = nfpmDepends();
|
|
34
|
+
expect(depends.length).toBeGreaterThan(0);
|
|
35
|
+
expect(depends).toContain('ansible');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('does not depend on bubblewrap', () => {
|
|
39
|
+
const offenders = nfpmDepends().filter((d) => d.split(/[\s(]/)[0] === 'bubblewrap');
|
|
40
|
+
expect(offenders).toEqual([]);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -115,7 +115,7 @@ export const CAPABILITY_SHAPE_BASELINE: Readonly<Record<string, CapabilityShape>
|
|
|
115
115
|
},
|
|
116
116
|
private_web: {
|
|
117
117
|
version: '2.0.0',
|
|
118
|
-
hash: '
|
|
118
|
+
hash: 'd1d1ef31d51f853d5e02460387f18a86c2833bd97e45122e8e2703ff44397a44',
|
|
119
119
|
requests:
|
|
120
120
|
'[{kind:"alias",members:[{name:"_self",optional:false,type:"Record<string, string | number | boolean>"}],name:"ClientConfig"},{kind:"interface",members:[{name:"defaultHostname",optional:false,type:"string"},{name:"getCaCertificate",optional:false,type:"Promise<string>"},{name:"publishStaticSite",optional:false,type:"Promise<PublishStaticSiteResult>"},{name:"registerReverseProxy",optional:false,type:"Promise<RegisterReverseProxyResult>"}],name:"PrivateWebCapability"},{kind:"interface",members:[{name:"clientConfig",optional:true,type:"ClientConfig"},{name:"hostname",optional:true,type:"string"},{name:"path",optional:false,type:"string"},{name:"spaFallback",optional:true,type:"boolean"}],name:"PublishStaticSiteRequest"},{kind:"interface",members:[{name:"contentHash",optional:false,type:"string"},{name:"filesUploaded",optional:false,type:"number"},{name:"path",optional:false,type:"string"},{name:"success",optional:false,type:"boolean"}],name:"PublishStaticSiteResult"},{kind:"interface",members:[{name:"hostname",optional:true,type:"string"},{name:"path",optional:false,type:"string"},{name:"targetHost",optional:false,type:"string"},{name:"targetPort",optional:false,type:"number"},{name:"websocket",optional:true,type:"boolean"}],name:"RegisterReverseProxyRequest"},{kind:"interface",members:[{name:"path",optional:false,type:"string"},{name:"success",optional:false,type:"boolean"}],name:"RegisterReverseProxyResult"}]',
|
|
121
121
|
},
|
|
@@ -149,4 +149,10 @@ export const CAPABILITY_SHAPE_BASELINE: Readonly<Record<string, CapabilityShape>
|
|
|
149
149
|
requests:
|
|
150
150
|
'[{kind:"alias",members:[{name:"_self",optional:false,type:"{\\n sourceDir: string;\\n overlayDir?: string;\\n} | {\\n unavailable: string;\\n}"}],name:"ConsumerSiteResolution"},{kind:"interface",members:[{name:"content",optional:false,type:"string"},{name:"path",optional:false,type:"string"}],name:"ProviderConfigFile"},{kind:"interface",members:[{name:"converge",optional:false,type:"Promise<{\\n success: boolean;\\n error?: string;\\n unresolved: string[];\\n}>"},{name:"resolveConsumerSite",optional:false,type:"Promise<ConsumerSiteResolution>"}],name:"ProviderConvergeView"},{kind:"interface",members:[{name:"contentHash",optional:false,type:"string"},{name:"hostnames",optional:false,type:"string[]"},{name:"moduleId",optional:false,type:"string"},{name:"overlayDir",optional:true,type:"string"},{name:"slug",optional:false,type:"string"},{name:"sourceDir",optional:false,type:"string"}],name:"ProviderSiteRequest"}]',
|
|
151
151
|
},
|
|
152
|
+
private_routes: {
|
|
153
|
+
version: '1.0.0',
|
|
154
|
+
hash: '6b15e73c0aeca909a1d9250177e4f8c26cb414481b67e9923888af18f902b963',
|
|
155
|
+
requests:
|
|
156
|
+
'[{kind:"interface",members:[{name:"all",optional:false,type:"Promise<PrivateWebRoute[]>"},{name:"replaceAll",optional:false,type:"Promise<void>"}],name:"PrivateRouteView"},{kind:"interface",members:[{name:"hostname",optional:false,type:"string"},{name:"module_id",optional:false,type:"string"},{name:"path",optional:false,type:"string"},{name:"slug",optional:false,type:"string"},{name:"target_host",optional:true,type:"string"},{name:"target_port",optional:true,type:"number"},{name:"type",optional:false,type:"\'static\' | \'reverse_proxy\'"},{name:"websocket",optional:true,type:"boolean"}],name:"PrivateWebRoute"}]',
|
|
157
|
+
},
|
|
152
158
|
};
|
|
@@ -187,7 +187,7 @@ describe('fixture capability coverage', () => {
|
|
|
187
187
|
}
|
|
188
188
|
|
|
189
189
|
const providerManifest = [
|
|
190
|
-
'celilo_contract: "
|
|
190
|
+
'celilo_contract: "2.0"',
|
|
191
191
|
'id: marker-provider',
|
|
192
192
|
'name: Marker Provider',
|
|
193
193
|
'version: 1.0.0',
|
|
@@ -208,7 +208,7 @@ describe('fixture capability coverage', () => {
|
|
|
208
208
|
'marker-provider': { manifest: providerManifest },
|
|
209
209
|
'marker-consumer': {
|
|
210
210
|
manifest: [
|
|
211
|
-
'celilo_contract: "
|
|
211
|
+
'celilo_contract: "2.0"',
|
|
212
212
|
'id: marker-consumer',
|
|
213
213
|
'name: Marker Consumer',
|
|
214
214
|
'version: 1.0.0',
|
|
@@ -235,7 +235,7 @@ describe('fixture capability coverage', () => {
|
|
|
235
235
|
'marker-provider': { manifest: providerManifest },
|
|
236
236
|
'marker-consumer': {
|
|
237
237
|
manifest: [
|
|
238
|
-
'celilo_contract: "
|
|
238
|
+
'celilo_contract: "2.0"',
|
|
239
239
|
'id: marker-consumer',
|
|
240
240
|
'name: Marker Consumer',
|
|
241
241
|
'version: 1.0.0',
|
|
@@ -270,7 +270,7 @@ describe('fixture capability coverage', () => {
|
|
|
270
270
|
mirrorTree(dir, {
|
|
271
271
|
'marker-consumer': {
|
|
272
272
|
manifest: [
|
|
273
|
-
'celilo_contract: "
|
|
273
|
+
'celilo_contract: "2.0"',
|
|
274
274
|
'id: marker-consumer',
|
|
275
275
|
'name: Marker Consumer',
|
|
276
276
|
'version: 1.0.0',
|
|
@@ -297,7 +297,7 @@ describe('fixture capability coverage', () => {
|
|
|
297
297
|
'marker-provider': { manifest: providerManifest },
|
|
298
298
|
'marker-consumer': {
|
|
299
299
|
manifest: [
|
|
300
|
-
'celilo_contract: "
|
|
300
|
+
'celilo_contract: "2.0"',
|
|
301
301
|
'id: marker-consumer',
|
|
302
302
|
'name: Marker Consumer',
|
|
303
303
|
'version: 1.0.0',
|
|
@@ -102,7 +102,7 @@ export const CAPABILITY_NAME_BASELINE: readonly CapabilityNameRow[] = [
|
|
|
102
102
|
{
|
|
103
103
|
file: 'apps/celilo/src/hooks/capability-loader.ts',
|
|
104
104
|
capability: 'provider_converge',
|
|
105
|
-
count:
|
|
105
|
+
count: 4,
|
|
106
106
|
why: 'PERMANENT — a provider VIEW, not a module-provided capability. Core injects the generic converge every provider renders through; naming it is framework plumbing, not core branching on a provider (providers-converge-declared-state)',
|
|
107
107
|
},
|
|
108
108
|
{
|
|
@@ -240,13 +240,13 @@ export const CAPABILITY_NAME_BASELINE: readonly CapabilityNameRow[] = [
|
|
|
240
240
|
{
|
|
241
241
|
file: 'apps/celilo/src/hooks/capability-loader.ts',
|
|
242
242
|
capability: 'private_web',
|
|
243
|
-
count:
|
|
243
|
+
count: 6,
|
|
244
244
|
why: 'L1-L12 — core branches on the capability NAME; becomes a provider declaration in Phase 3 (#938)',
|
|
245
245
|
},
|
|
246
246
|
{
|
|
247
247
|
file: 'apps/celilo/src/hooks/capability-loader.ts',
|
|
248
248
|
capability: 'public_web',
|
|
249
|
-
count:
|
|
249
|
+
count: 11,
|
|
250
250
|
why: 'L1-L12 — core branches on the capability NAME; becomes a provider declaration in Phase 3 (#938)',
|
|
251
251
|
},
|
|
252
252
|
{
|
|
@@ -465,8 +465,37 @@ export const CAPABILITY_NAME_BASELINE: readonly CapabilityNameRow[] = [
|
|
|
465
465
|
count: 3,
|
|
466
466
|
why: 'X8 — shared helpers that name the capability they serve; moves with X8 in Phase 5 (#940)',
|
|
467
467
|
},
|
|
468
|
+
{
|
|
469
|
+
file: 'apps/celilo/src/hooks/capability-loader.ts',
|
|
470
|
+
capability: 'private_routes',
|
|
471
|
+
count: 2,
|
|
472
|
+
why: "PERMANENT — a provider VIEW, not a module-provided capability, and the private-ingress sibling of the 'web_routes' row above. The name was registered so it stops being invisible to audits; these references are framework plumbing (providers-converge-declared-state slice 2)",
|
|
473
|
+
},
|
|
474
|
+
{
|
|
475
|
+
file: 'apps/celilo/src/db/schema.ts',
|
|
476
|
+
capability: 'private_routes',
|
|
477
|
+
count: 1,
|
|
478
|
+
why: "PERMANENT — the TABLE named private_routes, exactly as the 'web_routes' row above; the collision with the view name is nominal",
|
|
479
|
+
},
|
|
480
|
+
{
|
|
481
|
+
file: 'packages/capabilities/src/private-web.ts',
|
|
482
|
+
capability: 'private_routes',
|
|
483
|
+
count: 1,
|
|
484
|
+
why: "PERMANENT — the private_web implementation naming its own table; the capability package doing its job, as the public-web.ts 'web_routes' row above",
|
|
485
|
+
},
|
|
486
|
+
{
|
|
487
|
+
file: 'packages/capabilities/src/declared-tables.ts',
|
|
488
|
+
capability: 'private_web',
|
|
489
|
+
count: 1,
|
|
490
|
+
why: 'PERMANENT — the name-keyed aggregate of capability TABLE declarations, same justification as the public_web row above: naming the capability IS the mapping (openspec/changes/capability-owned-tables D2)',
|
|
491
|
+
},
|
|
492
|
+
{
|
|
493
|
+
file: 'packages/capabilities/src/capability-contract.ts',
|
|
494
|
+
capability: 'private_routes',
|
|
495
|
+
count: 1,
|
|
496
|
+
why: 'PERMANENT — a contract declaration with no implementation; this is the package doing its job',
|
|
497
|
+
},
|
|
468
498
|
];
|
|
469
|
-
|
|
470
499
|
export const SERVICE_FILENAME_BASELINE: readonly ServiceFilenameRow[] = [
|
|
471
500
|
{
|
|
472
501
|
file: 'apps/celilo/src/services/alerting/notification-responder.ts',
|