multimodel-dev-os 3.1.0 → 3.5.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 (102) hide show
  1. package/.ai/policies/registry-policy.yaml +29 -1
  2. package/.ai/registries/trusted-keys.yaml +12 -0
  3. package/.ai/schema/registry-manifest.schema.json +31 -2
  4. package/.ai/schema/registry-policy.schema.json +37 -1
  5. package/.ai/schema/trusted-keys.schema.json +69 -0
  6. package/AGENTS.md +22 -26
  7. package/MEMORY.md +34 -11
  8. package/README.md +2 -1
  9. package/RUNBOOK.md +28 -36
  10. package/TASKS.md +15 -5
  11. package/bin/multimodel-dev-os.js +1366 -548
  12. package/docs/.vitepress/config.js +3 -1
  13. package/docs/architecture.md +3 -1
  14. package/docs/index.md +5 -5
  15. package/docs/npm-publishing.md +5 -5
  16. package/docs/package-safety.md +17 -0
  17. package/docs/public/llms-full.txt +5 -1
  18. package/docs/public/llms.txt +6 -1
  19. package/docs/public/sitemap.xml +15 -0
  20. package/docs/registry-policy.md +29 -1
  21. package/docs/registry-security.md +73 -6
  22. package/docs/registry-signing.md +70 -0
  23. package/docs/registry-sync.md +5 -2
  24. package/docs/registry-trust-store.md +66 -0
  25. package/docs/release-policy.md +6 -5
  26. package/docs/security-threat-model.md +96 -0
  27. package/docs/testing.md +25 -2
  28. package/docs/trusted-registries.md +1 -1
  29. package/docs/v3-roadmap.md +17 -6
  30. package/docs/v3.5.0-readiness.md +46 -0
  31. package/package.json +5 -2
  32. package/scripts/build-cli.js +45 -3
  33. package/scripts/check-build-fresh.js +52 -0
  34. package/scripts/install.ps1 +1 -1
  35. package/scripts/install.sh +1 -1
  36. package/scripts/verify.js +327 -14
  37. package/scripts/verify.sh +10 -0
  38. package/src/catalog/loader.js +117 -0
  39. package/src/cli/args.js +118 -0
  40. package/src/cli/help.js +60 -0
  41. package/src/cli/main.js +6263 -0
  42. package/src/core/globals.js +52 -0
  43. package/src/core/hashes.js +15 -0
  44. package/src/core/policy.js +44 -0
  45. package/src/core/security.js +61 -0
  46. package/src/core/yaml.js +136 -0
  47. package/src/plugin/manifest.js +95 -0
  48. package/src/registry/provenance.js +114 -0
  49. package/src/registry/signing.js +392 -0
  50. package/src/registry/sources.js +40 -0
  51. package/src/registry/trust-store.js +41 -0
  52. package/src/registry/validation.js +45 -0
  53. package/src/registry/verdict.js +51 -0
  54. package/tests/README.md +37 -0
  55. package/tests/fixtures/README.md +22 -0
  56. package/tests/fixtures/custom-template-example/README.md +10 -0
  57. package/tests/fixtures/proposals/approved-append-line.md +28 -0
  58. package/tests/fixtures/proposals/approved-create-file.md +29 -0
  59. package/tests/fixtures/proposals/approved-replace-text.md +30 -0
  60. package/tests/fixtures/proposals/existing-create-file-no-overwrite.md +29 -0
  61. package/tests/fixtures/proposals/no-operations.md +18 -0
  62. package/tests/fixtures/proposals/path-traversal.md +29 -0
  63. package/tests/fixtures/proposals/pending-proposal.md +29 -0
  64. package/tests/fixtures/proposals/protected-path.md +29 -0
  65. package/tests/fixtures/proposals/replace-multiple-without-allow.md +30 -0
  66. package/tests/fixtures/registry-overrides/README.md +20 -0
  67. package/tests/fixtures/signed-registries/README.md +4 -0
  68. package/tests/fixtures/signed-registries/revoked-key/catalog.yaml +8 -0
  69. package/tests/fixtures/signed-registries/revoked-key/expected-verdict.json +7 -0
  70. package/tests/fixtures/signed-registries/revoked-key/registry-manifest.yaml +14 -0
  71. package/tests/fixtures/signed-registries/tampered-manifest/catalog.yaml +8 -0
  72. package/tests/fixtures/signed-registries/tampered-manifest/expected-verdict.json +7 -0
  73. package/tests/fixtures/signed-registries/tampered-manifest/registry-manifest.yaml +14 -0
  74. package/tests/fixtures/signed-registries/trusted-keys.yaml +23 -0
  75. package/tests/fixtures/signed-registries/unsigned-remote-required/catalog.yaml +8 -0
  76. package/tests/fixtures/signed-registries/unsigned-remote-required/expected-verdict.json +7 -0
  77. package/tests/fixtures/signed-registries/unsigned-remote-required/registry-manifest.yaml +9 -0
  78. package/tests/fixtures/signed-registries/unsupported-algorithm/catalog.yaml +8 -0
  79. package/tests/fixtures/signed-registries/unsupported-algorithm/expected-verdict.json +7 -0
  80. package/tests/fixtures/signed-registries/unsupported-algorithm/registry-manifest.yaml +14 -0
  81. package/tests/fixtures/signed-registries/valid-signed-registry/catalog.yaml +8 -0
  82. package/tests/fixtures/signed-registries/valid-signed-registry/expected-verdict.json +7 -0
  83. package/tests/fixtures/signed-registries/valid-signed-registry/registry-manifest.yaml +14 -0
  84. package/tests/fixtures/signed-registries/wrong-key/catalog.yaml +8 -0
  85. package/tests/fixtures/signed-registries/wrong-key/expected-verdict.json +7 -0
  86. package/tests/fixtures/signed-registries/wrong-key/registry-manifest.yaml +14 -0
  87. package/tests/smoke/README.md +37 -0
  88. package/tests/smoke/cli-smoke.md +49 -0
  89. package/tests/unit/build-output.test.js +40 -0
  90. package/tests/unit/catalog-loader.test.js +44 -0
  91. package/tests/unit/path-safety.test.js +62 -0
  92. package/tests/unit/plugin-manifest.test.js +94 -0
  93. package/tests/unit/prepublish-guard.test.js +35 -0
  94. package/tests/unit/registry-e2e-signature-fixtures.test.js +288 -0
  95. package/tests/unit/registry-policy.test.js +52 -0
  96. package/tests/unit/registry-provenance.test.js +185 -0
  97. package/tests/unit/registry-public-signing.test.js +109 -0
  98. package/tests/unit/registry-signature-policy.test.js +100 -0
  99. package/tests/unit/registry-signing.test.js +193 -0
  100. package/tests/unit/registry-trust-store.test.js +133 -0
  101. package/tests/unit/registry-url-validation.test.js +64 -0
  102. package/tests/unit/yaml.test.js +92 -0
