@openpalm/lib 0.9.8 → 0.10.1
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 +31 -71
- package/package.json +1 -1
- package/src/control-plane/audit.ts +4 -4
- package/src/control-plane/backup.ts +31 -0
- package/src/control-plane/channels.ts +88 -156
- package/src/control-plane/cleanup-guardrails.test.ts +289 -0
- package/src/control-plane/compose-args.test.ts +170 -0
- package/src/control-plane/compose-args.ts +57 -0
- package/src/control-plane/config-persistence.ts +270 -0
- package/src/control-plane/core-assets.ts +58 -234
- package/src/control-plane/crypto.ts +14 -0
- package/src/control-plane/docker.ts +94 -204
- package/src/control-plane/env-schema-validation.test.ts +118 -0
- package/src/control-plane/extends-support.test.ts +105 -0
- package/src/control-plane/home.ts +133 -0
- package/src/control-plane/install-edge-cases.test.ts +314 -717
- package/src/control-plane/lifecycle.ts +215 -233
- package/src/control-plane/lock.test.ts +194 -0
- package/src/control-plane/lock.ts +176 -0
- package/src/control-plane/memory-config.ts +34 -160
- package/src/control-plane/opencode-client.test.ts +154 -0
- package/src/control-plane/opencode-client.ts +113 -0
- package/src/control-plane/provider-config.ts +34 -0
- package/src/control-plane/redact-schema.ts +50 -0
- package/src/control-plane/registry-components.test.ts +313 -0
- package/src/control-plane/registry.test.ts +414 -0
- package/src/control-plane/registry.ts +418 -0
- package/src/control-plane/rollback.ts +128 -0
- package/src/control-plane/scheduler.ts +18 -190
- package/src/control-plane/secret-backend.test.ts +359 -0
- package/src/control-plane/secret-backend.ts +322 -0
- package/src/control-plane/secret-mappings.ts +185 -0
- package/src/control-plane/secrets.ts +186 -112
- package/src/control-plane/setup-config.schema.json +306 -0
- package/src/control-plane/setup-status.ts +15 -8
- package/src/control-plane/setup-validation.ts +90 -0
- package/src/control-plane/setup.test.ts +336 -929
- package/src/control-plane/setup.ts +159 -849
- package/src/control-plane/spec-to-env.test.ts +100 -0
- package/src/control-plane/spec-to-env.ts +195 -0
- package/src/control-plane/spec-validator.ts +159 -0
- package/src/control-plane/stack-spec.test.ts +150 -0
- package/src/control-plane/stack-spec.ts +101 -22
- package/src/control-plane/types.ts +6 -99
- package/src/control-plane/validate.ts +107 -0
- package/src/index.ts +101 -159
- package/src/provider-constants.ts +2 -31
- package/src/control-plane/connection-mapping.ts +0 -191
- package/src/control-plane/connection-migration-flags.ts +0 -40
- package/src/control-plane/connection-profiles.ts +0 -317
- package/src/control-plane/core-asset-provider.ts +0 -21
- package/src/control-plane/fs-asset-provider.ts +0 -65
- package/src/control-plane/fs-registry-provider.ts +0 -46
- package/src/control-plane/paths.ts +0 -77
- package/src/control-plane/registry-provider.ts +0 -19
- package/src/control-plane/staging.ts +0 -399
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://raw.githubusercontent.com/itlackey/openpalm/main/packages/lib/src/control-plane/setup-config.schema.json",
|
|
4
|
+
"title": "OpenPalm SetupConfig",
|
|
5
|
+
"description": "Structured configuration for the OpenPalm setup wizard. Defines connections, model assignments, channels, services, and security settings needed for first-time installation or reconfiguration.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["version", "security", "capabilities", "assignments"],
|
|
8
|
+
"additionalProperties": false,
|
|
9
|
+
"properties": {
|
|
10
|
+
"version": {
|
|
11
|
+
"const": 1,
|
|
12
|
+
"description": "Schema version. Must be 1."
|
|
13
|
+
},
|
|
14
|
+
"owner": {
|
|
15
|
+
"type": "object",
|
|
16
|
+
"description": "Optional owner identity for this OpenPalm instance.",
|
|
17
|
+
"additionalProperties": false,
|
|
18
|
+
"properties": {
|
|
19
|
+
"name": {
|
|
20
|
+
"type": "string",
|
|
21
|
+
"description": "Display name of the instance owner."
|
|
22
|
+
},
|
|
23
|
+
"email": {
|
|
24
|
+
"type": "string",
|
|
25
|
+
"description": "Email address of the instance owner.",
|
|
26
|
+
"format": "email"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"security": {
|
|
31
|
+
"type": "object",
|
|
32
|
+
"description": "Security settings for the instance.",
|
|
33
|
+
"required": ["adminToken"],
|
|
34
|
+
"additionalProperties": false,
|
|
35
|
+
"properties": {
|
|
36
|
+
"adminToken": {
|
|
37
|
+
"type": "string",
|
|
38
|
+
"description": "Admin API authentication token. Used to authenticate CLI and admin UI requests.",
|
|
39
|
+
"minLength": 8
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"capabilities": {
|
|
44
|
+
"type": "array",
|
|
45
|
+
"description": "LLM provider capabilities available to the instance. Must contain at least one entry.",
|
|
46
|
+
"minItems": 1,
|
|
47
|
+
"items": {
|
|
48
|
+
"$ref": "#/$defs/SetupCapability"
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"assignments": {
|
|
52
|
+
"$ref": "#/$defs/SetupConfigAssignments"
|
|
53
|
+
},
|
|
54
|
+
"memory": {
|
|
55
|
+
"type": "object",
|
|
56
|
+
"description": "Optional memory subsystem configuration.",
|
|
57
|
+
"additionalProperties": false,
|
|
58
|
+
"properties": {
|
|
59
|
+
"userId": {
|
|
60
|
+
"type": "string",
|
|
61
|
+
"pattern": "^[A-Za-z0-9_]+$",
|
|
62
|
+
"description": "User ID for the memory service. Alphanumeric and underscores only. Defaults to 'default_user' if omitted."
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
"channels": {
|
|
67
|
+
"type": "object",
|
|
68
|
+
"description": "Optional channel configurations. Keys are channel identifiers (e.g. 'chat', 'discord', 'api'). Values can be a boolean to enable (true) or skip (false) installation, or a credential object.",
|
|
69
|
+
"additionalProperties": {
|
|
70
|
+
"oneOf": [
|
|
71
|
+
{
|
|
72
|
+
"type": "boolean",
|
|
73
|
+
"description": "Enable (true) or disable (false) this channel with default settings."
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"$ref": "#/$defs/ChannelCredentials"
|
|
77
|
+
}
|
|
78
|
+
]
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
"services": {
|
|
82
|
+
"type": "object",
|
|
83
|
+
"description": "Optional service configurations. Keys are service identifiers (e.g. 'admin', 'ollama'). Values can be a boolean to enable (true) or skip (false) installation, or a config object.",
|
|
84
|
+
"additionalProperties": {
|
|
85
|
+
"oneOf": [
|
|
86
|
+
{
|
|
87
|
+
"type": "boolean",
|
|
88
|
+
"description": "Enable (true) or disable (false) this service with default settings."
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"$ref": "#/$defs/ServiceConfig"
|
|
92
|
+
}
|
|
93
|
+
]
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
"$defs": {
|
|
98
|
+
"SetupCapability": {
|
|
99
|
+
"type": "object",
|
|
100
|
+
"description": "An LLM provider capability with endpoint and credentials.",
|
|
101
|
+
"required": ["id", "name", "provider", "baseUrl"],
|
|
102
|
+
"additionalProperties": false,
|
|
103
|
+
"properties": {
|
|
104
|
+
"id": {
|
|
105
|
+
"type": "string",
|
|
106
|
+
"description": "Unique capability identifier. Must start with a letter or digit; allows A-Z, a-z, 0-9, underscore, and hyphen.",
|
|
107
|
+
"pattern": "^[A-Za-z0-9][A-Za-z0-9_-]*$"
|
|
108
|
+
},
|
|
109
|
+
"name": {
|
|
110
|
+
"type": "string",
|
|
111
|
+
"description": "Human-readable display name for this capability.",
|
|
112
|
+
"minLength": 1
|
|
113
|
+
},
|
|
114
|
+
"provider": {
|
|
115
|
+
"type": "string",
|
|
116
|
+
"description": "Provider identifier. Must be a recognized provider from the wizard scope. 'ollama-instack' is an internal alias used by the CLI to route Ollama capabilities to the in-stack Ollama service container.",
|
|
117
|
+
"enum": [
|
|
118
|
+
"openai",
|
|
119
|
+
"anthropic",
|
|
120
|
+
"ollama",
|
|
121
|
+
"groq",
|
|
122
|
+
"together",
|
|
123
|
+
"mistral",
|
|
124
|
+
"deepseek",
|
|
125
|
+
"xai",
|
|
126
|
+
"lmstudio",
|
|
127
|
+
"model-runner",
|
|
128
|
+
"ollama-instack",
|
|
129
|
+
"google",
|
|
130
|
+
"huggingface"
|
|
131
|
+
]
|
|
132
|
+
},
|
|
133
|
+
"baseUrl": {
|
|
134
|
+
"type": "string",
|
|
135
|
+
"description": "Base URL for the provider API endpoint. Can be empty for cloud providers that use default URLs."
|
|
136
|
+
},
|
|
137
|
+
"apiKey": {
|
|
138
|
+
"type": "string",
|
|
139
|
+
"description": "API key for authentication. Optional for local providers (e.g. Ollama, LM Studio) that do not require authentication."
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
"SetupConfigAssignments": {
|
|
144
|
+
"type": "object",
|
|
145
|
+
"description": "Model capability assignments mapping connections and models to system roles.",
|
|
146
|
+
"required": ["llm", "embeddings"],
|
|
147
|
+
"additionalProperties": false,
|
|
148
|
+
"properties": {
|
|
149
|
+
"llm": {
|
|
150
|
+
"type": "object",
|
|
151
|
+
"description": "Primary LLM assignment for the system.",
|
|
152
|
+
"required": ["capabilityId", "model"],
|
|
153
|
+
"additionalProperties": false,
|
|
154
|
+
"properties": {
|
|
155
|
+
"capabilityId": {
|
|
156
|
+
"type": "string",
|
|
157
|
+
"description": "ID of the connection to use for LLM inference. Must reference a capability in the capabilities array."
|
|
158
|
+
},
|
|
159
|
+
"model": {
|
|
160
|
+
"type": "string",
|
|
161
|
+
"description": "Model identifier for the primary LLM (e.g. 'gpt-4o', 'claude-sonnet-4-20250514')."
|
|
162
|
+
},
|
|
163
|
+
"smallModel": {
|
|
164
|
+
"type": "string",
|
|
165
|
+
"description": "Optional smaller/faster model for lightweight tasks (e.g. memory extraction)."
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
"embeddings": {
|
|
170
|
+
"type": "object",
|
|
171
|
+
"description": "Embedding model assignment for the memory subsystem.",
|
|
172
|
+
"required": ["capabilityId", "model"],
|
|
173
|
+
"additionalProperties": false,
|
|
174
|
+
"properties": {
|
|
175
|
+
"capabilityId": {
|
|
176
|
+
"type": "string",
|
|
177
|
+
"description": "ID of the connection to use for embeddings. Must reference a capability in the capabilities array."
|
|
178
|
+
},
|
|
179
|
+
"model": {
|
|
180
|
+
"type": "string",
|
|
181
|
+
"description": "Model identifier for embeddings (e.g. 'text-embedding-3-small', 'nomic-embed-text')."
|
|
182
|
+
},
|
|
183
|
+
"embeddingDims": {
|
|
184
|
+
"type": "integer",
|
|
185
|
+
"description": "Embedding vector dimensions. Auto-detected from known models if omitted.",
|
|
186
|
+
"minimum": 1
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
"tts": {
|
|
191
|
+
"description": "Optional text-to-speech assignment. Can be an engine name string, an object with engine details, or null to disable.",
|
|
192
|
+
"oneOf": [
|
|
193
|
+
{
|
|
194
|
+
"type": "string",
|
|
195
|
+
"description": "TTS engine name (e.g. 'kokoro', 'piper', 'openai-tts', 'browser-tts')."
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
"$ref": "#/$defs/VoiceAssignment"
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
"type": "null"
|
|
202
|
+
}
|
|
203
|
+
]
|
|
204
|
+
},
|
|
205
|
+
"stt": {
|
|
206
|
+
"description": "Optional speech-to-text assignment. Can be an engine name string, an object with engine details, or null to disable.",
|
|
207
|
+
"oneOf": [
|
|
208
|
+
{
|
|
209
|
+
"type": "string",
|
|
210
|
+
"description": "STT engine name (e.g. 'whisper-local', 'openai-stt', 'browser-stt')."
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
"$ref": "#/$defs/VoiceAssignment"
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
"type": "null"
|
|
217
|
+
}
|
|
218
|
+
]
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
"VoiceAssignment": {
|
|
223
|
+
"type": "object",
|
|
224
|
+
"description": "Detailed voice engine assignment with optional connection and model.",
|
|
225
|
+
"required": ["engine"],
|
|
226
|
+
"additionalProperties": false,
|
|
227
|
+
"properties": {
|
|
228
|
+
"engine": {
|
|
229
|
+
"type": "string",
|
|
230
|
+
"description": "Voice engine identifier."
|
|
231
|
+
},
|
|
232
|
+
"capabilityId": {
|
|
233
|
+
"type": "string",
|
|
234
|
+
"description": "Optional capability ID if the engine requires an API provider."
|
|
235
|
+
},
|
|
236
|
+
"model": {
|
|
237
|
+
"type": "string",
|
|
238
|
+
"description": "Optional model identifier for the voice engine."
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
},
|
|
242
|
+
"ChannelCredentials": {
|
|
243
|
+
"type": "object",
|
|
244
|
+
"description": "Channel configuration with optional credentials. Supports Discord, Slack, and custom channel fields.",
|
|
245
|
+
"properties": {
|
|
246
|
+
"enabled": {
|
|
247
|
+
"type": "boolean",
|
|
248
|
+
"description": "Whether this channel is enabled. Defaults to true if the channel entry exists."
|
|
249
|
+
},
|
|
250
|
+
"botToken": {
|
|
251
|
+
"type": "string",
|
|
252
|
+
"description": "Discord bot token."
|
|
253
|
+
},
|
|
254
|
+
"applicationId": {
|
|
255
|
+
"type": "string",
|
|
256
|
+
"description": "Discord application ID."
|
|
257
|
+
},
|
|
258
|
+
"registerCommands": {
|
|
259
|
+
"type": "boolean",
|
|
260
|
+
"description": "Whether to register Discord slash commands on startup."
|
|
261
|
+
},
|
|
262
|
+
"allowedGuilds": {
|
|
263
|
+
"type": "string",
|
|
264
|
+
"description": "Comma-separated list of allowed Discord guild IDs."
|
|
265
|
+
},
|
|
266
|
+
"allowedRoles": {
|
|
267
|
+
"type": "string",
|
|
268
|
+
"description": "Comma-separated list of allowed Discord role IDs."
|
|
269
|
+
},
|
|
270
|
+
"allowedUsers": {
|
|
271
|
+
"type": "string",
|
|
272
|
+
"description": "Comma-separated list of allowed user IDs."
|
|
273
|
+
},
|
|
274
|
+
"blockedUsers": {
|
|
275
|
+
"type": "string",
|
|
276
|
+
"description": "Comma-separated list of blocked user IDs."
|
|
277
|
+
},
|
|
278
|
+
"slackBotToken": {
|
|
279
|
+
"type": "string",
|
|
280
|
+
"description": "Slack bot token (xoxb-...)."
|
|
281
|
+
},
|
|
282
|
+
"slackAppToken": {
|
|
283
|
+
"type": "string",
|
|
284
|
+
"description": "Slack app-level token (xapp-...)."
|
|
285
|
+
},
|
|
286
|
+
"allowedChannels": {
|
|
287
|
+
"type": "string",
|
|
288
|
+
"description": "Comma-separated list of allowed Slack channel IDs."
|
|
289
|
+
}
|
|
290
|
+
},
|
|
291
|
+
"additionalProperties": true
|
|
292
|
+
},
|
|
293
|
+
"ServiceConfig": {
|
|
294
|
+
"type": "object",
|
|
295
|
+
"description": "Service configuration with an enabled flag and optional extra settings.",
|
|
296
|
+
"required": ["enabled"],
|
|
297
|
+
"properties": {
|
|
298
|
+
"enabled": {
|
|
299
|
+
"type": "boolean",
|
|
300
|
+
"description": "Whether this service is enabled."
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
"additionalProperties": true
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { userInfo } from "node:os";
|
|
2
2
|
import { parseEnvFile } from './env.js';
|
|
3
3
|
|
|
4
|
-
export function readSecretsKeys(
|
|
5
|
-
|
|
4
|
+
export function readSecretsKeys(vaultDir: string): Record<string, boolean> {
|
|
5
|
+
// System scope wins on overlap because vault/stack/stack.env is the
|
|
6
|
+
// authoritative source for system-managed credentials and flags.
|
|
7
|
+
const parsed = {
|
|
8
|
+
...parseEnvFile(`${vaultDir}/user/user.env`),
|
|
9
|
+
...parseEnvFile(`${vaultDir}/stack/stack.env`),
|
|
10
|
+
};
|
|
6
11
|
const result: Record<string, boolean> = {};
|
|
7
12
|
for (const [key, value] of Object.entries(parsed)) {
|
|
8
13
|
result[key] = value.length > 0;
|
|
@@ -20,12 +25,14 @@ export function detectUserId(): string {
|
|
|
20
25
|
}
|
|
21
26
|
}
|
|
22
27
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
28
|
+
/**
|
|
29
|
+
* Check if setup is complete by reading vault/stack/stack.env.
|
|
30
|
+
*/
|
|
31
|
+
export function isSetupComplete(vaultDir: string): boolean {
|
|
32
|
+
const parsed = parseEnvFile(`${vaultDir}/stack/stack.env`);
|
|
33
|
+
if ("OP_SETUP_COMPLETE" in parsed) {
|
|
34
|
+
return parsed.OP_SETUP_COMPLETE.toLowerCase() === "true";
|
|
27
35
|
}
|
|
28
36
|
|
|
29
|
-
|
|
30
|
-
return keys.OPENPALM_ADMIN_TOKEN === true || keys.ADMIN_TOKEN === true;
|
|
37
|
+
return (parsed.OP_ADMIN_TOKEN ?? "").length > 0;
|
|
31
38
|
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation logic for SetupSpec inputs.
|
|
3
|
+
* Extracted from setup.ts to reduce per-file complexity.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const CAPABILITY_ID_RE = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
|
|
7
|
+
|
|
8
|
+
function requireObj(val: unknown, msg: string, errors: string[]): Record<string, unknown> | null {
|
|
9
|
+
if (typeof val !== "object" || val === null) { errors.push(msg); return null; }
|
|
10
|
+
return val as Record<string, unknown>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function requireStr(obj: Record<string, unknown>, key: string, msg: string, errors: string[]): boolean {
|
|
14
|
+
if (typeof obj[key] !== "string" || !obj[key]) { errors.push(msg); return false; }
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function validateSetupSpec(input: unknown): { valid: boolean; errors: string[] } {
|
|
19
|
+
const errors: string[] = [];
|
|
20
|
+
const body = requireObj(input, "Input must be a non-null object", errors);
|
|
21
|
+
if (!body) return { valid: false, errors };
|
|
22
|
+
|
|
23
|
+
validateSecurity(body, errors);
|
|
24
|
+
validateOwner(body, errors);
|
|
25
|
+
validateConnectionsArray(body.connections, errors);
|
|
26
|
+
validateSpecCapabilities(body, errors);
|
|
27
|
+
if (body.channelCredentials !== undefined && (typeof body.channelCredentials !== "object" || body.channelCredentials === null)) {
|
|
28
|
+
errors.push("channelCredentials must be an object if provided");
|
|
29
|
+
}
|
|
30
|
+
return { valid: errors.length === 0, errors };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function validateSecurity(body: Record<string, unknown>, errors: string[]): void {
|
|
34
|
+
const security = requireObj(body.security, "security object is required", errors);
|
|
35
|
+
if (!security) return;
|
|
36
|
+
if (!requireStr(security, "adminToken", "security.adminToken is required and must be a non-empty string", errors)) return;
|
|
37
|
+
if ((security.adminToken as string).length < 8) errors.push("security.adminToken must be at least 8 characters");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function validateOwner(body: Record<string, unknown>, errors: string[]): void {
|
|
41
|
+
const owner = body.owner as Record<string, unknown> | undefined;
|
|
42
|
+
if (!owner) return; // owner is optional
|
|
43
|
+
if (owner.name !== undefined && typeof owner.name !== "string") errors.push("owner.name must be a string");
|
|
44
|
+
if (owner.email !== undefined && typeof owner.email !== "string") errors.push("owner.email must be a string");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function validateSpecCapabilities(body: Record<string, unknown>, errors: string[]): void {
|
|
48
|
+
if (body.version !== 2) errors.push("version must be 2");
|
|
49
|
+
const caps = requireObj(body.capabilities, "capabilities is required", errors);
|
|
50
|
+
if (!caps) return;
|
|
51
|
+
requireStr(caps, "llm", "capabilities.llm is required (format: 'provider/model')", errors);
|
|
52
|
+
const emb = requireObj(caps.embeddings, "capabilities.embeddings is required", errors);
|
|
53
|
+
if (emb) {
|
|
54
|
+
requireStr(emb, "provider", "capabilities.embeddings.provider is required", errors);
|
|
55
|
+
requireStr(emb, "model", "capabilities.embeddings.model is required", errors);
|
|
56
|
+
if (emb.dims !== undefined && emb.dims !== 0 && (typeof emb.dims !== "number" || !Number.isInteger(emb.dims) || emb.dims < 1)) {
|
|
57
|
+
errors.push("capabilities.embeddings.dims must be a positive integer or 0 (auto-resolve)");
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const mem = requireObj(caps.memory, "capabilities.memory is required", errors);
|
|
61
|
+
if (!mem) return;
|
|
62
|
+
if (mem.userId !== undefined && typeof mem.userId !== "string") errors.push("capabilities.memory.userId must be a string if provided");
|
|
63
|
+
if (typeof mem.userId === "string" && mem.userId && !/^[A-Za-z0-9_]+$/.test(mem.userId)) {
|
|
64
|
+
errors.push("capabilities.memory.userId contains invalid characters (alphanumeric and underscores only)");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function validateConnectionsArray(connections: unknown, errors: string[]): void {
|
|
69
|
+
if (!Array.isArray(connections)) {
|
|
70
|
+
errors.push("connections must be an array");
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const seenIds = new Set<string>();
|
|
74
|
+
for (let i = 0; i < connections.length; i++) {
|
|
75
|
+
const c = connections[i];
|
|
76
|
+
if (typeof c !== "object" || c === null) { errors.push(`connections[${i}] must be an object`); continue; }
|
|
77
|
+
const cap = c as Record<string, unknown>;
|
|
78
|
+
const id = typeof cap.id === "string" ? cap.id.trim() : "";
|
|
79
|
+
const provider = typeof cap.provider === "string" ? cap.provider.trim() : "";
|
|
80
|
+
const name = typeof cap.name === "string" ? cap.name.trim() : "";
|
|
81
|
+
|
|
82
|
+
if (!id) errors.push(`connections[${i}].id is required`);
|
|
83
|
+
else if (!CAPABILITY_ID_RE.test(id)) errors.push(`connections[${i}].id must start with a letter or digit (allowed: A-Z, a-z, 0-9, _, -)`);
|
|
84
|
+
else if (seenIds.has(id)) errors.push(`Duplicate capability ID: ${id}`);
|
|
85
|
+
else seenIds.add(id);
|
|
86
|
+
|
|
87
|
+
if (!name) errors.push(`connections[${i}].name is required`);
|
|
88
|
+
if (!provider) errors.push(`connections[${i}].provider is required`);
|
|
89
|
+
}
|
|
90
|
+
}
|