@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
|
@@ -5,31 +5,21 @@ import { join } from "node:path";
|
|
|
5
5
|
import {
|
|
6
6
|
validateSetupSpec,
|
|
7
7
|
buildSecretsFromSetup,
|
|
8
|
+
buildAuthJsonFromSetup,
|
|
8
9
|
buildSystemSecretsFromSetup,
|
|
9
10
|
performSetup,
|
|
10
11
|
} from "./setup.js";
|
|
11
12
|
import type { SetupSpec, SetupConnection } from "./setup.js";
|
|
12
13
|
import { STACK_SPEC_FILENAME, readStackSpec } from "./stack-spec.js";
|
|
13
|
-
import type { StackSpec } from "./stack-spec.js";
|
|
14
14
|
|
|
15
15
|
// ── Helpers ──────────────────────────────────────────────────────────────
|
|
16
16
|
|
|
17
17
|
function makeValidSpec(overrides?: Partial<SetupSpec>): SetupSpec {
|
|
18
18
|
return {
|
|
19
19
|
version: 2,
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
provider: "openai",
|
|
24
|
-
model: "text-embedding-3-small",
|
|
25
|
-
dims: 1536,
|
|
26
|
-
},
|
|
27
|
-
memory: {
|
|
28
|
-
userId: "test_user",
|
|
29
|
-
customInstructions: "",
|
|
30
|
-
},
|
|
31
|
-
},
|
|
32
|
-
security: { adminToken: "test-admin-token-12345" },
|
|
20
|
+
llm: { provider: "openai", model: "gpt-4o", baseUrl: "https://api.openai.com/v1" },
|
|
21
|
+
embedding: { provider: "openai", model: "text-embedding-3-small", dims: 1536, baseUrl: "https://api.openai.com/v1" },
|
|
22
|
+
security: { uiLoginPassword: "test-admin-token-12345" },
|
|
33
23
|
owner: { name: "Test User", email: "test@example.com" },
|
|
34
24
|
connections: [
|
|
35
25
|
{
|
|
@@ -46,19 +36,17 @@ function makeValidSpec(overrides?: Partial<SetupSpec>): SetupSpec {
|
|
|
46
36
|
|
|
47
37
|
/** Seed the minimal asset files that ensure* functions expect to find at OP_HOME. */
|
|
48
38
|
function seedRequiredAssets(homeDir: string): void {
|
|
49
|
-
mkdirSync(join(homeDir, "stack"), { recursive: true });
|
|
50
|
-
writeFileSync(join(homeDir, "stack", "core.compose.yml"), "services:\n assistant:\n image: assistant:latest\n");
|
|
51
|
-
mkdirSync(join(homeDir, "
|
|
52
|
-
writeFileSync(join(homeDir, "
|
|
53
|
-
writeFileSync(join(homeDir, "
|
|
54
|
-
mkdirSync(join(homeDir, "
|
|
55
|
-
|
|
56
|
-
mkdirSync(join(homeDir, "
|
|
57
|
-
writeFileSync(join(homeDir, "
|
|
58
|
-
|
|
59
|
-
writeFileSync(join(homeDir, "
|
|
60
|
-
writeFileSync(join(homeDir, "config", "automations", "cleanup-data.yml"), "name: cleanup-data\nschedule: weekly\n");
|
|
61
|
-
writeFileSync(join(homeDir, "config", "automations", "validate-config.yml"), "name: validate-config\nschedule: hourly\n");
|
|
39
|
+
mkdirSync(join(homeDir, "config", "stack"), { recursive: true });
|
|
40
|
+
writeFileSync(join(homeDir, "config", "stack", "core.compose.yml"), "services:\n assistant:\n image: assistant:latest\n");
|
|
41
|
+
mkdirSync(join(homeDir, "state", "assistant"), { recursive: true });
|
|
42
|
+
writeFileSync(join(homeDir, "state", "assistant", "opencode.jsonc"), '{"$schema":"https://opencode.ai/config.json"}\n');
|
|
43
|
+
writeFileSync(join(homeDir, "state", "assistant", "AGENTS.md"), "# Agents\n");
|
|
44
|
+
mkdirSync(join(homeDir, "state"), { recursive: true });
|
|
45
|
+
// Automations live in state/registry/automations (shipped catalog) and stash/tasks (user tasks)
|
|
46
|
+
mkdirSync(join(homeDir, "state", "registry", "automations"), { recursive: true });
|
|
47
|
+
writeFileSync(join(homeDir, "state", "registry", "automations", "cleanup-logs.md"), "---\nschedule: \"0 4 * * 0\"\ndescription: cleanup logs\n---\n");
|
|
48
|
+
writeFileSync(join(homeDir, "state", "registry", "automations", "cleanup-data.md"), "---\nschedule: \"0 5 * * 0\"\ndescription: cleanup data\n---\n");
|
|
49
|
+
writeFileSync(join(homeDir, "state", "registry", "automations", "validate-config.md"), "---\nschedule: \"0 3 * * *\"\ndescription: validate config\n---\n");
|
|
62
50
|
}
|
|
63
51
|
|
|
64
52
|
// ── Tests: validateSetupSpec ────────────────────────────────────────────
|
|
@@ -84,17 +72,17 @@ describe("validateSetupSpec", () => {
|
|
|
84
72
|
expect(result.errors.some((e) => e.includes("security object is required"))).toBe(true);
|
|
85
73
|
});
|
|
86
74
|
|
|
87
|
-
it("rejects missing security.
|
|
75
|
+
it("rejects missing security.uiLoginPassword", () => {
|
|
88
76
|
const spec = makeValidSpec();
|
|
89
|
-
spec.security.
|
|
77
|
+
spec.security.uiLoginPassword = "";
|
|
90
78
|
const result = validateSetupSpec(spec);
|
|
91
79
|
expect(result.valid).toBe(false);
|
|
92
|
-
expect(result.errors.some((e) => e.includes("security.
|
|
80
|
+
expect(result.errors.some((e) => e.includes("security.uiLoginPassword"))).toBe(true);
|
|
93
81
|
});
|
|
94
82
|
|
|
95
|
-
it("rejects short security.
|
|
83
|
+
it("rejects short security.uiLoginPassword", () => {
|
|
96
84
|
const spec = makeValidSpec();
|
|
97
|
-
spec.security.
|
|
85
|
+
spec.security.uiLoginPassword = "short";
|
|
98
86
|
const result = validateSetupSpec(spec);
|
|
99
87
|
expect(result.valid).toBe(false);
|
|
100
88
|
expect(result.errors.some((e) => e.includes("at least 8"))).toBe(true);
|
|
@@ -149,36 +137,36 @@ describe("validateSetupSpec", () => {
|
|
|
149
137
|
expect(result.errors.some((e) => e.includes("version must be 2"))).toBe(true);
|
|
150
138
|
});
|
|
151
139
|
|
|
152
|
-
it("rejects missing
|
|
140
|
+
it("rejects missing llm.model", () => {
|
|
153
141
|
const input = makeValidSpec();
|
|
154
|
-
(input.
|
|
142
|
+
(input.llm as Record<string, unknown>).model = "";
|
|
155
143
|
const result = validateSetupSpec(input);
|
|
156
144
|
expect(result.valid).toBe(false);
|
|
157
|
-
expect(result.errors.some((e) => e.includes("
|
|
145
|
+
expect(result.errors.some((e) => e.includes("llm.model"))).toBe(true);
|
|
158
146
|
});
|
|
159
147
|
|
|
160
|
-
it("rejects missing
|
|
148
|
+
it("rejects missing llm.provider", () => {
|
|
161
149
|
const input = makeValidSpec();
|
|
162
|
-
(input.
|
|
150
|
+
(input.llm as Record<string, unknown>).provider = "";
|
|
163
151
|
const result = validateSetupSpec(input);
|
|
164
152
|
expect(result.valid).toBe(false);
|
|
165
|
-
expect(result.errors.some((e) => e.includes("
|
|
153
|
+
expect(result.errors.some((e) => e.includes("llm.provider"))).toBe(true);
|
|
166
154
|
});
|
|
167
155
|
|
|
168
|
-
it("rejects
|
|
156
|
+
it("rejects non-integer embedding.dims", () => {
|
|
169
157
|
const input = makeValidSpec();
|
|
170
|
-
(input.
|
|
158
|
+
(input.embedding as Record<string, unknown>).dims = 1.5;
|
|
171
159
|
const result = validateSetupSpec(input);
|
|
172
160
|
expect(result.valid).toBe(false);
|
|
173
|
-
expect(result.errors.some((e) => e.includes("
|
|
161
|
+
expect(result.errors.some((e) => e.includes("dims must be a positive integer"))).toBe(true);
|
|
174
162
|
});
|
|
175
163
|
|
|
176
|
-
it("
|
|
164
|
+
it("accepts spec without llm or embedding (minimal)", () => {
|
|
177
165
|
const input = makeValidSpec();
|
|
178
|
-
input
|
|
166
|
+
delete (input as Record<string, unknown>).llm;
|
|
167
|
+
delete (input as Record<string, unknown>).embedding;
|
|
179
168
|
const result = validateSetupSpec(input);
|
|
180
|
-
expect(result.valid).toBe(
|
|
181
|
-
expect(result.errors.some((e) => e.includes("dims must be a positive integer"))).toBe(true); // or 0 (auto-resolve)
|
|
169
|
+
expect(result.valid).toBe(true);
|
|
182
170
|
});
|
|
183
171
|
|
|
184
172
|
it("accepts multiple connections with different IDs", () => {
|
|
@@ -192,29 +180,6 @@ describe("validateSetupSpec", () => {
|
|
|
192
180
|
expect(result.valid).toBe(true);
|
|
193
181
|
});
|
|
194
182
|
|
|
195
|
-
it("rejects memory.userId with dots", () => {
|
|
196
|
-
const input = makeValidSpec();
|
|
197
|
-
input.capabilities.memory.userId = "user.name";
|
|
198
|
-
const result = validateSetupSpec(input);
|
|
199
|
-
expect(result.valid).toBe(false);
|
|
200
|
-
expect(result.errors.some((e) => e.includes("alphanumeric and underscores only"))).toBe(true);
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
it("rejects memory.userId with hyphens", () => {
|
|
204
|
-
const input = makeValidSpec();
|
|
205
|
-
input.capabilities.memory.userId = "user-name";
|
|
206
|
-
const result = validateSetupSpec(input);
|
|
207
|
-
expect(result.valid).toBe(false);
|
|
208
|
-
expect(result.errors.some((e) => e.includes("alphanumeric and underscores only"))).toBe(true);
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
it("accepts memory.userId with underscores", () => {
|
|
212
|
-
const input = makeValidSpec();
|
|
213
|
-
input.capabilities.memory.userId = "user_name_123";
|
|
214
|
-
const result = validateSetupSpec(input);
|
|
215
|
-
expect(result.valid).toBe(true);
|
|
216
|
-
});
|
|
217
|
-
|
|
218
183
|
it("accepts valid owner fields", () => {
|
|
219
184
|
const spec = makeValidSpec({ owner: { name: "Alice", email: "alice@test.com" } });
|
|
220
185
|
const result = validateSetupSpec(spec);
|
|
@@ -229,25 +194,20 @@ describe("validateSetupSpec", () => {
|
|
|
229
194
|
expect(result.errors.some((e) => e.includes("owner.name"))).toBe(true);
|
|
230
195
|
});
|
|
231
196
|
|
|
232
|
-
it("accepts valid memory section", () => {
|
|
233
|
-
const spec = makeValidSpec();
|
|
234
|
-
spec.capabilities.memory.userId = "my_user";
|
|
235
|
-
const result = validateSetupSpec(spec);
|
|
236
|
-
expect(result.valid).toBe(true);
|
|
237
|
-
});
|
|
238
197
|
});
|
|
239
198
|
|
|
240
199
|
// ── Tests: buildSecretsFromSetup ─────────────────────────────────────────
|
|
241
200
|
|
|
242
201
|
describe("buildSecretsFromSetup", () => {
|
|
243
|
-
it("does not include
|
|
202
|
+
it("does not include UI login password in user secrets", () => {
|
|
244
203
|
const spec = makeValidSpec();
|
|
245
204
|
const secrets = buildSecretsFromSetup(spec.connections, spec.owner);
|
|
246
|
-
expect(secrets.
|
|
205
|
+
expect(secrets.OP_UI_LOGIN_PASSWORD).toBeUndefined();
|
|
206
|
+
expect(secrets.OP_UI_TOKEN).toBeUndefined();
|
|
247
207
|
expect(secrets.ADMIN_TOKEN).toBeUndefined();
|
|
248
208
|
});
|
|
249
209
|
|
|
250
|
-
it("does not include SYSTEM_LLM_* in user secrets
|
|
210
|
+
it("does not include SYSTEM_LLM_* in user secrets", () => {
|
|
251
211
|
const spec = makeValidSpec();
|
|
252
212
|
const secrets = buildSecretsFromSetup(spec.connections, spec.owner);
|
|
253
213
|
expect(secrets.SYSTEM_LLM_PROVIDER).toBeUndefined();
|
|
@@ -255,18 +215,6 @@ describe("buildSecretsFromSetup", () => {
|
|
|
255
215
|
expect(secrets.SYSTEM_LLM_BASE_URL).toBeUndefined();
|
|
256
216
|
});
|
|
257
217
|
|
|
258
|
-
it("persists OPENAI_BASE_URL from openai connection", () => {
|
|
259
|
-
const spec = makeValidSpec();
|
|
260
|
-
const secrets = buildSecretsFromSetup(spec.connections, spec.owner);
|
|
261
|
-
expect(secrets.OPENAI_BASE_URL).toBe("https://api.openai.com");
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
it("does not include MEMORY_USER_ID in user secrets (lives in stack.env via OP_CAP_*)", () => {
|
|
265
|
-
const spec = makeValidSpec();
|
|
266
|
-
const secrets = buildSecretsFromSetup(spec.connections, spec.owner);
|
|
267
|
-
expect(secrets.MEMORY_USER_ID).toBeUndefined();
|
|
268
|
-
});
|
|
269
|
-
|
|
270
218
|
it("sets owner info when provided", () => {
|
|
271
219
|
const spec = makeValidSpec();
|
|
272
220
|
const secrets = buildSecretsFromSetup(spec.connections, spec.owner);
|
|
@@ -281,21 +229,45 @@ describe("buildSecretsFromSetup", () => {
|
|
|
281
229
|
expect(secrets.OWNER_EMAIL).toBeUndefined();
|
|
282
230
|
});
|
|
283
231
|
|
|
284
|
-
it("
|
|
232
|
+
it("does NOT include provider API keys in stack.env updates", () => {
|
|
233
|
+
// Provider API keys now live in OpenCode's auth.json — buildSecretsFromSetup
|
|
234
|
+
// returns only non-credential vars. See buildAuthJsonFromSetup for the key flow.
|
|
285
235
|
const spec = makeValidSpec();
|
|
286
236
|
const secrets = buildSecretsFromSetup(spec.connections, spec.owner);
|
|
287
|
-
expect(secrets.OPENAI_API_KEY).
|
|
237
|
+
expect(secrets.OPENAI_API_KEY).toBeUndefined();
|
|
238
|
+
expect(secrets.ANTHROPIC_API_KEY).toBeUndefined();
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it("does not include Ollama base URL in stack.env secrets", () => {
|
|
242
|
+
const caps: SetupConnection[] = [
|
|
243
|
+
{ id: "ollama-1", name: "Ollama", provider: "ollama", baseUrl: "http://localhost:11434", apiKey: "" },
|
|
244
|
+
];
|
|
245
|
+
const secrets = buildSecretsFromSetup(caps);
|
|
246
|
+
expect(secrets.SYSTEM_LLM_BASE_URL).toBeUndefined();
|
|
247
|
+
expect(secrets.OLLAMA_BASE_URL).toBeUndefined();
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
describe("buildAuthJsonFromSetup", () => {
|
|
252
|
+
it("maps provider id → apiKey from the spec", () => {
|
|
253
|
+
const conns: SetupConnection[] = [
|
|
254
|
+
{ id: "openai-1", name: "OpenAI", provider: "openai", baseUrl: "", apiKey: "sk-from-spec" },
|
|
255
|
+
{ id: "anthropic-1", name: "Anthropic", provider: "anthropic", baseUrl: "", apiKey: "sk-ant" },
|
|
256
|
+
];
|
|
257
|
+
const keys = buildAuthJsonFromSetup(conns);
|
|
258
|
+
expect(keys.openai).toBe("sk-from-spec");
|
|
259
|
+
expect(keys.anthropic).toBe("sk-ant");
|
|
288
260
|
});
|
|
289
261
|
|
|
290
|
-
it("falls back to process.env when apiKey is empty", () => {
|
|
262
|
+
it("falls back to process.env when spec apiKey is empty", () => {
|
|
291
263
|
const saved = process.env.OPENAI_API_KEY;
|
|
292
264
|
process.env.OPENAI_API_KEY = "sk-from-env";
|
|
293
265
|
try {
|
|
294
|
-
const
|
|
266
|
+
const conns: SetupConnection[] = [
|
|
295
267
|
{ id: "openai-1", name: "OpenAI", provider: "openai", baseUrl: "", apiKey: "" },
|
|
296
268
|
];
|
|
297
|
-
const
|
|
298
|
-
expect(
|
|
269
|
+
const keys = buildAuthJsonFromSetup(conns);
|
|
270
|
+
expect(keys.openai).toBe("sk-from-env");
|
|
299
271
|
} finally {
|
|
300
272
|
if (saved !== undefined) process.env.OPENAI_API_KEY = saved;
|
|
301
273
|
else delete process.env.OPENAI_API_KEY;
|
|
@@ -306,35 +278,41 @@ describe("buildSecretsFromSetup", () => {
|
|
|
306
278
|
const saved = process.env.OPENAI_API_KEY;
|
|
307
279
|
process.env.OPENAI_API_KEY = "sk-from-env";
|
|
308
280
|
try {
|
|
309
|
-
const
|
|
281
|
+
const conns: SetupConnection[] = [
|
|
310
282
|
{ id: "openai-1", name: "OpenAI", provider: "openai", baseUrl: "", apiKey: "sk-from-spec" },
|
|
311
283
|
];
|
|
312
|
-
const
|
|
313
|
-
expect(
|
|
284
|
+
const keys = buildAuthJsonFromSetup(conns);
|
|
285
|
+
expect(keys.openai).toBe("sk-from-spec");
|
|
314
286
|
} finally {
|
|
315
287
|
if (saved !== undefined) process.env.OPENAI_API_KEY = saved;
|
|
316
288
|
else delete process.env.OPENAI_API_KEY;
|
|
317
289
|
}
|
|
318
290
|
});
|
|
319
291
|
|
|
320
|
-
it("
|
|
321
|
-
const
|
|
322
|
-
{ id: "
|
|
292
|
+
it("skips connections without a key in either spec or env", () => {
|
|
293
|
+
const conns: SetupConnection[] = [
|
|
294
|
+
{ id: "openai-1", name: "OpenAI", provider: "openai", baseUrl: "", apiKey: "" },
|
|
323
295
|
];
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
296
|
+
const saved = process.env.OPENAI_API_KEY;
|
|
297
|
+
delete process.env.OPENAI_API_KEY;
|
|
298
|
+
try {
|
|
299
|
+
const keys = buildAuthJsonFromSetup(conns);
|
|
300
|
+
expect(keys.openai).toBeUndefined();
|
|
301
|
+
} finally {
|
|
302
|
+
if (saved !== undefined) process.env.OPENAI_API_KEY = saved;
|
|
303
|
+
}
|
|
328
304
|
});
|
|
329
305
|
});
|
|
330
306
|
|
|
331
307
|
describe("buildSystemSecretsFromSetup", () => {
|
|
332
|
-
|
|
308
|
+
// Phase 4: assistant token was removed; the only stack.env secret this
|
|
309
|
+
// helper writes now is OP_UI_LOGIN_PASSWORD. OP_OPENCODE_PASSWORD is
|
|
310
|
+
// generated by ensureSystemSecrets() and persists across reruns.
|
|
311
|
+
it("returns OP_UI_LOGIN_PASSWORD equal to the supplied operator password", () => {
|
|
333
312
|
const secrets = buildSystemSecretsFromSetup("test-admin-token-12345");
|
|
334
|
-
expect(secrets.
|
|
335
|
-
expect(
|
|
336
|
-
expect(secrets.OP_ASSISTANT_TOKEN).
|
|
337
|
-
expect(typeof secrets.OP_MEMORY_TOKEN).toBe("string");
|
|
313
|
+
expect(secrets.OP_UI_LOGIN_PASSWORD).toBe("test-admin-token-12345");
|
|
314
|
+
expect(secrets.OP_UI_TOKEN).toBeUndefined();
|
|
315
|
+
expect(secrets.OP_ASSISTANT_TOKEN).toBeUndefined();
|
|
338
316
|
});
|
|
339
317
|
});
|
|
340
318
|
|
|
@@ -343,49 +321,46 @@ describe("buildSystemSecretsFromSetup", () => {
|
|
|
343
321
|
describe("performSetup", () => {
|
|
344
322
|
let homeDir: string;
|
|
345
323
|
let configDir: string;
|
|
346
|
-
let
|
|
347
|
-
let
|
|
348
|
-
let logsDir: string;
|
|
324
|
+
let stateDir: string;
|
|
325
|
+
let stackDir: string;
|
|
349
326
|
|
|
350
327
|
const savedEnv: Record<string, string | undefined> = {};
|
|
351
328
|
|
|
352
329
|
beforeEach(() => {
|
|
353
330
|
homeDir = mkdtempSync(join(tmpdir(), "openpalm-setup-"));
|
|
354
331
|
configDir = join(homeDir, "config");
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
logsDir = join(homeDir, "logs");
|
|
332
|
+
stateDir = join(homeDir, "state");
|
|
333
|
+
stackDir = join(configDir, "stack");
|
|
358
334
|
|
|
359
335
|
// Create required directory structure
|
|
360
336
|
for (const dir of [
|
|
361
337
|
homeDir,
|
|
362
338
|
configDir,
|
|
363
|
-
join(
|
|
364
|
-
join(configDir, "channels"),
|
|
339
|
+
join(homeDir, "state", "registry", "automations"),
|
|
365
340
|
join(configDir, "assistant"),
|
|
366
|
-
join(configDir, "
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
join(
|
|
370
|
-
join(
|
|
371
|
-
join(
|
|
372
|
-
join(
|
|
373
|
-
|
|
374
|
-
join(
|
|
375
|
-
|
|
376
|
-
join(
|
|
341
|
+
join(configDir, "akm"),
|
|
342
|
+
stackDir,
|
|
343
|
+
join(stackDir, "addons"),
|
|
344
|
+
join(homeDir, "stash"),
|
|
345
|
+
join(homeDir, "workspace"),
|
|
346
|
+
join(homeDir, "cache"),
|
|
347
|
+
join(homeDir, "cache", "akm"),
|
|
348
|
+
stateDir,
|
|
349
|
+
join(stateDir, "assistant"),
|
|
350
|
+
join(stateDir, "admin"),
|
|
351
|
+
join(stateDir, "guardian"),
|
|
352
|
+
join(stateDir, "logs"),
|
|
353
|
+
join(stateDir, "logs", "opencode"),
|
|
377
354
|
]) {
|
|
378
355
|
mkdirSync(dir, { recursive: true });
|
|
379
356
|
}
|
|
380
357
|
|
|
381
358
|
// Create stub stack.env so isSetupComplete doesn't crash
|
|
382
|
-
mkdirSync(join(vaultDir, "stack"), { recursive: true });
|
|
383
|
-
mkdirSync(join(vaultDir, "user"), { recursive: true });
|
|
384
359
|
writeFileSync(
|
|
385
|
-
join(
|
|
360
|
+
join(stackDir, "stack.env"),
|
|
386
361
|
[
|
|
387
362
|
"OP_SETUP_COMPLETE=false",
|
|
388
|
-
"
|
|
363
|
+
"OP_UI_LOGIN_PASSWORD=",
|
|
389
364
|
"OPENAI_API_KEY=",
|
|
390
365
|
"OPENAI_BASE_URL=",
|
|
391
366
|
"ANTHROPIC_API_KEY=",
|
|
@@ -398,17 +373,6 @@ describe("performSetup", () => {
|
|
|
398
373
|
].join("\n")
|
|
399
374
|
);
|
|
400
375
|
|
|
401
|
-
// Seed a user.env placeholder
|
|
402
|
-
writeFileSync(
|
|
403
|
-
join(vaultDir, "user", "user.env"),
|
|
404
|
-
[
|
|
405
|
-
"# OpenPalm — User Extensions",
|
|
406
|
-
"# Add any custom environment variables here.",
|
|
407
|
-
"# These are loaded by compose alongside stack.env.",
|
|
408
|
-
"",
|
|
409
|
-
].join("\n")
|
|
410
|
-
);
|
|
411
|
-
|
|
412
376
|
// Seed required asset files at OP_HOME
|
|
413
377
|
seedRequiredAssets(homeDir);
|
|
414
378
|
|
|
@@ -424,39 +388,41 @@ describe("performSetup", () => {
|
|
|
424
388
|
|
|
425
389
|
it("returns an error for invalid input", async () => {
|
|
426
390
|
const result = await performSetup(
|
|
427
|
-
{ security: {
|
|
391
|
+
{ security: { uiLoginPassword: "short" } } as SetupSpec
|
|
428
392
|
);
|
|
429
393
|
expect(result.ok).toBe(false);
|
|
430
394
|
expect(result.error).toBeDefined();
|
|
431
395
|
});
|
|
432
396
|
|
|
433
|
-
it("writes stack.env with the
|
|
397
|
+
it("writes stack.env with the UI login password", async () => {
|
|
434
398
|
const result = await performSetup(makeValidSpec());
|
|
435
399
|
expect(result.ok).toBe(true);
|
|
436
400
|
|
|
437
|
-
const secretsContent = readFileSync(join(
|
|
401
|
+
const secretsContent = readFileSync(join(stackDir, "stack.env"), "utf-8");
|
|
438
402
|
expect(secretsContent).toContain("test-admin-token-12345");
|
|
439
403
|
});
|
|
440
404
|
|
|
441
|
-
it("writes
|
|
405
|
+
it("writes akm config.json with llm and embedding", async () => {
|
|
442
406
|
const result = await performSetup(makeValidSpec());
|
|
443
407
|
expect(result.ok).toBe(true);
|
|
444
408
|
|
|
445
|
-
const
|
|
446
|
-
expect(
|
|
447
|
-
|
|
409
|
+
const akmConfigPath = join(homeDir, "config", "akm", "config.json");
|
|
410
|
+
expect(existsSync(akmConfigPath)).toBe(true);
|
|
411
|
+
const config = JSON.parse(readFileSync(akmConfigPath, "utf-8"));
|
|
412
|
+
expect(config.llm.model).toBe("gpt-4o");
|
|
413
|
+
expect(config.llm.provider).toBe("openai");
|
|
414
|
+
expect(config.embedding.model).toBe("text-embedding-3-small");
|
|
415
|
+
expect(config.embedding.provider).toBe("openai");
|
|
416
|
+
expect(config.embedding.dimension).toBe(1536);
|
|
448
417
|
});
|
|
449
418
|
|
|
450
|
-
it("writes
|
|
419
|
+
it("writes stack.yml v2 version marker", async () => {
|
|
451
420
|
const result = await performSetup(makeValidSpec());
|
|
452
421
|
expect(result.ok).toBe(true);
|
|
453
422
|
|
|
454
|
-
const spec = readStackSpec(
|
|
423
|
+
const spec = readStackSpec(stackDir);
|
|
455
424
|
expect(spec).not.toBeNull();
|
|
456
425
|
expect(spec!.version).toBe(2);
|
|
457
|
-
expect(spec!.capabilities.llm).toBe("openai/gpt-4o");
|
|
458
|
-
expect(spec!.capabilities.embeddings.model).toBe("text-embedding-3-small");
|
|
459
|
-
expect(spec!.capabilities.embeddings.provider).toBe("openai");
|
|
460
426
|
});
|
|
461
427
|
|
|
462
428
|
it("writes core compose file to stack/", async () => {
|
|
@@ -464,95 +430,42 @@ describe("performSetup", () => {
|
|
|
464
430
|
expect(result.ok).toBe(true);
|
|
465
431
|
|
|
466
432
|
// applyInstall should have written the compose file to stack/ (not config/components/)
|
|
467
|
-
const stagedCompose = join(homeDir, "stack", "core.compose.yml");
|
|
433
|
+
const stagedCompose = join(homeDir, "config", "stack", "core.compose.yml");
|
|
468
434
|
expect(existsSync(stagedCompose)).toBe(true);
|
|
469
435
|
});
|
|
470
436
|
|
|
471
|
-
it("writes
|
|
437
|
+
it("writes akm config.json with ollama llm settings", async () => {
|
|
472
438
|
const input = makeValidSpec({
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
embeddings: {
|
|
476
|
-
provider: "ollama",
|
|
477
|
-
model: "nomic-embed-text",
|
|
478
|
-
dims: 768,
|
|
479
|
-
},
|
|
480
|
-
memory: {
|
|
481
|
-
userId: "test_user",
|
|
482
|
-
customInstructions: "",
|
|
483
|
-
},
|
|
484
|
-
},
|
|
439
|
+
llm: { provider: "ollama", model: "llama3.2", baseUrl: "http://localhost:11434/v1" },
|
|
440
|
+
embedding: { provider: "ollama", model: "nomic-embed-text", dims: 768, baseUrl: "http://localhost:11434/v1" },
|
|
485
441
|
connections: [
|
|
486
|
-
{
|
|
487
|
-
id: "ollama-local",
|
|
488
|
-
name: "Ollama",
|
|
489
|
-
provider: "ollama",
|
|
490
|
-
baseUrl: "http://localhost:11434",
|
|
491
|
-
apiKey: "",
|
|
492
|
-
},
|
|
493
|
-
],
|
|
494
|
-
});
|
|
495
|
-
|
|
496
|
-
const result = await performSetup(input);
|
|
497
|
-
expect(result.ok).toBe(true);
|
|
498
|
-
|
|
499
|
-
// v2 spec should have correct capabilities without addon metadata
|
|
500
|
-
const spec = readStackSpec(configDir);
|
|
501
|
-
expect(spec).not.toBeNull();
|
|
502
|
-
expect(spec!.version).toBe(2);
|
|
503
|
-
expect(spec!.capabilities.llm).toBe("ollama/llama3.2");
|
|
504
|
-
});
|
|
505
|
-
|
|
506
|
-
it("resolves embedding dims from EMBEDDING_DIMS lookup", async () => {
|
|
507
|
-
const input = makeValidSpec({
|
|
508
|
-
capabilities: {
|
|
509
|
-
llm: "ollama/llama3.2",
|
|
510
|
-
embeddings: {
|
|
511
|
-
provider: "ollama",
|
|
512
|
-
model: "nomic-embed-text",
|
|
513
|
-
dims: 0, // Should be resolved from lookup
|
|
514
|
-
},
|
|
515
|
-
memory: {
|
|
516
|
-
userId: "test_user",
|
|
517
|
-
customInstructions: "",
|
|
518
|
-
},
|
|
519
|
-
},
|
|
520
|
-
connections: [
|
|
521
|
-
{
|
|
522
|
-
id: "ollama-local",
|
|
523
|
-
name: "Ollama",
|
|
524
|
-
provider: "ollama",
|
|
525
|
-
baseUrl: "http://localhost:11434",
|
|
526
|
-
apiKey: "",
|
|
527
|
-
},
|
|
442
|
+
{ id: "ollama-local", name: "Ollama", provider: "ollama", baseUrl: "http://localhost:11434", apiKey: "" },
|
|
528
443
|
],
|
|
529
444
|
});
|
|
530
445
|
|
|
531
446
|
const result = await performSetup(input);
|
|
532
447
|
expect(result.ok).toBe(true);
|
|
533
448
|
|
|
534
|
-
|
|
535
|
-
const
|
|
536
|
-
expect(
|
|
449
|
+
const akmConfigPath = join(homeDir, "config", "akm", "config.json");
|
|
450
|
+
const config = JSON.parse(readFileSync(akmConfigPath, "utf-8"));
|
|
451
|
+
expect(config.llm.provider).toBe("ollama");
|
|
452
|
+
expect(config.llm.model).toBe("llama3.2");
|
|
453
|
+
expect(config.embedding.dimension).toBe(768);
|
|
537
454
|
});
|
|
538
455
|
|
|
539
|
-
it("writes stack.yml
|
|
456
|
+
it("writes stack.yml as version marker only", async () => {
|
|
540
457
|
const result = await performSetup(makeValidSpec());
|
|
541
458
|
expect(result.ok).toBe(true);
|
|
542
459
|
|
|
543
|
-
const specPath = join(
|
|
460
|
+
const specPath = join(stackDir, STACK_SPEC_FILENAME);
|
|
544
461
|
expect(existsSync(specPath)).toBe(true);
|
|
545
462
|
|
|
546
|
-
const spec = readStackSpec(
|
|
463
|
+
const spec = readStackSpec(stackDir);
|
|
547
464
|
expect(spec).not.toBeNull();
|
|
548
465
|
expect(spec!.version).toBe(2);
|
|
549
|
-
expect(spec!.capabilities.llm).toBe("openai/gpt-4o");
|
|
550
|
-
expect(spec!.capabilities.embeddings.provider).toBe("openai");
|
|
551
|
-
expect(spec!.capabilities.embeddings.model).toBe("text-embedding-3-small");
|
|
552
|
-
expect(spec!.capabilities.memory.userId).toBe("test_user");
|
|
553
466
|
});
|
|
554
467
|
|
|
555
|
-
it("completes setup
|
|
468
|
+
it("completes setup with multiple connections", async () => {
|
|
556
469
|
const input = makeValidSpec({
|
|
557
470
|
connections: [
|
|
558
471
|
{ id: "openai_primary", name: "OpenAI Primary", provider: "openai", baseUrl: "https://api.openai.com", apiKey: "sk-primary" },
|
|
@@ -563,11 +476,9 @@ describe("performSetup", () => {
|
|
|
563
476
|
const result = await performSetup(input);
|
|
564
477
|
expect(result.ok).toBe(true);
|
|
565
478
|
|
|
566
|
-
|
|
567
|
-
const spec = readStackSpec(configDir);
|
|
479
|
+
const spec = readStackSpec(stackDir);
|
|
568
480
|
expect(spec).not.toBeNull();
|
|
569
481
|
expect(spec!.version).toBe(2);
|
|
570
|
-
expect(spec!.capabilities.llm).toBe("openai/gpt-4o");
|
|
571
482
|
});
|
|
572
483
|
|
|
573
484
|
it("writes channel credentials to stack.env when channelCredentials provided", async () => {
|
|
@@ -578,24 +489,12 @@ describe("performSetup", () => {
|
|
|
578
489
|
applicationId: "discord-app-id-123",
|
|
579
490
|
},
|
|
580
491
|
},
|
|
581
|
-
capabilities: {
|
|
582
|
-
llm: "openai/gpt-4o",
|
|
583
|
-
embeddings: {
|
|
584
|
-
provider: "openai",
|
|
585
|
-
model: "text-embedding-3-small",
|
|
586
|
-
dims: 1536,
|
|
587
|
-
},
|
|
588
|
-
memory: {
|
|
589
|
-
userId: "test_user",
|
|
590
|
-
customInstructions: "",
|
|
591
|
-
},
|
|
592
|
-
},
|
|
593
492
|
});
|
|
594
493
|
|
|
595
494
|
const result = await performSetup(input);
|
|
596
495
|
expect(result.ok).toBe(true);
|
|
597
496
|
|
|
598
|
-
const stackEnvContent = readFileSync(join(
|
|
497
|
+
const stackEnvContent = readFileSync(join(stackDir, "stack.env"), "utf-8");
|
|
599
498
|
expect(stackEnvContent).toContain("discord-bot-token-xyz");
|
|
600
499
|
expect(stackEnvContent).toContain("discord-app-id-123");
|
|
601
500
|
});
|