@openpalm/lib 0.10.2 → 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.
Files changed (59) 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 +105 -0
  5. package/src/control-plane/akm-vault.ts +307 -0
  6. package/src/control-plane/channels.ts +3 -3
  7. package/src/control-plane/cleanup-guardrails.test.ts +8 -9
  8. package/src/control-plane/compose-args.test.ts +25 -24
  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 +103 -65
  12. package/src/control-plane/core-assets.test.ts +104 -0
  13. package/src/control-plane/core-assets.ts +54 -57
  14. package/src/control-plane/docker.ts +55 -21
  15. package/src/control-plane/env.test.ts +25 -1
  16. package/src/control-plane/env.ts +80 -0
  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 +187 -289
  21. package/src/control-plane/install-lock.ts +157 -0
  22. package/src/control-plane/lifecycle.ts +34 -65
  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/paths.ts +82 -0
  27. package/src/control-plane/provider-config.ts +2 -2
  28. package/src/control-plane/provider-models.ts +154 -0
  29. package/src/control-plane/registry-components.test.ts +105 -27
  30. package/src/control-plane/registry.test.ts +49 -47
  31. package/src/control-plane/registry.ts +71 -50
  32. package/src/control-plane/rollback.ts +17 -16
  33. package/src/control-plane/scheduler.ts +75 -262
  34. package/src/control-plane/secret-backend.test.ts +98 -111
  35. package/src/control-plane/secret-backend.ts +221 -181
  36. package/src/control-plane/secret-mappings.ts +4 -8
  37. package/src/control-plane/secrets.ts +93 -51
  38. package/src/control-plane/setup-config.schema.json +5 -17
  39. package/src/control-plane/setup-status.ts +9 -29
  40. package/src/control-plane/setup-validation.ts +23 -23
  41. package/src/control-plane/setup.test.ts +138 -239
  42. package/src/control-plane/setup.ts +215 -130
  43. package/src/control-plane/skeleton-guardrail.test.ts +151 -0
  44. package/src/control-plane/spec-to-env.test.ts +59 -58
  45. package/src/control-plane/spec-to-env.ts +52 -142
  46. package/src/control-plane/spec-validator.ts +2 -99
  47. package/src/control-plane/stack-spec.test.ts +21 -77
  48. package/src/control-plane/stack-spec.ts +7 -83
  49. package/src/control-plane/types.ts +12 -28
  50. package/src/control-plane/ui-assets.ts +349 -0
  51. package/src/control-plane/validate.ts +44 -79
  52. package/src/index.ts +86 -48
  53. package/src/logger.test.ts +228 -0
  54. package/src/logger.ts +71 -1
  55. package/src/provider-constants.ts +22 -1
  56. package/src/control-plane/audit.ts +0 -40
  57. package/src/control-plane/env-schema-validation.test.ts +0 -118
  58. package/src/control-plane/memory-config.ts +0 -298
  59. package/src/control-plane/redact-schema.ts +0 -50
@@ -1,5 +1,5 @@
1
1
  import { randomBytes } from 'node:crypto';
2
- import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
2
+ import { existsSync, readdirSync, statSync } from 'node:fs';
3
3
  import { execFile as execFileCb, spawn } from 'node:child_process';
4
4
  import { promisify } from 'node:util';
5
5
  import { join, normalize, resolve } from 'node:path';
@@ -21,6 +21,7 @@ import {
21
21
  updateSecretsEnv,
22
22
  updateSystemSecretsEnv,
23
23
  } from './secrets.js';
24
+ import { readUserVaultSync } from './akm-vault.js';
24
25
 
25
26
  const execFile = promisify(execFileCb);
26
27
 
@@ -50,21 +51,23 @@ type ResolvedSecretTarget = {
50
51
  envKey?: string;
51
52
  };
52
53
 
