@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.
Files changed (63) hide show
  1. package/README.md +4 -2
  2. package/package.json +11 -3
  3. package/src/control-plane/akm-vault.test.ts +105 -0
  4. package/src/control-plane/akm-vault.ts +311 -0
  5. package/src/control-plane/channels.ts +11 -9
  6. package/src/control-plane/cleanup-guardrails.test.ts +8 -9
  7. package/src/control-plane/compose-args.test.ts +25 -33
  8. package/src/control-plane/compose-args.ts +0 -4
  9. package/src/control-plane/compose-errors.test.ts +106 -0
  10. package/src/control-plane/compose-errors.ts +117 -0
  11. package/src/control-plane/config-persistence.ts +148 -73
  12. package/src/control-plane/core-assets.test.ts +104 -0
  13. package/src/control-plane/core-assets.ts +111 -58
  14. package/src/control-plane/docker.ts +70 -25
  15. package/src/control-plane/env.test.ts +25 -1
  16. package/src/control-plane/env.ts +84 -1
  17. package/src/control-plane/home.ts +66 -69
  18. package/src/control-plane/host-opencode.test.ts +260 -0
  19. package/src/control-plane/host-opencode.ts +229 -0
  20. package/src/control-plane/install-edge-cases.test.ts +190 -292
  21. package/src/control-plane/install-lock.ts +157 -0
  22. package/src/control-plane/lifecycle.ts +65 -75
  23. package/src/control-plane/markdown-task.ts +200 -0
  24. package/src/control-plane/migrate-0110.test.ts +177 -0
  25. package/src/control-plane/migrate-0110.ts +99 -0
  26. package/src/control-plane/operator-ids.test.ts +130 -0
  27. package/src/control-plane/operator-ids.ts +89 -0
  28. package/src/control-plane/paths.ts +80 -0
  29. package/src/control-plane/provider-models.ts +154 -0
  30. package/src/control-plane/registry-components.test.ts +105 -27
  31. package/src/control-plane/registry.test.ts +247 -51
  32. package/src/control-plane/registry.ts +404 -54
  33. package/src/control-plane/rollback.ts +17 -16
  34. package/src/control-plane/scheduler.ts +75 -262
  35. package/src/control-plane/secret-mappings.ts +4 -8
  36. package/src/control-plane/secrets.ts +97 -55
  37. package/src/control-plane/setup-config.schema.json +5 -17
  38. package/src/control-plane/setup-status.ts +9 -29
  39. package/src/control-plane/setup-validation.ts +23 -23
  40. package/src/control-plane/setup.test.ts +143 -244
  41. package/src/control-plane/setup.ts +216 -133
  42. package/src/control-plane/skeleton-guardrail.test.ts +151 -0
  43. package/src/control-plane/spec-to-env.test.ts +75 -60
  44. package/src/control-plane/spec-to-env.ts +68 -153
  45. package/src/control-plane/stack-spec.test.ts +22 -84
  46. package/src/control-plane/stack-spec.ts +9 -89
  47. package/src/control-plane/types.ts +9 -29
  48. package/src/control-plane/ui-assets.ts +385 -0
  49. package/src/control-plane/validate.ts +44 -79
  50. package/src/index.ts +102 -56
  51. package/src/logger.test.ts +228 -0
  52. package/src/logger.ts +71 -1
  53. package/src/provider-constants.ts +22 -1
  54. package/src/control-plane/audit.ts +0 -40
  55. package/src/control-plane/env-schema-validation.test.ts +0 -118
  56. package/src/control-plane/lock.test.ts +0 -194
  57. package/src/control-plane/lock.ts +0 -176
  58. package/src/control-plane/memory-config.ts +0 -298
  59. package/src/control-plane/provider-config.ts +0 -34
  60. package/src/control-plane/redact-schema.ts +0 -50
  61. package/src/control-plane/secret-backend.test.ts +0 -359
  62. package/src/control-plane/secret-backend.ts +0 -322
  63. package/src/control-plane/spec-validator.ts +0 -159
