@openpalm/lib 0.10.2 → 0.11.0-beta.10
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 +4 -2
- package/package.json +11 -3
- package/src/control-plane/akm-vault.test.ts +105 -0
- package/src/control-plane/akm-vault.ts +311 -0
- package/src/control-plane/channels.ts +11 -9
- package/src/control-plane/cleanup-guardrails.test.ts +8 -9
- package/src/control-plane/compose-args.test.ts +25 -33
- package/src/control-plane/compose-args.ts +0 -4
- package/src/control-plane/compose-errors.test.ts +106 -0
- package/src/control-plane/compose-errors.ts +117 -0
- package/src/control-plane/config-persistence.ts +148 -73
- package/src/control-plane/core-assets.test.ts +104 -0
- package/src/control-plane/core-assets.ts +111 -58
- package/src/control-plane/docker.ts +70 -25
- package/src/control-plane/env.test.ts +25 -1
- package/src/control-plane/env.ts +84 -1
- package/src/control-plane/home.ts +66 -69
- package/src/control-plane/host-opencode.test.ts +260 -0
- package/src/control-plane/host-opencode.ts +229 -0
- package/src/control-plane/install-edge-cases.test.ts +190 -292
- package/src/control-plane/install-lock.ts +157 -0
- package/src/control-plane/lifecycle.ts +65 -75
- package/src/control-plane/markdown-task.ts +200 -0
- package/src/control-plane/migrate-0110.test.ts +177 -0
- package/src/control-plane/migrate-0110.ts +99 -0
- package/src/control-plane/operator-ids.test.ts +130 -0
- package/src/control-plane/operator-ids.ts +89 -0
- package/src/control-plane/paths.ts +80 -0
- package/src/control-plane/provider-models.ts +154 -0
- package/src/control-plane/registry-components.test.ts +105 -27
- package/src/control-plane/registry.test.ts +247 -51
- package/src/control-plane/registry.ts +404 -54
- package/src/control-plane/rollback.ts +17 -16
- package/src/control-plane/scheduler.ts +75 -262
- package/src/control-plane/secret-mappings.ts +4 -8
- package/src/control-plane/secrets.ts +97 -55
- package/src/control-plane/setup-config.schema.json +5 -17
- package/src/control-plane/setup-status.ts +9 -29
- package/src/control-plane/setup-validation.ts +23 -23
- package/src/control-plane/setup.test.ts +143 -244
- package/src/control-plane/setup.ts +216 -133
- package/src/control-plane/skeleton-guardrail.test.ts +151 -0
- package/src/control-plane/spec-to-env.test.ts +75 -60
- package/src/control-plane/spec-to-env.ts +68 -153
- package/src/control-plane/stack-spec.test.ts +22 -84
- package/src/control-plane/stack-spec.ts +9 -89
- package/src/control-plane/types.ts +9 -29
- package/src/control-plane/ui-assets.ts +385 -0
- package/src/control-plane/validate.ts +44 -79
- package/src/index.ts +102 -56
- package/src/logger.test.ts +228 -0
- package/src/logger.ts +71 -1
- package/src/provider-constants.ts +22 -1
- package/src/control-plane/audit.ts +0 -40
- package/src/control-plane/env-schema-validation.test.ts +0 -118
- package/src/control-plane/lock.test.ts +0 -194
- package/src/control-plane/lock.ts +0 -176
- package/src/control-plane/memory-config.ts +0 -298
- package/src/control-plane/provider-config.ts +0 -34
- package/src/control-plane/redact-schema.ts +0 -50
- package/src/control-plane/secret-backend.test.ts +0 -359
- package/src/control-plane/secret-backend.ts +0 -322
- package/src/control-plane/spec-validator.ts +0 -159
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider model discovery and API key resolution.
|
|
3
|
+
*
|
|
4
|
+
* Used by the admin capabilities test endpoint and the CLI setup wizard
|
|
5
|
+
* to enumerate the models a configured provider exposes.
|
|
6
|
+
*/
|
|
7
|
+
import { readStackEnv } from "./secrets.js";
|
|
8
|
+
import { PROVIDER_DEFAULT_URLS } from "../provider-constants.js";
|
|
9
|
+
|
|
10
|
+
/** Static model list for Anthropic (no listing API available). */
|
|
11
|
+
const ANTHROPIC_MODELS = [
|
|
12
|
+
"claude-opus-4-6",
|
|
13
|
+
"claude-sonnet-4-6",
|
|
14
|
+
"claude-opus-4-20250514",
|
|
15
|
+
"claude-sonnet-4-20250514",
|
|
16
|
+
"claude-haiku-4-5-20251001",
|
|
17
|
+
"claude-3-5-sonnet-20241022",
|
|
18
|
+
"claude-3-5-haiku-20241022",
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Resolve an API key reference.
|
|
24
|
+
*
|
|
25
|
+
* - Empty input → empty string.
|
|
26
|
+
* - `env:NAME` form → looks up `NAME` in `process.env` first, then falls back
|
|
27
|
+
* to `config/stack/stack.env` resolved against `stackDir`.
|
|
28
|
+
* - Anything else → returned verbatim (treated as a literal key value).
|
|
29
|
+
*/
|
|
30
|
+
function resolveApiKey(apiKeyRef: string, stackDir: string): string {
|
|
31
|
+
if (!apiKeyRef) return "";
|
|
32
|
+
if (!apiKeyRef.startsWith("env:")) return apiKeyRef;
|
|
33
|
+
|
|
34
|
+
const varName = apiKeyRef.slice(4);
|
|
35
|
+
if (process.env[varName]) return process.env[varName]!;
|
|
36
|
+
|
|
37
|
+
const secrets = readStackEnv(stackDir);
|
|
38
|
+
return secrets[varName] ?? "";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
export type ModelDiscoveryReason =
|
|
43
|
+
| 'none'
|
|
44
|
+
| 'provider_static'
|
|
45
|
+
| 'provider_http'
|
|
46
|
+
| 'missing_base_url'
|
|
47
|
+
| 'timeout'
|
|
48
|
+
| 'network';
|
|
49
|
+
|
|
50
|
+
export type ProviderModelsResult = {
|
|
51
|
+
models: string[];
|
|
52
|
+
status: 'ok' | 'recoverable_error';
|
|
53
|
+
reason: ModelDiscoveryReason;
|
|
54
|
+
error?: string;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const HTTP_STATUS_LABELS: Record<number, string> = {
|
|
58
|
+
401: 'Invalid or missing API key',
|
|
59
|
+
403: 'Access denied — check API key permissions',
|
|
60
|
+
404: 'Endpoint not found — verify the base URL',
|
|
61
|
+
429: 'Rate limited — try again shortly',
|
|
62
|
+
500: 'Provider internal error',
|
|
63
|
+
502: 'Provider returned a bad gateway error',
|
|
64
|
+
503: 'Provider is temporarily unavailable',
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Enumerate available models for a provider. Returns an `ok` result with a
|
|
69
|
+
* sorted model list when the provider responds successfully, or a
|
|
70
|
+
* `recoverable_error` with a structured reason otherwise. Network and timeout
|
|
71
|
+
* failures are caught and mapped to a result rather than thrown.
|
|
72
|
+
*/
|
|
73
|
+
export async function fetchProviderModels(
|
|
74
|
+
provider: string,
|
|
75
|
+
apiKeyRef: string,
|
|
76
|
+
baseUrl: string,
|
|
77
|
+
stackDir: string
|
|
78
|
+
): Promise<ProviderModelsResult> {
|
|
79
|
+
try {
|
|
80
|
+
if (provider === "anthropic") {
|
|
81
|
+
return { models: [...ANTHROPIC_MODELS], status: 'ok', reason: 'provider_static' };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const resolvedKey = resolveApiKey(apiKeyRef, stackDir);
|
|
85
|
+
|
|
86
|
+
if (provider === "ollama") {
|
|
87
|
+
const base = baseUrl?.trim() || PROVIDER_DEFAULT_URLS.ollama;
|
|
88
|
+
const url = `${base.replace(/\/+$/, "")}/api/tags`;
|
|
89
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(5000) });
|
|
90
|
+
if (!res.ok) {
|
|
91
|
+
return {
|
|
92
|
+
models: [],
|
|
93
|
+
status: 'recoverable_error',
|
|
94
|
+
reason: 'provider_http',
|
|
95
|
+
error: `Ollama API returned ${res.status}: ${(HTTP_STATUS_LABELS[res.status] ?? `HTTP ${res.status}`)}`,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
const data = (await res.json()) as { models?: { name: string }[] };
|
|
99
|
+
const models = (data.models ?? []).map((m) => m.name).sort();
|
|
100
|
+
return { models, status: 'ok', reason: 'none' };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const base = baseUrl?.trim() || PROVIDER_DEFAULT_URLS[provider] || "";
|
|
104
|
+
if (!base) {
|
|
105
|
+
return {
|
|
106
|
+
models: [],
|
|
107
|
+
status: 'recoverable_error',
|
|
108
|
+
reason: 'missing_base_url',
|
|
109
|
+
error: `No base URL configured for provider "${provider}"`,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
const url = `${base.replace(/\/+$/, "")}/v1/models`;
|
|
113
|
+
|
|
114
|
+
const headers: Record<string, string> = {};
|
|
115
|
+
if (resolvedKey) {
|
|
116
|
+
headers["Authorization"] = `Bearer ${resolvedKey}`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const res = await fetch(url, { headers, signal: AbortSignal.timeout(5000) });
|
|
120
|
+
if (!res.ok) {
|
|
121
|
+
let detail = '';
|
|
122
|
+
try {
|
|
123
|
+
const json = JSON.parse(await res.text()) as Record<string, unknown>;
|
|
124
|
+
const errObj = json.error as Record<string, unknown> | string | undefined;
|
|
125
|
+
detail = (typeof errObj === 'object' && errObj !== null && typeof errObj.message === 'string') ? errObj.message
|
|
126
|
+
: typeof errObj === 'string' ? errObj
|
|
127
|
+
: typeof json.message === 'string' ? json.message
|
|
128
|
+
: typeof json.detail === 'string' ? json.detail : '';
|
|
129
|
+
} catch { /* ignore parse errors */ }
|
|
130
|
+
return {
|
|
131
|
+
models: [],
|
|
132
|
+
status: 'recoverable_error',
|
|
133
|
+
reason: 'provider_http',
|
|
134
|
+
error: detail
|
|
135
|
+
? `Provider API returned ${res.status}: ${detail}`
|
|
136
|
+
: `Provider API returned ${res.status}: ${(HTTP_STATUS_LABELS[res.status] ?? `HTTP ${res.status}`)}`,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
const data = (await res.json()) as { data?: { id: string }[] };
|
|
140
|
+
const models = (data.data ?? []).map((m) => m.id).sort();
|
|
141
|
+
return { models, status: 'ok', reason: 'none' };
|
|
142
|
+
} catch (err) {
|
|
143
|
+
const message =
|
|
144
|
+
err instanceof Error && err.name === "TimeoutError"
|
|
145
|
+
? "Request timed out after 5s"
|
|
146
|
+
: String(err);
|
|
147
|
+
return {
|
|
148
|
+
models: [],
|
|
149
|
+
status: 'recoverable_error',
|
|
150
|
+
reason: err instanceof Error && err.name === 'TimeoutError' ? 'timeout' : 'network',
|
|
151
|
+
error: message,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Tests for the registry component directory format.
|
|
3
3
|
*
|
|
4
|
-
* Validates that all components in .openpalm/registry/addons/ follow the
|
|
4
|
+
* Validates that all components in .openpalm/state/registry/addons/ follow the
|
|
5
5
|
* component conventions: compose.yml with required labels, .env.schema
|
|
6
6
|
* with documented variables, proper service naming, and no security
|
|
7
7
|
* violations.
|
|
8
|
+
*
|
|
9
|
+
* Two component shapes are accepted:
|
|
10
|
+
*
|
|
11
|
+
* 1. Full addons — compose.yml + .env.schema. They introduce a new
|
|
12
|
+
* service, declare env vars, and must satisfy the full structural
|
|
13
|
+
* checklist (labels, network, healthcheck, restart policy, sensitive
|
|
14
|
+
* fields).
|
|
15
|
+
* 2. Overlay-only addons — compose.yml only. They patch existing
|
|
16
|
+
* services (ports, env, volumes) instead of introducing new ones,
|
|
17
|
+
* so they have no env vars to document and no service-shaped
|
|
18
|
+
* requirements. They still must satisfy the security invariants:
|
|
19
|
+
* no INSTANCE_ID, no container_name, no INSTANCE_DIR, no vault
|
|
20
|
+
* directory mounts, no docker socket.
|
|
8
21
|
*/
|
|
9
22
|
import { describe, expect, it } from "bun:test";
|
|
10
23
|
import {
|
|
@@ -18,7 +31,7 @@ import { join, resolve } from "node:path";
|
|
|
18
31
|
|
|
19
32
|
/** Resolve path from repo root */
|
|
20
33
|
const REPO_ROOT = resolve(import.meta.dir, "../../../..");
|
|
21
|
-
const REGISTRY_DIR = join(REPO_ROOT, ".openpalm/registry/addons");
|
|
34
|
+
const REGISTRY_DIR = join(REPO_ROOT, ".openpalm/state/registry/addons");
|
|
22
35
|
|
|
23
36
|
/** List all component directories in the registry */
|
|
24
37
|
function listComponentDirs(): string[] {
|
|
@@ -28,6 +41,19 @@ function listComponentDirs(): string[] {
|
|
|
28
41
|
.map((d) => d.name);
|
|
29
42
|
}
|
|
30
43
|
|
|
44
|
+
/** Overlay-only addons ship compose.yml only — no .env.schema. */
|
|
45
|
+
function isOverlayOnly(componentId: string): boolean {
|
|
46
|
+
return !existsSync(join(REGISTRY_DIR, componentId, ".env.schema"));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function listFullAddonIds(componentIds: string[]): string[] {
|
|
50
|
+
return componentIds.filter((id) => !isOverlayOnly(id));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function listOverlayOnlyAddonIds(componentIds: string[]): string[] {
|
|
54
|
+
return componentIds.filter(isOverlayOnly);
|
|
55
|
+
}
|
|
56
|
+
|
|
31
57
|
/** Read a file from a component directory */
|
|
32
58
|
function readComponentFile(componentId: string, filename: string): string {
|
|
33
59
|
return readFileSync(join(REGISTRY_DIR, componentId, filename), "utf-8");
|
|
@@ -118,24 +144,67 @@ describe("registry component discovery", () => {
|
|
|
118
144
|
|
|
119
145
|
describe("registry component required files", () => {
|
|
120
146
|
const componentIds = listComponentDirs();
|
|
147
|
+
const fullAddonIds = listFullAddonIds(componentIds);
|
|
121
148
|
|
|
122
149
|
for (const id of componentIds) {
|
|
123
150
|
it(`${id}: has compose.yml`, () => {
|
|
124
151
|
expect(existsSync(join(REGISTRY_DIR, id, "compose.yml"))).toBe(true);
|
|
125
152
|
});
|
|
153
|
+
}
|
|
126
154
|
|
|
127
|
-
|
|
155
|
+
for (const id of fullAddonIds) {
|
|
156
|
+
it(`${id}: has .env.schema (full addon)`, () => {
|
|
128
157
|
expect(existsSync(join(REGISTRY_DIR, id, ".env.schema"))).toBe(true);
|
|
129
158
|
});
|
|
130
159
|
}
|
|
131
160
|
});
|
|
132
161
|
|
|
162
|
+
// ── Overlay-only Addon Tests ─────────────────────────────────────────────
|
|
163
|
+
|
|
164
|
+
describe("registry overlay-only addons", () => {
|
|
165
|
+
const componentIds = listComponentDirs();
|
|
166
|
+
const overlayIds = listOverlayOnlyAddonIds(componentIds);
|
|
167
|
+
|
|
168
|
+
it("at least one overlay-only addon (ssh) is recognized as valid", () => {
|
|
169
|
+
expect(overlayIds).toContain("ssh");
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
for (const id of overlayIds) {
|
|
173
|
+
describe(id, () => {
|
|
174
|
+
it("ships only compose.yml (no .env.schema, no entrypoint, no Dockerfile)", () => {
|
|
175
|
+
const dirEntries = readdirSync(join(REGISTRY_DIR, id));
|
|
176
|
+
// compose.yml is required; an optional README.md is allowed; nothing
|
|
177
|
+
// else (no .env.schema, no entrypoint*, no Dockerfile, no scripts).
|
|
178
|
+
const allowed = new Set(["compose.yml", "README.md"]);
|
|
179
|
+
for (const file of dirEntries) {
|
|
180
|
+
expect(allowed.has(file)).toBe(true);
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("compose.yml does not introduce a new service (no image: or build:)", () => {
|
|
185
|
+
// Overlay-only addons may patch existing services with new ports/env,
|
|
186
|
+
// but they MUST NOT introduce a new service that needs its own
|
|
187
|
+
// network/healthcheck/restart contract — those would belong in a
|
|
188
|
+
// full addon. Reject service definition keys that imply a new
|
|
189
|
+
// service body. A pure overlay only sets `ports:`, `environment:`,
|
|
190
|
+
// `volumes:`, etc. on already-defined services.
|
|
191
|
+
const compose = readComponentFile(id, "compose.yml");
|
|
192
|
+
expect(compose).not.toMatch(/^\s+image:\s/m);
|
|
193
|
+
expect(compose).not.toMatch(/^\s+build:\s/m);
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
133
199
|
// ── Compose Overlay Validation Tests ─────────────────────────────────────
|
|
134
200
|
|
|
135
201
|
describe("registry compose.yml validation", () => {
|
|
136
202
|
const componentIds = listComponentDirs();
|
|
203
|
+
const fullAddonIds = listFullAddonIds(componentIds);
|
|
137
204
|
|
|
138
|
-
|
|
205
|
+
// Full-addon-only assertions: anything that requires a service body
|
|
206
|
+
// (labels, network, healthcheck, restart policy) is checked here.
|
|
207
|
+
for (const id of fullAddonIds) {
|
|
139
208
|
describe(id, () => {
|
|
140
209
|
const compose = readComponentFile(id, "compose.yml");
|
|
141
210
|
|
|
@@ -147,18 +216,6 @@ describe("registry compose.yml validation", () => {
|
|
|
147
216
|
expect(compose).toMatch(/openpalm\.description:/);
|
|
148
217
|
});
|
|
149
218
|
|
|
150
|
-
it("uses static service name (no INSTANCE_ID)", () => {
|
|
151
|
-
expect(compose).not.toContain("${INSTANCE_ID}");
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
it("does not use container_name", () => {
|
|
155
|
-
expect(compose).not.toMatch(/container_name:/);
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
it("does not reference INSTANCE_DIR", () => {
|
|
159
|
-
expect(compose).not.toContain("${INSTANCE_DIR}");
|
|
160
|
-
});
|
|
161
|
-
|
|
162
219
|
it("joins a valid stack network", () => {
|
|
163
220
|
const hasValidNetwork = compose.includes("channel_lan") || compose.includes("channel_public") || compose.includes("assistant_net");
|
|
164
221
|
expect(hasValidNetwork).toBe(true);
|
|
@@ -171,9 +228,28 @@ describe("registry compose.yml validation", () => {
|
|
|
171
228
|
it("has healthcheck", () => {
|
|
172
229
|
expect(compose).toMatch(/healthcheck:/);
|
|
173
230
|
});
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Security/hygiene assertions apply to ALL addons (full and overlay-only).
|
|
235
|
+
for (const id of componentIds) {
|
|
236
|
+
describe(`${id} (security)`, () => {
|
|
237
|
+
const compose = readComponentFile(id, "compose.yml");
|
|
238
|
+
|
|
239
|
+
it("uses static service name (no INSTANCE_ID)", () => {
|
|
240
|
+
expect(compose).not.toContain("${INSTANCE_ID}");
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("does not use container_name", () => {
|
|
244
|
+
expect(compose).not.toMatch(/container_name:/);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("does not reference INSTANCE_DIR", () => {
|
|
248
|
+
expect(compose).not.toContain("${INSTANCE_DIR}");
|
|
249
|
+
});
|
|
174
250
|
|
|
175
251
|
it("does not mount vault directory (single-file mounts allowed)", () => {
|
|
176
|
-
// Directory-level vault mounts are a security violation —
|
|
252
|
+
// Directory-level vault mounts are a security violation — no container may mount the full vault.
|
|
177
253
|
// Single-file mounts like vault/user/ov.conf are allowed (the source must end with a filename).
|
|
178
254
|
const lines = compose.split("\n");
|
|
179
255
|
for (const line of lines) {
|
|
@@ -193,8 +269,6 @@ describe("registry compose.yml validation", () => {
|
|
|
193
269
|
});
|
|
194
270
|
|
|
195
271
|
it("does not mount docker socket", () => {
|
|
196
|
-
// admin component is exempt — docker-socket-proxy IS the docker socket accessor by design
|
|
197
|
-
if (id === "admin") return;
|
|
198
272
|
expect(compose).not.toContain("/var/run/docker.sock");
|
|
199
273
|
});
|
|
200
274
|
|
|
@@ -209,8 +283,9 @@ describe("registry compose.yml validation", () => {
|
|
|
209
283
|
|
|
210
284
|
describe("registry .env.schema validation", () => {
|
|
211
285
|
const componentIds = listComponentDirs();
|
|
286
|
+
const fullAddonIds = listFullAddonIds(componentIds);
|
|
212
287
|
|
|
213
|
-
for (const id of
|
|
288
|
+
for (const id of fullAddonIds) {
|
|
214
289
|
describe(id, () => {
|
|
215
290
|
const schema = readComponentFile(id, ".env.schema");
|
|
216
291
|
const entries = parseEnvSchema(schema);
|
|
@@ -264,11 +339,13 @@ describe("registry .env.schema validation", () => {
|
|
|
264
339
|
|
|
265
340
|
describe("registry component sensitive fields", () => {
|
|
266
341
|
const componentIds = listComponentDirs();
|
|
342
|
+
const fullAddonIds = listFullAddonIds(componentIds);
|
|
267
343
|
|
|
268
|
-
for (const id of
|
|
344
|
+
for (const id of fullAddonIds) {
|
|
269
345
|
it(`${id}: has at least one @sensitive field (channel secret)`, () => {
|
|
270
|
-
// ollama
|
|
271
|
-
|
|
346
|
+
// ollama and voice are local inference servers — no channel secret
|
|
347
|
+
// or upstream API key needed (LAN-only, no auth by design).
|
|
348
|
+
if (id === "ollama" || id === "voice") return;
|
|
272
349
|
const schema = readComponentFile(id, ".env.schema");
|
|
273
350
|
const entries = parseEnvSchema(schema);
|
|
274
351
|
const sensitiveEntries = entries.filter((e) =>
|
|
@@ -283,10 +360,11 @@ describe("registry component sensitive fields", () => {
|
|
|
283
360
|
|
|
284
361
|
describe("cross-component consistency", () => {
|
|
285
362
|
const componentIds = listComponentDirs();
|
|
363
|
+
const fullAddonIds = listFullAddonIds(componentIds);
|
|
286
364
|
|
|
287
|
-
it("no duplicate openpalm.name labels across
|
|
365
|
+
it("no duplicate openpalm.name labels across full addons", () => {
|
|
288
366
|
const names = new Set<string>();
|
|
289
|
-
for (const id of
|
|
367
|
+
for (const id of fullAddonIds) {
|
|
290
368
|
const compose = readComponentFile(id, "compose.yml");
|
|
291
369
|
const nameMatch = compose.match(/openpalm\.name:\s*(.+)/);
|
|
292
370
|
expect(nameMatch).not.toBeNull();
|
|
@@ -296,8 +374,8 @@ describe("cross-component consistency", () => {
|
|
|
296
374
|
}
|
|
297
375
|
});
|
|
298
376
|
|
|
299
|
-
it("all
|
|
300
|
-
for (const id of
|
|
377
|
+
it("all full addons join a valid stack network", () => {
|
|
378
|
+
for (const id of fullAddonIds) {
|
|
301
379
|
const compose = readComponentFile(id, "compose.yml");
|
|
302
380
|
const hasValidNetwork = compose.includes("channel_lan") || compose.includes("channel_public") || compose.includes("assistant_net");
|
|
303
381
|
expect(hasValidNetwork).toBe(true);
|