@openpalm/lib 0.10.1 → 0.11.0-beta.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.
Files changed (55) hide show
  1. package/README.md +2 -2
  2. package/package.json +7 -3
  3. package/src/control-plane/admin-token.ts +73 -0
  4. package/src/control-plane/akm-vault.test.ts +108 -0
  5. package/src/control-plane/akm-vault.ts +307 -0
  6. package/src/control-plane/audit.ts +3 -2
  7. package/src/control-plane/channels.ts +3 -3
  8. package/src/control-plane/cleanup-guardrails.test.ts +8 -9
  9. package/src/control-plane/compose-args.test.ts +25 -21
  10. package/src/control-plane/config-persistence.ts +103 -64
  11. package/src/control-plane/core-assets.test.ts +104 -0
  12. package/src/control-plane/core-assets.ts +54 -57
  13. package/src/control-plane/docker.ts +55 -21
  14. package/src/control-plane/env.test.ts +25 -1
  15. package/src/control-plane/env.ts +80 -0
  16. package/src/control-plane/home.ts +66 -69
  17. package/src/control-plane/host-opencode.test.ts +263 -0
  18. package/src/control-plane/host-opencode.ts +229 -0
  19. package/src/control-plane/install-edge-cases.test.ts +182 -244
  20. package/src/control-plane/install-lock.ts +157 -0
  21. package/src/control-plane/lifecycle.ts +57 -56
  22. package/src/control-plane/markdown-task.ts +200 -0
  23. package/src/control-plane/paths.ts +75 -0
  24. package/src/control-plane/provider-config.ts +2 -2
  25. package/src/control-plane/provider-models.ts +154 -0
  26. package/src/control-plane/registry-components.test.ts +102 -25
  27. package/src/control-plane/registry.test.ts +49 -47
  28. package/src/control-plane/registry.ts +71 -50
  29. package/src/control-plane/rollback.ts +17 -16
  30. package/src/control-plane/scheduler.ts +75 -262
  31. package/src/control-plane/secret-backend.test.ts +98 -108
  32. package/src/control-plane/secret-backend.ts +221 -181
  33. package/src/control-plane/secret-mappings.ts +3 -6
  34. package/src/control-plane/secrets.ts +83 -47
  35. package/src/control-plane/setup-config.schema.json +2 -14
  36. package/src/control-plane/setup-status.ts +4 -29
  37. package/src/control-plane/setup-validation.ts +21 -21
  38. package/src/control-plane/setup.test.ts +122 -227
  39. package/src/control-plane/setup.ts +224 -125
  40. package/src/control-plane/skeleton-guardrail.test.ts +151 -0
  41. package/src/control-plane/spec-to-env.test.ts +59 -58
  42. package/src/control-plane/spec-to-env.ts +39 -140
  43. package/src/control-plane/spec-validator.ts +2 -99
  44. package/src/control-plane/stack-spec.test.ts +21 -77
  45. package/src/control-plane/stack-spec.ts +7 -83
  46. package/src/control-plane/types.ts +17 -15
  47. package/src/control-plane/ui-assets.ts +349 -0
  48. package/src/control-plane/validate.ts +44 -79
  49. package/src/index.ts +77 -44
  50. package/src/logger.test.ts +228 -0
  51. package/src/logger.ts +71 -1
  52. package/src/provider-constants.ts +22 -1
  53. package/src/control-plane/env-schema-validation.test.ts +0 -118
  54. package/src/control-plane/memory-config.ts +0 -298
  55. package/src/control-plane/redact-schema.ts +0 -50
@@ -1,5 +1,5 @@
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';
@@ -54,19 +54,16 @@ function mergeVaultEnvFile(path: string, updates: Record<string, string>, uncomm
54
54
  }
55
55
 
