multimodel-dev-os 3.0.1 → 3.2.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.
Files changed (59) hide show
  1. package/README.md +4 -0
  2. package/bin/multimodel-dev-os.js +3419 -3573
  3. package/docs/.vitepress/config.js +2 -2
  4. package/docs/index.md +5 -5
  5. package/docs/npm-publishing.md +5 -5
  6. package/docs/package-safety.md +24 -0
  7. package/docs/public/llms-full.txt +1 -1
  8. package/docs/public/llms.txt +1 -1
  9. package/docs/public/sitemap.xml +10 -0
  10. package/docs/registry-policy.md +4 -0
  11. package/docs/registry-security.md +7 -0
  12. package/docs/registry-sync.md +6 -0
  13. package/docs/release-policy.md +6 -5
  14. package/docs/testing.md +133 -0
  15. package/docs/trusted-registries.md +4 -0
  16. package/docs/v3-roadmap.md +20 -2
  17. package/package.json +10 -3
  18. package/scripts/build-cli.js +59 -0
  19. package/scripts/check-build-fresh.js +52 -0
  20. package/scripts/install.ps1 +1 -1
  21. package/scripts/install.sh +1 -1
  22. package/scripts/verify.js +221 -14
  23. package/scripts/verify.sh +11 -1
  24. package/src/catalog/loader.js +117 -0
  25. package/src/cli/args.js +118 -0
  26. package/src/cli/help.js +60 -0
  27. package/src/cli/main.js +5718 -0
  28. package/src/core/globals.js +52 -0
  29. package/src/core/hashes.js +15 -0
  30. package/src/core/policy.js +36 -0
  31. package/src/core/security.js +61 -0
  32. package/src/core/yaml.js +136 -0
  33. package/src/plugin/manifest.js +95 -0
  34. package/src/registry/sources.js +40 -0
  35. package/src/registry/validation.js +45 -0
  36. package/tests/README.md +37 -0
  37. package/tests/fixtures/README.md +22 -0
  38. package/tests/fixtures/custom-template-example/README.md +10 -0
  39. package/tests/fixtures/proposals/approved-append-line.md +28 -0
  40. package/tests/fixtures/proposals/approved-create-file.md +29 -0
  41. package/tests/fixtures/proposals/approved-replace-text.md +30 -0
  42. package/tests/fixtures/proposals/existing-create-file-no-overwrite.md +29 -0
  43. package/tests/fixtures/proposals/no-operations.md +18 -0
  44. package/tests/fixtures/proposals/path-traversal.md +29 -0
  45. package/tests/fixtures/proposals/pending-proposal.md +29 -0
  46. package/tests/fixtures/proposals/protected-path.md +29 -0
  47. package/tests/fixtures/proposals/replace-multiple-without-allow.md +30 -0
  48. package/tests/fixtures/registry-overrides/README.md +20 -0
  49. package/tests/smoke/README.md +37 -0
  50. package/tests/smoke/cli-smoke.md +49 -0
  51. package/tests/unit/build-output.test.js +40 -0
  52. package/tests/unit/catalog-loader.test.js +44 -0
  53. package/tests/unit/path-safety.test.js +62 -0
  54. package/tests/unit/plugin-manifest.test.js +94 -0
  55. package/tests/unit/prepublish-guard.test.js +35 -0
  56. package/tests/unit/registry-policy.test.js +46 -0
  57. package/tests/unit/registry-url-validation.test.js +64 -0
  58. package/tests/unit/yaml.test.js +92 -0
  59. package/docs/testing-v0.2.md +0 -73
