@openpalm/lib 0.11.0-beta.1 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openpalm/lib",
3
- "version": "0.11.0-beta.1",
3
+ "version": "0.11.0-beta.2",
4
4
  "license": "MPL-2.0",
5
5
  "type": "module",
6
6
  "description": "Shared control-plane library for OpenPalm — lifecycle, staging, secrets, channels, connections, scheduler",
@@ -21,19 +21,16 @@ import type { ControlPlaneState } from "./types.js";
21
21
 
22
22
  function makeState(homeDir: string): ControlPlaneState {
23
23
  return {
24
- adminToken: "test-admin",
25
- assistantToken: "test-assistant",
26
24
  homeDir,
27
25
  configDir: join(homeDir, "config"),
28
26
  stashDir: join(homeDir, "stash"),
29
27
  workspaceDir: join(homeDir, "workspace"),
30
- servicesDir: join(homeDir, "services"),
28
+ cacheDir: join(homeDir, "cache"),
31
29
  stateDir: join(homeDir, "state"),
32
30
  stackDir: join(homeDir, "stack"),
33
31
  services: {},
34
32
  artifacts: { compose: "" },
35
33
  artifactMeta: [],
36
- audit: [],
37
34
  };
38
35
  }
39
36
 
@@ -17,8 +17,6 @@ let tempDir: string;
17
17
  function makeState(overrides: Partial<ControlPlaneState> = {}): ControlPlaneState {
18
18
  const configDir = join(tempDir, "config");
19
19
  return {
20
- adminToken: "test",
21
- assistantToken: "test",
22
20
  homeDir: tempDir,
23
21
  configDir,
24
22
  stashDir: join(tempDir, "stash"),
@@ -29,7 +27,6 @@ function makeState(overrides: Partial<ControlPlaneState> = {}): ControlPlaneStat
29
27
  services: {},
30
28
  artifacts: { compose: "" },
31
29
  artifactMeta: [],
32
- audit: [],
33
30
  ...overrides,
34
31
  };
35
32
  }
