@openpalm/lib 0.10.2 → 0.11.0-beta.10

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 (63) hide show
  1. package/README.md +4 -2
  2. package/package.json +11 -3
  3. package/src/control-plane/akm-vault.test.ts +105 -0
  4. package/src/control-plane/akm-vault.ts +311 -0
  5. package/src/control-plane/channels.ts +11 -9
  6. package/src/control-plane/cleanup-guardrails.test.ts +8 -9
  7. package/src/control-plane/compose-args.test.ts +25 -33
  8. package/src/control-plane/compose-args.ts +0 -4
  9. package/src/control-plane/compose-errors.test.ts +106 -0
  10. package/src/control-plane/compose-errors.ts +117 -0
  11. package/src/control-plane/config-persistence.ts +148 -73
  12. package/src/control-plane/core-assets.test.ts +104 -0
  13. package/src/control-plane/core-assets.ts +111 -58
  14. package/src/control-plane/docker.ts +70 -25
  15. package/src/control-plane/env.test.ts +25 -1
  16. package/src/control-plane/env.ts +84 -1
  17. package/src/control-plane/home.ts +66 -69
  18. package/src/control-plane/host-opencode.test.ts +260 -0
  19. package/src/control-plane/host-opencode.ts +229 -0
  20. package/src/control-plane/install-edge-cases.test.ts +190 -292
  21. package/src/control-plane/install-lock.ts +157 -0
  22. package/src/control-plane/lifecycle.ts +65 -75
  23. package/src/control-plane/markdown-task.ts +200 -0
  24. package/src/control-plane/migrate-0110.test.ts +177 -0
  25. package/src/control-plane/migrate-0110.ts +99 -0
  26. package/src/control-plane/operator-ids.test.ts +130 -0
  27. package/src/control-plane/operator-ids.ts +89 -0
  28. package/src/control-plane/paths.ts +80 -0
  29. package/src/control-plane/provider-models.ts +154 -0
  30. package/src/control-plane/registry-components.test.ts +105 -27
  31. package/src/control-plane/registry.test.ts +247 -51
  32. package/src/control-plane/registry.ts +404 -54
  33. package/src/control-plane/rollback.ts +17 -16
  34. package/src/control-plane/scheduler.ts +75 -262
  35. package/src/control-plane/secret-mappings.ts +4 -8
  36. package/src/control-plane/secrets.ts +97 -55
  37. package/src/control-plane/setup-config.schema.json +5 -17
  38. package/src/control-plane/setup-status.ts +9 -29
  39. package/src/control-plane/setup-validation.ts +23 -23
  40. package/src/control-plane/setup.test.ts +143 -244
  41. package/src/control-plane/setup.ts +216 -133
  42. package/src/control-plane/skeleton-guardrail.test.ts +151 -0
  43. package/src/control-plane/spec-to-env.test.ts +75 -60
  44. package/src/control-plane/spec-to-env.ts +68 -153
  45. package/src/control-plane/stack-spec.test.ts +22 -84
  46. package/src/control-plane/stack-spec.ts +9 -89
  47. package/src/control-plane/types.ts +9 -29
  48. package/src/control-plane/ui-assets.ts +385 -0
  49. package/src/control-plane/validate.ts +44 -79
  50. package/src/index.ts +102 -56
  51. package/src/logger.test.ts +228 -0
  52. package/src/logger.ts +71 -1
  53. package/src/provider-constants.ts +22 -1
  54. package/src/control-plane/audit.ts +0 -40
  55. package/src/control-plane/env-schema-validation.test.ts +0 -118
  56. package/src/control-plane/lock.test.ts +0 -194
  57. package/src/control-plane/lock.ts +0 -176
  58. package/src/control-plane/memory-config.ts +0 -298
  59. package/src/control-plane/provider-config.ts +0 -34
  60. package/src/control-plane/redact-schema.ts +0 -50
  61. package/src/control-plane/secret-backend.test.ts +0 -359
  62. package/src/control-plane/secret-backend.ts +0 -322
  63. package/src/control-plane/spec-validator.ts +0 -159
@@ -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
- 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
- },
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, "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 ────────────────────────────────────────────
@@ -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.adminToken", () => {
75
+ it("rejects missing security.uiLoginPassword", () => {
88
76
  const spec = makeValidSpec();
89
- spec.security.adminToken = "";
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.adminToken"))).toBe(true);
80
+ expect(result.errors.some((e) => e.includes("security.uiLoginPassword"))).toBe(true);
93
81
  });
94
82
 
