@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.
- 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 +108 -0
- package/src/control-plane/akm-vault.ts +307 -0
- package/src/control-plane/audit.ts +3 -2
- 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 -21
- package/src/control-plane/config-persistence.ts +103 -64
- 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 +263 -0
- package/src/control-plane/host-opencode.ts +229 -0
- package/src/control-plane/install-edge-cases.test.ts +182 -244
- package/src/control-plane/install-lock.ts +157 -0
- package/src/control-plane/lifecycle.ts +57 -56
- package/src/control-plane/markdown-task.ts +200 -0
- package/src/control-plane/paths.ts +75 -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 +102 -25
- 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 -108
- package/src/control-plane/secret-backend.ts +221 -181
- package/src/control-plane/secret-mappings.ts +3 -6
- package/src/control-plane/secrets.ts +83 -47
- package/src/control-plane/setup-config.schema.json +2 -14
- package/src/control-plane/setup-status.ts +4 -29
- package/src/control-plane/setup-validation.ts +21 -21
- package/src/control-plane/setup.test.ts +122 -227
- package/src/control-plane/setup.ts +224 -125
- 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 +39 -140
- 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 +17 -15
- package/src/control-plane/ui-assets.ts +349 -0
- package/src/control-plane/validate.ts +44 -79
- package/src/index.ts +77 -44
- package/src/logger.test.ts +228 -0
- package/src/logger.ts +71 -1
- package/src/provider-constants.ts +22 -1
- 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,30 +5,20 @@ 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
|
-
embeddings: {
|
|
23
|
-
provider: "openai",
|
|
24
|
-
model: "text-embedding-3-small",
|
|
25
|
-
dims: 1536,
|
|
26
|
-
},
|
|
27
|
-
memory: {
|
|
28
|
-
userId: "test_user",
|
|
29
|
-
customInstructions: "",
|
|
30
|
-
},
|
|
31
|
-
},
|
|
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" },
|
|
32
22
|
security: { adminToken: "test-admin-token-12345" },
|
|
33
23
|
owner: { name: "Test User", email: "test@example.com" },
|
|
34
24
|
connections: [
|
|
@@ -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 ────────────────────────────────────────────
|
|
@@ -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,12 +194,6 @@ 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 ─────────────────────────────────────────
|
|
@@ -243,11 +202,11 @@ describe("buildSecretsFromSetup", () => {
|
|
|
243
202
|
it("does not include admin token 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_TOKEN).toBeUndefined();
|
|
247
206
|
expect(secrets.ADMIN_TOKEN).toBeUndefined();
|
|
248
207
|
});
|
|
249
208
|
|
|
250
|
-
it("does not include SYSTEM_LLM_* in user secrets
|
|
209
|
+
it("does not include SYSTEM_LLM_* in user secrets", () => {
|
|
251
210
|
const spec = makeValidSpec();
|
|
252
211
|
const secrets = buildSecretsFromSetup(spec.connections, spec.owner);
|
|
253
212
|
expect(secrets.SYSTEM_LLM_PROVIDER).toBeUndefined();
|
|
@@ -255,18 +214,6 @@ describe("buildSecretsFromSetup", () => {
|
|
|
255
214
|
expect(secrets.SYSTEM_LLM_BASE_URL).toBeUndefined();
|
|
256
215
|
});
|
|
257
216
|
|
|
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
217
|
it("sets owner info when provided", () => {
|
|
271
218
|
const spec = makeValidSpec();
|
|
272
219
|
const secrets = buildSecretsFromSetup(spec.connections, spec.owner);
|
|
@@ -281,21 +228,45 @@ describe("buildSecretsFromSetup", () => {
|
|
|
281
228
|
expect(secrets.OWNER_EMAIL).toBeUndefined();
|
|
282
229
|
});
|
|
283
230
|
|
|
284
|
-
it("
|
|
231
|
+
it("does NOT include provider API keys in stack.env updates", () => {
|
|
232
|
+
// Provider API keys now live in OpenCode's auth.json — buildSecretsFromSetup
|
|
233
|
+
// returns only non-credential vars. See buildAuthJsonFromSetup for the key flow.
|
|
285
234
|
const spec = makeValidSpec();
|
|
286
235
|
const secrets = buildSecretsFromSetup(spec.connections, spec.owner);
|
|
287
|
-
expect(secrets.OPENAI_API_KEY).
|
|
236
|
+
expect(secrets.OPENAI_API_KEY).toBeUndefined();
|
|
237
|
+
expect(secrets.ANTHROPIC_API_KEY).toBeUndefined();
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it("does not include Ollama base URL in stack.env secrets", () => {
|
|
241
|
+
const caps: SetupConnection[] = [
|
|
242
|
+
{ id: "ollama-1", name: "Ollama", provider: "ollama", baseUrl: "http://localhost:11434", apiKey: "" },
|
|
243
|
+
];
|
|
244
|
+
const secrets = buildSecretsFromSetup(caps);
|
|
245
|
+
expect(secrets.SYSTEM_LLM_BASE_URL).toBeUndefined();
|
|
246
|
+
expect(secrets.OLLAMA_BASE_URL).toBeUndefined();
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
describe("buildAuthJsonFromSetup", () => {
|
|
251
|
+
it("maps provider id → apiKey from the spec", () => {
|
|
252
|
+
const conns: SetupConnection[] = [
|
|
253
|
+
{ id: "openai-1", name: "OpenAI", provider: "openai", baseUrl: "", apiKey: "sk-from-spec" },
|
|
254
|
+
{ id: "anthropic-1", name: "Anthropic", provider: "anthropic", baseUrl: "", apiKey: "sk-ant" },
|
|
255
|
+
];
|
|
256
|
+
const keys = buildAuthJsonFromSetup(conns);
|
|
257
|
+
expect(keys.openai).toBe("sk-from-spec");
|
|
258
|
+
expect(keys.anthropic).toBe("sk-ant");
|
|
288
259
|
});
|
|
289
260
|
|
|
290
|
-
it("falls back to process.env when apiKey is empty", () => {
|
|
261
|
+
it("falls back to process.env when spec apiKey is empty", () => {
|
|
291
262
|
const saved = process.env.OPENAI_API_KEY;
|
|
292
263
|
process.env.OPENAI_API_KEY = "sk-from-env";
|
|
293
264
|
try {
|
|
294
|
-
const
|
|
265
|
+
const conns: SetupConnection[] = [
|
|
295
266
|
{ id: "openai-1", name: "OpenAI", provider: "openai", baseUrl: "", apiKey: "" },
|
|
296
267
|
];
|
|
297
|
-
const
|
|
298
|
-
expect(
|
|
268
|
+
const keys = buildAuthJsonFromSetup(conns);
|
|
269
|
+
expect(keys.openai).toBe("sk-from-env");
|
|
299
270
|
} finally {
|
|
300
271
|
if (saved !== undefined) process.env.OPENAI_API_KEY = saved;
|
|
301
272
|
else delete process.env.OPENAI_API_KEY;
|
|
@@ -306,35 +277,38 @@ describe("buildSecretsFromSetup", () => {
|
|
|
306
277
|
const saved = process.env.OPENAI_API_KEY;
|
|
307
278
|
process.env.OPENAI_API_KEY = "sk-from-env";
|
|
308
279
|
try {
|
|
309
|
-
const
|
|
280
|
+
const conns: SetupConnection[] = [
|
|
310
281
|
{ id: "openai-1", name: "OpenAI", provider: "openai", baseUrl: "", apiKey: "sk-from-spec" },
|
|
311
282
|
];
|
|
312
|
-
const
|
|
313
|
-
expect(
|
|
283
|
+
const keys = buildAuthJsonFromSetup(conns);
|
|
284
|
+
expect(keys.openai).toBe("sk-from-spec");
|
|
314
285
|
} finally {
|
|
315
286
|
if (saved !== undefined) process.env.OPENAI_API_KEY = saved;
|
|
316
287
|
else delete process.env.OPENAI_API_KEY;
|
|
317
288
|
}
|
|
318
289
|
});
|
|
319
290
|
|
|
320
|
-
it("
|
|
321
|
-
const
|
|
322
|
-
{ id: "
|
|
291
|
+
it("skips connections without a key in either spec or env", () => {
|
|
292
|
+
const conns: SetupConnection[] = [
|
|
293
|
+
{ id: "openai-1", name: "OpenAI", provider: "openai", baseUrl: "", apiKey: "" },
|
|
323
294
|
];
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
295
|
+
const saved = process.env.OPENAI_API_KEY;
|
|
296
|
+
delete process.env.OPENAI_API_KEY;
|
|
297
|
+
try {
|
|
298
|
+
const keys = buildAuthJsonFromSetup(conns);
|
|
299
|
+
expect(keys.openai).toBeUndefined();
|
|
300
|
+
} finally {
|
|
301
|
+
if (saved !== undefined) process.env.OPENAI_API_KEY = saved;
|
|
302
|
+
}
|
|
328
303
|
});
|
|
329
304
|
});
|
|
330
305
|
|
|
331
306
|
describe("buildSystemSecretsFromSetup", () => {
|
|
332
307
|
it("includes distinct admin and assistant credentials", () => {
|
|
333
308
|
const secrets = buildSystemSecretsFromSetup("test-admin-token-12345");
|
|
334
|
-
expect(secrets.
|
|
309
|
+
expect(secrets.OP_UI_TOKEN).toBe("test-admin-token-12345");
|
|
335
310
|
expect(typeof secrets.OP_ASSISTANT_TOKEN).toBe("string");
|
|
336
311
|
expect(secrets.OP_ASSISTANT_TOKEN).not.toBe("test-admin-token-12345");
|
|
337
|
-
expect(typeof secrets.OP_MEMORY_TOKEN).toBe("string");
|
|
338
312
|
});
|
|
339
313
|
});
|
|
340
314
|
|
|
@@ -343,49 +317,46 @@ describe("buildSystemSecretsFromSetup", () => {
|
|
|
343
317
|
describe("performSetup", () => {
|
|
344
318
|
let homeDir: string;
|
|
345
319
|
let configDir: string;
|
|
346
|
-
let
|
|
347
|
-
let
|
|
348
|
-
let logsDir: string;
|
|
320
|
+
let stateDir: string;
|
|
321
|
+
let stackDir: string;
|
|
349
322
|
|
|
350
323
|
const savedEnv: Record<string, string | undefined> = {};
|
|
351
324
|
|
|
352
325
|
beforeEach(() => {
|
|
353
326
|
homeDir = mkdtempSync(join(tmpdir(), "openpalm-setup-"));
|
|
354
327
|
configDir = join(homeDir, "config");
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
logsDir = join(homeDir, "logs");
|
|
328
|
+
stateDir = join(homeDir, "state");
|
|
329
|
+
stackDir = join(configDir, "stack");
|
|
358
330
|
|
|
359
331
|
// Create required directory structure
|
|
360
332
|
for (const dir of [
|
|
361
333
|
homeDir,
|
|
362
334
|
configDir,
|
|
363
|
-
join(
|
|
364
|
-
join(configDir, "channels"),
|
|
335
|
+
join(homeDir, "state", "registry", "automations"),
|
|
365
336
|
join(configDir, "assistant"),
|
|
366
|
-
join(configDir, "
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
join(
|
|
370
|
-
join(
|
|
371
|
-
join(
|
|
372
|
-
join(
|
|
373
|
-
|
|
374
|
-
join(
|
|
375
|
-
|
|
376
|
-
join(
|
|
337
|
+
join(configDir, "akm"),
|
|
338
|
+
stackDir,
|
|
339
|
+
join(stackDir, "addons"),
|
|
340
|
+
join(homeDir, "stash"),
|
|
341
|
+
join(homeDir, "workspace"),
|
|
342
|
+
join(homeDir, "cache"),
|
|
343
|
+
join(homeDir, "cache", "akm"),
|
|
344
|
+
stateDir,
|
|
345
|
+
join(stateDir, "assistant"),
|
|
346
|
+
join(stateDir, "admin"),
|
|
347
|
+
join(stateDir, "guardian"),
|
|
348
|
+
join(stateDir, "logs"),
|
|
349
|
+
join(stateDir, "logs", "opencode"),
|
|
377
350
|
]) {
|
|
378
351
|
mkdirSync(dir, { recursive: true });
|
|
379
352
|
}
|
|
380
353
|
|
|
381
354
|
// Create stub stack.env so isSetupComplete doesn't crash
|
|
382
|
-
mkdirSync(join(vaultDir, "stack"), { recursive: true });
|
|
383
|
-
mkdirSync(join(vaultDir, "user"), { recursive: true });
|
|
384
355
|
writeFileSync(
|
|
385
|
-
join(
|
|
356
|
+
join(stackDir, "stack.env"),
|
|
386
357
|
[
|
|
387
358
|
"OP_SETUP_COMPLETE=false",
|
|
388
|
-
"
|
|
359
|
+
"OP_UI_TOKEN=",
|
|
389
360
|
"OPENAI_API_KEY=",
|
|
390
361
|
"OPENAI_BASE_URL=",
|
|
391
362
|
"ANTHROPIC_API_KEY=",
|
|
@@ -398,17 +369,6 @@ describe("performSetup", () => {
|
|
|
398
369
|
].join("\n")
|
|
399
370
|
);
|
|
400
371
|
|
|
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
372
|
// Seed required asset files at OP_HOME
|
|
413
373
|
seedRequiredAssets(homeDir);
|
|
414
374
|
|
|
@@ -434,29 +394,31 @@ describe("performSetup", () => {
|
|
|
434
394
|
const result = await performSetup(makeValidSpec());
|
|
435
395
|
expect(result.ok).toBe(true);
|
|
436
396
|
|
|
437
|
-
const secretsContent = readFileSync(join(
|
|
397
|
+
const secretsContent = readFileSync(join(stackDir, "stack.env"), "utf-8");
|
|
438
398
|
expect(secretsContent).toContain("test-admin-token-12345");
|
|
439
399
|
});
|
|
440
400
|
|
|
441
|
-
it("writes
|
|
401
|
+
it("writes akm config.json with llm and embedding", async () => {
|
|
442
402
|
const result = await performSetup(makeValidSpec());
|
|
443
403
|
expect(result.ok).toBe(true);
|
|
444
404
|
|
|
445
|
-
const
|
|
446
|
-
expect(
|
|
447
|
-
|
|
405
|
+
const akmConfigPath = join(homeDir, "config", "akm", "config.json");
|
|
406
|
+
expect(existsSync(akmConfigPath)).toBe(true);
|
|
407
|
+
const config = JSON.parse(readFileSync(akmConfigPath, "utf-8"));
|
|
408
|
+
expect(config.llm.model).toBe("gpt-4o");
|
|
409
|
+
expect(config.llm.provider).toBe("openai");
|
|
410
|
+
expect(config.embedding.model).toBe("text-embedding-3-small");
|
|
411
|
+
expect(config.embedding.provider).toBe("openai");
|
|
412
|
+
expect(config.embedding.dimension).toBe(1536);
|
|
448
413
|
});
|
|
449
414
|
|
|
450
|
-
it("writes
|
|
415
|
+
it("writes stack.yml v2 version marker", async () => {
|
|
451
416
|
const result = await performSetup(makeValidSpec());
|
|
452
417
|
expect(result.ok).toBe(true);
|
|
453
418
|
|
|
454
|
-
const spec = readStackSpec(
|
|
419
|
+
const spec = readStackSpec(stackDir);
|
|
455
420
|
expect(spec).not.toBeNull();
|
|
456
421
|
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
422
|
});
|
|
461
423
|
|
|
462
424
|
it("writes core compose file to stack/", async () => {
|
|
@@ -464,95 +426,42 @@ describe("performSetup", () => {
|
|
|
464
426
|
expect(result.ok).toBe(true);
|
|
465
427
|
|
|
466
428
|
// applyInstall should have written the compose file to stack/ (not config/components/)
|
|
467
|
-
const stagedCompose = join(homeDir, "stack", "core.compose.yml");
|
|
429
|
+
const stagedCompose = join(homeDir, "config", "stack", "core.compose.yml");
|
|
468
430
|
expect(existsSync(stagedCompose)).toBe(true);
|
|
469
431
|
});
|
|
470
432
|
|
|
471
|
-
it("writes
|
|
433
|
+
it("writes akm config.json with ollama llm settings", async () => {
|
|
472
434
|
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
|
-
},
|
|
435
|
+
llm: { provider: "ollama", model: "llama3.2", baseUrl: "http://localhost:11434/v1" },
|
|
436
|
+
embedding: { provider: "ollama", model: "nomic-embed-text", dims: 768, baseUrl: "http://localhost:11434/v1" },
|
|
485
437
|
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
|
-
},
|
|
438
|
+
{ id: "ollama-local", name: "Ollama", provider: "ollama", baseUrl: "http://localhost:11434", apiKey: "" },
|
|
528
439
|
],
|
|
529
440
|
});
|
|
530
441
|
|
|
531
442
|
const result = await performSetup(input);
|
|
532
443
|
expect(result.ok).toBe(true);
|
|
533
444
|
|
|
534
|
-
|
|
535
|
-
const
|
|
536
|
-
expect(
|
|
445
|
+
const akmConfigPath = join(homeDir, "config", "akm", "config.json");
|
|
446
|
+
const config = JSON.parse(readFileSync(akmConfigPath, "utf-8"));
|
|
447
|
+
expect(config.llm.provider).toBe("ollama");
|
|
448
|
+
expect(config.llm.model).toBe("llama3.2");
|
|
449
|
+
expect(config.embedding.dimension).toBe(768);
|
|
537
450
|
});
|
|
538
451
|
|
|
539
|
-
it("writes stack.yml
|
|
452
|
+
it("writes stack.yml as version marker only", async () => {
|
|
540
453
|
const result = await performSetup(makeValidSpec());
|
|
541
454
|
expect(result.ok).toBe(true);
|
|
542
455
|
|
|
543
|
-
const specPath = join(
|
|
456
|
+
const specPath = join(stackDir, STACK_SPEC_FILENAME);
|
|
544
457
|
expect(existsSync(specPath)).toBe(true);
|
|
545
458
|
|
|
546
|
-
const spec = readStackSpec(
|
|
459
|
+
const spec = readStackSpec(stackDir);
|
|
547
460
|
expect(spec).not.toBeNull();
|
|
548
461
|
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
462
|
});
|
|
554
463
|
|
|
555
|
-
it("completes setup
|
|
464
|
+
it("completes setup with multiple connections", async () => {
|
|
556
465
|
const input = makeValidSpec({
|
|
557
466
|
connections: [
|
|
558
467
|
{ id: "openai_primary", name: "OpenAI Primary", provider: "openai", baseUrl: "https://api.openai.com", apiKey: "sk-primary" },
|
|
@@ -563,11 +472,9 @@ describe("performSetup", () => {
|
|
|
563
472
|
const result = await performSetup(input);
|
|
564
473
|
expect(result.ok).toBe(true);
|
|
565
474
|
|
|
566
|
-
|
|
567
|
-
const spec = readStackSpec(configDir);
|
|
475
|
+
const spec = readStackSpec(stackDir);
|
|
568
476
|
expect(spec).not.toBeNull();
|
|
569
477
|
expect(spec!.version).toBe(2);
|
|
570
|
-
expect(spec!.capabilities.llm).toBe("openai/gpt-4o");
|
|
571
478
|
});
|
|
572
479
|
|
|
573
480
|
it("writes channel credentials to stack.env when channelCredentials provided", async () => {
|
|
@@ -578,24 +485,12 @@ describe("performSetup", () => {
|
|
|
578
485
|
applicationId: "discord-app-id-123",
|
|
579
486
|
},
|
|
580
487
|
},
|
|
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
488
|
});
|
|
594
489
|
|
|
595
490
|
const result = await performSetup(input);
|
|
596
491
|
expect(result.ok).toBe(true);
|
|
597
492
|
|
|
598
|
-
const stackEnvContent = readFileSync(join(
|
|
493
|
+
const stackEnvContent = readFileSync(join(stackDir, "stack.env"), "utf-8");
|
|
599
494
|
expect(stackEnvContent).toContain("discord-bot-token-xyz");
|
|
600
495
|
expect(stackEnvContent).toContain("discord-app-id-123");
|
|
601
496
|
});
|