@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.
Files changed (55) hide show
  1. package/README.md +2 -2
  2. package/package.json +7 -3
  3. package/src/control-plane/admin-token.ts +73 -0
  4. package/src/control-plane/akm-vault.test.ts +108 -0
  5. package/src/control-plane/akm-vault.ts +307 -0
  6. package/src/control-plane/audit.ts +3 -2
  7. package/src/control-plane/channels.ts +3 -3
  8. package/src/control-plane/cleanup-guardrails.test.ts +8 -9
  9. package/src/control-plane/compose-args.test.ts +25 -21
  10. package/src/control-plane/config-persistence.ts +103 -64
  11. package/src/control-plane/core-assets.test.ts +104 -0
  12. package/src/control-plane/core-assets.ts +54 -57
  13. package/src/control-plane/docker.ts +55 -21
  14. package/src/control-plane/env.test.ts +25 -1
  15. package/src/control-plane/env.ts +80 -0
  16. package/src/control-plane/home.ts +66 -69
  17. package/src/control-plane/host-opencode.test.ts +263 -0
  18. package/src/control-plane/host-opencode.ts +229 -0
  19. package/src/control-plane/install-edge-cases.test.ts +182 -244
  20. package/src/control-plane/install-lock.ts +157 -0
  21. package/src/control-plane/lifecycle.ts +57 -56
  22. package/src/control-plane/markdown-task.ts +200 -0
  23. package/src/control-plane/paths.ts +75 -0
  24. package/src/control-plane/provider-config.ts +2 -2
  25. package/src/control-plane/provider-models.ts +154 -0
  26. package/src/control-plane/registry-components.test.ts +102 -25
  27. package/src/control-plane/registry.test.ts +49 -47
  28. package/src/control-plane/registry.ts +71 -50
  29. package/src/control-plane/rollback.ts +17 -16
  30. package/src/control-plane/scheduler.ts +75 -262
  31. package/src/control-plane/secret-backend.test.ts +98 -108
  32. package/src/control-plane/secret-backend.ts +221 -181
  33. package/src/control-plane/secret-mappings.ts +3 -6
  34. package/src/control-plane/secrets.ts +83 -47
  35. package/src/control-plane/setup-config.schema.json +2 -14
  36. package/src/control-plane/setup-status.ts +4 -29
  37. package/src/control-plane/setup-validation.ts +21 -21
  38. package/src/control-plane/setup.test.ts +122 -227
  39. package/src/control-plane/setup.ts +224 -125
  40. package/src/control-plane/skeleton-guardrail.test.ts +151 -0
  41. package/src/control-plane/spec-to-env.test.ts +59 -58
  42. package/src/control-plane/spec-to-env.ts +39 -140
  43. package/src/control-plane/spec-validator.ts +2 -99
  44. package/src/control-plane/stack-spec.test.ts +21 -77
  45. package/src/control-plane/stack-spec.ts +7 -83
  46. package/src/control-plane/types.ts +17 -15
  47. package/src/control-plane/ui-assets.ts +349 -0
  48. package/src/control-plane/validate.ts +44 -79
  49. package/src/index.ts +77 -44
  50. package/src/logger.test.ts +228 -0
  51. package/src/logger.ts +71 -1
  52. package/src/provider-constants.ts +22 -1
  53. package/src/control-plane/env-schema-validation.test.ts +0 -118
  54. package/src/control-plane/memory-config.ts +0 -298
  55. 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
- capabilities: {
21
- llm: "openai/gpt-4o",
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, "data", "assistant"), { recursive: true });
52
- writeFileSync(join(homeDir, "data", "assistant", "opencode.jsonc"), '{"$schema":"https://opencode.ai/config.json"}\n');
53
- writeFileSync(join(homeDir, "data", "assistant", "AGENTS.md"), "# Agents\n");
54
- mkdirSync(join(homeDir, "vault", "user"), { recursive: true });
55
- writeFileSync(join(homeDir, "vault", "user", "user.env.schema"), "ADMIN_TOKEN=string\n");
56
- mkdirSync(join(homeDir, "vault", "stack"), { recursive: true });
57
- writeFileSync(join(homeDir, "vault", "stack", "stack.env.schema"), "OP_IMAGE_TAG=string\n");
58
- mkdirSync(join(homeDir, "config", "automations"), { recursive: true });
59
- writeFileSync(join(homeDir, "config", "automations", "cleanup-logs.yml"), "name: cleanup-logs\nschedule: daily\n");
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 capabilities.llm", () => {
140
+ it("rejects missing llm.model", () => {
153
141
  const input = makeValidSpec();
154
- (input.capabilities as Record<string, unknown>).llm = "";
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("capabilities.llm"))).toBe(true);
145
+ expect(result.errors.some((e) => e.includes("llm.model"))).toBe(true);
158
146
  });
159
147
 
160
- it("rejects missing capabilities.embeddings", () => {
148
+ it("rejects missing llm.provider", () => {
161
149
  const input = makeValidSpec();
162
- (input.capabilities as Record<string, unknown>).embeddings = null;
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("capabilities.embeddings"))).toBe(true);
153
+ expect(result.errors.some((e) => e.includes("llm.provider"))).toBe(true);
166
154
  });