95
- it("rejects short security.adminToken", () => {
83
+ it("rejects short security.uiLoginPassword", () => {
96
84
  const spec = makeValidSpec();
97
- spec.security.adminToken = "short";
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 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,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 admin token in user secrets", () => {
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.OP_ADMIN_TOKEN).toBeUndefined();
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 (lives in stack.env via OP_CAP_*)", () => {
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,47 +215,59 @@ describe("buildSecretsFromSetup", () => {
255
215
  expect(secrets.SYSTEM_LLM_BASE_URL).toBeUndefined();
256
216
  });
257
217
 
258
- it("persists OPENAI_BASE_URL from openai connection", () => {
218
+ it("sets owner info when provided", () => {
259
219
  const spec = makeValidSpec();
260
220
  const secrets = buildSecretsFromSetup(spec.connections, spec.owner);
261
- expect(secrets.OPENAI_BASE_URL).toBe("https://api.openai.com");
221
+ expect(secrets.OP_OWNER_NAME).toBe("Test User");
222
+ expect(secrets.OP_OWNER_EMAIL).toBe("test@example.com");
262
223
  });
263
224
 
264
- it("does not include MEMORY_USER_ID in user secrets (lives in stack.env via OP_CAP_*)", () => {
265
- const spec = makeValidSpec();
225
+ it("omits owner info when empty", () => {
226
+ const spec = makeValidSpec({ owner: { name: "", email: "" } });
266
227
  const secrets = buildSecretsFromSetup(spec.connections, spec.owner);
267
- expect(secrets.MEMORY_USER_ID).toBeUndefined();
228
+ expect(secrets.OP_OWNER_NAME).toBeUndefined();
229
+ expect(secrets.OP_OWNER_EMAIL).toBeUndefined();
268
230
  });
269
231
 
270
- it("sets owner info when provided", () => {
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.
271
235
  const spec = makeValidSpec();
272
236
  const secrets = buildSecretsFromSetup(spec.connections, spec.owner);
273
- expect(secrets.OWNER_NAME).toBe("Test User");
274
- expect(secrets.OWNER_EMAIL).toBe("test@example.com");
237
+ expect(secrets.OPENAI_API_KEY).toBeUndefined();
238
+ expect(secrets.ANTHROPIC_API_KEY).toBeUndefined();
275
239
  });
276
240
 
277
- it("omits owner info when empty", () => {
278
- const spec = makeValidSpec({ owner: { name: "", email: "" } });
279
- const secrets = buildSecretsFromSetup(spec.connections, spec.owner);
280
- expect(secrets.OWNER_NAME).toBeUndefined();
281
- expect(secrets.OWNER_EMAIL).toBeUndefined();
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();
282
248
  });
249
+ });
283
250
 
284
- it("maps API key to correct env var", () => {
285
- const spec = makeValidSpec();
286
- const secrets = buildSecretsFromSetup(spec.connections, spec.owner);
287
- expect(secrets.OPENAI_API_KEY).toBe("sk-test-key-123");
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 caps: SetupConnection[] = [
266
+ const conns: SetupConnection[] = [
295
267
  { id: "openai-1", name: "OpenAI", provider: "openai", baseUrl: "", apiKey: "" },
296
268
  ];
297
- const secrets = buildSecretsFromSetup(caps);
298
- expect(secrets.OPENAI_API_KEY).toBe("sk-from-env");
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 caps: SetupConnection[] = [
281
+ const conns: SetupConnection[] = [
310
282
  { id: "openai-1", name: "OpenAI", provider: "openai", baseUrl: "", apiKey: "sk-from-spec" },
311
283
  ];
312
- const secrets = buildSecretsFromSetup(caps);
313
- expect(secrets.OPENAI_API_KEY).toBe("sk-from-spec");
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("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: "" },
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 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();
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
- it("includes distinct admin and assistant credentials", () => {
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.OP_ADMIN_TOKEN).toBe("test-admin-token-12345");
335
- expect(typeof secrets.OP_ASSISTANT_TOKEN).toBe("string");
336
- expect(secrets.OP_ASSISTANT_TOKEN).not.toBe("test-admin-token-12345");
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,68 +321,54 @@ describe("buildSystemSecretsFromSetup", () => {
343
321
  describe("performSetup", () => {
344
322
  let homeDir: string;
345
323
  let configDir: string;
346
- let vaultDir: string;
347
- let dataDir: string;
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
- vaultDir = join(homeDir, "vault");
356
- dataDir = join(homeDir, "data");
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(configDir, "automations"),
364
- join(configDir, "channels"),
339
+ join(homeDir, "state", "registry", "automations"),
365
340
  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"),
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(vaultDir, "stack", "stack.env"),
360
+ join(stackDir, "stack.env"),
386
361
  [
387
362
  "OP_SETUP_COMPLETE=false",
388
- "OP_ADMIN_TOKEN=",
363
+ "OP_UI_LOGIN_PASSWORD=",
389
364
  "OPENAI_API_KEY=",
390
365
  "OPENAI_BASE_URL=",
391
366
  "ANTHROPIC_API_KEY=",
392
367
  "GROQ_API_KEY=",
393
368
  "MISTRAL_API_KEY=",
394
369
  "GOOGLE_API_KEY=",
395
- "OWNER_NAME=",
396
- "OWNER_EMAIL=",
397
- "",
398
- ].join("\n")
399
- );
400
-
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.",
370
+ "OP_OWNER_NAME=",
371
+ "OP_OWNER_EMAIL=",
408
372
  "",
409
373
  ].join("\n")
410
374
  );
@@ -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: { adminToken: "short" } } as SetupSpec
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 admin token", async () => {
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(vaultDir, "stack", "stack.env"), "utf-8");
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 OP_CAP_* vars to stack.env for capabilities", async () => {
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 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");
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 capabilities to stack.yml v2", async () => {
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(configDir);
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 ollama capabilities without addon metadata in stack.yml", async () => {
472
- 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
- },
485
- 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 () => {
437
+ it("writes akm config.json with ollama llm settings", async () => {
507
438
  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
- },
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" },
520
441
  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
- // 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");
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 with correct v2 structure", async () => {
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(configDir, STACK_SPEC_FILENAME);
460
+ const specPath = join(stackDir, STACK_SPEC_FILENAME);
544
461
  expect(existsSync(specPath)).toBe(true);
545
462
 
546
- const spec = readStackSpec(configDir);
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 even when duplicate connection ID with hyphen is skipped by env var map", async () => {
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
- // v2 spec should still have correct capabilities
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(vaultDir, "stack", "stack.env"), "utf-8");
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
  });