56
56
  function ensureSystemSecrets(state: ControlPlaneState): void {
57
- const systemEnvPath = `${state.vaultDir}/stack/stack.env`;
57
+ const systemEnvPath = `${state.stackDir}/stack.env`;
58
58
  const existing = existsSync(systemEnvPath) ? parseEnvFile(systemEnvPath) : {};
59
59
  const updates: Record<string, string> = {};
60
60
 
61
- if (!existing.OP_ADMIN_TOKEN && state.adminToken) {
62
- updates.OP_ADMIN_TOKEN = state.adminToken;
61
+ if (!existing.OP_UI_TOKEN && state.adminToken) {
62
+ updates.OP_UI_TOKEN = state.adminToken;
63
63
  }
64
64
  if (!existing.OP_ASSISTANT_TOKEN) {
65
65
  updates.OP_ASSISTANT_TOKEN = randomBytes(32).toString("hex");
66
66
  }
67
- if (!existing.OP_MEMORY_TOKEN) {
68
- updates.OP_MEMORY_TOKEN = randomBytes(32).toString("hex");
69
- }
70
67
 
71
68
  if (!existsSync(systemEnvPath)) {
72
69
  const header = [
@@ -74,11 +71,10 @@ function ensureSystemSecrets(state: ControlPlaneState): void {
74
71
  "# All secrets and configuration live here. Advanced users may edit directly.",
75
72
  "",
76
73
  "# ── Authentication ──────────────────────────────────────────────────",
77
- "OP_ADMIN_TOKEN=",
74
+ "OP_UI_TOKEN=",
78
75
  "OP_ASSISTANT_TOKEN=",
79
76
  "",
80
77
  "# ── Service Auth ─────────────────────────────────────────────────────",
81
- "OP_MEMORY_TOKEN=",
82
78
  "OP_OPENCODE_PASSWORD=",
83
79
  "",
84
80
  "# ── Provider API Keys ────────────────────────────────────────────────",
@@ -88,7 +84,6 @@ function ensureSystemSecrets(state: ControlPlaneState): void {
88
84
  "GROQ_API_KEY=",
89
85
  "MISTRAL_API_KEY=",
90
86
  "GOOGLE_API_KEY=",
91
- "OPENVIKING_API_KEY=",
92
87
  "MCP_API_KEY=",
93
88
  "EMBEDDING_API_KEY=",
94
89
  "LMSTUDIO_API_KEY=",
@@ -107,37 +102,21 @@ function ensureSystemSecrets(state: ControlPlaneState): void {
107
102
  }
108
103
 
109
104
  export function ensureSecrets(state: ControlPlaneState): void {
110
- enforceVaultDirMode(state.vaultDir);
111
- mkdirSync(`${state.vaultDir}/stack`, { recursive: true, mode: VAULT_DIR_MODE });
112
- mkdirSync(`${state.vaultDir}/user`, { recursive: true, mode: VAULT_DIR_MODE });
113
-
114
- // user.env is an empty placeholder — users can add custom vars here.
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
- }
105
+ enforceVaultDirMode(state.stackDir);
127
106
 
128
107
  ensureSystemSecrets(state);
129
- ensureGuardianEnv(state.vaultDir);
130
- ensureAuthJson(state.vaultDir);
108
+ ensureGuardianEnv(state.stackDir);
109
+ ensureAuthJson(state.configDir);
131
110
  }
132
111
 
133
112
  /**
134
- * Ensure vault/stack/guardian.env exists.
113
+ * Ensure config/stack/guardian.env exists.
135
114
  * Channel HMAC secrets (CHANNEL_<NAME>_SECRET) live here exclusively.
136
115
  * This file is loaded by the guardian as an env_file and via GUARDIAN_SECRETS_PATH.
137
116
  */
138
- function ensureGuardianEnv(vaultDir: string): void {
139
- const guardianEnvPath = `${vaultDir}/stack/guardian.env`;
140
- mkdirSync(`${vaultDir}/stack`, { recursive: true, mode: VAULT_DIR_MODE });
117
+ function ensureGuardianEnv(stackDir: string): void {
118
+ const guardianEnvPath = `${stackDir}/guardian.env`;
119
+ mkdirSync(stackDir, { recursive: true, mode: VAULT_DIR_MODE });
141
120
  if (!existsSync(guardianEnvPath)) {
142
121
  writeVaultFile(guardianEnvPath, [
143
122
  "# Guardian channel HMAC secrets — managed by openpalm",
@@ -149,9 +128,9 @@ function ensureGuardianEnv(vaultDir: string): void {
149
128
  }
150
129
  }
151
130
 
152
- function ensureAuthJson(vaultDir: string): void {
153
- const authJsonPath = `${vaultDir}/stack/auth.json`;
154
- mkdirSync(`${vaultDir}/stack`, { recursive: true, mode: VAULT_DIR_MODE });
131
+ function ensureAuthJson(configDir: string): void {
132
+ const authJsonPath = `${configDir}/auth.json`;
133
+ mkdirSync(configDir, { recursive: true, mode: VAULT_DIR_MODE });
155
134
 
156
135
  if (existsSync(authJsonPath)) {
157
136
  try {
@@ -177,25 +156,82 @@ export function updateSecretsEnv(
177
156
  state: ControlPlaneState,
178
157
  updates: Record<string, string>
179
158
  ): void {
180
- const stackEnvPath = `${state.vaultDir}/stack/stack.env`;
159
+ const stackEnvPath = `${state.stackDir}/stack.env`;
181
160
  if (!existsSync(stackEnvPath)) {
182
- throw new Error("vault/stack/stack.env does not exist — run setup first");
161
+ throw new Error("config/stack/stack.env does not exist — run setup first");
183
162
  }
184
163
 
185
164
  mergeVaultEnvFile(stackEnvPath, updates, true);
186
165
  }
187
166
 
188
- /** Read and parse vault/stack/stack.env. Returns {} if the file does not exist. */
189
- export function readStackEnv(vaultDir: string): Record<string, string> {
190
- return parseEnvFile(`${vaultDir}/stack/stack.env`);
167
+ /**
168
+ * Merge-write provider API keys into OpenCode's auth.json at
169
+ * `${configDir}/auth.json`. Each entry uses OpenCode's schema for
170
+ * api-key auth: `{ <providerId>: { type: "api", key: "..." } }`.
171
+ *
172
+ * This file is bind-mounted into the assistant container so the chat
173
+ * assistant picks up new credentials on its next OpenCode restart —
174
+ * see core.compose.yml.
175
+ *
176
+ * Existing entries (OAuth tokens, other providers) are preserved.
177
+ * Empty values DELETE the corresponding entry.
178
+ */
179
+ export function writeAuthJsonProviderKeys(
180
+ state: ControlPlaneState,
181
+ providerKeys: Record<string, string>
182
+ ): void {
183
+ if (Object.keys(providerKeys).length === 0) return;
184
+
185
+ const authJsonPath = `${state.configDir}/auth.json`;
186
+ mkdirSync(state.configDir, { recursive: true, mode: VAULT_DIR_MODE });
187
+
188
+ let current: Record<string, unknown> = {};
189
+ if (existsSync(authJsonPath)) {
190
+ try {
191
+ const raw = readFileSync(authJsonPath, "utf-8").trim();
192
+ if (raw && raw !== "{}") current = JSON.parse(raw) as Record<string, unknown>;
193
+ } catch {
194
+ // Corrupt auth.json — rename it so the operator can recover, then start fresh.
195
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
196
+ const corruptPath = `${authJsonPath}.corrupt-${timestamp}`;
197
+ try {
198
+ renameSync(authJsonPath, corruptPath);
199
+ logger.warn("corrupt auth.json renamed for recovery", {
200
+ original: authJsonPath,
201
+ renamed: corruptPath,
202
+ });
203
+ } catch (renameErr) {
204
+ logger.warn("could not rename corrupt auth.json; starting fresh", {
205
+ path: authJsonPath,
206
+ error: renameErr instanceof Error ? renameErr.message : String(renameErr),
207
+ });
208
+ }
209
+ current = {};
210
+ }
211
+ }
212
+
213
+ for (const [providerId, key] of Object.entries(providerKeys)) {
214
+ if (key) {
215
+ current[providerId] = { type: "api", key };
216
+ } else {
217
+ delete current[providerId];
218
+ }
219
+ }
220
+
221
+ writeVaultFile(authJsonPath, JSON.stringify(current, null, 2) + "\n");
222
+ }
223
+
224
+ /** Read and parse config/stack/stack.env. Returns {} if the file does not exist. */
225
+ export function readStackEnv(stackDir: string): Record<string, string> {
226
+ return parseEnvFile(`${stackDir}/stack.env`);
191
227
  }
192
228
 
193
229
  export function updateSystemSecretsEnv(
194
230
  state: ControlPlaneState,
195
231
  updates: Record<string, string>
196
232
  ): void {
197
- const systemEnvPath = `${state.vaultDir}/stack/stack.env`;
198
- enforceVaultDirMode(state.vaultDir);
233
+ const systemEnvPath = `${state.stackDir}/stack.env`;
234
+ enforceVaultDirMode(state.stackDir);
199
235
  if (!existsSync(systemEnvPath)) {
200
236
  ensureSystemSecrets(state);
201
237
  }
@@ -203,14 +239,14 @@ export function updateSystemSecretsEnv(
203
239
  }
204
240
 
205
241
  export function patchSecretsEnvFile(
206
- vaultDir: string,
242
+ stackDir: string,
207
243
  patches: Record<string, string>
208
244
  ): void {
209
245
  if (Object.keys(patches).length === 0) return;
210
246
 
211
- const stackEnvPath = `${vaultDir}/stack/stack.env`;
212
- enforceVaultDirMode(vaultDir);
213
- mkdirSync(`${vaultDir}/stack`, { recursive: true, mode: VAULT_DIR_MODE });
247
+ const stackEnvPath = `${stackDir}/stack.env`;
248
+ enforceVaultDirMode(stackDir);
249
+ mkdirSync(stackDir, { recursive: true, mode: VAULT_DIR_MODE });
214
250
 
215
251
  let existingContent = "";
216
252
  try {
@@ -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 tasks (e.g. memory extraction)."
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 the memory subsystem.",
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,13 @@
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 vault/stack/stack.env.
4
+ * Check if setup is complete by reading config/stack/stack.env.
30
5
  */
31
- export function isSetupComplete(vaultDir: string): boolean {
32
- const parsed = parseEnvFile(`${vaultDir}/stack/stack.env`);
6
+ export function isSetupComplete(stackDir: string): boolean {
7
+ const parsed = parseEnvFile(`${stackDir}/stack.env`);
33
8
  if ("OP_SETUP_COMPLETE" in parsed) {
34
9
  return parsed.OP_SETUP_COMPLETE.toLowerCase() === "true";
35
10
  }
36
11
 
37
- return (parsed.OP_ADMIN_TOKEN ?? "").length > 0;
12
+ return (parsed.OP_UI_TOKEN ?? "").length > 0;
38
13
  }
@@ -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
- validateSpecCapabilities(body, errors);
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
  }
@@ -39,29 +40,28 @@ function validateSecurity(body: Record<string, unknown>, errors: string[]): void
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; // owner is optional
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 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)");
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