@@ -0,0 +1,18 @@
1
+ ---
2
+ id: proposal-20260611-333333
3
+ created_at: 2026-06-11T33:33:33Z
4
+ title: No Operations Proposal
5
+ problem: Vague proposal without machine readable block.
6
+ evidence: N/A
7
+ risk_level: low
8
+ affected_files:
9
+ - README.md
10
+ suggested_change: Edit README.md manually.
11
+ verify_command: npm run verify
12
+ rollback_plan: git checkout -- README.md
13
+ approval_status: approved
14
+ ---
15
+
16
+ # No Operations Proposal
17
+
18
+ Please edit the README.md to describe the codebase in more detail.
@@ -0,0 +1,29 @@
1
+ ---
2
+ id: proposal-20260611-000003
3
+ created_at: 2026-06-11T00:00:03Z
4
+ title: Path Traversal Proposal
5
+ problem: Test directory traversal boundaries check.
6
+ evidence: N/A
7
+ risk_level: low
8
+ affected_files:
9
+ - ../outside.md
10
+ suggested_change: Modify file outside root.
11
+ verify_command: npm run verify
12
+ rollback_plan: N/A
13
+ approval_status: approved
14
+ ---
15
+
16
+ # Path Traversal Proposal
17
+
18
+ ```json
19
+ {
20
+ "operations": [
21
+ {
22
+ "type": "create_file",
23
+ "path": "../outside-file.md",
24
+ "content": "outside content\n",
25
+ "overwrite": true
26
+ }
27
+ ]
28
+ }
29
+ ```
@@ -0,0 +1,29 @@
1
+ ---
2
+ id: proposal-20260611-111111
3
+ created_at: 2026-06-11T11:11:11Z
4
+ title: Pending Proposal
5
+ problem: Test pending proposal.
6
+ evidence: N/A
7
+ risk_level: low
8
+ affected_files:
9
+ - tests/fixtures/custom-template-example/pending.md
10
+ suggested_change: Create pending file.
11
+ verify_command: npm run verify
12
+ rollback_plan: rm tests/fixtures/custom-template-example/pending.md
13
+ approval_status: pending
14
+ ---
15
+
16
+ # Pending Proposal
17
+
18
+ ```json
19
+ {
20
+ "operations": [
21
+ {
22
+ "type": "create_file",
23
+ "path": "tests/fixtures/custom-template-example/pending.md",
24
+ "content": "pending\n",
25
+ "overwrite": true
26
+ }
27
+ ]
28
+ }
29
+ ```
@@ -0,0 +1,29 @@
1
+ ---
2
+ id: proposal-20260611-222222
3
+ created_at: 2026-06-11T22:22:22Z
4
+ title: Protected Path Proposal
5
+ problem: Try to modify protected path.
6
+ evidence: N/A
7
+ risk_level: low
8
+ affected_files:
9
+ - .env
10
+ suggested_change: Modify .env file.
11
+ verify_command: npm run verify
12
+ rollback_plan: git checkout -- .env
13
+ approval_status: approved
14
+ ---
15
+
16
+ # Protected Path Proposal
17
+
18
+ ```json
19
+ {
20
+ "operations": [
21
+ {
22
+ "type": "create_file",
23
+ "path": ".env",
24
+ "content": "SECRET_KEY=stolen\n",
25
+ "overwrite": true
26
+ }
27
+ ]
28
+ }
29
+ ```
@@ -0,0 +1,30 @@
1
+ ---
2
+ id: proposal-20260611-000005
3
+ created_at: 2026-06-11T00:00:05Z
4
+ title: Replace Multiple Without Allow Proposal
5
+ problem: Test replace text matching multiple times without allow_multiple.
6
+ evidence: N/A
7
+ risk_level: low
8
+ affected_files:
9
+ - tests/fixtures/custom-template-example/multiple.md
10
+ suggested_change: Replace multiple occurrences without allow_multiple flag.
11
+ verify_command: npm run verify
12
+ rollback_plan: N/A
13
+ approval_status: approved
14
+ ---
15
+
16
+ # Replace Multiple Without Allow Proposal
17
+
18
+ ```json
19
+ {
20
+ "operations": [
21
+ {
22
+ "type": "replace_text",
23
+ "path": "tests/fixtures/custom-template-example/multiple.md",
24
+ "find": "target",
25
+ "replace": "replaced",
26
+ "allow_multiple": false
27
+ }
28
+ ]
29
+ }
30
+ ```
@@ -0,0 +1,20 @@
1
+ # Registry Overrides Test Fixture Guide
2
+
3
+ This fixture folder demonstrates and validates overriding the default model/adapter/template registries using custom YAML files and the `--registry` flag.
4
+
5
+ ## Files Structure
6
+
7
+ For testing registry overrides, you can specify custom templates/adapters files:
8
+
9
+ * `custom-templates.yaml`: Defines mock template profiles.
10
+ * `custom-adapters.yaml`: Defines mock adapters setup.
11
+
12
+ ## Usage in Testing
13
+
14
+ ```bash
15
+ # List templates from the custom templates fixture file
16
+ node bin/multimodel-dev-os.js templates --registry tests/fixtures/registry-overrides/custom-templates.yaml
17
+
18
+ # Run validations on a custom template entry
19
+ node bin/multimodel-dev-os.js validate-template my-mock-template --registry tests/fixtures/registry-overrides/custom-templates.yaml
20
+ ```
@@ -0,0 +1,37 @@
1
+ # Smoke Tests Playbook
2
+
3
+ Follow these quick manual verification commands to assert the functional integrity of `multimodel-dev-os` before pushing tag releases.
4
+
5
+ ---
6
+
7
+ ## 1. Quick Verification Pipeline
8
+
9
+ Run the following actions sequentially inside a target sandbox:
10
+
11
+ ```bash
12
+ # 1. Inspect version parameters and command menu
13
+ node bin/multimodel-dev-os.js --help
14
+
15
+ # 2. View all built-in configurations
16
+ node bin/multimodel-dev-os.js templates
17
+
18
+ # 3. Inspect a specific template's specifications
19
+ node bin/multimodel-dev-os.js show-template nextjs-saas
20
+
21
+ # 4. Dry-run scaffold inside the current repository
22
+ node bin/multimodel-dev-os.js init --template general-app --dry-run
23
+
24
+ # 5. Execute strict validation lints
25
+ node bin/multimodel-dev-os.js validate --target .
26
+
27
+ # 6. Execute diagnostic doctor checkups
28
+ node bin/multimodel-dev-os.js doctor --target .
29
+ ```
30
+
31
+ ---
32
+
33
+ ## 2. Dry-Run Verification
34
+
35
+ Assert that no physical files are created when running the `--dry-run` parameter:
36
+ - Verify stdout logs indicate `[DRY-RUN] Would create...`
37
+ - Confirm `git status` reports zero untracked files or modifications.
@@ -0,0 +1,49 @@
1
+ # CLI Smoke Test Specification
2
+
3
+ This document lists the essential CLI commands to execute when verifying the MultiModel Dev OS installation stability.
4
+
5
+ ## Sanity Commands
6
+
7
+ ```bash
8
+ # Verify help output
9
+ node bin/multimodel-dev-os.js --help
10
+
11
+ # List templates
12
+ node bin/multimodel-dev-os.js templates
13
+ node bin/multimodel-dev-os.js templates --json
14
+
15
+ # List models and adapters
16
+ node bin/multimodel-dev-os.js models --json
17
+ node bin/multimodel-dev-os.js adapters --json
18
+
19
+ # List active skills
20
+ node bin/multimodel-dev-os.js skills
21
+ ```
22
+
23
+ ## Validation & Doctor Commands
24
+
25
+ ```bash
26
+ # Validate built-in resources
27
+ node bin/multimodel-dev-os.js validate-template nextjs-saas
28
+ node bin/multimodel-dev-os.js validate-adapter cursor
29
+ node bin/multimodel-dev-os.js validate-skill custom-skill.example
30
+
31
+ # Validate all registry entries
32
+ node bin/multimodel-dev-os.js validate --all-registries
33
+
34
+ # Release advisory checkup
35
+ node bin/multimodel-dev-os.js doctor --release
36
+
37
+ # Token budget checks
38
+ node bin/multimodel-dev-os.js doctor --tokens --threshold 100KB
39
+ ```
40
+
41
+ ## Initialization Dry-Runs
42
+
43
+ ```bash
44
+ # Dry-run general app init
45
+ node bin/multimodel-dev-os.js init --dry-run --template general-app
46
+
47
+ # Dry-run with adapter injections
48
+ node bin/multimodel-dev-os.js init --dry-run --template nextjs-saas -a cursor -a claude
49
+ ```
@@ -0,0 +1,40 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { existsSync, readFileSync } from 'fs';
3
+ import { join } from 'path';
4
+ import { execSync } from 'child_process';
5
+
6
+ describe('Build Output Verification', () => {
7
+ const buildPath = join(process.cwd(), 'bin', 'multimodel-dev-os.js');
8
+
9
+ it('should compile the output file successfully', () => {
10
+ expect(existsSync(buildPath)).toBe(true);
11
+ });
12
+
13
+ it('should contain exactly one shebang at the top', () => {
14
+ const content = readFileSync(buildPath, 'utf8');
15
+ const totalShebangs = (content.match(/#!/g) || []).length;
16
+
17
+ expect(content.startsWith('#!/usr/bin/env node')).toBe(true);
18
+ expect(totalShebangs).toBe(1);
19
+ });
20
+
21
+ it('should contain the generation warning header', () => {
22
+ const content = readFileSync(buildPath, 'utf8');
23
+ expect(content).toContain('// Generated from src/. Do not edit directly.');
24
+ });
25
+
26
+ it('should not contain dangerous registry URL interpolation', () => {
27
+ const content = readFileSync(buildPath, 'utf8');
28
+
29
+ const hasUnsafeSync = content.includes("mod.get('${targetUrl}'") || (content.includes('execSync(`node -e "') && content.includes('${targetUrl}'));
30
+ expect(hasUnsafeSync).toBe(false);
31
+
32
+ expect(content).toContain('execFileSync(process.execPath');
33
+ });
34
+
35
+ it('should be completely fresh and match the source modules', () => {
36
+ expect(() => {
37
+ execSync('node scripts/check-build-fresh.js', { stdio: 'ignore' });
38
+ }).not.toThrow();
39
+ });
40
+ });
@@ -0,0 +1,44 @@
1
+ import { describe, it, expect, beforeAll, afterAll } from 'vitest';
2
+ import { loadCatalogFromSource, loadCatalog } from '../../src/catalog/loader.js';
3
+ import { existsSync, mkdirSync, writeFileSync, rmSync } from 'fs';
4
+ import { join } from 'path';
5
+
6
+ describe('Catalog Loader', () => {
7
+ const tempDir = join(process.cwd(), 'temp-catalog-test');
8
+ const localCatalogDir = join(tempDir, '.ai', 'plugins');
9
+ const localCatalogFile = join(localCatalogDir, 'catalog.yaml');
10
+
11
+ beforeAll(() => {
12
+ mkdirSync(localCatalogDir, { recursive: true });
13
+ });
14
+
15
+ afterAll(() => {
16
+ if (existsSync(tempDir)) {
17
+ rmSync(tempDir, { recursive: true, force: true });
18
+ }
19
+ });
20
+
21
+ it('should load bundled catalog from fallback or global if no target is specified', () => {
22
+ const catalog = loadCatalog();
23
+ expect(catalog).toBeDefined();
24
+ expect(Array.isArray(catalog.plugins)).toBe(true);
25
+ });
26
+
27
+ it('should load local catalog when source is "local"', () => {
28
+ const localYaml = `
29
+ catalog:
30
+ plugins:
31
+ - name: local-plugin
32
+ slug: local-slug
33
+ version: 1.0.0
34
+ description: local test
35
+ author: test
36
+ `;
37
+ writeFileSync(localCatalogFile, localYaml, 'utf8');
38
+
39
+ const result = loadCatalogFromSource('local', { target: tempDir });
40
+ expect(result.plugins).toHaveLength(1);
41
+ expect(result.plugins[0].name).toBe('local-plugin');
42
+ expect(result.plugins[0]._source).toBe('local');
43
+ });
44
+ });
@@ -0,0 +1,62 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { isSafePath, shouldIgnorePath } from '../../src/core/security.js';
3
+
4
+ describe('Path Safety and Ignore Engine', () => {
5
+ describe('isSafePath', () => {
6
+ const policy = {
7
+ allowed_write_roots: ['.ai/', 'adapters/'],
8
+ blocked_paths: ['.env', '.npmrc', '.git/', 'node_modules/', 'package.json', 'package-lock.json']
9
+ };
10
+
11
+ it('should allow paths under allowed write roots', () => {
12
+ expect(isSafePath('.ai/plugins/my-plugin.yaml', policy)).toBe(true);
13
+ expect(isSafePath('adapters/vscode/settings.json', policy)).toBe(true);
14
+ });
15
+
16
+ it('should reject paths outside allowed write roots', () => {
17
+ expect(isSafePath('src/cli/main.js', policy)).toBe(false);
18
+ expect(isSafePath('index.js', policy)).toBe(false);
19
+ });
20
+
21
+ it('should reject paths with directory traversal', () => {
22
+ expect(isSafePath('.ai/plugins/../../index.js', policy)).toBe(false);
23
+ expect(isSafePath('adapters/../package.json', policy)).toBe(false);
24
+ });
25
+
26
+ it('should reject absolute paths', () => {
27
+ expect(isSafePath('/etc/passwd', policy)).toBe(false);
28
+ expect(isSafePath('C:/Windows/System32', policy)).toBe(false);
29
+ });
30
+
31
+ it('should reject blacklisted paths', () => {
32
+ expect(isSafePath('.ai/plugins/.env', policy)).toBe(false);
33
+ expect(isSafePath('adapters/.npmrc', policy)).toBe(false);
34
+ expect(isSafePath('adapters/node_modules/foo', policy)).toBe(false);
35
+ });
36
+ });
37
+
38
+ describe('shouldIgnorePath', () => {
39
+ it('should ignore dependency and git folders', () => {
40
+ expect(shouldIgnorePath('node_modules/lodash/index.js')).toBe(true);
41
+ expect(shouldIgnorePath('.git/config')).toBe(true);
42
+ expect(shouldIgnorePath('dist/bundle.js')).toBe(true);
43
+ });
44
+
45
+ it('should ignore generated runtime memory files', () => {
46
+ expect(shouldIgnorePath('.ai/intelligence/memory.hash.json')).toBe(true);
47
+ expect(shouldIgnorePath('.ai/intelligence/feedback-log.jsonl')).toBe(true);
48
+ });
49
+
50
+ it('should ignore credential and secret files', () => {
51
+ expect(shouldIgnorePath('.env')).toBe(true);
52
+ expect(shouldIgnorePath('src/.env.production')).toBe(true);
53
+ expect(shouldIgnorePath('key.pem')).toBe(true);
54
+ expect(shouldIgnorePath('id_rsa')).toBe(true);
55
+ });
56
+
57
+ it('should not ignore standard application source files', () => {
58
+ expect(shouldIgnorePath('src/core/yaml.js')).toBe(false);
59
+ expect(shouldIgnorePath('README.md')).toBe(false);
60
+ });
61
+ });
62
+ });
@@ -0,0 +1,94 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { validatePluginManifest } from '../../src/plugin/manifest.js';
3
+
4
+ describe('Plugin Manifest Validator', () => {
5
+ it('should accept a completely valid plugin manifest', () => {
6
+ const manifest = {
7
+ name: 'My Test Plugin',
8
+ slug: 'my-test-plugin',
9
+ version: '1.0.0',
10
+ description: 'A valid test plugin description',
11
+ author: 'Tester',
12
+ allowed_file_patterns: ['.ai/plugins/config.yaml', 'adapters/vscode/settings.json']
13
+ };
14
+
15
+ const result = validatePluginManifest(manifest);
16
+ expect(result.success).toBe(true);
17
+ expect(result.errors).toHaveLength(0);
18
+ });
19
+
20
+ it('should reject missing metadata keys', () => {
21
+ const manifest = {
22
+ name: 'My Test Plugin',
23
+ version: '1.0.0',
24
+ description: 'A description',
25
+ author: 'Tester'
26
+ // slug is missing
27
+ };
28
+
29
+ const result = validatePluginManifest(manifest);
30
+ expect(result.success).toBe(false);
31
+ expect(result.errors).toContain('Missing required key: slug');
32
+ });
33
+
34
+ it('should reject invalid slug formats', () => {
35
+ const manifest = {
36
+ name: 'My Test Plugin',
37
+ slug: 'invalid slug here',
38
+ version: '1.0.0',
39
+ description: 'A description',
40
+ author: 'Tester'
41
+ };
42
+
43
+ const result = validatePluginManifest(manifest);
44
+ expect(result.success).toBe(false);
45
+ expect(result.errors).toContain("Key 'slug' must be alphanumeric with dashes or underscores only");
46
+ });
47
+
48
+ it('should reject patterns writing outside allowed write roots', () => {
49
+ const manifest = {
50
+ name: 'My Test Plugin',
51
+ slug: 'my-test-plugin',
52
+ version: '1.0.0',
53
+ description: 'A description',
54
+ author: 'Tester',
55
+ allowed_file_patterns: ['src/cli/main.js'] // not in .ai/ or adapters/
56
+ };
57
+
58
+ const result = validatePluginManifest(manifest);
59
+ expect(result.success).toBe(false);
60
+ expect(result.errors[0]).toContain('violates safety boundaries');
61
+ });
62
+
63
+ it('should reject patterns containing traversal or leading slash', () => {
64
+ const manifest = {
65
+ name: 'My Test Plugin',
66
+ slug: 'my-test-plugin',
67
+ version: '1.0.0',
68
+ description: 'A description',
69
+ author: 'Tester',
70
+ allowed_file_patterns: ['.ai/plugins/../../index.js', '/.ai/plugins/test.yaml']
71
+ };
72
+
73
+ const result = validatePluginManifest(manifest);
74
+ expect(result.success).toBe(false);
75
+ expect(result.errors).toHaveLength(2);
76
+ expect(result.errors[0]).toContain('violates safety boundaries');
77
+ expect(result.errors[1]).toContain('violates safety boundaries');
78
+ });
79
+
80
+ it('should reject patterns containing blacklisted files', () => {
81
+ const manifest = {
82
+ name: 'My Test Plugin',
83
+ slug: 'my-test-plugin',
84
+ version: '1.0.0',
85
+ description: 'A description',
86
+ author: 'Tester',
87
+ allowed_file_patterns: ['.ai/plugins/.env', 'adapters/package.json']
88
+ };
89
+
90
+ const result = validatePluginManifest(manifest);
91
+ expect(result.success).toBe(false);
92
+ expect(result.errors).toHaveLength(2);
93
+ });
94
+ });
@@ -0,0 +1,35 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { execSync } from 'child_process';
3
+ import { join } from 'path';
4
+
5
+ describe('Prepublish Guard', () => {
6
+ const guardPath = join(process.cwd(), 'scripts', 'prepublish-guard.js');
7
+
8
+ it('should block publish when MMDO_ALLOW_PUBLISH is not set', () => {
9
+ try {
10
+ execSync(`node "${guardPath}"`, {
11
+ stdio: 'pipe',
12
+ env: {
13
+ ...process.env,
14
+ MMDO_ALLOW_PUBLISH: 'false' // explicitly set to false to override any ambient env
15
+ }
16
+ });
17
+ throw new Error('Prepublish guard did not block the publish.');
18
+ } catch (err) {
19
+ expect(err.status).toBe(1);
20
+ expect(err.stderr.toString()).toContain('Publishing requires explicit release approval');
21
+ }
22
+ });
23
+
24
+ it('should allow publish when MMDO_ALLOW_PUBLISH=true for stable version >= 2', () => {
25
+ const output = execSync(`node "${guardPath}"`, {
26
+ env: {
27
+ ...process.env,
28
+ MMDO_ALLOW_PUBLISH: 'true',
29
+ MMDO_ALLOW_PRERELEASE_PUBLISH: undefined
30
+ },
31
+ encoding: 'utf8'
32
+ });
33
+ expect(output).toContain('Prepublish guard passed');
34
+ });
35
+ });
@@ -0,0 +1,46 @@
1
+ import { describe, it, expect, beforeAll, afterAll } from 'vitest';
2
+ import { loadRegistryPolicy } from '../../src/core/policy.js';
3
+ import { existsSync, mkdirSync, writeFileSync, rmSync } from 'fs';
4
+ import { join } from 'path';
5
+
6
+ describe('Registry Policy Engine', () => {
7
+ const tempDir = join(process.cwd(), 'temp-policy-test');
8
+ const policySubdir = join(tempDir, '.ai', 'policies');
9
+ const policyFile = join(policySubdir, 'registry-policy.yaml');
10
+
11
+ beforeAll(() => {
12
+ mkdirSync(policySubdir, { recursive: true });
13
+ });
14
+
15
+ afterAll(() => {
16
+ if (existsSync(tempDir)) {
17
+ rmSync(tempDir, { recursive: true, force: true });
18
+ }
19
+ });
20
+
21
+ it('should load default policy fields if no file is found', () => {
22
+ const policy = loadRegistryPolicy('non-existent-directory-random-path');
23
+ expect(policy.allow_remote_registries).toBe(false);
24
+ expect(policy.allow_http_localhost).toBe(false);
25
+ expect(policy.require_checksum).toBe(true);
26
+ expect(policy.allowed_write_roots).toEqual(['.ai/', 'adapters/']);
27
+ });
28
+
29
+ it('should override default fields with written policy configurations', () => {
30
+ const yamlConfig = `
31
+ allow_remote_registries: true
32
+ allow_http_localhost: true
33
+ require_checksum: false
34
+ max_plugin_files: 50
35
+ `;
36
+ writeFileSync(policyFile, yamlConfig, 'utf8');
37
+
38
+ const policy = loadRegistryPolicy(tempDir);
39
+ expect(policy.allow_remote_registries).toBe(true);
40
+ expect(policy.allow_http_localhost).toBe(true);
41
+ expect(policy.require_checksum).toBe(false);
42
+ expect(policy.max_plugin_files).toBe(50);
43
+ // Unspecified fields should keep defaults
44
+ expect(policy.max_registry_cache_size_kb).toBe(512);
45
+ });
46
+ });
@@ -0,0 +1,64 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { validateRegistryUrl } from '../../src/registry/validation.js';
3
+
4
+ describe('Registry URL Validation', () => {
5
+ it('should accept valid HTTPS URLs', () => {
6
+ expect(() => validateRegistryUrl('https://example.com/catalog.yaml')).not.toThrow();
7
+ expect(() => validateRegistryUrl('https://raw.githubusercontent.com/user/repo/main/catalog.yaml')).not.toThrow();
8
+ });
9
+
10
+ it('should reject empty or non-string URLs', () => {
11
+ expect(() => validateRegistryUrl(null)).toThrow('Registry URL must be a non-empty string.');
12
+ expect(() => validateRegistryUrl('')).toThrow('Registry URL must be a non-empty string.');
13
+ expect(() => validateRegistryUrl(123)).toThrow('Registry URL must be a non-empty string.');
14
+ });
15
+
16
+ it('should reject URLs with whitespace or control characters', () => {
17
+ expect(() => validateRegistryUrl('https://example.com/cat alog.yaml')).toThrow('whitespace or control characters');
18
+ expect(() => validateRegistryUrl('https://example.com/catalog.yaml\n')).toThrow('whitespace or control characters');
19
+ });
20
+
21
+ it('should reject URLs containing quotes or backticks', () => {
22
+ expect(() => validateRegistryUrl("https://example.com/catalog.yaml'")).toThrow('quotes or backticks');
23
+ expect(() => validateRegistryUrl('https://example.com/catalog.yaml"')).toThrow('quotes or backticks');
24
+ expect(() => validateRegistryUrl('https://example.com/catalog.yaml`')).toThrow('quotes or backticks');
25
+ });
26
+
27
+ it('should reject URLs containing shell metacharacters', () => {
28
+ expect(() => validateRegistryUrl('https://example.com/catalog.yaml;echo')).toThrow('shell metacharacters');
29
+ expect(() => validateRegistryUrl('https://example.com/catalog.yaml&foo')).toThrow('shell metacharacters');
30
+ expect(() => validateRegistryUrl('https://example.com/catalog.yaml$foo')).toThrow('shell metacharacters');
31
+ });
32
+
33
+ it('should reject malformed URLs', () => {
34
+ expect(() => validateRegistryUrl('not-a-url')).toThrow('malformed or invalid');
35
+ });
36
+
37
+ it('should reject URLs containing credentials', () => {
38
+ expect(() => validateRegistryUrl('https://user:password@example.com/catalog.yaml')).toThrow('credentials');
39
+ expect(() => validateRegistryUrl('https://user@example.com/catalog.yaml')).toThrow('credentials');
40
+ });
41
+
42
+ it('should reject non-HTTPS remote URLs by default', () => {
43
+ expect(() => validateRegistryUrl('http://example.com/catalog.yaml')).toThrow('Only HTTPS is permitted');
44
+ expect(() => validateRegistryUrl('ftp://example.com/catalog.yaml')).toThrow('Only HTTPS is permitted');
45
+ });
46
+
47
+ describe('Localhost HTTP Allowance', () => {
48
+ it('should reject HTTP localhost by default', () => {
49
+ expect(() => validateRegistryUrl('http://localhost/catalog.yaml')).toThrow('Only HTTPS is permitted');
50
+ expect(() => validateRegistryUrl('http://127.0.0.1/catalog.yaml')).toThrow('Only HTTPS is permitted');
51
+ });
52
+
53
+ it('should allow HTTP localhost if policy explicitly allows it', () => {
54
+ const policy = { allow_http_localhost: true };
55
+ expect(() => validateRegistryUrl('http://localhost/catalog.yaml', policy)).not.toThrow();
56
+ expect(() => validateRegistryUrl('http://127.0.0.1/catalog.yaml', policy)).not.toThrow();
57
+ });
58
+
59
+ it('should still reject HTTP non-localhost even if policy allows HTTP localhost', () => {
60
+ const policy = { allow_http_localhost: true };
61
+ expect(() => validateRegistryUrl('http://example.com/catalog.yaml', policy)).toThrow('Only HTTPS is permitted');
62
+ });
63
+ });
64
+ });