@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
@@ -11,11 +11,12 @@ import type { ControlPlaneState } from "./types.js";
11
11
  import { resolveRollbackDir } from "./home.js";
12
12
 
13
13
  /** Files that are tracked for rollback (relative to homeDir).
14
- * Only vault/stack/ files are included — vault/user/ and config/ are
15
- * user-owned and never overwritten by lifecycle operations. */
14
+ * Only config/ system files are included — user-editable config files
15
+ * are never overwritten by lifecycle operations. */
16
16
  const SNAPSHOT_FILES = [
17
- "vault/stack/stack.env",
18
- "vault/stack/guardian.env",
17
+ "config/stack/stack.env",
18
+ "config/stack/guardian.env",
19
+ "config/auth.json",
19
20
  ];
20
21
 
21
22
  /**
@@ -43,12 +44,12 @@ export function snapshotCurrentState(state: ControlPlaneState): void {
43
44
  safeCopy(src, dest);
44
45
  }
45
46
 
46
- // Snapshot stack/core.compose.yml
47
- const coreCompose = join(state.homeDir, "stack/core.compose.yml");
48
- safeCopy(coreCompose, join(rollbackDir, "stack/core.compose.yml"));
47
+ // Snapshot config/stack/core.compose.yml
48
+ const coreCompose = join(state.homeDir, "config/stack/core.compose.yml");
49
+ safeCopy(coreCompose, join(rollbackDir, "config/stack/core.compose.yml"));
49
50
 
50
- // Snapshot stack/addons/*/compose.yml
51
- const addonsDir = join(state.homeDir, "stack/addons");
51
+ // Snapshot config/stack/addons/*/compose.yml
52
+ const addonsDir = join(state.homeDir, "config/stack/addons");
52
53
  if (existsSync(addonsDir)) {
53
54
  for (const entry of readdirSync(addonsDir, { withFileTypes: true })) {
54
55
  if (entry.isDirectory()) {
@@ -56,7 +57,7 @@ export function snapshotCurrentState(state: ControlPlaneState): void {
56
57
  if (existsSync(addonCompose)) {
57
58
  safeCopy(
58
59
  addonCompose,
59
- join(rollbackDir, "stack/addons", entry.name, "compose.yml"),
60
+ join(rollbackDir, "config/stack/addons", entry.name, "compose.yml"),
60
61
  );
61
62
  }
62
63
  }
@@ -87,14 +88,14 @@ export function restoreSnapshot(state: ControlPlaneState): void {
87
88
  safeCopy(src, dest);
88
89
  }
89
90
 
90
- // Restore stack/core.compose.yml
91
- const srcCoreCompose = join(rollbackDir, "stack/core.compose.yml");
91
+ // Restore config/stack/core.compose.yml
92
+ const srcCoreCompose = join(rollbackDir, "config/stack/core.compose.yml");
92
93
  if (existsSync(srcCoreCompose)) {
93
- safeCopy(srcCoreCompose, join(state.homeDir, "stack/core.compose.yml"));
94
+ safeCopy(srcCoreCompose, join(state.homeDir, "config/stack/core.compose.yml"));
94
95
  }
95
96
 
96
- // Restore stack/addons/*/compose.yml
97
- const srcAddons = join(rollbackDir, "stack/addons");
97
+ // Restore config/stack/addons/*/compose.yml
98
+ const srcAddons = join(rollbackDir, "config/stack/addons");
98
99
  if (existsSync(srcAddons)) {
99
100
  for (const entry of readdirSync(srcAddons, { withFileTypes: true })) {
100
101
  if (entry.isDirectory()) {
@@ -102,7 +103,7 @@ export function restoreSnapshot(state: ControlPlaneState): void {
102
103
  if (existsSync(srcAddonCompose)) {
103
104
  safeCopy(
104
105
  srcAddonCompose,
105
- join(state.homeDir, "stack/addons", entry.name, "compose.yml"),
106
+ join(state.homeDir, "config/stack/addons", entry.name, "compose.yml"),
106
107
  );
107
108
  }
108
109
  }
@@ -1,24 +1,27 @@
1
- /** Automation scheduler — types, parsing, and action execution. */
2
- import { parse as parseYaml } from "yaml";
1
+ /**
2
+ * Automation scheduler types and akm CLI integration.
3
+ *
4
+ * Automations are AKM markdown task files at ${stashDir}/tasks/*.md.
5
+ * Scheduling is handled by the OS cron daemon (via `akm tasks sync`).
6
+ * Execution is handled by `akm tasks run <id>`.
7
+ */
3
8
  import { execFile } from "node:child_process";
4
- import { existsSync, readdirSync, readFileSync } from "node:fs";
9
+ import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
5
10
  import { join } from "node:path";
6
11
  import { createLogger } from "../logger.js";
12
+ import { loadMarkdownTasks, taskToAutomationConfig } from "./markdown-task.js";
7
13
 
8
14
  const logger = createLogger("scheduler");
9
15
 
16
+ // ── Types ─────────────────────────────────────────────────────────────────
10
17
 
11
- export type ActionType = "api" | "http" | "shell" | "assistant";
18
+ export type ActionType = "api" | "http" | "shell" | "assistant" | "workflow";
12
19
 
13
20
  export type AutomationAction = {
14
21
  type: ActionType;
15
22
  method?: string;
16
23
  path?: string;
17
24
  url?: string;
18
- body?: unknown;
19
- headers?: Record<string, string>;
20
- command?: string[];
21
- timeout?: number;
22
25
  content?: string;
23
26
  agent?: string;
24
27
  };
@@ -34,13 +37,7 @@ export type AutomationConfig = {
34
37
  fileName: string;
35
38
  };
36
39
 
37
- export type ExecutionLogEntry = {
38
- at: string;
39
- ok: boolean;
40
- durationMs: number;
41
- error?: string;
42
- };
43
-
40
+ // ── Schedule presets (UI display labels only) ─────────────────────────────
44
41
 
45
42
  export const SCHEDULE_PRESETS: Record<string, string> = {
46
43
  "every-minute": "* * * * *",
@@ -54,213 +51,55 @@ export const SCHEDULE_PRESETS: Record<string, string> = {
54
51
  "weekly-sunday-4am": "0 4 * * 0"
55
52
  };
56
53
 
57
- /** Resolve a preset name to cron expression, or pass through raw cron. */
58
- export function resolveSchedule(schedule: string): string {
59
- return SCHEDULE_PRESETS[schedule] ?? schedule;
60
- }
61
-
62
- export function parseAutomationYaml(
63
- content: string,
64
- fileName: string
65
- ): AutomationConfig | null {
66
- let doc: Record<string, unknown>;
67
- try {
68
- doc = parseYaml(content) as Record<string, unknown>;
69
- } catch (err) {
70
- logger.warn("failed to parse automation YAML", { fileName, error: String(err) });
71
- return null;
72
- }
73
-
74
- if (!doc || typeof doc !== "object") {
75
- logger.warn("automation YAML is not an object", { fileName });
76
- return null;
77
- }
54
+ // ── Load automations from AKM task files ──────────────────────────────────
78
55
 
79
- const rawSchedule = doc.schedule;
80
- if (typeof rawSchedule !== "string" || !rawSchedule.trim()) {
81
- logger.warn("automation missing or empty 'schedule'", { fileName });
82
- return null;
83
- }
84
-
85
- const action = doc.action;
86
- if (!action || typeof action !== "object") {
87
- logger.warn("automation missing or invalid 'action'", { fileName });
88
- return null;
89
- }
90
-
91
- const actionObj = action as Record<string, unknown>;
92
- const actionType = actionObj.type as string | undefined;
93
- if (!actionType || !["api", "http", "shell", "assistant"].includes(actionType)) {
94
- logger.warn("automation action has invalid 'type'", {
95
- fileName,
96
- type: String(actionType)
97
- });
98
- return null;
99
- }
100
-
101
- if (actionType === "api" && typeof actionObj.path !== "string") {
102
- logger.warn("api action missing 'path'", { fileName });
103
- return null;
104
- }
105
- if (actionType === "http" && typeof actionObj.url !== "string") {
106
- logger.warn("http action missing 'url'", { fileName });
107
- return null;
108
- }
109
- if (actionType === "shell") {
110
- if (!Array.isArray(actionObj.command) || actionObj.command.length === 0) {
111
- logger.warn("shell action missing or empty 'command' array", { fileName });
112
- return null;
113
- }
114
- }
115
- if (actionType === "assistant") {
116
- if (typeof actionObj.content !== "string" || !actionObj.content.trim()) {
117
- logger.warn("assistant action missing or empty 'content'", { fileName });
118
- return null;
119
- }
120
- }
121
-
122
- const schedule = resolveSchedule(rawSchedule.trim());
123
-
124
- return {
125
- name: typeof doc.name === "string" ? doc.name : fileName.replace(/\.yml$/, ""),
126
- description: typeof doc.description === "string" ? doc.description : "",
127
- schedule,
128
- timezone: typeof doc.timezone === "string" ? doc.timezone : "UTC",
129
- enabled: doc.enabled !== false,
130
- action: {
131
- type: actionType as ActionType,
132
- method: typeof actionObj.method === "string" ? actionObj.method : "GET",
133
- path: typeof actionObj.path === "string" ? actionObj.path : undefined,
134
- url: typeof actionObj.url === "string" ? actionObj.url : undefined,
135
- body: actionObj.body,
136
- headers: (actionObj.headers && typeof actionObj.headers === "object" && !Array.isArray(actionObj.headers) &&
137
- Object.values(actionObj.headers as Record<string, unknown>).every((v) => typeof v === "string"))
138
- ? (actionObj.headers as Record<string, string>) : undefined,
139
- command: Array.isArray(actionObj.command)
140
- ? actionObj.command.map(String)
141
- : undefined,
142
- content: typeof actionObj.content === "string" ? actionObj.content : undefined,
143
- agent: typeof actionObj.agent === "string" ? actionObj.agent : undefined,
144
- timeout:
145
- typeof actionObj.timeout === "number"
146
- ? actionObj.timeout
147
- : actionType === "assistant" ? 120_000 : 30_000
148
- },
149
- on_failure:
150
- doc.on_failure === "audit" ? "audit" : "log",
151
- fileName
152
- };
153
- }
154
-
155
- export function loadAutomations(configDir: string): AutomationConfig[] {
156
- const dir = join(configDir, "automations");
157
- if (!existsSync(dir)) return [];
158
-
159
- const files = readdirSync(dir, { withFileTypes: true });
160
- const configs: AutomationConfig[] = [];
161
-
162
- for (const entry of files) {
163
- if (!entry.isFile()) continue;
164
- if (!entry.name.endsWith(".yml")) {
165
- logger.warn("non-.yml file in automations dir (ignored)", {
166
- file: entry.name,
167
- hint: "automation files must use .yml extension"
168
- });
169
- continue;
170
- }
171
-
172
- const content = readFileSync(join(dir, entry.name), "utf-8");
173
- const config = parseAutomationYaml(content, entry.name);
174
- if (config) configs.push(config);
175
- }
176
-
177
- return configs;
56
+ export function loadAutomations(stashDir: string): AutomationConfig[] {
57
+ return loadMarkdownTasks(stashDir).map(taskToAutomationConfig);
178
58
  }
179
59
 
60
+ // ── Execute an automation via akm tasks run ───────────────────────────────
180
61
 
181
- export const SAFE_PATH_RE = /^\/admin\/[a-zA-Z0-9/._-]+$/;
182
-
183
- export async function executeApiAction(
184
- action: AutomationAction,
185
- adminToken: string
186
- ): Promise<void> {
187
- if (!action.path || !SAFE_PATH_RE.test(action.path) || action.path.includes('..')) {
188
- logger.warn(`Scheduler: rejecting unsafe action path: ${action.path}`);
189
- return;
190
- }
191
- const adminUrl = process.env.OP_ADMIN_API_URL || "http://admin:8100";
192
- const url = `${adminUrl}${action.path}`;
193
- const { "x-admin-token": _dropped, "authorization": _dropped2, ...safeHeaders } = action.headers ?? {};
194
- const headers: Record<string, string> = {
195
- ...safeHeaders,
196
- "x-admin-token": adminToken,
197
- "x-requested-by": "automation",
198
- };
199
- if (action.body) {
200
- headers["content-type"] = "application/json";
201
- }
202
- const controller = new AbortController();
203
- const timer = setTimeout(() => controller.abort(), action.timeout ?? 30_000);
204
- try {
205
- const resp = await fetch(url, {
206
- method: action.method ?? "GET",
207
- headers,
208
- body: action.body ? JSON.stringify(action.body) : undefined,
209
- signal: controller.signal
210
- });
211
- if (!resp.ok) {
212
- throw new Error(`HTTP ${resp.status} ${resp.statusText}`);
213
- }
214
- } finally {
215
- clearTimeout(timer);
216
- }
62
+ export interface AutomationRunResult {
63
+ ok: boolean;
64
+ status: string;
65
+ error?: string;
217
66
  }
218
67
 
219
- export async function executeHttpAction(action: AutomationAction): Promise<void> {
220
- if (!action.url) throw new Error("http action requires a url");
221
- const headers: Record<string, string> = { ...action.headers };
222
- if (action.body) {
223
- headers["content-type"] = headers["content-type"] ?? "application/json";
224
- }
225
- const controller = new AbortController();
226
- const timer = setTimeout(() => controller.abort(), action.timeout ?? 30_000);
227
- try {
228
- const resp = await fetch(action.url, {
229
- method: action.method ?? "GET",
230
- headers,
231
- body: action.body ? JSON.stringify(action.body) : undefined,
232
- signal: controller.signal
233
- });
234
- if (!resp.ok) {
235
- throw new Error(`HTTP ${resp.status} ${resp.statusText}`);
236
- }
237
- } finally {
238
- clearTimeout(timer);
239
- }
68
+ export async function executeAutomation(
69
+ id: string,
70
+ akmEnv: NodeJS.ProcessEnv,
71
+ ): Promise<AutomationRunResult> {
72
+ // Strip .md suffix if caller passes the full filename
73
+ const taskId = id.replace(/\.md$/, "");
74
+ return new Promise((resolve) => {
75
+ execFile(
76
+ "akm",
77
+ ["tasks", "run", taskId],
78
+ { env: { ...process.env, ...akmEnv } },
79
+ (error, _stdout, stderr) => {
80
+ if (error) {
81
+ const msg = stderr?.trim() || error.message;
82
+ logger.warn("akm tasks run failed", { id: taskId, error: msg });
83
+ resolve({ ok: false, status: "failed", error: msg });
84
+ } else {
85
+ resolve({ ok: true, status: "completed" });
86
+ }
87
+ }
88
+ );
89
+ });
240
90
  }
241
91
 
242
- const SHELL_SAFE_ENV_KEYS = [
243
- "PATH", "HOME", "LANG", "LC_ALL", "TZ", "NODE_ENV",
244
- "OP_HOME",
245
- ];
246
-
247
- export function executeShellAction(action: AutomationAction): Promise<void> {
248
- if (!action.command?.length) throw new Error("shell action requires a non-empty command array");
249
- const cmd = action.command;
250
-
251
- const safeEnv: Record<string, string> = {};
252
- for (const key of SHELL_SAFE_ENV_KEYS) {
253
- if (process.env[key]) safeEnv[key] = process.env[key]!;
254
- }
92
+ // ── Sync crontab with stash/tasks/*.md ───────────────────────────────────
255
93
 
94
+ export async function syncAutomations(akmEnv: NodeJS.ProcessEnv): Promise<void> {
256
95
  return new Promise((resolve, reject) => {
257
96
  execFile(
258
- cmd[0],
259
- cmd.slice(1),
260
- { env: safeEnv, timeout: action.timeout ?? 30_000 },
97
+ "akm",
98
+ ["tasks", "sync"],
99
+ { env: { ...process.env, ...akmEnv } },
261
100
  (error, _stdout, stderr) => {
262
101
  if (error) {
263
- reject(new Error(`shell command failed: ${stderr || error.message}`));
102
+ reject(new Error(stderr?.trim() || error.message));
264
103
  } else {
265
104
  resolve();
266
105
  }
@@ -269,58 +108,32 @@ export function executeShellAction(action: AutomationAction): Promise<void> {
269
108
  });
270
109
  }
271
110
 
272
- export async function executeAssistantAction(action: AutomationAction): Promise<void> {
273
- if (!action.content) {
274
- throw new Error("assistant action requires a non-empty 'content' field");
275
- }
276
-
277
- const baseUrl = process.env.OPENCODE_API_URL ?? "http://assistant:4096";
278
- const password = process.env.OPENCODE_SERVER_PASSWORD;
279
- const headers: Record<string, string> = { "content-type": "application/json" };
280
- if (password) {
281
- headers["authorization"] = `Basic ${Buffer.from(`opencode:${password}`, "utf8").toString("base64")}`;
282
- }
283
-
284
- const sessionRes = await fetch(`${baseUrl}/session`, {
285
- method: "POST",
286
- headers,
287
- signal: AbortSignal.timeout(10_000),
288
- body: JSON.stringify({ title: `automation/${action.agent ?? "default"}` }),
289
- });
290
- if (!sessionRes.ok) {
291
- const body = await sessionRes.text().catch(() => "");
292
- throw new Error(`OpenCode POST /session ${sessionRes.status}: ${body}`);
293
- }
294
- const { id: sessionId } = (await sessionRes.json()) as { id: string };
295
- if (typeof sessionId !== "string" || !/^[a-zA-Z0-9_-]+$/.test(sessionId)) {
296
- throw new Error("Invalid session ID from assistant");
297
- }
298
-
299
- const msgRes = await fetch(`${baseUrl}/session/${sessionId}/message`, {
300
- method: "POST",
301
- headers,
302
- signal: AbortSignal.timeout(action.timeout ?? 120_000),
303
- body: JSON.stringify({ parts: [{ type: "text", text: action.content }] }),
304
- });
305
- if (!msgRes.ok) {
306
- const body = await msgRes.text().catch(() => "");
307
- throw new Error(`OpenCode POST /session/${sessionId}/message ${msgRes.status}: ${body}`);
308
- }
309
- logger.info("assistant action completed");
310
- }
311
-
312
- export async function executeAction(
313
- action: AutomationAction,
314
- adminToken: string
315
- ): Promise<void> {
316
- switch (action.type) {
317
- case "api":
318
- return executeApiAction(action, adminToken);
319
- case "http":
320
- return executeHttpAction(action);
321
- case "shell":
322
- return executeShellAction(action);
323
- case "assistant":
324
- return executeAssistantAction(action);
111
+ // ── Read akm task execution logs ──────────────────────────────────────────
112
+
113
+ export function readAutomationLogs(
114
+ id: string,
115
+ cacheDir: string,
116
+ limit: number = 50,
117
+ ): string[] {
118
+ const taskId = id.replace(/\.md$/, "");
119
+ const logDir = join(cacheDir, "akm", "tasks", "logs", taskId);
120
+ if (!existsSync(logDir)) return [];
121
+
122
+ const logFiles = readdirSync(logDir, { withFileTypes: true })
123
+ .filter((e) => e.isFile() && e.name.endsWith(".log"))
124
+ .map((e) => ({ name: e.name, path: join(logDir, e.name) }))
125
+ .sort((a, b) => b.name.localeCompare(a.name)); // newest first (ISO timestamp names)
126
+
127
+ const lines: string[] = [];
128
+ for (const { path } of logFiles) {
129
+ if (lines.length >= limit) break;
130
+ try {
131
+ const content = readFileSync(path, "utf-8");
132
+ const fileLines = content.split("\n").filter(Boolean).reverse(); // newest within file last
133
+ lines.push(...fileLines.slice(0, limit - lines.length));
134
+ } catch {
135
+ // skip unreadable log files
136
+ }
325
137
  }
138
+ return lines.slice(0, limit);
326
139
  }
@@ -29,10 +29,8 @@ type CoreSecretMapping = {
29
29
  };
30
30
 
31
31
  const STATIC_CORE_MAPPINGS: CoreSecretMapping[] = [
32
- // Core authentication tokens
33
- { secretKey: 'openpalm/admin-token', envKey: 'OP_ADMIN_TOKEN', scope: 'system' },
34
- { secretKey: 'openpalm/assistant-token', envKey: 'OP_ASSISTANT_TOKEN', scope: 'system' },
35
- { secretKey: 'openpalm/memory/auth-token', envKey: 'OP_MEMORY_TOKEN', scope: 'system' },
32
+ // Core authentication
33
+ { secretKey: 'openpalm/ui-login-password', envKey: 'OP_UI_LOGIN_PASSWORD', scope: 'system' },
36
34
  { secretKey: 'openpalm/opencode/server-password', envKey: 'OP_OPENCODE_PASSWORD', scope: 'system' },
37
35
  // LLM provider API keys
38
36
  { secretKey: 'openpalm/openai/api-key', envKey: 'OPENAI_API_KEY', scope: 'user' },
@@ -47,8 +45,6 @@ const STATIC_CORE_MAPPINGS: CoreSecretMapping[] = [
47
45
  { secretKey: 'openpalm/mcp/api-key', envKey: 'MCP_API_KEY', scope: 'user' },
48
46
  { secretKey: 'openpalm/embedding/api-key', envKey: 'EMBEDDING_API_KEY', scope: 'user' },
49
47
  { secretKey: 'openpalm/lmstudio/api-key', envKey: 'LMSTUDIO_API_KEY', scope: 'user' },
50
- { secretKey: 'openpalm/openviking/api-key', envKey: 'OPENVIKING_API_KEY', scope: 'user' },
51
- { secretKey: 'openpalm/openviking/vlm-api-key', envKey: 'VLM_API_KEY', scope: 'user' },
52
48
  // Channel-specific credentials
53
49
  { secretKey: 'openpalm/discord/bot-token', envKey: 'DISCORD_BOT_TOKEN', scope: 'user' },
54
50
  { secretKey: 'openpalm/slack/bot-token', envKey: 'SLACK_BOT_TOKEN', scope: 'user' },
@@ -66,7 +62,7 @@ type SecretIndexFile = {
66
62
  };
67
63
 
68
64
  function secretIndexPath(state: ControlPlaneState): string {
69
- return `${state.dataDir}/secrets/plaintext-index.json`;
65
+ return `${state.stateDir}/secrets/plaintext-index.json`;
70
66
  }
71
67
 
72
68
  function normalizeIndexedKey(key: string): string {
@@ -148,7 +144,7 @@ export function readPlaintextSecretIndex(state: ControlPlaneState): SecretIndexF
148
144
  }
149
145
 
150
146
  export function writePlaintextSecretIndex(state: ControlPlaneState, index: SecretIndexFile): void {
151
- const dir = `${state.dataDir}/secrets`;
147
+ const dir = `${state.stateDir}/secrets`;
152
148
  mkdirSync(dir, { recursive: true });
153
149
  writeFileSync(secretIndexPath(state), JSON.stringify(index, null, 2) + '\n');
154
150
  }