@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.
Files changed (56) hide show
  1. package/README.md +31 -71
  2. package/package.json +1 -1
  3. package/src/control-plane/audit.ts +4 -4
  4. package/src/control-plane/backup.ts +31 -0
  5. package/src/control-plane/channels.ts +88 -156
  6. package/src/control-plane/cleanup-guardrails.test.ts +289 -0
  7. package/src/control-plane/compose-args.test.ts +170 -0
  8. package/src/control-plane/compose-args.ts +57 -0
  9. package/src/control-plane/config-persistence.ts +270 -0
  10. package/src/control-plane/core-assets.ts +58 -234
  11. package/src/control-plane/crypto.ts +14 -0
  12. package/src/control-plane/docker.ts +94 -204
  13. package/src/control-plane/env-schema-validation.test.ts +118 -0
  14. package/src/control-plane/extends-support.test.ts +105 -0
  15. package/src/control-plane/home.ts +133 -0
  16. package/src/control-plane/install-edge-cases.test.ts +314 -717
  17. package/src/control-plane/lifecycle.ts +215 -233
  18. package/src/control-plane/lock.test.ts +194 -0
  19. package/src/control-plane/lock.ts +176 -0
  20. package/src/control-plane/memory-config.ts +34 -160
  21. package/src/control-plane/opencode-client.test.ts +154 -0
  22. package/src/control-plane/opencode-client.ts +113 -0
  23. package/src/control-plane/provider-config.ts +34 -0
  24. package/src/control-plane/redact-schema.ts +50 -0
  25. package/src/control-plane/registry-components.test.ts +313 -0
  26. package/src/control-plane/registry.test.ts +414 -0
  27. package/src/control-plane/registry.ts +418 -0
  28. package/src/control-plane/rollback.ts +128 -0
  29. package/src/control-plane/scheduler.ts +18 -190
  30. package/src/control-plane/secret-backend.test.ts +359 -0
  31. package/src/control-plane/secret-backend.ts +322 -0
  32. package/src/control-plane/secret-mappings.ts +185 -0
  33. package/src/control-plane/secrets.ts +186 -112
  34. package/src/control-plane/setup-config.schema.json +306 -0
  35. package/src/control-plane/setup-status.ts +15 -8
  36. package/src/control-plane/setup-validation.ts +90 -0
  37. package/src/control-plane/setup.test.ts +336 -929
  38. package/src/control-plane/setup.ts +159 -849
  39. package/src/control-plane/spec-to-env.test.ts +100 -0
  40. package/src/control-plane/spec-to-env.ts +195 -0
  41. package/src/control-plane/spec-validator.ts +159 -0
  42. package/src/control-plane/stack-spec.test.ts +150 -0
  43. package/src/control-plane/stack-spec.ts +101 -22
  44. package/src/control-plane/types.ts +6 -99
  45. package/src/control-plane/validate.ts +107 -0
  46. package/src/index.ts +101 -159
  47. package/src/provider-constants.ts +2 -31
  48. package/src/control-plane/connection-mapping.ts +0 -191
  49. package/src/control-plane/connection-migration-flags.ts +0 -40
  50. package/src/control-plane/connection-profiles.ts +0 -317
  51. package/src/control-plane/core-asset-provider.ts +0 -21
  52. package/src/control-plane/fs-asset-provider.ts +0 -65
  53. package/src/control-plane/fs-registry-provider.ts +0 -46
  54. package/src/control-plane/paths.ts +0 -77
  55. package/src/control-plane/registry-provider.ts +0 -19
  56. package/src/control-plane/staging.ts +0 -399
@@ -1,40 +1,104 @@
1
1
  /**
2
- * Stack specification file (openpalm.yaml) management.
2
+ * Stack specification file (stack.yml) management.
3
3
  *
4
4
  * The stack spec is a YAML document that captures the high-level
5
- * configuration of an OpenPalm installation: connections, capability
6
- * assignments, and feature flags. It lives in CONFIG_HOME.
5
+ * configuration of an OpenPalm installation: capabilities only.
6
+ * It lives in CONFIG_HOME.
7
+ *
8
+ * v2: Capabilities-based schema. No connections array — capabilities
9
+ * carry their own provider info.
7
10
  */
8
11
  import { mkdirSync, writeFileSync, readFileSync, existsSync } from "node:fs";
9
12
  import { stringify as yamlStringify, parse as yamlParse } from "yaml";
10
13
 
11
- // ── Types ──────────────────────────────────────────────────────────────
14
+ // ── Capability Types ────────────────────────────────────────────────────
12
15
 