@@ -0,0 +1,106 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import {
3
+ parseComposeStderr,
4
+ summarizeComposeStderr,
5
+ } from "./compose-errors.js";
6
+
7
+ describe("parseComposeStderr", () => {
8
+ it("returns empty for empty input", () => {
9
+ expect(parseComposeStderr("")).toEqual([]);
10
+ expect(parseComposeStderr("\n\n")).toEqual([]);
11
+ });
12
+
13
+ it("extracts pull access denied for a single service", () => {
14
+ const stderr = [
15
+ " Network openpalm_default Created",
16
+ " voice Pulling",
17
+ " voice Error pull access denied for openpalm/voice, repository does not exist or may require 'docker login'",
18
+ "Error response from daemon: pull access denied for openpalm/voice, repository does not exist or may require 'docker login': denied: requested access to the resource is denied",
19
+ ].join("\n");
20
+
21
+ const failures = parseComposeStderr(stderr);
22
+ expect(failures.length).toBeGreaterThanOrEqual(1);
23
+ expect(failures[0].service).toBe("voice");
24
+ expect(failures[0].reason).toMatch(/pull access denied/);
25
+ });
26
+
27
+ it("handles spinner / status prefix glyphs", () => {
28
+ const stderr = " ⠿ voice Error pull access denied for openpalm/voice";
29
+ const failures = parseComposeStderr(stderr);
30
+ expect(failures).toHaveLength(1);
31
+ expect(failures[0].service).toBe("voice");
32
+ expect(failures[0].reason).toMatch(/pull access denied/);
33
+ });
34
+
35
+ it("captures quoted Service failed lines", () => {
36
+ const stderr =
37
+ 'Service "discord" failed to build: failed to solve: process did not complete';
38
+ const failures = parseComposeStderr(stderr);
39
+ expect(failures).toHaveLength(1);
40
+ expect(failures[0].service).toBe("discord");
41
+ expect(failures[0].reason).toMatch(/failed to solve/);
42
+ });
43
+
44
+ it("deduplicates identical (service, reason) pairs", () => {
45
+ const stderr = [
46
+ "voice Error pull access denied for openpalm/voice",
47
+ "voice Error pull access denied for openpalm/voice",
48
+ ].join("\n");
49
+ const failures = parseComposeStderr(stderr);
50
+ expect(failures).toHaveLength(1);
51
+ });
52
+
53
+ it("returns multiple distinct failures", () => {
54
+ const stderr = [
55
+ "voice Error pull access denied for openpalm/voice",
56
+ "discord Error no such image: openpalm/discord:latest",
57
+ ].join("\n");
58
+ const failures = parseComposeStderr(stderr);
59
+ expect(failures).toHaveLength(2);
60
+ expect(failures.map((f) => f.service).sort()).toEqual(["discord", "voice"]);
61
+ });
62
+
63
+ it("falls back to image name when only daemon error is present", () => {
64
+ const stderr =
65
+ "Error response from daemon: pull access denied for openpalm/voice, repository does not exist";
66
+ const failures = parseComposeStderr(stderr);
67
+ expect(failures).toHaveLength(1);
68
+ expect(failures[0].service).toBe("openpalm/voice");
69
+ expect(failures[0].reason).toMatch(/pull access denied/);
70
+ });
71
+
72
+ it("ignores non-error noise (Pulling/Created/Started)", () => {
73
+ const stderr = [
74
+ " Network openpalm_default Created",
75
+ " Container openpalm-guardian-1 Started",
76
+ " assistant Pulling",
77
+ ].join("\n");
78
+ expect(parseComposeStderr(stderr)).toEqual([]);
79
+ });
80
+
81
+ it("does not treat 'Error response from daemon' as a service name", () => {
82
+ const stderr = "Error response from daemon: something bad happened";
83
+ // No service-prefixed line, no pull access denied, no quoted service —
84
+ // parser should NOT invent a service called "Error".
85
+ expect(parseComposeStderr(stderr)).toEqual([]);
86
+ });
87
+ });
88
+
89
+ describe("summarizeComposeStderr", () => {
90
+ it("returns first non-empty line", () => {
91
+ expect(summarizeComposeStderr("\n\n hello world \nnext line")).toBe(
92
+ "hello world"
93
+ );
94
+ });
95
+
96
+ it("truncates long lines", () => {
97
+ const long = "x".repeat(800);
98
+ const out = summarizeComposeStderr(long, 100);
99
+ expect(out.length).toBe(100);
100
+ expect(out.endsWith("…")).toBe(true);
101
+ });
102
+
103
+ it("returns empty string for empty input", () => {
104
+ expect(summarizeComposeStderr("")).toBe("");
105
+ });
106
+ });
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Parse `docker compose` stderr for per-service failures.
3
+ *
4
+ * `docker compose up -d` reports its progress on stderr — one or more
5
+ * status lines per service, plus a daemon-level "Error response from daemon"
6
+ * summary. When a single addon service fails to pull or start, the rest of
7
+ * the stack often comes up fine, so the only signal that anything is wrong
8
+ * is whatever appears on stderr. This helper extracts the per-service
9
+ * failure messages so callers can surface them to operators.
10
+ */
11
+ export type ComposeServiceFailure = {
12
+ service: string;
13
+ reason: string;
14
+ };
15
+
16
+ /**
17
+ * Lines we recognise as per-service failure indicators. The compose CLI
18
+ * has rendered these in a few different shapes across versions:
19
+ *
20
+ * "voice Error pull access denied for openpalm/voice ..."
21
+ * " ⠿ voice Error pull access denied for openpalm/voice ..."
22
+ * "Service \"voice\" failed to build: ..."
23
+ *
24
+ * We also pick up the bare daemon error and attribute it to the service
25
+ * named in nearby lines when no service-prefixed line is present.
26
+ */
27
+ const SERVICE_ERROR_RE = /^[\s⠦⠧⠇⠏⠋⠙⠹⠸⠼⠴⠿✔✘×]*\s*([A-Za-z0-9._-]+)\s+(Error|Failed|failed)\s+(.+)$/;
28
+ const SERVICE_FAILED_QUOTED_RE = /Service\s+["']([A-Za-z0-9._-]+)["']\s+failed[^:]*:\s*(.+)$/i;
29
+ const SERVICE_NOT_FOUND_RE = /no such service:\s*([A-Za-z0-9._-]+)/i;
30
+ const PULL_ACCESS_DENIED_RE = /pull access denied for\s+([^\s,]+)/i;
31
+
32
+ function pushUnique(
33
+ failures: ComposeServiceFailure[],
34
+ entry: ComposeServiceFailure
35
+ ): void {
36
+ const trimmed = { service: entry.service.trim(), reason: entry.reason.trim() };
37
+ if (!trimmed.service || !trimmed.reason) return;
38
+ const dup = failures.find(
39
+ (f) => f.service === trimmed.service && f.reason === trimmed.reason
40
+ );
41
+ if (!dup) failures.push(trimmed);
42
+ }
43
+
44
+ /**
45
+ * Best-effort extraction of failures from compose stderr.
46
+ *
47
+ * - Returns one entry per (service, reason) pair, in stderr order.
48
+ * - Does NOT fabricate service names: if a daemon error appears without
49
+ * any nearby service-prefixed line, the caller's intended-services list
50
+ * is used by the route, not this parser.
51
+ */
52
+ export function parseComposeStderr(stderr: string): ComposeServiceFailure[] {
53
+ const failures: ComposeServiceFailure[] = [];
54
+ if (!stderr) return failures;
55
+
56
+ const lines = stderr.split(/\r?\n/);
57
+
58
+ for (const raw of lines) {
59
+ const line = raw.replace(/\s+$/, "");
60
+ if (!line.trim()) continue;
61
+
62
+ const quoted = SERVICE_FAILED_QUOTED_RE.exec(line);
63
+ if (quoted) {
64
+ pushUnique(failures, { service: quoted[1], reason: quoted[2] });
65
+ continue;
66
+ }
67
+
68
+ const m = SERVICE_ERROR_RE.exec(line);
69
+ if (m) {
70
+ // Skip generic prefixes that look like services but aren't
71
+ // (e.g. "Error response from daemon ..." would match if the parser
72
+ // is too lenient — the verb word would be the second token).
73
+ const candidate = m[1];
74
+ if (candidate.toLowerCase() === "error") continue;
75
+ pushUnique(failures, { service: candidate, reason: m[3] });
76
+ continue;
77
+ }
78
+
79
+ const notFound = SERVICE_NOT_FOUND_RE.exec(line);
80
+ if (notFound) {
81
+ pushUnique(failures, {
82
+ service: notFound[1],
83
+ reason: `no such service: ${notFound[1]}`,
84
+ });
85
+ continue;
86
+ }
87
+ }
88
+
89
+ // If we still found nothing but the stderr clearly mentions a pull
90
+ // access denied, surface the offending image as the "service" identifier
91
+ // — better than swallowing the failure entirely.
92
+ if (failures.length === 0) {
93
+ const denied = PULL_ACCESS_DENIED_RE.exec(stderr);
94
+ if (denied) {
95
+ pushUnique(failures, {
96
+ service: denied[1],
97
+ reason: `pull access denied for ${denied[1]}`,
98
+ });
99
+ }
100
+ }
101
+
102
+ return failures;
103
+ }
104
+
105
+ /**
106
+ * Summarise compose stderr in a single short line, suitable for log
107
+ * envelopes / API error messages when no per-service parse succeeded.
108
+ * Returns the first non-empty stderr line, capped.
109
+ */
110
+ export function summarizeComposeStderr(stderr: string, maxLen = 500): string {
111
+ if (!stderr) return "";
112
+ const first = stderr
113
+ .split(/\r?\n/)
114
+ .map((l) => l.trim())
115
+ .find((l) => l.length > 0) ?? "";
116
+ return first.length > maxLen ? first.slice(0, maxLen - 1) + "…" : first;
117
+ }
@@ -85,8 +85,7 @@ function generateFallbackSystemEnv(state: ControlPlaneState): string {
85
85
  "# Auto-generated fallback.",
86
86
  "",
87
87
  "# ── Authentication ──────────────────────────────────────────────────",
88
- `OP_UI_TOKEN=\${OP_UI_TOKEN}`,
89
- `OP_ASSISTANT_TOKEN=\${OP_ASSISTANT_TOKEN}`,
88
+ `OP_UI_LOGIN_PASSWORD=\${OP_UI_LOGIN_PASSWORD}`,
90
89
  "",
91
90
  "# ── Service Auth ─────────────────────────────────────────────────────",
92
91
  "OP_OPENCODE_PASSWORD=",
@@ -14,8 +14,6 @@ import type { ControlPlaneState } from "./types.js";
14
14
 
15
15
  function makeState(homeDir: string): ControlPlaneState {
16
16
  return {
17
- adminToken: "test-admin",
18
- assistantToken: "test-assistant",
19
17
  homeDir,
20
18
  configDir: join(homeDir, "config"),
21
19
  stashDir: join(homeDir, "stash"),
@@ -26,7 +24,6 @@ function makeState(homeDir: string): ControlPlaneState {
26
24
  services: {},
27
25
  artifacts: { compose: "" },
28
26
  artifactMeta: [],
29
- audit: [],
30
27
  };
31
28
  }
32
29
 
@@ -36,7 +36,7 @@ function makeValidSpec(overrides?: Partial<SetupSpec>): SetupSpec {
36
36
  version: 2,
37
37
  llm: { provider: "openai", model: "gpt-4o", baseUrl: "https://api.openai.com/v1" },
38
38
  embedding: { provider: "openai", model: "text-embedding-3-small", dims: 1536, baseUrl: "https://api.openai.com/v1" },
39
- security: { adminToken: "test-admin-token-12345" },
39
+ security: { uiLoginPassword: "test-admin-token-12345" },
40
40
  owner: { name: "Test User", email: "test@example.com" },
41
41
  connections: [
42
42
  {
@@ -135,8 +135,7 @@ function seedMinimalEnvFiles(): void {
135
135
  join(stackDir, "stack.env"),
136
136
  [
137
137
  "# OpenPalm — Stack Configuration",
138
- "OP_UI_TOKEN=",
139
- "OP_ASSISTANT_TOKEN=",
138
+ "OP_UI_LOGIN_PASSWORD=",
140
139
  "OPENAI_API_KEY=",
141
140
  "OPENAI_BASE_URL=",
142
141
  "ANTHROPIC_API_KEY=",
@@ -171,8 +170,6 @@ describe("Fresh Install", () => {
171
170
  // does create stack.env with required keys when files do not exist.
172
171
  it("ensureSecrets creates state/stack.env with required keys on fresh install", () => {
173
172
  const state: ControlPlaneState = {
174
- adminToken: "",
175
- assistantToken: "",
176
173
  homeDir,
177
174
  configDir,
178
175
  stashDir: join(homeDir, "stash"),
@@ -183,7 +180,6 @@ describe("Fresh Install", () => {
183
180
  services: {},
184
181
  artifacts: { compose: "" },
185
182
  artifactMeta: [],
186
- audit: [],
187
183
  };
188
184
 
189
185
  ensureSecrets(state);
@@ -253,11 +249,9 @@ describe("Existing Install", () => {
253
249
  // Scenario 5: ensureSecrets does NOT overwrite existing stack.env
254
250
  it("ensureSecrets does not overwrite existing stack.env tokens", () => {
255
251
  mkdirSync(stateDir, { recursive: true });
256
- writeFileSync(join(stackDir, "stack.env"), "OP_UI_TOKEN=my-custom-token\nOP_ASSISTANT_TOKEN=existing-token\n");
252
+ writeFileSync(join(stackDir, "stack.env"), "OP_UI_LOGIN_PASSWORD=my-custom-password\n");
257
253
 
258
254
  const state: ControlPlaneState = {
259
- adminToken: "",
260
- assistantToken: "",
261
255
  homeDir,
262
256
  configDir,
263
257
  stashDir: join(homeDir, "stash"),
@@ -268,57 +262,29 @@ describe("Existing Install", () => {
268
262
  services: {},
269
263
  artifacts: { compose: "" },
270
264
  artifactMeta: [],
271
- audit: [],
272
265
  };
273
266
 
274
267
  ensureSecrets(state);
275
268
 
276
- // Existing tokens must be preserved
269
+ // Existing password must be preserved
277
270
  const afterContent = readFileSync(join(stackDir, "stack.env"), "utf-8");
278
- expect(afterContent).toContain("OP_UI_TOKEN=my-custom-token");
279
- expect(afterContent).toContain("OP_ASSISTANT_TOKEN=existing-token");
271
+ expect(afterContent).toContain("OP_UI_LOGIN_PASSWORD=my-custom-password");
280
272
  });
281
273
 
282
- // Scenario 6: performSetup re-run preserves OP_ASSISTANT_TOKEN
283
- it("performSetup re-run preserves OP_ASSISTANT_TOKEN from first run", async () => {
284
- // First setup
285
- await performSetup(makeValidSpec());
274
+ // Scenario 6: performSetup re-run rewrites OP_UI_LOGIN_PASSWORD when the
275
+ // operator supplies a new one in the spec. This is intentional — the
276
+ // wizard "rerun" path is how an operator rotates the password. The
277
+ // legacy OP_ASSISTANT_TOKEN preservation test was removed with the token.
278
+ it("performSetup re-run rewrites OP_UI_LOGIN_PASSWORD when spec changes", async () => {
279
+ await performSetup(makeValidSpec({ security: { uiLoginPassword: "first-password-12345" } }));
286
280
 
287
- const secretsAfterFirst = readFileSync(
288
- join(stackDir, "stack.env"),
289
- "utf-8"
290
- );
291
- const firstMatch = secretsAfterFirst.match(
292
- /OP_ASSISTANT_TOKEN=([a-f0-9]+)/
293
- );
294
- expect(firstMatch).not.toBeNull();
295
- const firstToken = firstMatch![1];
281
+ const afterFirst = readFileSync(join(stackDir, "stack.env"), "utf-8");
282
+ expect(afterFirst).toContain("OP_UI_LOGIN_PASSWORD=first-password-12345");
296
283
 
297
- // Second setup (re-run with different API key)
298
- await performSetup(
299
- makeValidSpec({
300
- connections: [
301
- {
302
- id: "openai-main",
303
- name: "OpenAI",
304
- provider: "openai",
305
- baseUrl: "https://api.openai.com",
306
- apiKey: "sk-different-key-999",
307
- },
308
- ],
309
- })
310
- );
284
+ await performSetup(makeValidSpec({ security: { uiLoginPassword: "second-password-12345" } }));
311
285
 
312
- const secretsAfterSecond = readFileSync(
313
- join(stackDir, "stack.env"),
314
- "utf-8"
315
- );
316
- const secondMatch = secretsAfterSecond.match(
317
- /OP_ASSISTANT_TOKEN=([a-f0-9]+)/
318
- );
319
- expect(secondMatch).not.toBeNull();
320
- // OP_ASSISTANT_TOKEN should be preserved across setups
321
- expect(secondMatch![1]).toBe(firstToken);
286
+ const afterSecond = readFileSync(join(stackDir, "stack.env"), "utf-8");
287
+ expect(afterSecond).toContain("OP_UI_LOGIN_PASSWORD=second-password-12345");
322
288
  });
323
289
 
324
290
  // Scenario 7: performSetup must NOT mark OP_SETUP_COMPLETE — see scenario
@@ -386,11 +352,9 @@ describe("Broken/Corrupt State", () => {
386
352
  // Scenario 9: ensureSecrets is idempotent on repeated calls
387
353
  it("ensureSecrets is idempotent — second call does not overwrite existing stack.env", () => {
388
354
  mkdirSync(stateDir, { recursive: true });
389
- writeFileSync(join(stackDir, "stack.env"), "OP_UI_TOKEN=existing-token\nOP_ASSISTANT_TOKEN=existing-assistant\n");
355
+ writeFileSync(join(stackDir, "stack.env"), "OP_UI_LOGIN_PASSWORD=existing-password\n");
390
356
 
391
357
  const state: ControlPlaneState = {
392
- adminToken: "",
393
- assistantToken: "",
394
358
  homeDir,
395
359
  configDir,
396
360
  stashDir: join(homeDir, "stash"),
@@ -401,15 +365,13 @@ describe("Broken/Corrupt State", () => {
401
365
  services: {},
402
366
  artifacts: { compose: "" },
403
367
  artifactMeta: [],
404
- audit: [],
405
368
  };
406
369
 
407
370
  ensureSecrets(state);
408
371
 
409
- // Existing tokens must be preserved
372
+ // Existing password must be preserved
410
373
  const content = readFileSync(join(stackDir, "stack.env"), "utf-8");
411
- expect(content).toContain("OP_UI_TOKEN=existing-token");
412
- expect(content).toContain("OP_ASSISTANT_TOKEN=existing-assistant");
374
+ expect(content).toContain("OP_UI_LOGIN_PASSWORD=existing-password");
413
375
  });
414
376
 
415
377
  // Scenario 10: env file with malformed lines
@@ -447,11 +409,11 @@ describe("Broken/Corrupt State", () => {
447
409
  expect(isSetupComplete(stackDir)).toBe(false);
448
410
  });
449
411
 
450
- it("isSetupComplete falls back to true when admin token is set but OP_SETUP_COMPLETE missing", () => {
412
+ it("isSetupComplete falls back to true when UI login password is set but OP_SETUP_COMPLETE missing", () => {
451
413
  mkdirSync(stateDir, { recursive: true });
452
414
  writeFileSync(
453
415
  join(stackDir, "stack.env"),
454
- "OP_IMAGE_TAG=latest\nexport OP_UI_TOKEN=my-real-token\n"
416
+ "OP_IMAGE_TAG=latest\nexport OP_UI_LOGIN_PASSWORD=my-real-password\n"
455
417
  );
456
418
 
457
419
  expect(isSetupComplete(stackDir)).toBe(true);
@@ -527,12 +489,12 @@ describe("Environment Edge Cases", () => {
527
489
  rmSync(homeDir, { recursive: true, force: true });
528
490
  });
529
491
 
530
- // Scenario 16: Commented-out ADMIN_TOKEN but OP_UI_TOKEN set
531
- it("isSetupComplete detects OP_UI_TOKEN when ADMIN_TOKEN is commented out", () => {
492
+ // Scenario 16: isSetupComplete picks up OP_UI_LOGIN_PASSWORD when set
493
+ it("isSetupComplete detects OP_UI_LOGIN_PASSWORD", () => {
532
494
  mkdirSync(stateDir, { recursive: true });
533
495
  writeFileSync(
534
496
  join(stackDir, "stack.env"),
535
- "SOME_OTHER_KEY=value\nexport OP_UI_TOKEN=real-token-here\n"
497
+ "SOME_OTHER_KEY=value\nexport OP_UI_LOGIN_PASSWORD=real-password-here\n"
536
498
  );
537
499
 
538
500
  expect(isSetupComplete(stackDir)).toBe(true);
@@ -705,13 +667,11 @@ describe("performSetup end-to-end artifacts", () => {
705
667
  ).toBe(true);
706
668
  });
707
669
 
708
- it("writes admin and assistant tokens to stack.env", async () => {
670
+ it("writes the UI login password to stack.env", async () => {
709
671
  await performSetup(makeValidSpec());
710
672
 
711
673
  const secrets = parseEnvFile(join(stackDir, "stack.env"));
712
- expect(secrets.OP_UI_TOKEN).toBe("test-admin-token-12345");
713
- expect(typeof secrets.OP_ASSISTANT_TOKEN).toBe("string");
714
- expect(secrets.OP_ASSISTANT_TOKEN).not.toBe("test-admin-token-12345");
674
+ expect(secrets.OP_UI_LOGIN_PASSWORD).toBe("test-admin-token-12345");
715
675
  });
716
676
 
717
677
  it("writes akm config with llm provider and model", async () => {
@@ -1,7 +1,7 @@
1
1
  /** Lifecycle helpers — state factory, apply transitions, compose file list. */
2
2
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
3
3
  import { parseEnvFile, mergeEnvContent } from "./env.js";
4
- import type { ControlPlaneState, CallerType, AuditContext } from "./types.js";
4
+ import type { ControlPlaneState, CallerType } from "./types.js";
5
5
  import { CORE_SERVICES } from "./types.js";
6
6
  import {
7
7
  resolveOpenPalmHome,
@@ -12,7 +12,7 @@ import {
12
12
  resolveStateDir,
13
13
  resolveStackDir,
14
14
  } from "./home.js";
15
- import { ensureSecrets, readStackEnv, updateSystemSecretsEnv } from "./secrets.js";
15
+ import { ensureSecrets } from "./secrets.js";
16
16
  import {
17
17
  resolveRuntimeFiles,
18
18
  writeRuntimeFiles,
@@ -26,16 +26,13 @@ import { isSetupComplete } from "./setup-status.js";
26
26
  import { snapshotCurrentState } from "./rollback.js";
27
27
  import { checkDocker, composePreflight, composePull, composeUp, composeConfigServices, resolveComposeProjectName } from "./docker.js";
28
28
  import { acquireLock, releaseLock } from "./lock.js";
29
- import { appendAudit } from "./audit.js";
30
29
  import { listEnabledAddonIds } from "./registry.js";
31
30
 
32
31
  const IMAGE_NAMESPACE_RE = /^[a-z0-9]+(?:[._-][a-z0-9]+)*$/;
33
32
  const SEMVER_TAG_RE = /^v\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/;
34
33
 
35
34
 
36
- export function createState(
37
- adminToken?: string
38
- ): ControlPlaneState {
35
+ export function createState(): ControlPlaneState {
39
36
  const homeDir = resolveOpenPalmHome();
40
37
  const configDir = resolveConfigDir();
41
38
  const stashDir = resolveStashDir();
@@ -50,8 +47,6 @@ export function createState(
50
47
  }
51
48
 
52
49
  const bootstrapState: ControlPlaneState = {
53
- adminToken: adminToken ?? process.env.OP_UI_TOKEN ?? "",
54
- assistantToken: "",
55
50
  homeDir,
56
51
  configDir,
57
52
  stashDir,
@@ -62,23 +57,10 @@ export function createState(
62
57
  services,
63
58
  artifacts: { compose: "" },
64
59
  artifactMeta: [],
65
- audit: [],
66
60
  };
67
61
 
68
62
  ensureSecrets(bootstrapState);
69
63
 
70
- const stackEnv = readStackEnv(stackDir);
71
- // Precedence: explicit parameter > stack.env > process.env.
72
- bootstrapState.adminToken =
73
- adminToken
74
- ?? stackEnv.OP_UI_TOKEN
75
- ?? process.env.OP_UI_TOKEN
76
- ?? "";
77
- bootstrapState.assistantToken =
78
- stackEnv.OP_ASSISTANT_TOKEN
79
- ?? process.env.OP_ASSISTANT_TOKEN
80
- ?? "";
81
-
82
64
  return bootstrapState;
83
65
  }
84
66
 
@@ -142,7 +124,7 @@ async function reconcileCore(
142
124
  return active;
143
125
  }
144
126
 
145
- export async function applyInstall(state: ControlPlaneState, ctx?: AuditContext): Promise<void> {
127
+ export async function applyInstall(state: ControlPlaneState): Promise<void> {
146
128
  const lock = acquireLock(state.homeDir, "install");
147
129
  try {
148
130
  await reconcileCore(state, { activateServices: true });
@@ -150,38 +132,24 @@ export async function applyInstall(state: ControlPlaneState, ctx?: AuditContext)
150
132
  // Docker doesn't create them root-owned (which causes EACCES inside
151
133
  // non-root containers).
152
134
  ensureComposeVolumeTargets(state);
153
- if (ctx) appendAudit(state, ctx.actor, "install", {}, true, ctx.requestId ?? "", ctx.callerType ?? "unknown");
154
- } catch (err) {
155
- if (ctx) appendAudit(state, ctx.actor, "install", { error: String(err) }, false, ctx.requestId ?? "", ctx.callerType ?? "unknown");
156
- throw err;
157
135
  } finally {
158
136
  releaseLock(lock);
159
137
  }
160
138
  }
161
139
 
162
- export async function applyUpdate(state: ControlPlaneState, ctx?: AuditContext): Promise<{ restarted: string[] }> {
140
+ export async function applyUpdate(state: ControlPlaneState): Promise<{ restarted: string[] }> {
163
141
  const lock = acquireLock(state.homeDir, "update");
164
142
  try {
165
- const result = { restarted: await reconcileCore(state, {}) };
166
- if (ctx) appendAudit(state, ctx.actor, "update", { restarted: result.restarted }, true, ctx.requestId ?? "", ctx.callerType ?? "unknown");
167
- return result;
168
- } catch (err) {
169
- if (ctx) appendAudit(state, ctx.actor, "update", { error: String(err) }, false, ctx.requestId ?? "", ctx.callerType ?? "unknown");
170
- throw err;
143
+ return { restarted: await reconcileCore(state, {}) };
171
144
  } finally {
172
145
  releaseLock(lock);
173
146
  }
174
147
  }
175
148
 
176
- export async function applyUninstall(state: ControlPlaneState, ctx?: AuditContext): Promise<{ stopped: string[] }> {
149
+ export async function applyUninstall(state: ControlPlaneState): Promise<{ stopped: string[] }> {
177
150
  const lock = acquireLock(state.homeDir, "uninstall");
178
151
  try {
179
- const result = { stopped: await reconcileCore(state, { deactivateServices: true }) };
180
- if (ctx) appendAudit(state, ctx.actor, "uninstall", { stopped: result.stopped }, true, ctx.requestId ?? "", ctx.callerType ?? "unknown");
181
- return result;
182
- } catch (err) {
183
- if (ctx) appendAudit(state, ctx.actor, "uninstall", { error: String(err) }, false, ctx.requestId ?? "", ctx.callerType ?? "unknown");
184
- throw err;
152
+ return { stopped: await reconcileCore(state, { deactivateServices: true }) };
185
153
  } finally {
186
154
  releaseLock(lock);
187
155
  }