@openpalm/lib 0.10.2 → 0.11.0-beta.2
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 +2 -2
- package/package.json +7 -3
- package/src/control-plane/admin-token.ts +73 -0
- package/src/control-plane/akm-vault.test.ts +105 -0
- package/src/control-plane/akm-vault.ts +307 -0
- package/src/control-plane/channels.ts +3 -3
- package/src/control-plane/cleanup-guardrails.test.ts +8 -9
- package/src/control-plane/compose-args.test.ts +25 -24
- 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 +103 -65
- package/src/control-plane/core-assets.test.ts +104 -0
- package/src/control-plane/core-assets.ts +54 -57
- package/src/control-plane/docker.ts +55 -21
- package/src/control-plane/env.test.ts +25 -1
- package/src/control-plane/env.ts +80 -0
- 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 +187 -289
- package/src/control-plane/install-lock.ts +157 -0
- package/src/control-plane/lifecycle.ts +34 -65
- 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/paths.ts +82 -0
- package/src/control-plane/provider-config.ts +2 -2
- 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 +49 -47
- package/src/control-plane/registry.ts +71 -50
- package/src/control-plane/rollback.ts +17 -16
- package/src/control-plane/scheduler.ts +75 -262
- package/src/control-plane/secret-backend.test.ts +98 -111
- package/src/control-plane/secret-backend.ts +221 -181
- package/src/control-plane/secret-mappings.ts +4 -8
- package/src/control-plane/secrets.ts +93 -51
- 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 +138 -239
- package/src/control-plane/setup.ts +215 -130
- package/src/control-plane/skeleton-guardrail.test.ts +151 -0
- package/src/control-plane/spec-to-env.test.ts +59 -58
- package/src/control-plane/spec-to-env.ts +52 -142
- package/src/control-plane/spec-validator.ts +2 -99
- package/src/control-plane/stack-spec.test.ts +21 -77
- package/src/control-plane/stack-spec.ts +7 -83
- package/src/control-plane/types.ts +12 -28
- package/src/control-plane/ui-assets.ts +349 -0
- package/src/control-plane/validate.ts +44 -79
- package/src/index.ts +86 -48
- 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/memory-config.ts +0 -298
- package/src/control-plane/redact-schema.ts +0 -50
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/** Secrets and capability key management. */
|
|
2
|
-
import { mkdirSync, writeFileSync, readFileSync, existsSync, chmodSync, lstatSync, rmSync } from "node:fs";
|
|
2
|
+
import { mkdirSync, writeFileSync, readFileSync, existsSync, chmodSync, lstatSync, rmSync, renameSync } from "node:fs";
|
|
3
3
|
import { randomBytes } from "node:crypto";
|
|
4
4
|
import { createLogger } from "../logger.js";
|
|
5
5
|
import { parseEnvFile, mergeEnvContent } from './env.js';
|
|
6
|
+
import { migrateAuth0110 } from './migrate-0110.js';
|
|
6
7
|
import type { ControlPlaneState } from "./types.js";
|
|
7
8
|
import { resolveConfigDir } from "./home.js";
|
|
8
9
|
|
|
@@ -54,18 +55,17 @@ function mergeVaultEnvFile(path: string, updates: Record<string, string>, uncomm
|
|
|
54
55
|
}
|
|
55
56
|
|
|
56
57
|
function ensureSystemSecrets(state: ControlPlaneState): void {
|
|
57
|
-
const systemEnvPath = `${state.
|
|
58
|
+
const systemEnvPath = `${state.stackDir}/stack.env`;
|
|
58
59
|
const existing = existsSync(systemEnvPath) ? parseEnvFile(systemEnvPath) : {};
|
|
59
60
|
const updates: Record<string, string> = {};
|
|
60
61
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
updates.OP_MEMORY_TOKEN = randomBytes(32).toString("hex");
|
|
62
|
+
// OP_UI_LOGIN_PASSWORD seeds the operator login secret. ensureSecrets
|
|
63
|
+
// generates a random fallback the first time so the stack is never
|
|
64
|
+
// installed with an empty password slot; the wizard / CLI install path
|
|
65
|
+
// overwrites it with the operator's chosen value via
|
|
66
|
+
// buildSystemSecretsFromSetup().
|
|
67
|
+
if (!existing.OP_UI_LOGIN_PASSWORD) {
|
|
68
|
+
updates.OP_UI_LOGIN_PASSWORD = randomBytes(32).toString("hex");
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
if (!existsSync(systemEnvPath)) {
|
|
@@ -74,11 +74,9 @@ function ensureSystemSecrets(state: ControlPlaneState): void {
|
|
|
74
74
|
"# All secrets and configuration live here. Advanced users may edit directly.",
|
|
75
75
|
"",
|
|
76
76
|
"# ── Authentication ──────────────────────────────────────────────────",
|
|
77
|
-
"
|
|
78
|
-
"OP_ASSISTANT_TOKEN=",
|
|
77
|
+
"OP_UI_LOGIN_PASSWORD=",
|
|
79
78
|
"",
|
|
80
79
|
"# ── Service Auth ─────────────────────────────────────────────────────",
|
|
81
|
-
"OP_MEMORY_TOKEN=",
|
|
82
80
|
"OP_OPENCODE_PASSWORD=",
|
|
83
81
|
"",
|
|
84
82
|
"# ── Provider API Keys ────────────────────────────────────────────────",
|
|
@@ -88,7 +86,6 @@ function ensureSystemSecrets(state: ControlPlaneState): void {
|
|
|
88
86
|
"GROQ_API_KEY=",
|
|
89
87
|
"MISTRAL_API_KEY=",
|
|
90
88
|
"GOOGLE_API_KEY=",
|
|
91
|
-
"OPENVIKING_API_KEY=",
|
|
92
89
|
"MCP_API_KEY=",
|
|
93
90
|
"EMBEDDING_API_KEY=",
|
|
94
91
|
"LMSTUDIO_API_KEY=",
|
|
@@ -107,37 +104,25 @@ function ensureSystemSecrets(state: ControlPlaneState): void {
|
|
|
107
104
|
}
|
|
108
105
|
|
|
109
106
|
export function ensureSecrets(state: ControlPlaneState): void {
|
|
110
|
-
enforceVaultDirMode(state.
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
// All standard config lives in stack.env.
|
|
116
|
-
const userEnvPath = `${state.vaultDir}/user/user.env`;
|
|
117
|
-
if (!existsSync(userEnvPath)) {
|
|
118
|
-
writeVaultFile(userEnvPath, [
|
|
119
|
-
"# OpenPalm — User Extensions",
|
|
120
|
-
"# Add any custom environment variables here.",
|
|
121
|
-
"# These are loaded by compose alongside stack.env.",
|
|
122
|
-
"",
|
|
123
|
-
].join("\n"));
|
|
124
|
-
} else {
|
|
125
|
-
try { chmodSync(userEnvPath, VAULT_FILE_MODE); } catch { /* best-effort */ }
|
|
126
|
-
}
|
|
107
|
+
enforceVaultDirMode(state.stackDir);
|
|
108
|
+
|
|
109
|
+
// Migrate pre-0.11.0 installs (OP_UI_TOKEN/OP_ASSISTANT_TOKEN → OP_UI_LOGIN_PASSWORD)
|
|
110
|
+
// before any code path that reads OP_UI_LOGIN_PASSWORD sees an empty value.
|
|
111
|
+
migrateAuth0110(state);
|
|
127
112
|
|
|
128
113
|
ensureSystemSecrets(state);
|
|
129
|
-
ensureGuardianEnv(state.
|
|
130
|
-
ensureAuthJson(state.
|
|
114
|
+
ensureGuardianEnv(state.stackDir);
|
|
115
|
+
ensureAuthJson(state.configDir);
|
|
131
116
|
}
|
|
132
117
|
|
|
133
118
|
/**
|
|
134
|
-
* Ensure
|
|
119
|
+
* Ensure config/stack/guardian.env exists.
|
|
135
120
|
* Channel HMAC secrets (CHANNEL_<NAME>_SECRET) live here exclusively.
|
|
136
121
|
* This file is loaded by the guardian as an env_file and via GUARDIAN_SECRETS_PATH.
|
|
137
122
|
*/
|
|
138
|
-
function ensureGuardianEnv(
|
|
139
|
-
const guardianEnvPath = `${
|
|
140
|
-
mkdirSync(
|
|
123
|
+
function ensureGuardianEnv(stackDir: string): void {
|
|
124
|
+
const guardianEnvPath = `${stackDir}/guardian.env`;
|
|
125
|
+
mkdirSync(stackDir, { recursive: true, mode: VAULT_DIR_MODE });
|
|
141
126
|
if (!existsSync(guardianEnvPath)) {
|
|
142
127
|
writeVaultFile(guardianEnvPath, [
|
|
143
128
|
"# Guardian channel HMAC secrets — managed by openpalm",
|
|
@@ -149,9 +134,9 @@ function ensureGuardianEnv(vaultDir: string): void {
|
|
|
149
134
|
}
|
|
150
135
|
}
|
|
151
136
|
|
|
152
|
-
function ensureAuthJson(
|
|
153
|
-
const authJsonPath = `${
|
|
154
|
-
mkdirSync(
|
|
137
|
+
function ensureAuthJson(configDir: string): void {
|
|
138
|
+
const authJsonPath = `${configDir}/auth.json`;
|
|
139
|
+
mkdirSync(configDir, { recursive: true, mode: VAULT_DIR_MODE });
|
|
155
140
|
|
|
156
141
|
if (existsSync(authJsonPath)) {
|
|
157
142
|
try {
|
|
@@ -177,25 +162,82 @@ export function updateSecretsEnv(
|
|
|
177
162
|
state: ControlPlaneState,
|
|
178
163
|
updates: Record<string, string>
|
|
179
164
|
): void {
|
|
180
|
-
const stackEnvPath = `${state.
|
|
165
|
+
const stackEnvPath = `${state.stackDir}/stack.env`;
|
|
181
166
|
if (!existsSync(stackEnvPath)) {
|
|
182
|
-
throw new Error("
|
|
167
|
+
throw new Error("config/stack/stack.env does not exist — run setup first");
|
|
183
168
|
}
|
|
184
169
|
|
|
185
170
|
mergeVaultEnvFile(stackEnvPath, updates, true);
|
|
186
171
|
}
|
|
187
172
|
|
|
188
|
-
/**
|
|
189
|
-
|
|
190
|
-
|
|
173
|
+
/**
|
|
174
|
+
* Merge-write provider API keys into OpenCode's auth.json at
|
|
175
|
+
* `${configDir}/auth.json`. Each entry uses OpenCode's schema for
|
|
176
|
+
* api-key auth: `{ <providerId>: { type: "api", key: "..." } }`.
|
|
177
|
+
*
|
|
178
|
+
* This file is bind-mounted into the assistant container so the chat
|
|
179
|
+
* assistant picks up new credentials on its next OpenCode restart —
|
|
180
|
+
* see core.compose.yml.
|
|
181
|
+
*
|
|
182
|
+
* Existing entries (OAuth tokens, other providers) are preserved.
|
|
183
|
+
* Empty values DELETE the corresponding entry.
|
|
184
|
+
*/
|
|
185
|
+
export function writeAuthJsonProviderKeys(
|
|
186
|
+
state: ControlPlaneState,
|
|
187
|
+
providerKeys: Record<string, string>
|
|
188
|
+
): void {
|
|
189
|
+
if (Object.keys(providerKeys).length === 0) return;
|
|
190
|
+
|
|
191
|
+
const authJsonPath = `${state.configDir}/auth.json`;
|
|
192
|
+
mkdirSync(state.configDir, { recursive: true, mode: VAULT_DIR_MODE });
|
|
193
|
+
|
|
194
|
+
let current: Record<string, unknown> = {};
|
|
195
|
+
if (existsSync(authJsonPath)) {
|
|
196
|
+
try {
|
|
197
|
+
const raw = readFileSync(authJsonPath, "utf-8").trim();
|
|
198
|
+
if (raw && raw !== "{}") current = JSON.parse(raw) as Record<string, unknown>;
|
|
199
|
+
} catch {
|
|
200
|
+
// Corrupt auth.json — rename it so the operator can recover, then start fresh.
|
|
201
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
202
|
+
const corruptPath = `${authJsonPath}.corrupt-${timestamp}`;
|
|
203
|
+
try {
|
|
204
|
+
renameSync(authJsonPath, corruptPath);
|
|
205
|
+
logger.warn("corrupt auth.json renamed for recovery", {
|
|
206
|
+
original: authJsonPath,
|
|
207
|
+
renamed: corruptPath,
|
|
208
|
+
});
|
|
209
|
+
} catch (renameErr) {
|
|
210
|
+
logger.warn("could not rename corrupt auth.json; starting fresh", {
|
|
211
|
+
path: authJsonPath,
|
|
212
|
+
error: renameErr instanceof Error ? renameErr.message : String(renameErr),
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
current = {};
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
for (const [providerId, key] of Object.entries(providerKeys)) {
|
|
220
|
+
if (key) {
|
|
221
|
+
current[providerId] = { type: "api", key };
|
|
222
|
+
} else {
|
|
223
|
+
delete current[providerId];
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
writeVaultFile(authJsonPath, JSON.stringify(current, null, 2) + "\n");
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Read and parse config/stack/stack.env. Returns {} if the file does not exist. */
|
|
231
|
+
export function readStackEnv(stackDir: string): Record<string, string> {
|
|
232
|
+
return parseEnvFile(`${stackDir}/stack.env`);
|
|
191
233
|
}
|
|
192
234
|
|
|
193
235
|
export function updateSystemSecretsEnv(
|
|
194
236
|
state: ControlPlaneState,
|
|
195
237
|
updates: Record<string, string>
|
|
196
238
|
): void {
|
|
197
|
-
const systemEnvPath = `${state.
|
|
198
|
-
enforceVaultDirMode(state.
|
|
239
|
+
const systemEnvPath = `${state.stackDir}/stack.env`;
|
|
240
|
+
enforceVaultDirMode(state.stackDir);
|
|
199
241
|
if (!existsSync(systemEnvPath)) {
|
|
200
242
|
ensureSystemSecrets(state);
|
|
201
243
|
}
|
|
@@ -203,14 +245,14 @@ export function updateSystemSecretsEnv(
|
|
|
203
245
|
}
|
|
204
246
|
|
|
205
247
|
export function patchSecretsEnvFile(
|
|
206
|
-
|
|
248
|
+
stackDir: string,
|
|
207
249
|
patches: Record<string, string>
|
|
208
250
|
): void {
|
|
209
251
|
if (Object.keys(patches).length === 0) return;
|
|
210
252
|
|
|
211
|
-
const stackEnvPath = `${
|
|
212
|
-
enforceVaultDirMode(
|
|
213
|
-
mkdirSync(
|
|
253
|
+
const stackEnvPath = `${stackDir}/stack.env`;
|
|
254
|
+
enforceVaultDirMode(stackDir);
|
|
255
|
+
mkdirSync(stackDir, { recursive: true, mode: VAULT_DIR_MODE });
|
|
214
256
|
|
|
215
257
|
let existingContent = "";
|
|
216
258
|
try {
|
|
@@ -30,12 +30,12 @@
|
|
|
30
30
|
"security": {
|
|
31
31
|
"type": "object",
|
|
32
32
|
"description": "Security settings for the instance.",
|
|
33
|
-
"required": ["
|
|
33
|
+
"required": ["uiLoginPassword"],
|
|
34
34
|
"additionalProperties": false,
|
|
35
35
|
"properties": {
|
|
36
|
-
"
|
|
36
|
+
"uiLoginPassword": {
|
|
37
37
|
"type": "string",
|
|
38
|
-
"description": "
|
|
38
|
+
"description": "Operator login password for the OpenPalm UI. Persisted to stack.env as OP_UI_LOGIN_PASSWORD; the UI's op_session cookie value is compared against it on every authenticated request.",
|
|
39
39
|
"minLength": 8
|
|
40
40
|
}
|
|
41
41
|
}
|
|
@@ -51,18 +51,6 @@
|
|
|
51
51
|
"assignments": {
|
|
52
52
|
"$ref": "#/$defs/SetupConfigAssignments"
|
|
53
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
54
|
"channels": {
|
|
67
55
|
"type": "object",
|
|
68
56
|
"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.",
|
|
@@ -162,13 +150,13 @@
|
|
|
162
150
|
},
|
|
163
151
|
"smallModel": {
|
|
164
152
|
"type": "string",
|
|
165
|
-
"description": "Optional smaller/faster model for lightweight
|
|
153
|
+
"description": "Optional smaller/faster model for lightweight akm operations."
|
|
166
154
|
}
|
|
167
155
|
}
|
|
168
156
|
},
|
|
169
157
|
"embeddings": {
|
|
170
158
|
"type": "object",
|
|
171
|
-
"description": "Embedding model assignment for
|
|
159
|
+
"description": "Embedding model assignment for akm and semantic operations.",
|
|
172
160
|
"required": ["capabilityId", "model"],
|
|
173
161
|
"additionalProperties": false,
|
|
174
162
|
"properties": {
|
|
@@ -1,38 +1,18 @@
|
|
|
1
|
-
import { userInfo } from "node:os";
|
|
2
1
|
import { parseEnvFile } from './env.js';
|
|
3
2
|
|
|
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
|
-
};
|
|
11
|
-
const result: Record<string, boolean> = {};
|
|
12
|
-
for (const [key, value] of Object.entries(parsed)) {
|
|
13
|
-
result[key] = value.length > 0;
|
|
14
|
-
}
|
|
15
|
-
return result;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function detectUserId(): string {
|
|
19
|
-
const envUser = process.env.USER ?? process.env.LOGNAME ?? "";
|
|
20
|
-
if (envUser) return envUser;
|
|
21
|
-
try {
|
|
22
|
-
return userInfo().username || "default_user";
|
|
23
|
-
} catch {
|
|
24
|
-
return "default_user";
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
3
|
/**
|
|
29
|
-
* Check if setup is complete by reading
|
|
4
|
+
* Check if setup is complete by reading config/stack/stack.env.
|
|
5
|
+
*
|
|
6
|
+
* Phase 4 of the auth/proxy refactor replaced the legacy `OP_UI_TOKEN`
|
|
7
|
+
* sentinel with `OP_UI_LOGIN_PASSWORD`. The presence of a non-empty value
|
|
8
|
+
* implies the operator (or the install wizard) has seeded the login
|
|
9
|
+
* secret; `OP_SETUP_COMPLETE=true` is still authoritative when present.
|
|
30
10
|
*/
|
|
31
|
-
export function isSetupComplete(
|
|
32
|
-
const parsed = parseEnvFile(`${
|
|
11
|
+
export function isSetupComplete(stackDir: string): boolean {
|
|
12
|
+
const parsed = parseEnvFile(`${stackDir}/stack.env`);
|
|
33
13
|
if ("OP_SETUP_COMPLETE" in parsed) {
|
|
34
14
|
return parsed.OP_SETUP_COMPLETE.toLowerCase() === "true";
|
|
35
15
|
}
|
|
36
16
|
|
|
37
|
-
return (parsed.
|
|
17
|
+
return (parsed.OP_UI_LOGIN_PASSWORD ?? "").length > 0;
|
|
38
18
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Validation logic for SetupSpec inputs.
|
|
3
|
-
* Extracted from setup.ts to reduce per-file complexity.
|
|
4
3
|
*/
|
|
5
4
|
|
|
6
5
|
const CAPABILITY_ID_RE = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
|
|
@@ -20,10 +19,12 @@ export function validateSetupSpec(input: unknown): { valid: boolean; errors: str
|
|
|
20
19
|
const body = requireObj(input, "Input must be a non-null object", errors);
|
|
21
20
|
if (!body) return { valid: false, errors };
|
|
22
21
|
|
|
22
|
+
if (body.version !== 2) errors.push("version must be 2");
|
|
23
23
|
validateSecurity(body, errors);
|
|
24
24
|
validateOwner(body, errors);
|
|
25
25
|
validateConnectionsArray(body.connections, errors);
|
|
26
|
-
|
|
26
|
+
validateLlm(body, errors);
|
|
27
|
+
validateEmbedding(body, errors);
|
|
27
28
|
if (body.channelCredentials !== undefined && (typeof body.channelCredentials !== "object" || body.channelCredentials === null)) {
|
|
28
29
|
errors.push("channelCredentials must be an object if provided");
|
|
29
30
|
}
|
|
@@ -33,35 +34,34 @@ export function validateSetupSpec(input: unknown): { valid: boolean; errors: str
|
|
|
33
34
|
function validateSecurity(body: Record<string, unknown>, errors: string[]): void {
|
|
34
35
|
const security = requireObj(body.security, "security object is required", errors);
|
|
35
36
|
if (!security) return;
|
|
36
|
-
if (!requireStr(security, "
|
|
37
|
-
if ((security.
|
|
37
|
+
if (!requireStr(security, "uiLoginPassword", "security.uiLoginPassword is required and must be a non-empty string", errors)) return;
|
|
38
|
+
if ((security.uiLoginPassword as string).length < 8) errors.push("security.uiLoginPassword must be at least 8 characters");
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
function validateOwner(body: Record<string, unknown>, errors: string[]): void {
|
|
41
42
|
const owner = body.owner as Record<string, unknown> | undefined;
|
|
42
|
-
if (!owner) return;
|
|
43
|
+
if (!owner) return;
|
|
43
44
|
if (owner.name !== undefined && typeof owner.name !== "string") errors.push("owner.name must be a string");
|
|
44
45
|
if (owner.email !== undefined && typeof owner.email !== "string") errors.push("owner.email must be a string");
|
|
45
46
|
}
|
|
46
47
|
|
|
47
|
-
function
|
|
48
|
-
if (body.
|
|
49
|
-
const
|
|
50
|
-
if (!
|
|
51
|
-
requireStr(
|
|
52
|
-
|
|
53
|
-
if (
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
if (
|
|
63
|
-
|
|
64
|
-
errors.push("capabilities.memory.userId contains invalid characters (alphanumeric and underscores only)");
|
|
48
|
+
function validateLlm(body: Record<string, unknown>, errors: string[]): void {
|
|
49
|
+
if (body.llm === undefined) return;
|
|
50
|
+
const llm = requireObj(body.llm, "llm must be an object if provided", errors);
|
|
51
|
+
if (!llm) return;
|
|
52
|
+
requireStr(llm, "provider", "llm.provider is required", errors);
|
|
53
|
+
requireStr(llm, "model", "llm.model is required", errors);
|
|
54
|
+
if (llm.baseUrl !== undefined && typeof llm.baseUrl !== "string") errors.push("llm.baseUrl must be a string");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function validateEmbedding(body: Record<string, unknown>, errors: string[]): void {
|
|
58
|
+
if (body.embedding === undefined) return;
|
|
59
|
+
const emb = requireObj(body.embedding, "embedding must be an object if provided", errors);
|
|
60
|
+
if (!emb) return;
|
|
61
|
+
requireStr(emb, "provider", "embedding.provider is required", errors);
|
|
62
|
+
requireStr(emb, "model", "embedding.model is required", errors);
|
|
63
|
+
if (emb.dims !== undefined && (typeof emb.dims !== "number" || !Number.isInteger(emb.dims) || emb.dims < 1)) {
|
|
64
|
+
errors.push("embedding.dims must be a positive integer");
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
|