@openpalm/lib 0.11.0-beta.10 → 0.11.0-beta.13
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/README.md +1 -1
- package/package.json +1 -1
- package/src/control-plane/akm-user-env.test.ts +113 -0
- package/src/control-plane/akm-user-env.ts +144 -0
- package/src/control-plane/backup.ts +14 -5
- package/src/control-plane/channels.ts +48 -29
- package/src/control-plane/cleanup-guardrails.test.ts +1 -1
- package/src/control-plane/compose-args.test.ts +90 -31
- package/src/control-plane/compose-args.ts +119 -9
- package/src/control-plane/config-persistence.ts +87 -133
- package/src/control-plane/core-assets.test.ts +9 -9
- package/src/control-plane/core-assets.ts +24 -8
- package/src/control-plane/docker.ts +15 -14
- package/src/control-plane/env.test.ts +10 -10
- package/src/control-plane/env.ts +1 -1
- package/src/control-plane/extends-support.test.ts +8 -8
- package/src/control-plane/home.ts +34 -46
- package/src/control-plane/host-opencode.test.ts +82 -10
- package/src/control-plane/host-opencode.ts +42 -13
- package/src/control-plane/install-edge-cases.test.ts +94 -102
- package/src/control-plane/install-lock.ts +7 -7
- package/src/control-plane/lifecycle.ts +36 -34
- package/src/control-plane/markdown-task.ts +30 -50
- package/src/control-plane/paths.ts +62 -42
- package/src/control-plane/profile-ids.ts +21 -0
- package/src/control-plane/provider-models.ts +3 -3
- package/src/control-plane/registry.test.ts +97 -88
- package/src/control-plane/registry.ts +142 -110
- package/src/control-plane/rollback.ts +8 -38
- package/src/control-plane/scheduler.ts +7 -7
- package/src/control-plane/secret-audit.test.ts +159 -0
- package/src/control-plane/secret-audit.ts +255 -0
- package/src/control-plane/secret-mappings.ts +2 -2
- package/src/control-plane/secrets-files.test.ts +60 -0
- package/src/control-plane/secrets-files.ts +66 -0
- package/src/control-plane/secrets.ts +113 -86
- package/src/control-plane/setup-config.schema.json +1 -1
- package/src/control-plane/setup-status.ts +6 -11
- package/src/control-plane/setup.test.ts +60 -40
- package/src/control-plane/setup.ts +36 -31
- package/src/control-plane/skeleton-guardrail.test.ts +64 -55
- package/src/control-plane/spec-to-env.test.ts +22 -17
- package/src/control-plane/spec-to-env.ts +7 -2
- package/src/control-plane/stack-spec.test.ts +10 -0
- package/src/control-plane/stack-spec.ts +28 -1
- package/src/control-plane/types.ts +2 -4
- package/src/control-plane/ui-assets.ts +60 -58
- package/src/control-plane/validate.ts +13 -15
- package/src/index.ts +47 -15
- package/src/control-plane/akm-vault.test.ts +0 -105
- package/src/control-plane/akm-vault.ts +0 -311
- package/src/control-plane/migrate-0110.test.ts +0 -177
- package/src/control-plane/migrate-0110.ts +0 -99
- package/src/control-plane/registry-components.test.ts +0 -391
|
@@ -12,13 +12,13 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import {
|
|
14
14
|
existsSync, mkdirSync, readdirSync, copyFileSync,
|
|
15
|
-
|
|
15
|
+
writeFileSync, rmSync, realpathSync, renameSync,
|
|
16
16
|
} from 'node:fs';
|
|
17
17
|
import { join, dirname, relative } from 'node:path';
|
|
18
18
|
import { fileURLToPath } from 'node:url';
|
|
19
19
|
import { createHash } from 'node:crypto';
|
|
20
20
|
import { x as tarExtract } from 'tar';
|
|
21
|
-
import {
|
|
21
|
+
import { resolveBackupsDir, resolveDataDir } from './home.js';
|
|
22
22
|
import { createLogger } from '../logger.js';
|
|
23
23
|
|
|
24
24
|
const logger = createLogger('lib:ui-assets');
|
|
@@ -87,13 +87,15 @@ export function resolveLocalOpenpalmDir(): string | null {
|
|
|
87
87
|
() => process.env.OPENPALM_REPO_ROOT
|
|
88
88
|
? join(process.env.OPENPALM_REPO_ROOT, '.openpalm')
|
|
89
89
|
: null,
|
|
90
|
-
// 2.
|
|
90
|
+
// 2. Electron extraResources — openpalm-skeleton/ placed alongside the asar
|
|
91
|
+
() => process.env.OPENPALM_SKELETON_DIR ?? null,
|
|
92
|
+
// 3. Relative to this source file (dev / bun run)
|
|
91
93
|
() => {
|
|
92
94
|
const meta = fileURLToPath(import.meta.url);
|
|
93
95
|
if (meta.startsWith('/$bunfs/')) return null;
|
|
94
96
|
return join(dirname(meta), '..', '..', '..', '..', '.openpalm');
|
|
95
97
|
},
|
|
96
|
-
//
|
|
98
|
+
// 4. Relative to the compiled binary on disk
|
|
97
99
|
() => join(dirname(realpathSync(process.execPath)), '..', '..', '..', '.openpalm'),
|
|
98
100
|
);
|
|
99
101
|
}
|
|
@@ -109,14 +111,12 @@ export async function seedOpenPalmDir(
|
|
|
109
111
|
repoRef: string,
|
|
110
112
|
homeDir: string,
|
|
111
113
|
_configDir: string,
|
|
112
|
-
|
|
114
|
+
_dataDir: string,
|
|
113
115
|
): Promise<void> {
|
|
114
116
|
const local = resolveLocalOpenpalmDir();
|
|
115
117
|
if (local) {
|
|
116
118
|
logger.debug('seeding .openpalm from local source', { src: local });
|
|
117
119
|
copyTree(local, homeDir, { skipExisting: true });
|
|
118
|
-
// Registry is system-managed — always refresh so addon overlays stay current.
|
|
119
|
-
copyTree(join(local, 'state', 'registry'), join(homeDir, 'state', 'registry'));
|
|
120
120
|
return;
|
|
121
121
|
}
|
|
122
122
|
|
|
@@ -137,8 +137,6 @@ export async function seedOpenPalmDir(
|
|
|
137
137
|
const srcOpenpalm = join(tmpDir, '.openpalm');
|
|
138
138
|
if (!existsSync(srcOpenpalm)) throw new Error('.openpalm/ not found in tarball');
|
|
139
139
|
copyTree(srcOpenpalm, homeDir, { skipExisting: true });
|
|
140
|
-
// Registry is system-managed — always refresh so addon overlays stay current.
|
|
141
|
-
copyTree(join(srcOpenpalm, 'state', 'registry'), join(homeDir, 'state', 'registry'));
|
|
142
140
|
} finally {
|
|
143
141
|
rmSync(tmpDir, { recursive: true, force: true });
|
|
144
142
|
}
|
|
@@ -179,47 +177,28 @@ export function resolveLocalUiBuild(): string | null {
|
|
|
179
177
|
);
|
|
180
178
|
}
|
|
181
179
|
|
|
182
|
-
function readUiVersionFile(dir: string): string | null {
|
|
183
|
-
try { return readFileSync(join(dir, 'version.txt'), 'utf-8').trim(); } catch { return null; }
|
|
184
|
-
}
|
|
185
|
-
|
|
186
180
|
/**
|
|
187
181
|
* Resolve the best available UI build directory at runtime.
|
|
188
182
|
*
|
|
189
183
|
* Priority:
|
|
190
|
-
* 1. OP_HOME/
|
|
191
|
-
* 2. Bundled / local build (Electron extraResources, source checkout)
|
|
192
|
-
* 3. OP_HOME/state/ui/ — fallback when no bundled build exists
|
|
193
|
-
*
|
|
194
|
-
* This means GitHub-downloaded updates are applied automatically (disk wins
|
|
195
|
-
* when newer), but a fresh AppImage install always works without a download.
|
|
184
|
+
* 1. OP_HOME/data/ui/ — user-installed or auto-updated build
|
|
185
|
+
* 2. Bundled / local build (Electron extraResources, OPENPALM_REPO_ROOT, source checkout)
|
|
196
186
|
*/
|
|
197
187
|
export function resolveUiBuildDir(): string {
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
if (existsSync(join(stateBuild, 'index.js')) && localBuild) {
|
|
202
|
-
const diskVer = readUiVersionFile(stateBuild);
|
|
203
|
-
const bundledVer = readUiVersionFile(localBuild);
|
|
204
|
-
if (diskVer && bundledVer && compareVersionTags(diskVer, bundledVer) > 0) {
|
|
205
|
-
return stateBuild;
|
|
206
|
-
}
|
|
207
|
-
return localBuild;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
if (localBuild) return localBuild;
|
|
211
|
-
return stateBuild;
|
|
188
|
+
const dataBuild = join(resolveDataDir(), 'ui');
|
|
189
|
+
if (existsSync(join(dataBuild, 'index.js'))) return dataBuild;
|
|
190
|
+
return resolveLocalUiBuild() ?? dataBuild;
|
|
212
191
|
}
|
|
213
192
|
|
|
214
193
|
/**
|
|
215
|
-
* Install the UI build to OP_HOME/
|
|
194
|
+
* Install the UI build to OP_HOME/data/ui/.
|
|
216
195
|
*
|
|
217
196
|
* Copies from local packages/ui/build/ when running from source,
|
|
218
197
|
* otherwise downloads ui-build.tar.gz from the GitHub release.
|
|
219
198
|
* Called during install and update; always replaces existing content.
|
|
220
199
|
*
|
|
221
|
-
*
|
|
222
|
-
* backupOpenPalmHome() copies all of OP_HOME/
|
|
200
|
+
* data/ui/ is automatically included in backups because
|
|
201
|
+
* backupOpenPalmHome() copies all of OP_HOME/data/.
|
|
223
202
|
*/
|
|
224
203
|
/** SHA-256 hex digest of arbitrary bytes. */
|
|
225
204
|
function sha256Hex(data: Uint8Array): string {
|
|
@@ -241,21 +220,14 @@ function parseChecksumsFile(content: string): Map<string, string> {
|
|
|
241
220
|
return map;
|
|
242
221
|
}
|
|
243
222
|
|
|
244
|
-
export function
|
|
245
|
-
const
|
|
246
|
-
if (!existsSync(versionFile)) return null;
|
|
247
|
-
return readFileSync(versionFile, 'utf-8').trim() || null;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
export async function seedUiBuild(repoRef: string, stateDir: string, options?: { forceRemote?: boolean }): Promise<void> {
|
|
251
|
-
const uiDir = join(stateDir, 'ui');
|
|
223
|
+
export async function seedUiBuild(repoRef: string, dataDir: string, options?: { forceRemote?: boolean }): Promise<void> {
|
|
224
|
+
const uiDir = join(dataDir, 'ui');
|
|
252
225
|
mkdirSync(uiDir, { recursive: true });
|
|
253
226
|
|
|
254
227
|
const local = options?.forceRemote ? null : resolveLocalUiBuild();
|
|
255
228
|
if (local) {
|
|
256
229
|
logger.debug('seeding UI build from local source', { src: local });
|
|
257
230
|
copyTree(local, uiDir);
|
|
258
|
-
writeFileSync(join(uiDir, 'version.txt'), repoRef.replace(/^v/, ''));
|
|
259
231
|
return;
|
|
260
232
|
}
|
|
261
233
|
|
|
@@ -264,7 +236,7 @@ export async function seedUiBuild(repoRef: string, stateDir: string, options?: {
|
|
|
264
236
|
const checksumUrl = `${base}/checksums-sha256.txt`;
|
|
265
237
|
logger.debug('downloading UI build', { url: tarballUrl });
|
|
266
238
|
|
|
267
|
-
const tmpTar = join(
|
|
239
|
+
const tmpTar = join(dataDir, '.ui-build.tar.gz.tmp');
|
|
268
240
|
try {
|
|
269
241
|
// Download tarball and checksums file in parallel (checksums best-effort)
|
|
270
242
|
const [tarRes, csRes] = await Promise.all([
|
|
@@ -295,7 +267,6 @@ export async function seedUiBuild(repoRef: string, stateDir: string, options?: {
|
|
|
295
267
|
mkdirSync(uiDir, { recursive: true });
|
|
296
268
|
// Cross-platform extraction via the `tar` npm package — no shell dependency
|
|
297
269
|
await tarExtract({ file: tmpTar, cwd: uiDir, strip: 1 });
|
|
298
|
-
writeFileSync(join(uiDir, 'version.txt'), repoRef.replace(/^v/, ''));
|
|
299
270
|
} finally {
|
|
300
271
|
rmSync(tmpTar, { force: true });
|
|
301
272
|
}
|
|
@@ -305,14 +276,45 @@ export async function seedUiBuild(repoRef: string, stateDir: string, options?: {
|
|
|
305
276
|
|
|
306
277
|
const GITHUB_API = 'https://api.github.com';
|
|
307
278
|
|
|
308
|
-
/** Returns 1 if a > b, -1 if a < b, 0 if equal. Strips leading 'v'. */
|
|
279
|
+
/** Returns 1 if a > b, -1 if a < b, 0 if equal. Strips leading 'v'. Handles pre-release tags. */
|
|
309
280
|
function compareVersionTags(a: string, b: string): number {
|
|
310
|
-
const parse = (v: string) =>
|
|
311
|
-
|
|
312
|
-
|
|
281
|
+
const parse = (v: string): [number, number, number, string | null] => {
|
|
282
|
+
const clean = v.replace(/^v/, '');
|
|
283
|
+
const dashIdx = clean.indexOf('-');
|
|
284
|
+
const main = dashIdx === -1 ? clean : clean.slice(0, dashIdx);
|
|
285
|
+
const pre = dashIdx === -1 ? null : clean.slice(dashIdx + 1);
|
|
286
|
+
const parts = main.split('.').map(Number);
|
|
287
|
+
return [parts[0] ?? 0, parts[1] ?? 0, parts[2] ?? 0, pre];
|
|
288
|
+
};
|
|
289
|
+
const comparePre = (x: string, y: string): number => {
|
|
290
|
+
const xp = x.split('.');
|
|
291
|
+
const yp = y.split('.');
|
|
292
|
+
for (let i = 0; i < Math.max(xp.length, yp.length); i++) {
|
|
293
|
+
if (i >= xp.length) return -1;
|
|
294
|
+
if (i >= yp.length) return 1;
|
|
295
|
+
const xn = Number(xp[i]);
|
|
296
|
+
const yn = Number(yp[i]);
|
|
297
|
+
const xIsNum = !isNaN(xn);
|
|
298
|
+
const yIsNum = !isNaN(yn);
|
|
299
|
+
if (xIsNum && yIsNum) {
|
|
300
|
+
if (xn !== yn) return xn > yn ? 1 : -1;
|
|
301
|
+
} else if (xIsNum !== yIsNum) {
|
|
302
|
+
return xIsNum ? -1 : 1; // numeric < alphanumeric per semver
|
|
303
|
+
} else {
|
|
304
|
+
if (xp[i] !== yp[i]) return xp[i]! > yp[i]! ? 1 : -1;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return 0;
|
|
308
|
+
};
|
|
309
|
+
const [aM, am, ap, aPre] = parse(a);
|
|
310
|
+
const [bM, bm, bp, bPre] = parse(b);
|
|
313
311
|
if (aM !== bM) return aM > bM ? 1 : -1;
|
|
314
312
|
if (am !== bm) return am > bm ? 1 : -1;
|
|
315
313
|
if (ap !== bp) return ap > bp ? 1 : -1;
|
|
314
|
+
// Same numeric version: stable > pre-release (semver spec)
|
|
315
|
+
if (aPre === null && bPre !== null) return 1;
|
|
316
|
+
if (aPre !== null && bPre === null) return -1;
|
|
317
|
+
if (aPre !== null && bPre !== null) return comparePre(aPre, bPre);
|
|
316
318
|
return 0;
|
|
317
319
|
}
|
|
318
320
|
|
|
@@ -326,15 +328,15 @@ export interface UiBuildUpdateResult {
|
|
|
326
328
|
* Check GitHub for a newer UI build and apply it if one exists.
|
|
327
329
|
*
|
|
328
330
|
* When an update is available:
|
|
329
|
-
* 1. Move
|
|
330
|
-
* 2. Download ui-build.tar.gz from the latest release and extract to
|
|
331
|
+
* 1. Move data/ui/ → data/backups/ui-{timestamp}/ (preserves the old build)
|
|
332
|
+
* 2. Download ui-build.tar.gz from the latest release and extract to data/ui/
|
|
331
333
|
*
|
|
332
334
|
* Non-fatal: any network or extraction error returns { updated: false, error }.
|
|
333
335
|
* The caller should proceed with the existing build on failure.
|
|
334
336
|
*/
|
|
335
337
|
export async function checkAndUpdateUiBuild(
|
|
336
338
|
currentVersion: string,
|
|
337
|
-
|
|
339
|
+
dataDir: string,
|
|
338
340
|
): Promise<UiBuildUpdateResult> {
|
|
339
341
|
try {
|
|
340
342
|
const res = await fetch(
|
|
@@ -365,15 +367,15 @@ export async function checkAndUpdateUiBuild(
|
|
|
365
367
|
}
|
|
366
368
|
|
|
367
369
|
// Back up the existing UI build before replacing it
|
|
368
|
-
const uiDir = join(
|
|
370
|
+
const uiDir = join(dataDir, 'ui');
|
|
369
371
|
if (existsSync(join(uiDir, 'index.js'))) {
|
|
370
|
-
const backupDir = join(
|
|
371
|
-
mkdirSync(
|
|
372
|
+
const backupDir = join(resolveBackupsDir(), `ui-${Date.now()}`);
|
|
373
|
+
mkdirSync(resolveBackupsDir(), { recursive: true });
|
|
372
374
|
renameSync(uiDir, backupDir);
|
|
373
375
|
logger.debug('backed up UI build before update', { backup: backupDir });
|
|
374
376
|
}
|
|
375
377
|
|
|
376
|
-
await seedUiBuild(latestTag,
|
|
378
|
+
await seedUiBuild(latestTag, dataDir);
|
|
377
379
|
logger.debug('UI build updated', { from: currentVersion, to: latestVersion });
|
|
378
380
|
|
|
379
381
|
return { updated: true, latestVersion };
|
|
@@ -2,26 +2,26 @@
|
|
|
2
2
|
* Runtime configuration validation for the OpenPalm control plane.
|
|
3
3
|
*
|
|
4
4
|
* Validation is a presence check on the canonical env keys we expect in
|
|
5
|
-
* the live config/stack
|
|
5
|
+
* the live config/stack files. The
|
|
6
6
|
* historical schema files and external validation binary were retired in
|
|
7
7
|
* #391; everything advisory is surfaced as a non-blocking warning. The
|
|
8
8
|
* function never shells out and never reads schemas.
|
|
9
9
|
*/
|
|
10
10
|
import { existsSync } from "node:fs";
|
|
11
|
-
import {
|
|
11
|
+
import { readStackRuntimeEnv } from "./secrets.js";
|
|
12
12
|
import { getCoreSecretMappings } from "./secret-mappings.js";
|
|
13
13
|
import type { ControlPlaneState } from "./types.js";
|
|
14
14
|
|
|
15
15
|
// Stack-scoped env keys that must always exist and carry a non-empty value
|
|
16
16
|
// for the platform to boot. Keep this list small — anything optional
|
|
17
17
|
// belongs in the warning bucket instead.
|
|
18
|
-
const
|
|
18
|
+
const REQUIRED_SECRET_KEYS = ["OP_UI_LOGIN_PASSWORD"] as const;
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* Validate the live configuration files.
|
|
22
22
|
*
|
|
23
23
|
* Checks:
|
|
24
|
-
* 1.
|
|
24
|
+
* 1. knowledge/env/stack.env exists and carries every required key with a
|
|
25
25
|
* non-empty value.
|
|
26
26
|
* 2. Every secret env key in getCoreSecretMappings() is present (key only
|
|
27
27
|
* — blank values are warned about, never erred on, because operators
|
|
@@ -38,32 +38,30 @@ export async function validateProposedState(state: ControlPlaneState): Promise<{
|
|
|
38
38
|
const errors: string[] = [];
|
|
39
39
|
const warnings: string[] = [];
|
|
40
40
|
|
|
41
|
-
const stackEnvPath = `${state.
|
|
41
|
+
const stackEnvPath = `${state.stashDir}/env/stack.env`;
|
|
42
42
|
|
|
43
43
|
if (!existsSync(stackEnvPath)) {
|
|
44
44
|
errors.push(`ERROR: stack env file missing at ${stackEnvPath}`);
|
|
45
45
|
return { ok: false, errors, warnings };
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
const
|
|
49
|
-
const userEnv: Record<string, string> = {};
|
|
48
|
+
const runtimeEnv = readStackRuntimeEnv(state.stackDir);
|
|
50
49
|
|
|
51
|
-
for (const key of
|
|
52
|
-
const value =
|
|
50
|
+
for (const key of REQUIRED_SECRET_KEYS) {
|
|
51
|
+
const value = runtimeEnv[key];
|
|
53
52
|
if (!value || value.trim().length === 0) {
|
|
54
|
-
errors.push(`ERROR: required
|
|
53
|
+
errors.push(`ERROR: required secret ${key} is missing or empty in knowledge/secrets/${key.toLowerCase()}`);
|
|
55
54
|
}
|
|
56
55
|
}
|
|
57
56
|
|
|
58
57
|
// Every canonical secret should at least appear as a key somewhere in
|
|
59
58
|
// the env files so the operator sees the slot. Missing slots warn (not
|
|
60
59
|
// error) since not every provider is in use on every install.
|
|
61
|
-
for (const mapping of getCoreSecretMappings(
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
if (!inStack && !inUser) {
|
|
60
|
+
for (const mapping of getCoreSecretMappings(runtimeEnv)) {
|
|
61
|
+
const inRuntime = Object.prototype.hasOwnProperty.call(runtimeEnv, mapping.envKey);
|
|
62
|
+
if (!inRuntime) {
|
|
65
63
|
warnings.push(
|
|
66
|
-
`WARN: ${mapping.envKey} (akm ${mapping.secretKey}) is not declared in
|
|
64
|
+
`WARN: ${mapping.envKey} (akm ${mapping.secretKey}) is not declared in knowledge/secrets/${mapping.envKey.toLowerCase()}`,
|
|
67
65
|
);
|
|
68
66
|
}
|
|
69
67
|
}
|
package/src/index.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
export {
|
|
11
11
|
LLM_PROVIDERS,
|
|
12
12
|
EMBEDDING_DIMS,
|
|
13
|
+
PROVIDER_KEY_MAP,
|
|
13
14
|
lookupEmbeddingDims,
|
|
14
15
|
} from "./provider-constants.js";
|
|
15
16
|
|
|
@@ -75,8 +76,7 @@ export {
|
|
|
75
76
|
resolveConfigDir,
|
|
76
77
|
resolveStashDir,
|
|
77
78
|
resolveWorkspaceDir,
|
|
78
|
-
|
|
79
|
-
resolveStateDir,
|
|
79
|
+
resolveDataDir,
|
|
80
80
|
resolveStackDir,
|
|
81
81
|
resolveLogsDir,
|
|
82
82
|
ensureHomeDirs,
|
|
@@ -109,12 +109,36 @@ export {
|
|
|
109
109
|
updateSecretsEnv,
|
|
110
110
|
writeAuthJsonProviderKeys,
|
|
111
111
|
readStackEnv,
|
|
112
|
+
readStackSecretEnv,
|
|
113
|
+
readStackRuntimeEnv,
|
|
114
|
+
writeStackSecretEnv,
|
|
112
115
|
patchSecretsEnvFile,
|
|
113
116
|
maskSecretValue,
|
|
114
117
|
ensureOpenCodeConfig,
|
|
118
|
+
assertNoSecretLikeStackEnvKeys,
|
|
115
119
|
} from "./control-plane/secrets.js";
|
|
116
|
-
export {
|
|
117
|
-
|
|
120
|
+
export {
|
|
121
|
+
resolveSecretsDir,
|
|
122
|
+
secretPath,
|
|
123
|
+
readSecret,
|
|
124
|
+
writeSecret,
|
|
125
|
+
ensureSecret,
|
|
126
|
+
removeSecret,
|
|
127
|
+
listSecretNames,
|
|
128
|
+
} from './control-plane/secrets-files.js';
|
|
129
|
+
export type {
|
|
130
|
+
SecretAuditIssue,
|
|
131
|
+
SecretAuditOptions,
|
|
132
|
+
SecretAuditResult,
|
|
133
|
+
SecretAuditSeverity,
|
|
134
|
+
} from "./control-plane/secret-audit.js";
|
|
135
|
+
export {
|
|
136
|
+
auditComposeSecrets,
|
|
137
|
+
auditFileBasedSecrets,
|
|
138
|
+
auditSecretFilesystem,
|
|
139
|
+
auditStackEnv,
|
|
140
|
+
isSecretLikeKey,
|
|
141
|
+
} from "./control-plane/secret-audit.js";
|
|
118
142
|
// ── Setup Status ────────────────────────────────────────────────────────
|
|
119
143
|
export {
|
|
120
144
|
isSetupComplete,
|
|
@@ -156,8 +180,8 @@ export {
|
|
|
156
180
|
buildRuntimeFileMeta,
|
|
157
181
|
writeRuntimeFiles,
|
|
158
182
|
writeSystemEnv,
|
|
159
|
-
|
|
160
|
-
|
|
183
|
+
channelSecretName,
|
|
184
|
+
ensureChannelSecret,
|
|
161
185
|
ensureComposeVolumeTargets,
|
|
162
186
|
} from "./control-plane/config-persistence.js";
|
|
163
187
|
|
|
@@ -230,8 +254,16 @@ export { detectLocalProviders } from "./control-plane/model-runner.js";
|
|
|
230
254
|
export {
|
|
231
255
|
buildComposeOptions,
|
|
232
256
|
buildComposeCliArgs,
|
|
257
|
+
resolveActiveProfiles,
|
|
258
|
+
writeRunScript,
|
|
233
259
|
} from "./control-plane/compose-args.js";
|
|
234
260
|
|
|
261
|
+
export {
|
|
262
|
+
addonProfileId,
|
|
263
|
+
canonicalAddonProfileSelection,
|
|
264
|
+
resolveHardwareProfileVariant,
|
|
265
|
+
} from "./control-plane/profile-ids.js";
|
|
266
|
+
|
|
235
267
|
// ── Compose Error Parsing ────────────────────────────────────────────────
|
|
236
268
|
export type { ComposeServiceFailure } from "./control-plane/compose-errors.js";
|
|
237
269
|
export {
|
|
@@ -289,16 +321,17 @@ export {
|
|
|
289
321
|
importHostOpenCode,
|
|
290
322
|
} from "./control-plane/host-opencode.js";
|
|
291
323
|
|
|
292
|
-
// ── AKM
|
|
324
|
+
// ── AKM user env (env:user) ──────────────────────────────────────────────
|
|
293
325
|
export {
|
|
294
|
-
|
|
326
|
+
AKM_USER_ENV_REF,
|
|
295
327
|
buildAkmEnv,
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
328
|
+
ensureAkmUserEnv,
|
|
329
|
+
writeUserEnvKey,
|
|
330
|
+
deleteUserEnvKey,
|
|
331
|
+
readUserEnvFile,
|
|
332
|
+
readUserEnvSync,
|
|
333
|
+
userEnvPathSync,
|
|
334
|
+
} from "./control-plane/akm-user-env.js";
|
|
302
335
|
|
|
303
336
|
// ── UI asset seeding and resolution ─────────────────────────────────────────
|
|
304
337
|
export type { UiBuildUpdateResult } from "./control-plane/ui-assets.js";
|
|
@@ -309,5 +342,4 @@ export {
|
|
|
309
342
|
resolveUiBuildDir,
|
|
310
343
|
seedUiBuild,
|
|
311
344
|
checkAndUpdateUiBuild,
|
|
312
|
-
readCurrentUiBuildVersion,
|
|
313
345
|
} from "./control-plane/ui-assets.js";
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for the akm vault helpers. The vault helpers spawn `akm vault set
|
|
3
|
-
* <ref> <key>` and feed the secret VALUE on stdin (akm-cli >= 0.8.0).
|
|
4
|
-
*
|
|
5
|
-
* Tests gate on the akm CLI being on PATH so the suite stays green in
|
|
6
|
-
* environments without akm installed.
|
|
7
|
-
*/
|
|
8
|
-
import { describe, expect, it, beforeEach, afterEach, mock } from "bun:test";
|
|
9
|
-
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
10
|
-
import { tmpdir } from "node:os";
|
|
11
|
-
import { join } from "node:path";
|
|
12
|
-
import { execFileSync } from "node:child_process";
|
|
13
|
-
import {
|
|
14
|
-
ensureAkmUserVault,
|
|
15
|
-
readAkmUserVaultFile,
|
|
16
|
-
writeAkmVaultKey,
|
|
17
|
-
deleteAkmVaultKey,
|
|
18
|
-
AKM_USER_VAULT_REF,
|
|
19
|
-
} from "./akm-vault.js";
|
|
20
|
-
import type { ControlPlaneState } from "./types.js";
|
|
21
|
-
|
|
22
|
-
function makeState(homeDir: string): ControlPlaneState {
|
|
23
|
-
return {
|
|
24
|
-
homeDir,
|
|
25
|
-
configDir: join(homeDir, "config"),
|
|
26
|
-
stashDir: join(homeDir, "stash"),
|
|
27
|
-
workspaceDir: join(homeDir, "workspace"),
|
|
28
|
-
cacheDir: join(homeDir, "cache"),
|
|
29
|
-
stateDir: join(homeDir, "state"),
|
|
30
|
-
stackDir: join(homeDir, "stack"),
|
|
31
|
-
services: {},
|
|
32
|
-
artifacts: { compose: "" },
|
|
33
|
-
artifactMeta: [],
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function hasAkmCli(): boolean {
|
|
38
|
-
try {
|
|
39
|
-
execFileSync("akm", ["--version"], { stdio: "ignore" });
|
|
40
|
-
return true;
|
|
41
|
-
} catch {
|
|
42
|
-
return false;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const AKM_AVAILABLE = hasAkmCli();
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
describe("writeAkmVaultKey", () => {
|
|
50
|
-
let homeDir: string;
|
|
51
|
-
let state: ControlPlaneState;
|
|
52
|
-
|
|
53
|
-
beforeEach(() => {
|
|
54
|
-
homeDir = mkdtempSync(join(tmpdir(), "openpalm-akm-write-"));
|
|
55
|
-
state = makeState(homeDir);
|
|
56
|
-
mkdirSync(state.stashDir, { recursive: true });
|
|
57
|
-
mkdirSync(`${state.stateDir}/cache/akm`, { recursive: true });
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
afterEach(() => {
|
|
61
|
-
rmSync(homeDir, { recursive: true, force: true });
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it.skipIf(!AKM_AVAILABLE)("writes a key via `akm vault set` (stdin mode, no argv leak)", async () => {
|
|
65
|
-
const value = "argv-free-secret-9988";
|
|
66
|
-
const ok = await writeAkmVaultKey(state, "TOKEN", value);
|
|
67
|
-
expect(ok).toBe(true);
|
|
68
|
-
|
|
69
|
-
const vaultPath = await ensureAkmUserVault(state);
|
|
70
|
-
expect(vaultPath).not.toBeNull();
|
|
71
|
-
if (vaultPath) {
|
|
72
|
-
const stored = readAkmUserVaultFile(vaultPath);
|
|
73
|
-
expect(stored.TOKEN).toBe(value);
|
|
74
|
-
}
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it.skipIf(!AKM_AVAILABLE)("deleteAkmVaultKey removes a key via `akm vault unset`", async () => {
|
|
78
|
-
await writeAkmVaultKey(state, "TOKEN_A", "value-a");
|
|
79
|
-
await writeAkmVaultKey(state, "TOKEN_B", "value-b");
|
|
80
|
-
|
|
81
|
-
const ok = await deleteAkmVaultKey(state, "TOKEN_A");
|
|
82
|
-
expect(ok).toBe(true);
|
|
83
|
-
|
|
84
|
-
const vaultPath = await ensureAkmUserVault(state);
|
|
85
|
-
if (vaultPath) {
|
|
86
|
-
const stored = readAkmUserVaultFile(vaultPath);
|
|
87
|
-
expect(stored.TOKEN_A).toBeUndefined();
|
|
88
|
-
expect(stored.TOKEN_B).toBe("value-b");
|
|
89
|
-
}
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it.skipIf(!AKM_AVAILABLE)("deleteAkmVaultKey is idempotent on a missing key", async () => {
|
|
93
|
-
// Deleting a key that was never set should not throw — `akm vault unset`
|
|
94
|
-
// either exits 0 or emits a "not found" message we tolerate.
|
|
95
|
-
const ok = await deleteAkmVaultKey(state, "NEVER_SET_KEY");
|
|
96
|
-
expect(ok).toBe(true);
|
|
97
|
-
});
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
describe("AKM_USER_VAULT_REF", () => {
|
|
102
|
-
it("exports the canonical akm ref string", () => {
|
|
103
|
-
expect(AKM_USER_VAULT_REF).toBe("vault:user");
|
|
104
|
-
});
|
|
105
|
-
});
|