@@ -1,298 +0,0 @@
1
- /** Memory LLM & Embedding configuration management. */
2
- import { mkdirSync, writeFileSync, readFileSync, existsSync, rmSync } from "node:fs";
3
- import { readStackEnv } from "./secrets.js";
4
- import { EMBEDDING_DIMS, PROVIDER_DEFAULT_URLS } from "../provider-constants.js";
5
-
6
-
7
- export type MemoryConfig = {
8
- mem0: {
9
- llm: { provider: string; config: Record<string, unknown> };
10
- embedder: { provider: string; config: Record<string, unknown> };
11
- vector_store: {
12
- provider: "sqlite-vec" | "qdrant";
13
- config: {
14
- collection_name: string;
15
- db_path?: string;
16
- path?: string;
17
- embedding_model_dims: number;
18
- };
19
- };
20
- };
21
- memory: { custom_instructions: string };
22
- };
23
-
24
-
25
- export const EMBED_PROVIDERS = [
26
- "openai", "ollama", "huggingface", "lmstudio"
27
- ] as const;
28
-
29
- /** Static model list for Anthropic (no listing API available). */
30
- const ANTHROPIC_MODELS = [
31
- "claude-opus-4-6",
32
- "claude-sonnet-4-6",
33
- "claude-opus-4-20250514",
34
- "claude-sonnet-4-20250514",
35
- "claude-haiku-4-5-20251001",
36
- "claude-3-5-sonnet-20241022",
37
- "claude-3-5-haiku-20241022",
38
- ];
39
-
40
-
41
- export function resolveApiKey(apiKeyRef: string, configDir: string): string {
42
- if (!apiKeyRef) return "";
43
- if (!apiKeyRef.startsWith("env:")) return apiKeyRef;
44
-
45
- const varName = apiKeyRef.slice(4);
46
- if (process.env[varName]) return process.env[varName]!;
47
-
48
- const secrets = readStackEnv(configDir);
49
- return secrets[varName] ?? "";
50
- }
51
-
52
-
53
- export type ModelDiscoveryReason =
54
- | 'none'
55
- | 'provider_static'
56
- | 'provider_http'
57
- | 'missing_base_url'
58
- | 'timeout'
59
- | 'network';
60
-
61
- export type ProviderModelsResult = {
62
- models: string[];
63
- status: 'ok' | 'recoverable_error';
64
- reason: ModelDiscoveryReason;
65
- error?: string;
66
- };
67
-
68
- const HTTP_STATUS_LABELS: Record<number, string> = {
69
- 401: 'Invalid or missing API key',
70
- 403: 'Access denied — check API key permissions',
71
- 404: 'Endpoint not found — verify the base URL',
72
- 429: 'Rate limited — try again shortly',
73
- 500: 'Provider internal error',
74
- 502: 'Provider returned a bad gateway error',
75
- 503: 'Provider is temporarily unavailable',
76
- };
77
-
78
- export async function fetchProviderModels(
79
- provider: string,
80
- apiKeyRef: string,
81
- baseUrl: string,
82
- configDir: string
83
- ): Promise<ProviderModelsResult> {
84
- try {
85
- if (provider === "anthropic") {
86
- return { models: [...ANTHROPIC_MODELS], status: 'ok', reason: 'provider_static' };
87
- }
88
-
89
- const resolvedKey = resolveApiKey(apiKeyRef, configDir);
90
-
91
- if (provider === "ollama") {
92
- const base = baseUrl?.trim() || PROVIDER_DEFAULT_URLS.ollama;
93
- const url = `${base.replace(/\/+$/, "")}/api/tags`;
94
- const res = await fetch(url, { signal: AbortSignal.timeout(5000) });
95
- if (!res.ok) {
96
- return {
97
- models: [],
98
- status: 'recoverable_error',
99
- reason: 'provider_http',
100
- error: `Ollama API returned ${res.status}: ${(HTTP_STATUS_LABELS[res.status] ?? `HTTP ${res.status}`)}`,
101
- };
102
- }
103
- const data = (await res.json()) as { models?: { name: string }[] };
104
- const models = (data.models ?? []).map((m) => m.name).sort();
105
- return { models, status: 'ok', reason: 'none' };
106
- }
107
-
108
- const base = baseUrl?.trim() || PROVIDER_DEFAULT_URLS[provider] || "";
109
- if (!base) {
110
- return {
111
- models: [],
112
- status: 'recoverable_error',
113
- reason: 'missing_base_url',
114
- error: `No base URL configured for provider "${provider}"`,
115
- };
116
- }
117
- const url = `${base.replace(/\/+$/, "")}/v1/models`;
118
-
119
- const headers: Record<string, string> = {};
120
- if (resolvedKey) {
121
- headers["Authorization"] = `Bearer ${resolvedKey}`;
122
- }
123
-
124
- const res = await fetch(url, { headers, signal: AbortSignal.timeout(5000) });
125
- if (!res.ok) {
126
- let detail = '';
127
- try {
128
- const json = JSON.parse(await res.text()) as Record<string, unknown>;
129
- const errObj = json.error as Record<string, unknown> | string | undefined;
130
- detail = (typeof errObj === 'object' && errObj !== null && typeof errObj.message === 'string') ? errObj.message
131
- : typeof errObj === 'string' ? errObj
132
- : typeof json.message === 'string' ? json.message
133
- : typeof json.detail === 'string' ? json.detail : '';
134
- } catch { /* ignore parse errors */ }
135
- return {
136
- models: [],
137
- status: 'recoverable_error',
138
- reason: 'provider_http',
139
- error: detail
140
- ? `Provider API returned ${res.status}: ${detail}`
141
- : `Provider API returned ${res.status}: ${(HTTP_STATUS_LABELS[res.status] ?? `HTTP ${res.status}`)}`,
142
- };
143
- }
144
- const data = (await res.json()) as { data?: { id: string }[] };
145
- const models = (data.data ?? []).map((m) => m.id).sort();
146
- return { models, status: 'ok', reason: 'none' };
147
- } catch (err) {
148
- const message =
149
- err instanceof Error && err.name === "TimeoutError"
150
- ? "Request timed out after 5s"
151
- : String(err);
152
- return {
153
- models: [],
154
- status: 'recoverable_error',
155
- reason: err instanceof Error && err.name === 'TimeoutError' ? 'timeout' : 'network',
156
- error: message,
157
- };
158
- }
159
- }
160
-
161
-
162
- export function getDefaultConfig(): MemoryConfig {
163
- return {
164
- mem0: {
165
- llm: {
166
- provider: "openai",
167
- config: {
168
- model: "gpt-4o-mini",
169
- temperature: 0.1,
170
- max_tokens: 2000,
171
- api_key: "env:OPENAI_API_KEY",
172
- },
173
- },
174
- embedder: {
175
- provider: "openai",
176
- config: {
177
- model: "text-embedding-3-small",
178
- api_key: "env:OPENAI_API_KEY",
179
- },
180
- },
181
- vector_store: {
182
- provider: "sqlite-vec",
183
- config: {
184
- collection_name: "memory",
185
- db_path: "/data/memory.db",
186
- embedding_model_dims: 1536,
187
- },
188
- },
189
- },
190
- memory: { custom_instructions: "" },
191
- };
192
- }
193
-
194
-
195
- export function readMemoryConfig(dataDir: string): MemoryConfig {
196
- const path = `${dataDir}/memory/default_config.json`;
197
- if (!existsSync(path)) return getDefaultConfig();
198
- try {
199
- return JSON.parse(readFileSync(path, "utf-8")) as MemoryConfig;
200
- } catch {
201
- return getDefaultConfig();
202
- }
203
- }
204
-
205
- export function writeMemoryConfig(dataDir: string, config: MemoryConfig): void {
206
- mkdirSync(`${dataDir}/memory`, { recursive: true });
207
- writeFileSync(`${dataDir}/memory/default_config.json`, JSON.stringify(config, null, 2) + "\n");
208
- }
209
-
210
- export function ensureMemoryConfig(dataDir: string): void {
211
- if (existsSync(`${dataDir}/memory/default_config.json`)) return;
212
- writeMemoryConfig(dataDir, getDefaultConfig());
213
- }
214
-
215
-
216
- export type VectorDimensionResult = {
217
- match: boolean;
218
- currentDims?: number;
219
- expectedDims: number;
220
- };
221
-
222
- export function checkVectorDimensions(
223
- dataDir: string,
224
- newConfig: MemoryConfig
225
- ): VectorDimensionResult {
226
- const expectedDims = newConfig.mem0.vector_store.config.embedding_model_dims;
227
- const persisted = readMemoryConfig(dataDir);
228
- const currentDims = persisted.mem0.vector_store.config.embedding_model_dims;
229
- return { match: currentDims === expectedDims, currentDims, expectedDims };
230
- }
231
-
232
- export function resetVectorStore(
233
- dataDir: string
234
- ): { ok: boolean; error?: string } {
235
- const persisted = readMemoryConfig(dataDir);
236
- const configuredPath = persisted.mem0.vector_store.config.db_path;
237
-
238
- let dbPath: string;
239
- if (configuredPath && configuredPath.startsWith('/data/')) {
240
- dbPath = `${dataDir}/memory/${configuredPath.slice('/data/'.length)}`;
241
- } else if (configuredPath && !configuredPath.startsWith('/')) {
242
- dbPath = `${dataDir}/memory/${configuredPath}`;
243
- } else {
244
- dbPath = `${dataDir}/memory/memory.db`;
245
- }
246
- const qdrantPath = `${dataDir}/memory/qdrant`;
247
- try {
248
- if (existsSync(dbPath)) {
249
- rmSync(dbPath, { force: true });
250
- }
251
- for (const suffix of ['-wal', '-shm']) {
252
- const walPath = `${dbPath}${suffix}`;
253
- if (existsSync(walPath)) rmSync(walPath, { force: true });
254
- }
255
- if (existsSync(qdrantPath)) {
256
- rmSync(qdrantPath, { recursive: true, force: true });
257
- }
258
- return { ok: true };
259
- } catch (err) {
260
- return { ok: false, error: String(err) };
261
- }
262
- }
263
-
264
-
265
- async function callMemoryApi(path: string, init?: RequestInit): Promise<Response> {
266
- const configured = process.env.MEMORY_API_URL?.trim() || process.env.OP_MEMORY_API_URL?.trim();
267
- const bases = configured ? [configured.replace(/\/+$/, "")] : ["http://memory:8765", "http://127.0.0.1:8765"];
268
- const token = process.env.MEMORY_AUTH_TOKEN?.trim();
269
- const authHeaders: Record<string, string> = token ? { authorization: `Bearer ${token}` } : {};
270
- let lastError: unknown;
271
-
272
- for (let i = 0; i < bases.length; i++) {
273
- try {
274
- const headers = { ...authHeaders, ...(init?.headers as Record<string, string>) };
275
- return await fetch(`${bases[i]}${path}`, { ...init, headers });
276
- } catch (err) {
277
- lastError = err;
278
- if (i === bases.length - 1) throw err;
279
- }
280
- }
281
- throw lastError ?? new Error("Memory API request failed");
282
- }
283
-
284
- export async function provisionMemoryUser(
285
- userId: string,
286
- ): Promise<{ ok: boolean; error?: string }> {
287
- try {
288
- const res = await callMemoryApi("/api/v1/users", {
289
- method: "POST",
290
- headers: { "content-type": "application/json" },
291
- body: JSON.stringify({ user_id: userId }),
292
- signal: AbortSignal.timeout(5_000),
293
- });
294
- return { ok: res.ok };
295
- } catch (err) {
296
- return { ok: false, error: String(err) };
297
- }
298
- }
@@ -1,34 +0,0 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
- import type { ControlPlaneState } from './types.js';
3
-
4
- export type SecretProviderConfig = {
5
- provider: 'plaintext' | 'pass';
6
- passwordStoreDir?: string;
7
- passPrefix?: string;
8
- };
9
-
10
- function providerConfigPath(state: ControlPlaneState): string {
11
- return `${state.dataDir}/secrets/provider.json`;
12
- }
13
-
14
- export function readSecretProviderConfig(state: ControlPlaneState): SecretProviderConfig | null {
15
- const path = providerConfigPath(state);
16
- if (!existsSync(path)) return null;
17
-
18
- try {
19
- const parsed = JSON.parse(readFileSync(path, 'utf-8')) as SecretProviderConfig;
20
- if (parsed?.provider === 'plaintext' || parsed?.provider === 'pass') {
21
- return parsed;
22
- }
23
- } catch {
24
- // ignore malformed provider config and fall back to schema detection
25
- }
26
-
27
- return null;
28
- }
29
-
30
- export function writeSecretProviderConfig(state: ControlPlaneState, config: SecretProviderConfig): void {
31
- const dir = `${state.dataDir}/secrets`;
32
- mkdirSync(dir, { recursive: true });
33
- writeFileSync(providerConfigPath(state), JSON.stringify(config, null, 2) + '\n');
34
- }
@@ -1,50 +0,0 @@
1
- /**
2
- * Auto-generates a `redact.env.schema` from the canonical secret mappings.
3
- *
4
- * This ensures that every env var carrying a secret is marked for redaction
5
- * by varlock, without requiring manual maintenance of the schema file.
6
- */
7
- import { getCoreSecretMappings } from './secret-mappings.js';
8
-
9
- /**
10
- * Generate a redact.env.schema string from the canonical secret mappings.
11
- *
12
- * @param systemEnv - The current system env (used to discover dynamic channel secrets)
13
- * @returns A complete `@env-spec` schema suitable for varlock redaction
14
- */
15
- export function generateRedactSchema(systemEnv: Record<string, string>): string {
16
- const lines: string[] = [
17
- '# OpenPalm — Runtime Redaction Schema (auto-generated)',
18
- '# Marks env vars as @sensitive so varlock redacts their values from',
19
- '# stdout/stderr before they reach docker compose logs.',
20
- '#',
21
- '# @defaultSensitive=true',
22
- '# @defaultRequired=false',
23
- '# ---',
24
- '',
25
- ];
26
-
27
- const envKeys = new Set<string>();
28
- for (const mapping of getCoreSecretMappings(systemEnv)) {
29
- envKeys.add(mapping.envKey);
30
- }
31
-
32
- // Include container-runtime env names that differ from env-file keys
33
- // (compose maps OP_MEMORY_TOKEN -> MEMORY_AUTH_TOKEN, etc.)
34
- envKeys.add('ADMIN_TOKEN');
35
- envKeys.add('MEMORY_AUTH_TOKEN');
36
- envKeys.add('OPENCODE_SERVER_PASSWORD');
37
-
38
- // Resolved capability API keys (written to stack.env by spec-to-env)
39
- envKeys.add('OP_CAP_LLM_API_KEY');
40
- envKeys.add('OP_CAP_EMBEDDINGS_API_KEY');
41
- envKeys.add('OP_CAP_TTS_API_KEY');
42
- envKeys.add('OP_CAP_STT_API_KEY');
43
- envKeys.add('OP_CAP_SLM_API_KEY');
44
-
45
- for (const key of [...envKeys].sort()) {
46
- lines.push(`${key}=`);
47
- }
48
-
49
- return lines.join('\n') + '\n';
50
- }