@openpalm/lib 0.11.0-beta.11 → 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 +42 -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
|
@@ -8,15 +8,13 @@ import {
|
|
|
8
8
|
resolveConfigDir,
|
|
9
9
|
resolveStashDir,
|
|
10
10
|
resolveWorkspaceDir,
|
|
11
|
-
|
|
12
|
-
resolveStateDir,
|
|
11
|
+
resolveDataDir,
|
|
13
12
|
resolveStackDir,
|
|
14
13
|
} from "./home.js";
|
|
15
|
-
import { ensureSecrets } from "./secrets.js";
|
|
14
|
+
import { ensureSecrets, readStackSecretEnv } from "./secrets.js";
|
|
16
15
|
import {
|
|
17
16
|
resolveRuntimeFiles,
|
|
18
17
|
writeRuntimeFiles,
|
|
19
|
-
buildEnvFiles,
|
|
20
18
|
discoverStackOverlays,
|
|
21
19
|
ensureComposeVolumeTargets,
|
|
22
20
|
} from "./config-persistence.js";
|
|
@@ -24,8 +22,9 @@ import { refreshCoreAssets } from "./core-assets.js";
|
|
|
24
22
|
import { isSetupComplete } from "./setup-status.js";
|
|
25
23
|
import { snapshotCurrentState } from "./rollback.js";
|
|
26
24
|
import { checkDocker, composePreflight, composePull, composeUp, composeConfigServices, resolveComposeProjectName } from "./docker.js";
|
|
25
|
+
import { buildComposeOptions, writeRunScript } from "./compose-args.js";
|
|
27
26
|
import { acquireInstallLock, releaseInstallLock } from "./install-lock.js";
|
|
28
|
-
import { listEnabledAddonIds } from "./registry.js";
|
|
27
|
+
import { getAddonServiceNames, listEnabledAddonIds } from "./registry.js";
|
|
29
28
|
|
|
30
29
|
const IMAGE_NAMESPACE_RE = /^[a-z0-9]+(?:[._-][a-z0-9]+)*$/;
|
|
31
30
|
const SEMVER_TAG_RE = /^v\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/;
|
|
@@ -36,8 +35,7 @@ export function createState(): ControlPlaneState {
|
|
|
36
35
|
const configDir = resolveConfigDir();
|
|
37
36
|
const stashDir = resolveStashDir();
|
|
38
37
|
const workspaceDir = resolveWorkspaceDir();
|
|
39
|
-
const
|
|
40
|
-
const stateDir = resolveStateDir();
|
|
38
|
+
const dataDir = resolveDataDir();
|
|
41
39
|
const stackDir = resolveStackDir();
|
|
42
40
|
|
|
43
41
|
const services: Record<string, "running" | "stopped"> = {};
|
|
@@ -50,8 +48,7 @@ export function createState(): ControlPlaneState {
|
|
|
50
48
|
configDir,
|
|
51
49
|
stashDir,
|
|
52
50
|
workspaceDir,
|
|
53
|
-
|
|
54
|
-
stateDir,
|
|
51
|
+
dataDir,
|
|
55
52
|
stackDir,
|
|
56
53
|
services,
|
|
57
54
|
artifacts: { compose: "" },
|
|
@@ -59,6 +56,7 @@ export function createState(): ControlPlaneState {
|
|
|
59
56
|
};
|
|
60
57
|
|
|
61
58
|
ensureSecrets(bootstrapState);
|
|
59
|
+
Object.assign(process.env, readStackSecretEnv(stackDir));
|
|
62
60
|
|
|
63
61
|
return bootstrapState;
|
|
64
62
|
}
|
|
@@ -73,7 +71,7 @@ async function reconcileCore(
|
|
|
73
71
|
}
|
|
74
72
|
|
|
75
73
|
for (const addonName of listEnabledAddonIds(state.homeDir)) {
|
|
76
|
-
mkdirSync(`${state.
|
|
74
|
+
mkdirSync(`${state.dataDir}/${addonName}`, { recursive: true });
|
|
77
75
|
}
|
|
78
76
|
|
|
79
77
|
const active: string[] = [];
|
|
@@ -88,8 +86,7 @@ async function reconcileCore(
|
|
|
88
86
|
// Preflight: validate compose merge before mutation.
|
|
89
87
|
// Mandatory when compose files exist and OP_SKIP_COMPOSE_PREFLIGHT is not set.
|
|
90
88
|
// Fails if Docker is unavailable (Docker is required for any compose operation).
|
|
91
|
-
const files =
|
|
92
|
-
const envFiles = buildEnvFiles(state);
|
|
89
|
+
const { files, envFiles, profiles } = buildComposeOptions(state);
|
|
93
90
|
if (files.length > 0 && !process.env.OP_SKIP_COMPOSE_PREFLIGHT) {
|
|
94
91
|
const dockerCheck = await checkDocker();
|
|
95
92
|
if (!dockerCheck.ok) {
|
|
@@ -98,12 +95,13 @@ async function reconcileCore(
|
|
|
98
95
|
"Docker must be running before install/update/apply operations."
|
|
99
96
|
);
|
|
100
97
|
}
|
|
101
|
-
const preflight = await composePreflight({ files, envFiles });
|
|
98
|
+
const preflight = await composePreflight({ files, envFiles, profiles });
|
|
102
99
|
if (!preflight.ok) {
|
|
103
|
-
const projectName = resolveComposeProjectName();
|
|
100
|
+
const projectName = resolveComposeProjectName(Object.assign({}, ...envFiles.map((f) => parseEnvFile(f))));
|
|
104
101
|
const fileArgs = files.flatMap((f) => ["-f", f]).join(" ");
|
|
105
102
|
const envArgs = envFiles.filter(existsSync).flatMap((f) => ["--env-file", f]).join(" ");
|
|
106
|
-
const
|
|
103
|
+
const profileArgs = profiles.flatMap((p) => ["--profile", p]).join(" ");
|
|
104
|
+
const resolvedCmd = `docker compose ${fileArgs} --project-name ${projectName} ${envArgs} ${profileArgs} config --quiet`;
|
|
107
105
|
throw new Error(
|
|
108
106
|
`Compose preflight failed: ${preflight.stderr}\n` +
|
|
109
107
|
`Resolved command: ${resolvedCmd}\n` +
|
|
@@ -124,7 +122,7 @@ async function reconcileCore(
|
|
|
124
122
|
}
|
|
125
123
|
|
|
126
124
|
export async function applyInstall(state: ControlPlaneState): Promise<void> {
|
|
127
|
-
const lock = acquireInstallLock(state.
|
|
125
|
+
const lock = acquireInstallLock(state.dataDir);
|
|
128
126
|
if (!lock) throw new Error("Another install is already in progress");
|
|
129
127
|
try {
|
|
130
128
|
await reconcileCore(state, { activateServices: true });
|
|
@@ -138,7 +136,7 @@ export async function applyInstall(state: ControlPlaneState): Promise<void> {
|
|
|
138
136
|
}
|
|
139
137
|
|
|
140
138
|
export async function applyUpdate(state: ControlPlaneState): Promise<{ restarted: string[] }> {
|
|
141
|
-
const lock = acquireInstallLock(state.
|
|
139
|
+
const lock = acquireInstallLock(state.dataDir);
|
|
142
140
|
if (!lock) throw new Error("Another install is already in progress");
|
|
143
141
|
try {
|
|
144
142
|
return { restarted: await reconcileCore(state, {}) };
|
|
@@ -148,7 +146,7 @@ export async function applyUpdate(state: ControlPlaneState): Promise<{ restarted
|
|
|
148
146
|
}
|
|
149
147
|
|
|
150
148
|
export async function applyUninstall(state: ControlPlaneState): Promise<{ stopped: string[] }> {
|
|
151
|
-
const lock = acquireInstallLock(state.
|
|
149
|
+
const lock = acquireInstallLock(state.dataDir);
|
|
152
150
|
if (!lock) throw new Error("Another install is already in progress");
|
|
153
151
|
try {
|
|
154
152
|
return { stopped: await reconcileCore(state, { deactivateServices: true }) };
|
|
@@ -178,7 +176,7 @@ export async function updateStackEnvToLatestImageTag(state: ControlPlaneState):
|
|
|
178
176
|
namespace: string;
|
|
179
177
|
tag: string;
|
|
180
178
|
}> {
|
|
181
|
-
const systemEnvPath = `${state.
|
|
179
|
+
const systemEnvPath = `${state.stashDir}/env/stack.env`;
|
|
182
180
|
const parsed = parseEnvFile(systemEnvPath);
|
|
183
181
|
const namespace = (parsed.OP_IMAGE_NAMESPACE ?? process.env.OP_IMAGE_NAMESPACE ?? "openpalm").trim().toLowerCase();
|
|
184
182
|
|
|
@@ -220,7 +218,7 @@ export async function applyUpgrade(
|
|
|
220
218
|
updated: string[];
|
|
221
219
|
restarted: string[];
|
|
222
220
|
}> {
|
|
223
|
-
const lock = acquireInstallLock(state.
|
|
221
|
+
const lock = acquireInstallLock(state.dataDir);
|
|
224
222
|
if (!lock) throw new Error("Another install is already in progress");
|
|
225
223
|
try {
|
|
226
224
|
const { backupDir, updated } = await refreshCoreAssets();
|
|
@@ -246,15 +244,14 @@ export type UpgradeResult = {
|
|
|
246
244
|
* Callers handle their own audit logging and admin self-recreation.
|
|
247
245
|
*/
|
|
248
246
|
export async function performUpgrade(state: ControlPlaneState): Promise<UpgradeResult> {
|
|
249
|
-
const
|
|
250
|
-
const envFiles = buildEnvFiles(state);
|
|
247
|
+
const composeOpts = buildComposeOptions(state);
|
|
251
248
|
|
|
252
249
|
// Compose preflight runs inside `applyUpgrade` -> `reconcileCore`, so we
|
|
253
250
|
// skip the redundant top-level call. Any merge failure aborts before
|
|
254
251
|
// mutation just the same.
|
|
255
252
|
|
|
256
253
|
// 1. Snapshot stack.env for rollback on failure
|
|
257
|
-
const stackEnvPath = `${state.
|
|
254
|
+
const stackEnvPath = `${state.stashDir}/env/stack.env`;
|
|
258
255
|
let originalStackEnv: string | null = null;
|
|
259
256
|
try {
|
|
260
257
|
originalStackEnv = readFileSync(stackEnvPath, "utf-8");
|
|
@@ -277,19 +274,22 @@ export async function performUpgrade(state: ControlPlaneState): Promise<UpgradeR
|
|
|
277
274
|
throw e;
|
|
278
275
|
}
|
|
279
276
|
|
|
280
|
-
// 3. Pull images
|
|
281
|
-
const pullResult = await composePull(
|
|
277
|
+
// 3. Pull all images (core + addons, including profile-gated voice)
|
|
278
|
+
const pullResult = await composePull(composeOpts);
|
|
282
279
|
if (!pullResult.ok) {
|
|
283
280
|
throw new Error(`Failed to pull images: ${pullResult.stderr}`);
|
|
284
281
|
}
|
|
285
282
|
|
|
286
|
-
// 4. Recreate containers
|
|
283
|
+
// 4. Recreate containers (includes profiles for voice addon)
|
|
287
284
|
const services = await buildManagedServices(state);
|
|
288
|
-
const upResult = await composeUp({
|
|
285
|
+
const upResult = await composeUp({ ...composeOpts, services, removeOrphans: true });
|
|
289
286
|
if (!upResult.ok) {
|
|
290
287
|
throw new Error(`Images pulled but failed to recreate containers: ${upResult.stderr}`);
|
|
291
288
|
}
|
|
292
289
|
|
|
290
|
+
// 5. Write run.sh with the final compose command
|
|
291
|
+
writeRunScript(state);
|
|
292
|
+
|
|
293
293
|
return {
|
|
294
294
|
imageTag,
|
|
295
295
|
namespace,
|
|
@@ -304,10 +304,11 @@ export async function performUpgrade(state: ControlPlaneState): Promise<UpgradeR
|
|
|
304
304
|
* Used by the admin "set version" action — skips the auto-detect step in performUpgrade.
|
|
305
305
|
*/
|
|
306
306
|
export async function applyTagChange(state: ControlPlaneState, tag: string): Promise<UpgradeResult> {
|
|
307
|
-
const stackEnvPath = `${state.
|
|
307
|
+
const stackEnvPath = `${state.stashDir}/env/stack.env`;
|
|
308
308
|
const currentContent = existsSync(stackEnvPath) ? readFileSync(stackEnvPath, "utf-8") : "";
|
|
309
309
|
writeFileSync(stackEnvPath, mergeEnvContent(currentContent, { OP_IMAGE_TAG: tag }, { uncomment: true }));
|
|
310
310
|
const upgradeResult = await applyUpgrade(state);
|
|
311
|
+
writeRunScript(state);
|
|
311
312
|
return {
|
|
312
313
|
imageTag: tag,
|
|
313
314
|
namespace: "openpalm",
|
|
@@ -318,16 +319,15 @@ export async function applyTagChange(state: ControlPlaneState, tag: string): Pro
|
|
|
318
319
|
}
|
|
319
320
|
|
|
320
321
|
export function buildComposeFileList(state: ControlPlaneState): string[] {
|
|
321
|
-
return discoverStackOverlays(state.stackDir);
|
|
322
|
+
return discoverStackOverlays(state.stackDir, state.homeDir);
|
|
322
323
|
}
|
|
323
324
|
|
|
324
325
|
export async function buildManagedServices(state: ControlPlaneState): Promise<string[]> {
|
|
325
|
-
const
|
|
326
|
-
const envFiles = buildEnvFiles(state);
|
|
326
|
+
const composeOpts = buildComposeOptions(state);
|
|
327
327
|
|
|
328
328
|
// Prefer compose-derived service list when Docker is available
|
|
329
|
-
if (files.length > 0 && !process.env.OP_SKIP_COMPOSE_PREFLIGHT) {
|
|
330
|
-
const result = await composeConfigServices(
|
|
329
|
+
if (composeOpts.files.length > 0 && !process.env.OP_SKIP_COMPOSE_PREFLIGHT) {
|
|
330
|
+
const result = await composeConfigServices(composeOpts);
|
|
331
331
|
if (result.ok && result.services.length > 0) {
|
|
332
332
|
return result.services;
|
|
333
333
|
}
|
|
@@ -335,7 +335,9 @@ export async function buildManagedServices(state: ControlPlaneState): Promise<st
|
|
|
335
335
|
|
|
336
336
|
// Fallback: static inference from CORE_SERVICES + active addon overlays
|
|
337
337
|
const services: string[] = [...CORE_SERVICES];
|
|
338
|
-
|
|
338
|
+
for (const addon of listEnabledAddonIds(state.homeDir)) {
|
|
339
|
+
services.push(...getAddonServiceNames(state.homeDir, addon));
|
|
340
|
+
}
|
|
339
341
|
return services;
|
|
340
342
|
}
|
|
341
343
|
|
|
@@ -1,21 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* AKM
|
|
2
|
+
* AKM task parser.
|
|
3
3
|
*
|
|
4
|
-
* Task files are
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* Supported target types:
|
|
8
|
-
* command — `command: [...]` YAML array (argv), run via Bun.spawn / akm tasks run
|
|
9
|
-
* prompt — `prompt: inline` + markdown body as the prompt text
|
|
4
|
+
* Task files are YAML documents in knowledge/tasks/. Supported target types:
|
|
5
|
+
* command — `command: [...]` YAML array (argv)
|
|
6
|
+
* prompt — `prompt: <text>` inline prompt text
|
|
10
7
|
* workflow — `workflow: workflow:<ref>` + optional `params` map
|
|
11
8
|
*/
|
|
12
9
|
import { parse as parseYaml } from "yaml";
|
|
13
10
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
14
|
-
import { join } from "node:path";
|
|
11
|
+
import { basename, join } from "node:path";
|
|
15
12
|
import type { AutomationConfig } from "./scheduler.js";
|
|
16
13
|
import { createLogger } from "../logger.js";
|
|
17
14
|
|
|
18
|
-
const logger = createLogger("
|
|
15
|
+
const logger = createLogger("task-file");
|
|
19
16
|
|
|
20
17
|
// ── Types ─────────────────────────────────────────────────────────────────
|
|
21
18
|
|
|
@@ -35,29 +32,11 @@ export type MarkdownTaskTarget =
|
|
|
35
32
|
| { kind: "prompt"; profile?: string; body: string }
|
|
36
33
|
| { kind: "workflow"; ref: string; params: Record<string, unknown> };
|
|
37
34
|
|
|
38
|
-
// ── Frontmatter splitter ──────────────────────────────────────────────────
|
|
39
|
-
|
|
40
|
-
interface ParsedFile {
|
|
41
|
-
frontmatter: string;
|
|
42
|
-
body: string;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function splitFrontmatter(content: string): ParsedFile | null {
|
|
46
|
-
// Must start with ---
|
|
47
|
-
if (!content.startsWith("---")) return null;
|
|
48
|
-
const after = content.slice(3);
|
|
49
|
-
const end = after.indexOf("\n---");
|
|
50
|
-
if (end === -1) return null;
|
|
51
|
-
return {
|
|
52
|
-
frontmatter: after.slice(0, end).trim(),
|
|
53
|
-
body: after.slice(end + 4).trim(),
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
|
|
57
35
|
// ── Parser ────────────────────────────────────────────────────────────────
|
|
58
36
|
|
|
59
37
|
export function parseMarkdownTask(filePath: string): MarkdownTask | null {
|
|
60
|
-
const
|
|
38
|
+
const fileName = basename(filePath);
|
|
39
|
+
const id = fileName.replace(/\.(?:ya?ml|md)$/, "");
|
|
61
40
|
let raw: string;
|
|
62
41
|
try {
|
|
63
42
|
raw = readFileSync(filePath, "utf-8");
|
|
@@ -66,22 +45,17 @@ export function parseMarkdownTask(filePath: string): MarkdownTask | null {
|
|
|
66
45
|
return null;
|
|
67
46
|
}
|
|
68
47
|
|
|
69
|
-
const
|
|
70
|
-
if (!parts) {
|
|
71
|
-
logger.warn("task file missing frontmatter delimiters", { filePath });
|
|
72
|
-
return null;
|
|
73
|
-
}
|
|
74
|
-
|
|
48
|
+
const { frontmatter, body } = splitTaskSource(raw);
|
|
75
49
|
let fm: Record<string, unknown>;
|
|
76
50
|
try {
|
|
77
|
-
|
|
51
|
+
const parsed = parseYaml(frontmatter);
|
|
52
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
53
|
+
logger.warn("task YAML is not an object", { filePath });
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
fm = parsed as Record<string, unknown>;
|
|
78
57
|
} catch (err) {
|
|
79
|
-
logger.warn("failed to parse task
|
|
80
|
-
return null;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
if (!fm || typeof fm !== "object") {
|
|
84
|
-
logger.warn("task frontmatter is not an object", { filePath });
|
|
58
|
+
logger.warn("failed to parse task YAML", { filePath, error: String(err) });
|
|
85
59
|
return null;
|
|
86
60
|
}
|
|
87
61
|
|
|
@@ -106,19 +80,19 @@ export function parseMarkdownTask(filePath: string): MarkdownTask | null {
|
|
|
106
80
|
}
|
|
107
81
|
target = { kind: "command", cmd };
|
|
108
82
|
} else if (fm.prompt !== undefined) {
|
|
109
|
-
if (fm.prompt !== "
|
|
110
|
-
|
|
111
|
-
logger.warn("task 'prompt' supports only 'inline' currently", { filePath });
|
|
83
|
+
if (typeof fm.prompt !== "string" || !fm.prompt.trim()) {
|
|
84
|
+
logger.warn("task 'prompt' must be a non-empty string", { filePath });
|
|
112
85
|
return null;
|
|
113
86
|
}
|
|
114
|
-
|
|
115
|
-
|
|
87
|
+
const promptBody = fm.prompt.trim() === "inline" ? body.trim() : fm.prompt.trim();
|
|
88
|
+
if (!promptBody) {
|
|
89
|
+
logger.warn("task prompt body is empty", { filePath });
|
|
116
90
|
return null;
|
|
117
91
|
}
|
|
118
92
|
target = {
|
|
119
93
|
kind: "prompt",
|
|
120
94
|
profile: typeof fm.profile === "string" ? fm.profile : undefined,
|
|
121
|
-
body:
|
|
95
|
+
body: promptBody,
|
|
122
96
|
};
|
|
123
97
|
} else if (fm.workflow !== undefined) {
|
|
124
98
|
if (typeof fm.workflow !== "string") {
|
|
@@ -149,13 +123,19 @@ export function parseMarkdownTask(filePath: string): MarkdownTask | null {
|
|
|
149
123
|
};
|
|
150
124
|
}
|
|
151
125
|
|
|
126
|
+
function splitTaskSource(raw: string): { frontmatter: string; body: string } {
|
|
127
|
+
const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
|
128
|
+
if (!match) return { frontmatter: raw, body: "" };
|
|
129
|
+
return { frontmatter: match[1] ?? "", body: match[2] ?? "" };
|
|
130
|
+
}
|
|
131
|
+
|
|
152
132
|
export function loadMarkdownTasks(stashDir: string): MarkdownTask[] {
|
|
153
133
|
const dir = join(stashDir, "tasks");
|
|
154
134
|
if (!existsSync(dir)) return [];
|
|
155
135
|
|
|
156
136
|
const tasks: MarkdownTask[] = [];
|
|
157
137
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
158
|
-
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
|
|
138
|
+
if (!entry.isFile() || (!entry.name.endsWith(".md") && !entry.name.endsWith(".yml") && !entry.name.endsWith(".yaml"))) continue;
|
|
159
139
|
const task = parseMarkdownTask(join(dir, entry.name));
|
|
160
140
|
if (task) tasks.push(task);
|
|
161
141
|
}
|
|
@@ -195,6 +175,6 @@ export function taskToAutomationConfig(task: MarkdownTask): AutomationConfig {
|
|
|
195
175
|
agent,
|
|
196
176
|
},
|
|
197
177
|
on_failure: "log",
|
|
198
|
-
fileName:
|
|
178
|
+
fileName: basename(task.source.path),
|
|
199
179
|
};
|
|
200
180
|
}
|
|
@@ -5,76 +5,96 @@
|
|
|
5
5
|
* When the directory layout changes, update this file only.
|
|
6
6
|
*
|
|
7
7
|
* Layout:
|
|
8
|
-
* config/ — user-editable config + system config files (
|
|
9
|
-
* config/stack/ — compose runtime + stack config (stack.env,
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* stash/ — akm knowledge (skills, vaults, agents)
|
|
8
|
+
* config/ — user-editable config + system config files (akm/)
|
|
9
|
+
* config/stack/ — compose runtime + stack config (stack.env, stack.yml, auth.json, fixed compose files)
|
|
10
|
+
* data/ — persistent service data, logs, backups, rollback
|
|
11
|
+
* knowledge/ — akm knowledge (skills, env, secrets, agents)
|
|
13
12
|
* workspace/ — shared work area
|
|
14
13
|
*/
|
|
14
|
+
import { dirname, basename } from "node:path";
|
|
15
15
|
import type { ControlPlaneState } from "./types.js";
|
|
16
16
|
|
|
17
17
|
// ── Config directory — user + system config ─────────────────────────────────
|
|
18
18
|
|
|
19
|
-
/**
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
/**
|
|
20
|
+
* OpenCode auth token store. Provider credentials are sensitive, so they live
|
|
21
|
+
* under knowledge/secrets/ (out of config/stack/) and are bind-mounted into
|
|
22
|
+
* every OpenCode-based container (assistant + guardian).
|
|
23
|
+
*/
|
|
24
|
+
export const authJsonPath = (s: ControlPlaneState): string => `${s.stashDir}/secrets/auth.json`;
|
|
25
|
+
/** akm config directory mounted at /etc/akm */
|
|
22
26
|
export const akmConfigDir = (s: ControlPlaneState): string => `${s.configDir}/akm`;
|
|
23
27
|
/** akm setup config file (written by admin on capability save) */
|
|
24
28
|
export const akmConfigPath = (s: ControlPlaneState): string => `${s.configDir}/akm/config.json`;
|
|
25
29
|
export const tasksDir = (s: ControlPlaneState): string => `${s.stashDir}/tasks`;
|
|
26
30
|
export const assistantConfigDir = (s: ControlPlaneState): string => `${s.configDir}/assistant`;
|
|
31
|
+
/** Guardian OpenCode global config dir — bind-mounted at /etc/opencode */
|
|
32
|
+
export const guardianConfigDir = (s: ControlPlaneState): string => `${s.configDir}/guardian`;
|
|
27
33
|
|
|
28
34
|
// ── Config/stack directory — compose runtime + stack config ─────────────────
|
|
29
35
|
|
|
30
|
-
/**
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
+
/**
|
|
37
|
+
* System env: non-secret runtime configuration (the Compose `--env-file`).
|
|
38
|
+
* Lives under knowledge/env/ alongside the user env file (akm `env:stack`).
|
|
39
|
+
*/
|
|
40
|
+
export const stackEnvPath = (s: ControlPlaneState): string => `${s.stashDir}/env/stack.env`;
|
|
41
|
+
/**
|
|
42
|
+
* Resolve the OP_HOME root from a stackDir. Normally `<home>/config/stack`;
|
|
43
|
+
* falls back to the stackDir itself for callers/tests that pass a home-shaped
|
|
44
|
+
* dir. Mirrors `resolveHomeDirFromStackDir` in secrets-files.ts so the env and
|
|
45
|
+
* secret dirs resolve consistently from the same input.
|
|
46
|
+
*/
|
|
47
|
+
const homeFromStackDir = (stackDir: string): string =>
|
|
48
|
+
basename(stackDir) === "stack" && basename(dirname(stackDir)) === "config"
|
|
49
|
+
? dirname(dirname(stackDir))
|
|
50
|
+
: stackDir;
|
|
36
51
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
52
|
+
/**
|
|
53
|
+
* Same as `stackEnvPath` but resolved from a `stackDir` for the few callers
|
|
54
|
+
* that only have the stack dir, not full state.
|
|
55
|
+
*/
|
|
56
|
+
export const stackEnvPathFromStackDir = (stackDir: string): string => `${homeFromStackDir(stackDir)}/knowledge/env/stack.env`;
|
|
40
57
|
|
|
41
|
-
// ──
|
|
58
|
+
// ── Operational state directories ───────────────────────────────────────────
|
|
42
59
|
|
|
43
|
-
export const
|
|
44
|
-
export const
|
|
45
|
-
export const
|
|
46
|
-
export const guardianStashDir = (s: ControlPlaneState): string => `${s.stateDir}/guardian/stash`;
|
|
47
|
-
export const guardianAkmDir = (s: ControlPlaneState): string => `${s.stateDir}/guardian/akm`;
|
|
48
|
-
/** Shared akm operational data (data/, state/ — NOT config, which lives in config/akm/) */
|
|
49
|
-
export const akmStateDir = (s: ControlPlaneState): string => `${s.stateDir}/akm`;
|
|
50
|
-
export const taskLogDir = (s: ControlPlaneState, id: string): string => `${s.cacheDir}/akm/tasks/logs/${id}`;
|
|
51
|
-
export const taskLogsRootDir = (s: ControlPlaneState): string => `${s.cacheDir}/akm/tasks/logs`;
|
|
52
|
-
export const logsDir = (s: ControlPlaneState): string => `${s.stateDir}/logs`;
|
|
60
|
+
export const akmCacheDir = (s: ControlPlaneState): string => `${s.dataDir}/akm/cache`;
|
|
61
|
+
export const rollbackDir = (s: ControlPlaneState): string => `${s.dataDir}/rollback`;
|
|
62
|
+
export const logsDir = (s: ControlPlaneState): string => `${s.dataDir}/logs`;
|
|
53
63
|
/**
|
|
54
64
|
* Guardian's own audit log of channel ingress (HMAC verify, replay, rate
|
|
55
65
|
* limit). Phase 6 of the auth/proxy refactor removed the OpenPalm-side
|
|
56
66
|
* `admin-audit.jsonl` — OpenCode session logs are the audit trail for
|
|
57
67
|
* chat + tool activity.
|
|
58
68
|
*/
|
|
59
|
-
export const guardianAuditPath = (s: ControlPlaneState): string => `${s.
|
|
69
|
+
export const guardianAuditPath = (s: ControlPlaneState): string => `${s.dataDir}/logs/guardian-audit.log`;
|
|
60
70
|
/** One-shot 0.11.0 migration log (OP_UI_TOKEN → OPENCODE_SERVER_PASSWORD, endpoints.json move) */
|
|
61
|
-
export const migration0110LogPath = (s: ControlPlaneState): string => `${s.
|
|
62
|
-
export const backupsDir = (s: ControlPlaneState): string => `${s.
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
export const registryAutomationsDir = (s: ControlPlaneState): string => `${s.stateDir}/registry/automations`;
|
|
66
|
-
export const secretsDir = (s: ControlPlaneState): string => `${s.stateDir}/secrets`;
|
|
67
|
-
export const secretProviderPath = (s: ControlPlaneState): string => `${s.stateDir}/secrets/provider.json`;
|
|
68
|
-
export const secretsIndexPath = (s: ControlPlaneState): string => `${s.stateDir}/secrets/plaintext-index.json`;
|
|
69
|
-
export const passStoreDir = (s: ControlPlaneState): string => `${s.stateDir}/secrets/pass-store`;
|
|
71
|
+
export const migration0110LogPath = (s: ControlPlaneState): string => `${s.dataDir}/logs/migration-0.11.0.log`;
|
|
72
|
+
export const backupsDir = (s: ControlPlaneState): string => `${s.dataDir}/backups`;
|
|
73
|
+
|
|
74
|
+
// ── State directory — persistent service data ───────────────────────────────
|
|
70
75
|
|
|
71
|
-
|
|
76
|
+
export const assistantServiceDir = (s: ControlPlaneState): string => `${s.dataDir}/assistant`;
|
|
77
|
+
export const adminServiceDir = (s: ControlPlaneState): string => `${s.dataDir}/admin`;
|
|
78
|
+
export const guardianServiceDir = (s: ControlPlaneState): string => `${s.dataDir}/guardian`;
|
|
79
|
+
export const guardianAkmDir = (s: ControlPlaneState): string => `${s.dataDir}/guardian/akm`;
|
|
80
|
+
/** akm durable data — NOT config, which lives in config/akm/ */
|
|
81
|
+
export const akmDataDir = (s: ControlPlaneState): string => `${s.dataDir}/akm/data`;
|
|
82
|
+
export const taskLogDir = (s: ControlPlaneState, id: string): string => `${s.dataDir}/akm/cache/tasks/logs/${id}`;
|
|
83
|
+
export const taskLogsRootDir = (s: ControlPlaneState): string => `${s.dataDir}/akm/cache/tasks/logs`;
|
|
84
|
+
export const secretsDir = (s: ControlPlaneState): string => `${s.dataDir}/secrets`;
|
|
85
|
+
export const secretProviderPath = (s: ControlPlaneState): string => `${s.dataDir}/secrets/provider.json`;
|
|
86
|
+
export const secretsIndexPath = (s: ControlPlaneState): string => `${s.dataDir}/secrets/plaintext-index.json`;
|
|
87
|
+
export const passStoreDir = (s: ControlPlaneState): string => `${s.dataDir}/secrets/pass-store`;
|
|
72
88
|
|
|
73
|
-
|
|
74
|
-
|
|
89
|
+
// ── Knowledge directory ─────────────────────────────────────────────────────
|
|
90
|
+
// The akm env:user file path (`knowledge/env/user.env`) is owned by
|
|
91
|
+
// `akm-user-env.ts` (`userEnvPathSync`), which also handles its read/write and
|
|
92
|
+
// legacy migration — kept there rather than duplicated as a bare path here.
|
|
75
93
|
|
|
76
94
|
// ── Stack directory ─────────────────────────────────────────────────────────
|
|
77
95
|
|
|
78
96
|
export const coreComposePath = (s: ControlPlaneState): string => `${s.stackDir}/core.compose.yml`;
|
|
79
|
-
export const
|
|
97
|
+
export const servicesComposePath = (s: ControlPlaneState): string => `${s.stackDir}/services.compose.yml`;
|
|
98
|
+
export const channelsComposePath = (s: ControlPlaneState): string => `${s.stackDir}/channels.compose.yml`;
|
|
99
|
+
export const customComposePath = (s: ControlPlaneState): string => `${s.stackDir}/custom.compose.yml`;
|
|
80
100
|
export const addonComposePath = (s: ControlPlaneState, name: string): string => `${s.stackDir}/addons/${name}/compose.yml`;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type HardwareProfileVariant = 'cpu' | 'cuda' | 'rocm';
|
|
2
|
+
|
|
3
|
+
const PROFILE_ID_RE = /^addon\.([a-z0-9-]+)(?:\.(cpu|cuda|rocm))?$/;
|
|
4
|
+
|
|
5
|
+
export function addonProfileId(addon: string, variant: HardwareProfileVariant): string {
|
|
6
|
+
return `addon.${addon}.${variant}`;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function resolveHardwareProfileVariant(profileId: string): HardwareProfileVariant | null {
|
|
10
|
+
return (profileId.match(PROFILE_ID_RE)?.[2] as HardwareProfileVariant | undefined) ?? null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function canonicalAddonProfileSelection(addon: string, profile: string): string {
|
|
14
|
+
const trimmed = profile.trim();
|
|
15
|
+
if (!trimmed) return '';
|
|
16
|
+
|
|
17
|
+
const match = trimmed.match(PROFILE_ID_RE);
|
|
18
|
+
if (!match || match[1] !== addon) return '';
|
|
19
|
+
|
|
20
|
+
return trimmed;
|
|
21
|
+
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Used by the admin capabilities test endpoint and the CLI setup wizard
|
|
5
5
|
* to enumerate the models a configured provider exposes.
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
7
|
+
import { readStackRuntimeEnv } from "./secrets.js";
|
|
8
8
|
import { PROVIDER_DEFAULT_URLS } from "../provider-constants.js";
|
|
9
9
|
|
|
10
10
|
/** Static model list for Anthropic (no listing API available). */
|
|
@@ -24,7 +24,7 @@ const ANTHROPIC_MODELS = [
|
|
|
24
24
|
*
|
|
25
25
|
* - Empty input → empty string.
|
|
26
26
|
* - `env:NAME` form → looks up `NAME` in `process.env` first, then falls back
|
|
27
|
-
|
|
27
|
+
* to `knowledge/secrets/<NAME>` resolved against `stackDir`.
|
|
28
28
|
* - Anything else → returned verbatim (treated as a literal key value).
|
|
29
29
|
*/
|
|
30
30
|
function resolveApiKey(apiKeyRef: string, stackDir: string): string {
|
|
@@ -34,7 +34,7 @@ function resolveApiKey(apiKeyRef: string, stackDir: string): string {
|
|
|
34
34
|
const varName = apiKeyRef.slice(4);
|
|
35
35
|
if (process.env[varName]) return process.env[varName]!;
|
|
36
36
|
|
|
37
|
-
const secrets =
|
|
37
|
+
const secrets = readStackRuntimeEnv(stackDir);
|
|
38
38
|
return secrets[varName] ?? "";
|
|
39
39
|
}
|
|
40
40
|
|