@@ -0,0 +1,133 @@
1
+ import { describe, it, expect, beforeAll, afterAll } from 'vitest';
2
+ import { existsSync, mkdirSync, rmSync, writeFileSync } from 'fs';
3
+ import { join } from 'path';
4
+ import { loadTrustedKeys } from '../../src/registry/trust-store.js';
5
+ import { verifySignatureBlock } from '../../src/registry/signing.js';
6
+
7
+ const tempDir = join(process.cwd(), 'temp-trust-store-test');
8
+
9
+ describe('Registry Trust Store — loadTrustedKeys', () => {
10
+ beforeAll(() => {
11
+ mkdirSync(join(tempDir, '.ai', 'registries'), { recursive: true });
12
+ });
13
+
14
+ afterAll(() => {
15
+ if (existsSync(tempDir)) {
16
+ rmSync(tempDir, { recursive: true, force: true });
17
+ }
18
+ });
19
+
20
+ it('returns empty array when no trusted-keys.yaml exists', () => {
21
+ const noKeysDir = join(tempDir, 'no-keys-dir');
22
+ mkdirSync(noKeysDir, { recursive: true });
23
+ const result = loadTrustedKeys(noKeysDir);
24
+ expect(result).toEqual([]);
25
+ });
26
+
27
+ it('returns parsed trusted keys list when trusted-keys.yaml exists', () => {
28
+ const yamlContent = `
29
+ trusted_publishers:
30
+ - key_id: official-key
31
+ name: Official Maintainer
32
+ algorithm: ed25519
33
+ public_key: "-----BEGIN PUBLIC KEY-----\\n..."
34
+ scopes:
35
+ - registry
36
+ status: active
37
+ `;
38
+ writeFileSync(join(tempDir, '.ai', 'registries', 'trusted-keys.yaml'), yamlContent, 'utf8');
39
+ const result = loadTrustedKeys(tempDir);
40
+ expect(result).toHaveLength(1);
41
+ expect(result[0].key_id).toBe('official-key');
42
+ expect(result[0].name).toBe('Official Maintainer');
43
+ });
44
+ });
45
+
46
+ describe('Registry Trust Store — Status & Scope Validation', () => {
47
+ const trustedKeys = [
48
+ {
49
+ key_id: 'active-key',
50
+ name: 'Active Key',
51
+ algorithm: 'ed25519',
52
+ public_key: 'MCowBQYDK2VwAyEA9vWwyE5+fY0dvEzl9S1UcvtoMkOAIDhDCzZAkP+CVNo=',
53
+ scopes: ['registry'],
54
+ status: 'active'
55
+ },
56
+ {
57
+ key_id: 'disabled-key',
58
+ name: 'Disabled Key',
59
+ algorithm: 'ed25519',
60
+ public_key: 'MCowBQYDK2VwAyEA9vWwyE5+fY0dvEzl9S1UcvtoMkOAIDhDCzZAkP+CVNo=',
61
+ scopes: ['registry'],
62
+ status: 'disabled'
63
+ },
64
+ {
65
+ key_id: 'revoked-key',
66
+ name: 'Revoked Key',
67
+ algorithm: 'ed25519',
68
+ public_key: 'MCowBQYDK2VwAyEA9vWwyE5+fY0dvEzl9S1UcvtoMkOAIDhDCzZAkP+CVNo=',
69
+ scopes: ['registry'],
70
+ status: 'revoked'
71
+ },
72
+ {
73
+ key_id: 'wrong-scope-key',
74
+ name: 'Wrong Scope Key',
75
+ algorithm: 'ed25519',
76
+ public_key: 'MCowBQYDK2VwAyEA9vWwyE5+fY0dvEzl9S1UcvtoMkOAIDhDCzZAkP+CVNo=',
77
+ scopes: ['other'],
78
+ status: 'active'
79
+ }
80
+ ];
81
+
82
+ const manifestTemplate = {
83
+ registry_name: 'test-registry',
84
+ publisher: 'Test Publisher',
85
+ version: '1.0.0',
86
+ catalog_hash: 'sha256:abc',
87
+ signature: {
88
+ algorithm: 'ed25519',
89
+ key_id: 'active-key',
90
+ signature: 'sig',
91
+ signed_fields: ['registry_name', 'version', 'catalog_hash']
92
+ }
93
+ };
94
+
95
+ it('fails verification if key is disabled', () => {
96
+ const manifest = {
97
+ ...manifestTemplate,
98
+ signature: {
99
+ ...manifestTemplate.signature,
100
+ key_id: 'disabled-key'
101
+ }
102
+ };
103
+ const res = verifySignatureBlock({ manifest, trustedKeys });
104
+ expect(res.verified).toBe(false);
105
+ expect(res.errors[0]).toContain('must be active');
106
+ });
107
+
108
+ it('fails verification if key is revoked', () => {
109
+ const manifest = {
110
+ ...manifestTemplate,
111
+ signature: {
112
+ ...manifestTemplate.signature,
113
+ key_id: 'revoked-key'
114
+ }
115
+ };
116
+ const res = verifySignatureBlock({ manifest, trustedKeys });
117
+ expect(res.verified).toBe(false);
118
+ expect(res.errors[0]).toContain('must be active');
119
+ });
120
+
121
+ it('fails verification if scope does not include registry or catalog', () => {
122
+ const manifest = {
123
+ ...manifestTemplate,
124
+ signature: {
125
+ ...manifestTemplate.signature,
126
+ key_id: 'wrong-scope-key'
127
+ }
128
+ };
129
+ const res = verifySignatureBlock({ manifest, trustedKeys });
130
+ expect(res.verified).toBe(false);
131
+ expect(res.errors[0]).toContain('does not have required scope');
132
+ });
133
+ });
@@ -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
+ });
@@ -0,0 +1,92 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { parseYaml, parseFlowArray } from '../../src/core/yaml.js';
3
+
4
+ describe('YAML Parser', () => {
5
+ describe('parseFlowArray', () => {
6
+ it('should parse standard flow arrays', () => {
7
+ expect(parseFlowArray('[a, b, c]')).toEqual(['a', 'b', 'c']);
8
+ expect(parseFlowArray('["a", \'b\', c]')).toEqual(['a', 'b', 'c']);
9
+ expect(parseFlowArray('[true, false, null, 123, -45]')).toEqual([true, false, null, 123, -45]);
10
+ });
11
+
12
+ it('should handle empty flow array', () => {
13
+ expect(parseFlowArray('[]')).toEqual([]);
14
+ });
15
+ });
16
+
17
+ describe('parseYaml', () => {
18
+ it('should parse basic key-value pairs', () => {
19
+ const yaml = `
20
+ name: test-plugin
21
+ version: "1.0.0"
22
+ enabled: true
23
+ count: 42
24
+ `;
25
+ const result = parseYaml(yaml);
26
+ expect(result).toEqual({
27
+ name: 'test-plugin',
28
+ version: '1.0.0',
29
+ enabled: true,
30
+ count: 42
31
+ });
32
+ });
33
+
34
+ it('should ignore comments', () => {
35
+ const yaml = `
36
+ # This is a comment
37
+ name: test-plugin # trailing comment
38
+ version: '1.0.0'
39
+ `;
40
+ const result = parseYaml(yaml);
41
+ expect(result).toEqual({
42
+ name: 'test-plugin',
43
+ version: '1.0.0'
44
+ });
45
+ });
46
+
47
+ it('should not treat comments inside quoted strings as comments', () => {
48
+ const yaml = `
49
+ name: "test # plugin"
50
+ description: 'simple # description'
51
+ `;
52
+ const result = parseYaml(yaml);
53
+ expect(result).toEqual({
54
+ name: 'test # plugin',
55
+ description: 'simple # description'
56
+ });
57
+ });
58
+
59
+ it('should parse nested objects', () => {
60
+ const yaml = `
61
+ meta:
62
+ name: test
63
+ slug: test-slug
64
+ `;
65
+ const result = parseYaml(yaml);
66
+ expect(result).toEqual({
67
+ meta: {
68
+ name: 'test',
69
+ slug: 'test-slug'
70
+ }
71
+ });
72
+ });
73
+
74
+ it('should parse list arrays', () => {
75
+ const yaml = `
76
+ items:
77
+ - one
78
+ - "two"
79
+ - three
80
+ `;
81
+ const result = parseYaml(yaml);
82
+ expect(result).toEqual({
83
+ items: ['one', 'two', 'three']
84
+ });
85
+ });
86
+
87
+ it('should return empty object on malformed YAML', () => {
88
+ const result = parseYaml(null);
89
+ expect(result).toEqual({});
90
+ });
91
+ });
92
+ });