13
- export type StackSpecConnection = {
14
- id: string;
15
- name: string;
16
+ export type StackSpecEmbeddings = {
16
17
  provider: string;
17
- baseUrl: string;
18
+ model: string;
19
+ dims: number;
20
+ };
21
+
22
+ export type StackSpecMemory = {
23
+ userId: string;
24
+ customInstructions?: string;
25
+ };
26
+
27
+ export type StackSpecTts = {
28
+ enabled: boolean;
29
+ provider?: string;
30
+ model?: string;
31
+ voice?: string;
32
+ format?: string;
33
+ };
34
+
35
+ export type StackSpecStt = {
36
+ enabled: boolean;
37
+ provider?: string;
38
+ model?: string;
39
+ language?: string;
40
+ };
41
+
42
+ export type StackSpecReranker = {
43
+ enabled: boolean;
44
+ provider?: string;
45
+ mode?: "llm" | "dedicated";
46
+ model?: string;
47
+ topK?: number;
48
+ topN?: number;
18
49
  };
19
50
 
20
- export type StackSpecAssignments = {
21
- llm: { connectionId: string; model: string; smallModel?: string };
22
- embeddings: { connectionId: string; model: string; embeddingDims?: number };
51
+ export type StackSpecCapabilities = {
52
+ /** Primary LLM: "provider/model" */
53
+ llm: string;
54
+ /** Small/fast model: "provider/model" */
55
+ slm?: string;
56
+ embeddings: StackSpecEmbeddings;
57
+ memory: StackSpecMemory;
58
+ tts?: StackSpecTts;
59
+ stt?: StackSpecStt;
60
+ reranking?: StackSpecReranker;
23
61
  };
24
62
 
63
+ // ── StackSpec v2 ────────────────────────────────────────────────────────
64
+
25
65
  export type StackSpec = {
26
- version: 3;
27
- connections: StackSpecConnection[];
28
- assignments: StackSpecAssignments;
29
- ollamaEnabled: boolean;
30
- voice?: { tts?: string; stt?: string };
31
- channels?: string[];
32
- services?: Record<string, boolean>;
66
+ version: 2;
67
+ capabilities: StackSpecCapabilities;
33
68
  };
34
69
 
35
- // ── Constants ──────────────────────────────────────────────────────────
70
+ // ── Constants ───────────────────────────────────────────────────────────
71
+
72
+ export const STACK_SPEC_FILENAME = "stack.yml";
73
+
74
+ export const SPEC_DEFAULTS = {
75
+ ports: {
76
+ assistant: 3800,
77
+ admin: 3880,
78
+ adminOpencode: 3881,
79
+ memory: 3898,
80
+ guardian: 3899,
81
+ assistantSsh: 2222,
82
+ },
83
+ image: {
84
+ namespace: "openpalm",
85
+ tag: "latest",
86
+ },
87
+ } as const;
36
88
 
37
- export const STACK_SPEC_FILENAME = "openpalm.yaml";
89
+ // ── Capability Helpers ──────────────────────────────────────────────────
90
+
91
+ /** Parse a "provider/model" capability string into parts */
92
+ export function parseCapabilityString(cap: string): { provider: string; model: string } {
93
+ const idx = cap.indexOf("/");
94
+ if (idx < 0) return { provider: cap, model: "" };
95
+ return { provider: cap.slice(0, idx), model: cap.slice(idx + 1) };
96
+ }
97
+
98
+ /** Format provider + model into a capability string */
99
+ export function formatCapabilityString(provider: string, model: string): string {
100
+ return `${provider}/${model}`;
101
+ }
38
102
 
39
103
  // ── Read / Write ────────────────────────────────────────────────────────
40
104
 
@@ -48,17 +112,32 @@ export function writeStackSpec(configDir: string, spec: StackSpec): void {
48
112
  writeFileSync(stackSpecPath(configDir), content);
49
113
  }
50
114
 
115
+ /**
116
+ * Read the stack spec. Returns null for missing, corrupt, or unrecognized version files.
117
+ */
51
118
  export function readStackSpec(configDir: string): StackSpec | null {
52
119
  const path = stackSpecPath(configDir);
53
120
  if (!existsSync(path)) return null;
121
+
54
122
  let raw: unknown;
55
123
  try {
56
- raw = yamlParse(readFileSync(path, "utf-8"));
124
+ raw = yamlParse(readFileSync(path, "utf-8"), { maxAliasCount: 100 });
57
125
  } catch {
58
126
  return null;
59
127
  }
60
128
  if (typeof raw !== "object" || raw === null) return null;
61
129
  const obj = raw as Record<string, unknown>;
62
- if (obj.version !== 3) return null;
130
+ if (obj.version !== 2) return null;
131
+ if (typeof obj.capabilities !== "object" || obj.capabilities === null) return null;
63
132
  return obj as unknown as StackSpec;
64
133
  }
134
+
135
+ /**
136
+ * Update a single capability key in the stack spec.
137
+ */
138
+ export function updateCapability(configDir: string, key: string, value: unknown): void {
139
+ const spec = readStackSpec(configDir);
140
+ if (!spec) throw new Error("stack.yml not found or invalid");
141
+ (spec.capabilities as Record<string, unknown>)[key] = value;
142
+ writeStackSpec(configDir, spec);
143
+ }
@@ -10,90 +10,15 @@ export type CoreServiceName =
10
10
  | "memory"
11
11
  | "scheduler";
12
12
 
13
- export type OptionalServiceName = "admin" | "caddy" | "docker-socket-proxy";
13
+ export type OptionalServiceName = "admin" | "docker-socket-proxy";
14
14
 
15
15
  export type AccessScope = "host" | "lan";
16
16
  export type CallerType = "assistant" | "cli" | "ui" | "system" | "test" | "unknown";
17
17
 
18
- export type ConnectionKind =
19
- | "openai_compatible_remote"
20
- | "openai_compatible_local"
21
- | "ollama_local";
22
-
23
- export type ConnectionAuthMode = "api_key" | "none";
24
-
25
- export type CanonicalConnectionProfile = {
26
- id: string;
27
- name: string;
28
- kind: ConnectionKind;
29
- provider: string;
30
- baseUrl: string;
31
- auth: {
32
- mode: ConnectionAuthMode;
33
- apiKeySecretRef?: string;
34
- };
35
- };
36
-
37
- export type RequiredCapability = "llm" | "embeddings";
38
- export type OptionalCapability = "reranking" | "tts" | "stt";
39
- export type Capability = RequiredCapability | OptionalCapability;
40
-
41
- export type LlmAssignment = {
42
- connectionId: string;
43
- model: string;
44
- smallModel?: string;
45
- };
46
-
47
- export type EmbeddingsAssignment = {
48
- connectionId: string;
49
- model: string;
50
- embeddingDims?: number;
51
- };
52
-
53
- export type RerankerAssignment = {
54
- enabled: boolean;
55
- connectionId?: string;
56
- mode?: "llm" | "dedicated";
57
- model?: string;
58
- topK?: number;
59
- topN?: number;
60
- };
61
-
62
- export type TtsAssignment = {
63
- enabled: boolean;
64
- connectionId?: string;
65
- model?: string;
66
- voice?: string;
67
- format?: string;
68
- };
69
-
70
- export type SttAssignment = {
71
- enabled: boolean;
72
- connectionId?: string;
73
- model?: string;
74
- language?: string;
75
- };
76
-
77
- export type CapabilityAssignments = {
78
- llm: LlmAssignment;
79
- embeddings: EmbeddingsAssignment;
80
- reranking?: RerankerAssignment;
81
- tts?: TtsAssignment;
82
- stt?: SttAssignment;
83
- };
84
-
85
- export type CanonicalConnectionsDocument = {
86
- version: 1;
87
- profiles: CanonicalConnectionProfile[];
88
- assignments: CapabilityAssignments;
89
- };
90
-
91
18
  /** Info about a discovered channel */
92
19
  export type ChannelInfo = {
93
20
  name: string;
94
- hasRoute: boolean;
95
21
  ymlPath: string;
96
- caddyPath: string | null;
97
22
  };
98
23
 
99
24
  export type AuditEntry = {
@@ -115,18 +40,20 @@ export type ArtifactMeta = {
115
40
 
116
41
  export type ControlPlaneState = {
117
42
  adminToken: string;
43
+ assistantToken: string;
118
44
  setupToken: string;
119
- stateDir: string;
45
+ homeDir: string;
120
46
  configDir: string;
47
+ vaultDir: string;
121
48
  dataDir: string;
49
+ logsDir: string;
50
+ cacheDir: string;
122
51
  services: Record<string, "running" | "stopped">;
123
52
  artifacts: {
124
53
  compose: string;
125
- caddyfile: string;
126
54
  };
127
55
  artifactMeta: ArtifactMeta[];
128
56
  audit: AuditEntry[];
129
- channelSecrets: Record<string, string>;
130
57
  };
131
58
 
132
59
  // ── Constants ──────────────────────────────────────────────────────────
@@ -139,26 +66,6 @@ export const CORE_SERVICES: CoreServiceName[] = [
139
66
  ];
140
67
 
141
68
  export const OPTIONAL_SERVICES: OptionalServiceName[] = [
142
- "caddy",
143
69
  "admin",
144
70
  "docker-socket-proxy",
145
71
  ];
146
-
147
- export const CONNECTION_KINDS: ConnectionKind[] = [
148
- "openai_compatible_remote",
149
- "openai_compatible_local",
150
- "ollama_local",
151
- ];
152
-
153
- export const REQUIRED_CAPABILITIES: RequiredCapability[] = [
154
- "llm",
155
- "embeddings",
156
- ];
157
-
158
- export const OPTIONAL_CAPABILITIES: OptionalCapability[] = [
159
- "reranking",
160
- "tts",
161
- "stt",
162
- ];
163
-
164
- export const MAX_AUDIT_MEMORY = 1000;
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Runtime configuration validation for the OpenPalm control plane.
3
+ *
4
+ * Proposed changes are validated against temp copies before writing
5
+ * to live paths.
6
+ */
7
+ import { existsSync, copyFileSync, mkdirSync, rmSync } from "node:fs";
8
+ import { join } from "node:path";
9
+ import { mkdtempSync } from "node:fs";
10
+ import { tmpdir } from "node:os";
11
+ import { execFile } from "node:child_process";
12
+ import { promisify } from "node:util";
13
+ import type { ControlPlaneState } from "./types.js";
14
+
15
+ const execFileAsync = promisify(execFile);
16
+
17
+ /** Resolve the varlock binary path — honours VARLOCK_BIN for dev environments. */
18
+ const envVarlockBin = process.env.VARLOCK_BIN;
19
+ let VARLOCK_BIN = "varlock";
20
+ if (envVarlockBin) {
21
+ if (envVarlockBin === "varlock" || envVarlockBin.startsWith("/")) {
22
+ VARLOCK_BIN = envVarlockBin;
23
+ }
24
+ }
25
+
26
+ function sanitizeVarlockMessage(msg: string): string {
27
+ return msg
28
+ .replace(/sk-[A-Za-z0-9]{20,}/g, "[REDACTED]")
29
+ .replace(/gsk_[A-Za-z0-9]{30,}/g, "[REDACTED]")
30
+ .replace(/AIza[A-Za-z0-9_\-]{35}/g, "[REDACTED]")
31
+ .replace(/[0-9a-f]{32,}/gi, "[REDACTED]")
32
+ .replace(/value '([^']*)'/g, "value '[REDACTED]'");
33
+ }
34
+
35
+ async function runVarlockLoad(
36
+ schemaFile: string,
37
+ envFile: string,
38
+ ): Promise<void> {
39
+ const tmpDir = mkdtempSync(join(tmpdir(), "varlock-"));
40
+ try {
41
+ copyFileSync(schemaFile, join(tmpDir, ".env.schema"));
42
+ copyFileSync(envFile, join(tmpDir, ".env"));
43
+ await execFileAsync(
44
+ VARLOCK_BIN,
45
+ ["load", "--path", `${tmpDir}/`],
46
+ { timeout: 10000 },
47
+ );
48
+ } finally {
49
+ rmSync(tmpDir, { recursive: true, force: true });
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Validate the current live configuration files in place.
55
+ *
56
+ * Checks:
57
+ * 1. vault/user/user.env against vault/user/user.env.schema
58
+ * 2. vault/stack/stack.env against vault/stack/stack.env.schema
59
+ */
60
+ export async function validateProposedState(state: ControlPlaneState): Promise<{
61
+ ok: boolean;
62
+ errors: string[];
63
+ warnings: string[];
64
+ }> {
65
+ const errors: string[] = [];
66
+ const warnings: string[] = [];
67
+ let anyFailed = false;
68
+
69
+ function collectOutput(stderr: string): void {
70
+ for (const line of stderr.split("\n")) {
71
+ const trimmed = sanitizeVarlockMessage(line.trim());
72
+ if (!trimmed) continue;
73
+ if (trimmed.includes("ERROR")) errors.push(trimmed);
74
+ else if (trimmed.includes("WARN")) warnings.push(trimmed);
75
+ }
76
+ }
77
+
78
+ // Validate user.env
79
+ const userEnvSchema = `${state.vaultDir}/user/user.env.schema`;
80
+ const userEnv = `${state.vaultDir}/user/user.env`;
81
+ if (existsSync(userEnvSchema) && existsSync(userEnv)) {
82
+ try {
83
+ await runVarlockLoad(userEnvSchema, userEnv);
84
+ } catch (err: unknown) {
85
+ anyFailed = true;
86
+ if (err && typeof err === "object" && "stderr" in err) {
87
+ collectOutput(String((err as { stderr: string }).stderr));
88
+ }
89
+ }
90
+ }
91
+
92
+ // Validate stack.env
93
+ const systemEnvSchema = `${state.vaultDir}/stack/stack.env.schema`;
94
+ const systemEnv = `${state.vaultDir}/stack/stack.env`;
95
+ if (existsSync(systemEnvSchema) && existsSync(systemEnv)) {
96
+ try {
97
+ await runVarlockLoad(systemEnvSchema, systemEnv);
98
+ } catch (err: unknown) {
99
+ anyFailed = true;
100
+ if (err && typeof err === "object" && "stderr" in err) {
101
+ collectOutput(String((err as { stderr: string }).stderr));
102
+ }
103
+ }
104
+ }
105
+
106
+ return { ok: !anyFailed && errors.length === 0, errors, warnings };
107
+ }