@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
|
@@ -1,40 +1,104 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Stack specification file (
|
|
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:
|
|
6
|
-
*
|
|
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
|
|
14
|
-
id: string;
|
|
15
|
-
name: string;
|
|
16
|
+
export type StackSpecEmbeddings = {
|
|
16
17
|
provider: string;
|
|
17
|
-
|
|
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
|
|
21
|
-
|
|
22
|
-
|
|
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:
|
|
27
|
-
|
|
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
|
-
|
|
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 !==
|
|
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" | "
|
|
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
|
-
|
|
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
|
+
}
|