53
- export type SecretBackendCapabilities = {
54
- generate: boolean;
55
- remove: boolean;
56
- rename: boolean;
57
- };
58
-
59
- export interface SecretBackend {
54
+ /**
55
+ * Public shape returned by `detectSecretBackend`. Both the plaintext and
56
+ * pass implementations expose the same surface — a small set of async
57
+ * methods plus a `provider` tag and a flat `capabilities` object.
58
+ *
59
+ * Kept as a `type` alias of an inline object literal so consumers don't
60
+ * have to import a separate interface or capabilities type.
61
+ */
62
+ export type SecretBackend = {
60
63
  readonly provider: 'plaintext' | 'pass';
61
- readonly capabilities: SecretBackendCapabilities;
64
+ readonly capabilities: { generate: boolean; remove: boolean; rename: boolean };
62
65
  list(prefix?: string): Promise<SecretEntryMetadata[]>;
63
66
  write(key: string, value: string): Promise<SecretEntryMetadata>;
64
67
  generate(key: string, length?: number): Promise<SecretEntryMetadata>;
65
68
  remove(key: string): Promise<void>;
66
69
  exists(key: string): Promise<boolean>;
67
- }
70
+ };
68
71
 
69
72
  function generateSecretValue(length = 32): string {
70
73
  // Hex encoding produces two output characters per byte. Clamp to at least
@@ -73,7 +76,7 @@ function generateSecretValue(length = 32): string {
73
76
  }
74
77
 
75
78
  function resolvePlaintextTarget(state: ControlPlaneState, key: string): ResolvedSecretTarget {
76
- const systemEnv = readStackEnv(state.vaultDir);
79
+ const systemEnv = readStackEnv(state.stackDir);
77
80
  const coreMapping = findCoreSecretByKey(key, systemEnv);
78
81
  if (coreMapping) {
79
82
  return { key, scope: coreMapping.scope, envKey: coreMapping.envKey };
@@ -85,101 +88,118 @@ function resolvePlaintextTarget(state: ControlPlaneState, key: string): Resolved
85
88
 
86
89
  function currentValueForTarget(state: ControlPlaneState, target: ResolvedSecretTarget): string {
87
90
  if (!target.envKey) return '';
88
- const env = target.scope === 'system'
89
- ? readStackEnv(state.vaultDir)
90
- : readStackEnv(state.vaultDir);
91
- return env[target.envKey] ?? '';
91
+ if (target.scope === 'system') {
92
+ return readStackEnv(state.stackDir)[target.envKey] ?? '';
93
+ }
94
+ // User scope: the akm `vault:user` store is the canonical user-managed env
95
+ // namespace post-#421. Fall back to stack.env for legacy/consolidated
96
+ // secrets so older layouts keep resolving.
97
+ const userEnv = readUserVaultSync(state);
98
+ if (target.envKey in userEnv) return userEnv[target.envKey];
99
+ return readStackEnv(state.stackDir)[target.envKey] ?? '';
92
100
  }
93
101
 
94
- export class PlaintextBackend implements SecretBackend {
95
- readonly provider = 'plaintext' as const;
96
- readonly capabilities = { generate: true, remove: true, rename: false } as const;
97
-
98
- constructor(private readonly state: ControlPlaneState) {}
99
-
100
- async list(prefix = 'openpalm/'): Promise<SecretEntryMetadata[]> {
101
- const userEnv = readStackEnv(this.state.vaultDir);
102
- const systemEnv = readStackEnv(this.state.vaultDir);
103
- const index = readPlaintextSecretIndex(this.state);
104
- const entries: SecretEntryMetadata[] = [];
105
-
106
- for (const mapping of getCoreSecretMappings(systemEnv)) {
107
- if (!mapping.secretKey.startsWith(prefix)) continue;
108
- const env = mapping.scope === 'system' ? systemEnv : userEnv;
109
- entries.push({
110
- key: mapping.secretKey,
111
- scope: mapping.scope,
112
- kind: 'core',
113
- provider: this.provider,
114
- present: Boolean(env[mapping.envKey]),
115
- envKey: mapping.envKey,
116
- });
117
- }
102
+ // ── Plaintext backend ─────────────────────────────────────────────────────
103
+
104
+ export async function plaintextList(state: ControlPlaneState, prefix = 'openpalm/'): Promise<SecretEntryMetadata[]> {
105
+ const systemEnv = readStackEnv(state.stackDir);
106
+ const userEnvFile = readUserVaultSync(state);
107
+ // Legacy/consolidated secrets may live in stack.env even for user scope.
108
+ // Layer the user vault on top so explicit user-managed values win.
109
+ const userEnv: Record<string, string> = { ...systemEnv, ...userEnvFile };
110
+ const index = readPlaintextSecretIndex(state);
111
+ const entries: SecretEntryMetadata[] = [];
112
+
113
+ for (const mapping of getCoreSecretMappings(systemEnv)) {
114
+ if (!mapping.secretKey.startsWith(prefix)) continue;
115
+ const env = mapping.scope === 'system' ? systemEnv : userEnv;
116
+ entries.push({
117
+ key: mapping.secretKey,
118
+ scope: mapping.scope,
119
+ kind: 'core',
120
+ provider: 'plaintext',
121
+ present: Boolean(env[mapping.envKey]),
122
+ envKey: mapping.envKey,
123
+ });
124
+ }
118
125
 
119
- for (const [key, entry] of Object.entries(index.entries)) {
120
- if (!key.startsWith(prefix)) continue;
121
- const env = entry.scope === 'system' ? systemEnv : userEnv;
122
- entries.push({
123
- key,
124
- scope: entry.scope,
125
- kind: entry.kind,
126
- provider: this.provider,
127
- present: Boolean(env[entry.envKey]),
128
- envKey: entry.envKey,
129
- updatedAt: entry.updatedAt,
130
- });
131
- }
126
+ for (const [key, entry] of Object.entries(index.entries)) {
127
+ if (!key.startsWith(prefix)) continue;
128
+ const env = entry.scope === 'system' ? systemEnv : userEnv;
129
+ entries.push({
130
+ key,
131
+ scope: entry.scope,
132
+ kind: entry.kind,
133
+ provider: 'plaintext',
134
+ present: Boolean(env[entry.envKey]),
135
+ envKey: entry.envKey,
136
+ updatedAt: entry.updatedAt,
137
+ });
138
+ }
132
139
 
133
- entries.sort((a, b) => a.key.localeCompare(b.key));
134
- return entries;
140
+ entries.sort((a, b) => a.key.localeCompare(b.key));
141
+ return entries;
142
+ }
143
+
144
+ export async function plaintextWrite(state: ControlPlaneState, key: string, value: string): Promise<SecretEntryMetadata> {
145
+ const target = resolvePlaintextTarget(state, key);
146
+ if (!target.envKey) {
147
+ throw new Error(`Unable to resolve env key for secret ${key}`);
135
148
  }
136
149
 
137
- async write(key: string, value: string): Promise<SecretEntryMetadata> {
138
- const target = resolvePlaintextTarget(this.state, key);
139
- if (!target.envKey) {
140
- throw new Error(`Unable to resolve env key for secret ${key}`);
141
- }
150
+ if (target.scope === 'system') {
151
+ updateSystemSecretsEnv(state, { [target.envKey]: value });
152
+ } else {
153
+ updateSecretsEnv(state, { [target.envKey]: value });
154
+ }
155
+
156
+ return {
157
+ key,
158
+ scope: target.scope,
159
+ kind: key.startsWith('openpalm/component/') ? 'component' : key.startsWith('openpalm/custom/') ? 'custom' : 'core',
160
+ provider: 'plaintext',
161
+ present: true,
162
+ envKey: target.envKey,
163
+ };
164
+ }
165
+
166
+ export async function plaintextGenerate(state: ControlPlaneState, key: string, length = 32): Promise<SecretEntryMetadata> {
167
+ return plaintextWrite(state, key, generateSecretValue(length));
168
+ }
142
169
 
170
+ export async function plaintextRemove(state: ControlPlaneState, key: string): Promise<void> {
171
+ const target = resolvePlaintextTarget(state, key);
172
+ if (target.envKey) {
143
173
  if (target.scope === 'system') {
144
- updateSystemSecretsEnv(this.state, { [target.envKey]: value });
174
+ updateSystemSecretsEnv(state, { [target.envKey]: '' });
145
175
  } else {
146
- updateSecretsEnv(this.state, { [target.envKey]: value });
176
+ updateSecretsEnv(state, { [target.envKey]: '' });
147
177
  }
148
-
149
- return {
150
- key,
151
- scope: target.scope,
152
- kind: key.startsWith('openpalm/component/') ? 'component' : key.startsWith('openpalm/custom/') ? 'custom' : 'core',
153
- provider: this.provider,
154
- present: true,
155
- envKey: target.envKey,
156
- };
157
178
  }
158
-
159
- async generate(key: string, length = 32): Promise<SecretEntryMetadata> {
160
- return this.write(key, generateSecretValue(length));
179
+ if (!findCoreSecretByKey(key, readStackEnv(state.stackDir))) {
180
+ removePlaintextSecretEntry(state, key);
161
181
  }
182
+ }
162
183
 
163
- async remove(key: string): Promise<void> {
164
- const target = resolvePlaintextTarget(this.state, key);
165
- if (target.envKey) {
166
- if (target.scope === 'system') {
167
- updateSystemSecretsEnv(this.state, { [target.envKey]: '' });
168
- } else {
169
- updateSecretsEnv(this.state, { [target.envKey]: '' });
170
- }
171
- }
172
- if (!findCoreSecretByKey(key, readStackEnv(this.state.vaultDir))) {
173
- removePlaintextSecretEntry(this.state, key);
174
- }
175
- }
184
+ export async function plaintextExists(state: ControlPlaneState, key: string): Promise<boolean> {
185
+ const target = resolvePlaintextTarget(state, key);
186
+ return currentValueForTarget(state, target).length > 0;
187
+ }
176
188
 
177
- async exists(key: string): Promise<boolean> {
178
- const target = resolvePlaintextTarget(this.state, key);
179
- return currentValueForTarget(this.state, target).length > 0;
180
- }
189
+ function makePlaintextBackend(state: ControlPlaneState): SecretBackend {
190
+ return {
191
+ provider: 'plaintext',
192
+ capabilities: { generate: true, remove: true, rename: false },
193
+ list: (prefix) => plaintextList(state, prefix),
194
+ write: (key, value) => plaintextWrite(state, key, value),
195
+ generate: (key, length) => plaintextGenerate(state, key, length),
196
+ remove: (key) => plaintextRemove(state, key),
197
+ exists: (key) => plaintextExists(state, key),
198
+ };
181
199
  }
182
200
 
201
+ // ── Pass backend ──────────────────────────────────────────────────────────
202
+
183
203
  export function validatePassEntryName(entry: string): string {
184
204
  const trimmed = entry.trim().replace(/^\/+|\/+$/g, '');
185
205
  if (!trimmed) {
@@ -211,112 +231,132 @@ function walkPassStore(dir: string, prefix = ''): string[] {
211
231
  return entries;
212
232
  }
213
233
 
214
- export class PassBackend implements SecretBackend {
215
- readonly provider = 'pass' as const;
216
- readonly capabilities = { generate: true, remove: true, rename: false } as const;
217
- private readonly passwordStoreDir: string;
218
- private readonly passPrefix: string;
219
-
220
- constructor(private readonly state: ControlPlaneState) {
221
- const config = readSecretProviderConfig(state);
222
- this.passwordStoreDir = config?.passwordStoreDir ?? `${state.dataDir}/secrets/pass-store`;
223
- this.passPrefix = config?.passPrefix ?? '';
224
- }
234
+ /**
235
+ * Resolved pass-backend configuration. Kept as a small in-file struct so the
236
+ * five `pass*` helpers below share one place where defaults
237
+ * (`${dataDir}/secrets/pass-store`, empty prefix) are applied — inlining the
238
+ * `?? defaults` logic into each helper would multiply the truth source.
239
+ * The two strings travel together everywhere (passEnv, prefixedEntry,
240
+ * passKeyPath) so they're worth bundling.
241
+ */
242
+ type PassContext = {
243
+ passwordStoreDir: string;
244
+ passPrefix: string;
245
+ };
225
246
 
226
- private env(): NodeJS.ProcessEnv {
227
- return {
228
- ...process.env,
229
- PASSWORD_STORE_DIR: this.passwordStoreDir,
230
- };
231
- }
247
+ function passContext(state: ControlPlaneState): PassContext {
248
+ const config = readSecretProviderConfig(state);
249
+ return {
250
+ passwordStoreDir: config?.passwordStoreDir ?? `${state.stateDir}/secrets/pass-store`,
251
+ passPrefix: config?.passPrefix ?? '',
252
+ };
253
+ }
232
254
 
233
- /** Prepend passPrefix to a canonical key for pass store operations. */
234
- private prefixedEntry(canonicalKey: string): string {
235
- const entry = validatePassEntryName(canonicalKey);
236
- return this.passPrefix ? `${this.passPrefix}/${entry}` : entry;
237
- }
255
+ function passEnv(ctx: PassContext): NodeJS.ProcessEnv {
256
+ return {
257
+ ...process.env,
258
+ PASSWORD_STORE_DIR: ctx.passwordStoreDir,
259
+ };
260
+ }
238
261
 
239
- private keyPath(key: string): string {
240
- const prefixed = this.prefixedEntry(key);
241
- const normalizedEntry = normalize(prefixed);
242
- const resolvedPath = resolve(this.passwordStoreDir, `${normalizedEntry}.gpg`);
243
- const resolvedStore = resolve(this.passwordStoreDir);
244
- if (!resolvedPath.startsWith(`${resolvedStore}/`)) {
245
- throw new Error('Secret key resolves outside the password store');
246
- }
247
- return resolvedPath;
248
- }
262
+ /** Prepend passPrefix to a canonical key for pass store operations. */
263
+ function prefixedEntry(ctx: PassContext, canonicalKey: string): string {
264
+ const entry = validatePassEntryName(canonicalKey);
265
+ return ctx.passPrefix ? `${ctx.passPrefix}/${entry}` : entry;
266
+ }
249
267
 
250
- async list(prefix = 'openpalm/'): Promise<SecretEntryMetadata[]> {
251
- // Scope walk to the passPrefix subdirectory
252
- const walkDir = this.passPrefix
253
- ? join(this.passwordStoreDir, this.passPrefix)
254
- : this.passwordStoreDir;
255
- return walkPassStore(walkDir)
256
- .filter((entry) => entry.startsWith(prefix))
257
- .sort((a, b) => a.localeCompare(b))
258
- .map((key) => ({
259
- key,
260
- scope: classifySecretScope(key),
261
- kind: classifySecretKey(key),
262
- provider: this.provider,
263
- present: true,
264
- }));
268
+ function passKeyPath(ctx: PassContext, key: string): string {
269
+ const prefixed = prefixedEntry(ctx, key);
270
+ const normalizedEntry = normalize(prefixed);
271
+ const resolvedPath = resolve(ctx.passwordStoreDir, `${normalizedEntry}.gpg`);
272
+ const resolvedStore = resolve(ctx.passwordStoreDir);
273
+ if (!resolvedPath.startsWith(`${resolvedStore}/`)) {
274
+ throw new Error('Secret key resolves outside the password store');
265
275
  }
276
+ return resolvedPath;
277
+ }
266
278
 
267
- async write(key: string, value: string): Promise<SecretEntryMetadata> {
268
- const canonicalKey = validatePassEntryName(key);
269
- const storeEntry = this.prefixedEntry(canonicalKey);
270
- await execWithInput('pass', ['insert', '-m', '-f', storeEntry], `${value}\n`, this.env());
271
- return {
272
- key: canonicalKey,
273
- scope: classifySecretScope(canonicalKey),
274
- kind: classifySecretKey(canonicalKey),
275
- provider: this.provider,
279
+ export async function passList(state: ControlPlaneState, prefix = 'openpalm/'): Promise<SecretEntryMetadata[]> {
280
+ const ctx = passContext(state);
281
+ // Scope walk to the passPrefix subdirectory
282
+ const walkDir = ctx.passPrefix
283
+ ? join(ctx.passwordStoreDir, ctx.passPrefix)
284
+ : ctx.passwordStoreDir;
285
+ return walkPassStore(walkDir)
286
+ .filter((entry) => entry.startsWith(prefix))
287
+ .sort((a, b) => a.localeCompare(b))
288
+ .map((key) => ({
289
+ key,
290
+ scope: classifySecretScope(key),
291
+ kind: classifySecretKey(key),
292
+ provider: 'pass',
276
293
  present: true,
277
- };
278
- }
294
+ }));
295
+ }
279
296
 
280
- async generate(key: string, length = 32): Promise<SecretEntryMetadata> {
281
- const canonicalKey = validatePassEntryName(key);
282
- const storeEntry = this.prefixedEntry(canonicalKey);
283
- await execFile('pass', ['generate', '-n', '-f', storeEntry, String(length)], {
284
- env: this.env(),
285
- });
286
- return {
287
- key: canonicalKey,
288
- scope: classifySecretScope(canonicalKey),
289
- kind: classifySecretKey(canonicalKey),
290
- provider: this.provider,
291
- present: true,
292
- };
293
- }
297
+ export async function passWrite(state: ControlPlaneState, key: string, value: string): Promise<SecretEntryMetadata> {
298
+ const ctx = passContext(state);
299
+ const canonicalKey = validatePassEntryName(key);
300
+ const storeEntry = prefixedEntry(ctx, canonicalKey);
301
+ await execWithInput('pass', ['insert', '-m', '-f', storeEntry], `${value}\n`, passEnv(ctx));
302
+ return {
303
+ key: canonicalKey,
304
+ scope: classifySecretScope(canonicalKey),
305
+ kind: classifySecretKey(canonicalKey),
306
+ provider: 'pass',
307
+ present: true,
308
+ };
309
+ }
294
310
 
295
- async remove(key: string): Promise<void> {
296
- const storeEntry = this.prefixedEntry(key);
297
- await execFile('pass', ['rm', '-f', storeEntry], {
298
- env: this.env(),
299
- });
300
- }
311
+ export async function passGenerate(state: ControlPlaneState, key: string, length = 32): Promise<SecretEntryMetadata> {
312
+ const ctx = passContext(state);
313
+ const canonicalKey = validatePassEntryName(key);
314
+ const storeEntry = prefixedEntry(ctx, canonicalKey);
315
+ await execFile('pass', ['generate', '-n', '-f', storeEntry, String(length)], {
316
+ env: passEnv(ctx),
317
+ });
318
+ return {
319
+ key: canonicalKey,
320
+ scope: classifySecretScope(canonicalKey),
321
+ kind: classifySecretKey(canonicalKey),
322
+ provider: 'pass',
323
+ present: true,
324
+ };
325
+ }
301
326
 
302
- async exists(key: string): Promise<boolean> {
303
- return existsSync(this.keyPath(key));
304
- }
327
+ export async function passRemove(state: ControlPlaneState, key: string): Promise<void> {
328
+ const ctx = passContext(state);
329
+ const storeEntry = prefixedEntry(ctx, key);
330
+ await execFile('pass', ['rm', '-f', storeEntry], {
331
+ env: passEnv(ctx),
332
+ });
333
+ }
334
+
335
+ export async function passExists(state: ControlPlaneState, key: string): Promise<boolean> {
336
+ const ctx = passContext(state);
337
+ return existsSync(passKeyPath(ctx, key));
338
+ }
339
+
340
+ function makePassBackend(state: ControlPlaneState): SecretBackend {
341
+ return {
342
+ provider: 'pass',
343
+ capabilities: { generate: true, remove: true, rename: false },
344
+ list: (prefix) => passList(state, prefix),
345
+ write: (key, value) => passWrite(state, key, value),
346
+ generate: (key, length) => passGenerate(state, key, length),
347
+ remove: (key) => passRemove(state, key),
348
+ exists: (key) => passExists(state, key),
349
+ };
305
350
  }
306
351
 
307
352
  export function detectSecretBackend(state: ControlPlaneState): SecretBackend {
308
353
  const providerConfig = readSecretProviderConfig(state);
309
354
  if (providerConfig?.provider === 'pass') {
310
- return new PassBackend(state);
311
- }
312
-
313
- for (const schemaPath of [`${state.vaultDir}/user/user.env.schema`, `${state.vaultDir}/stack/stack.env.schema`]) {
314
- if (!existsSync(schemaPath)) continue;
315
- const content = readFileSync(schemaPath, 'utf-8');
316
- if (content.includes('@varlock/pass-plugin')) {
317
- return new PassBackend(state);
318
- }
355
+ return makePassBackend(state);
319
356
  }
320
357
 
321
- return new PlaintextBackend(state);
358
+ // Historical fallback: pre-#391 we sniffed `.env.schema` files for a
359
+ // `@varlock/pass-plugin` marker. Schemas are gone; operators who want
360
+ // `pass` set `secret-provider.json` to `{ "provider": "pass" }`.
361
+ return makePlaintextBackend(state);
322
362
  }
@@ -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
  }