167
155
 
168
- it("rejects missing capabilities.memory", () => {
156
+ it("rejects non-integer embedding.dims", () => {
169
157
  const input = makeValidSpec();
170
- (input.capabilities as Record<string, unknown>).memory = null;
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("capabilities.memory"))).toBe(true);
161
+ expect(result.errors.some((e) => e.includes("dims must be a positive integer"))).toBe(true);
174
162
  });
175
163
 
176
- it("rejects non-integer embeddings.dims", () => {
164
+ it("accepts spec without llm or embedding (minimal)", () => {
177
165
  const input = makeValidSpec();
178
- input.capabilities.embeddings.dims = 1.5;
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(false);
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.OP_ADMIN_TOKEN).toBeUndefined();
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 (lives in stack.env via OP_CAP_*)", () => {
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("maps API key to correct env var", () => {
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).toBe("sk-test-key-123");
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 caps: SetupConnection[] = [
265
+ const conns: SetupConnection[] = [
295
266
  { id: "openai-1", name: "OpenAI", provider: "openai", baseUrl: "", apiKey: "" },
296
267
  ];
297
- const secrets = buildSecretsFromSetup(caps);
298
- expect(secrets.OPENAI_API_KEY).toBe("sk-from-env");
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 caps: SetupConnection[] = [
280
+ const conns: SetupConnection[] = [
310
281
  { id: "openai-1", name: "OpenAI", provider: "openai", baseUrl: "", apiKey: "sk-from-spec" },
311
282
  ];
312
- const secrets = buildSecretsFromSetup(caps);
313
- expect(secrets.OPENAI_API_KEY).toBe("sk-from-spec");
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("does not include Ollama base URL in user secrets when ollamaEnabled (lives in stack.env via OP_CAP_*)", () => {
321
- const caps: SetupConnection[] = [
322
- { id: "ollama-1", name: "Ollama", provider: "ollama", baseUrl: "http://localhost:11434", apiKey: "" },
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 secrets = buildSecretsFromSetup(caps);
325
- // These are no longer written to user.env — they live in stack.env via OP_CAP_* vars
326
- expect(secrets.SYSTEM_LLM_BASE_URL).toBeUndefined();
327
- expect(secrets.OPENAI_BASE_URL).toBeUndefined();
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.OP_ADMIN_TOKEN).toBe("test-admin-token-12345");
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 vaultDir: string;
347
- let dataDir: string;
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
- vaultDir = join(homeDir, "vault");
356
- dataDir = join(homeDir, "data");
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(configDir, "automations"),
364
- join(configDir, "channels"),
335
+ join(homeDir, "state", "registry", "automations"),
365
336
  join(configDir, "assistant"),
366
- join(configDir, "stash"),
367
- vaultDir,
368
- dataDir,
369
- join(dataDir, "admin"),
370
- join(dataDir, "memory"),
371
- join(dataDir, "assistant"),
372
- join(dataDir, "guardian"),
373
- join(dataDir, "automations"),
374
- join(dataDir, "opencode"),
375
- logsDir,
376
- join(logsDir, "opencode"),
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(vaultDir, "stack", "stack.env"),
356
+ join(stackDir, "stack.env"),
386
357
  [
387
358
  "OP_SETUP_COMPLETE=false",
388
- "OP_ADMIN_TOKEN=",
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(vaultDir, "stack", "stack.env"), "utf-8");
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 OP_CAP_* vars to stack.env for capabilities", async () => {
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 stackEnvContent = readFileSync(join(vaultDir, "stack", "stack.env"), "utf-8");
446
- expect(stackEnvContent).toContain("OP_CAP_LLM_MODEL=gpt-4o");
447
- expect(stackEnvContent).toContain("OP_CAP_EMBEDDINGS_MODEL=text-embedding-3-small");
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 capabilities to stack.yml v2", async () => {
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(configDir);
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 ollama capabilities without addon metadata in stack.yml", async () => {
433
+ it("writes akm config.json with ollama llm settings", async () => {
472
434
  const input = makeValidSpec({
473
- capabilities: {
474
- llm: "ollama/llama3.2",
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
- // nomic-embed-text is 768 dims per EMBEDDING_DIMS — verify via stack.env OP_CAP_EMBEDDINGS_DIMS
535
- const stackEnvContent = readFileSync(join(vaultDir, "stack", "stack.env"), "utf-8");
536
- expect(stackEnvContent).toContain("OP_CAP_EMBEDDINGS_DIMS=768");
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 with correct v2 structure", async () => {
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(configDir, STACK_SPEC_FILENAME);
456
+ const specPath = join(stackDir, STACK_SPEC_FILENAME);
544
457
  expect(existsSync(specPath)).toBe(true);
545
458
 
546
- const spec = readStackSpec(configDir);
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 even when duplicate connection ID with hyphen is skipped by env var map", async () => {
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
- // v2 spec should still have correct capabilities
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(vaultDir, "stack", "stack.env"), "utf-8");
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
  });