@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.
- package/README.md +4 -2
- package/package.json +11 -3
- package/src/control-plane/akm-vault.test.ts +105 -0
- package/src/control-plane/akm-vault.ts +311 -0
- package/src/control-plane/channels.ts +11 -9
- package/src/control-plane/cleanup-guardrails.test.ts +8 -9
- package/src/control-plane/compose-args.test.ts +25 -33
- package/src/control-plane/compose-args.ts +0 -4
- package/src/control-plane/compose-errors.test.ts +106 -0
- package/src/control-plane/compose-errors.ts +117 -0
- package/src/control-plane/config-persistence.ts +148 -73
- package/src/control-plane/core-assets.test.ts +104 -0
- package/src/control-plane/core-assets.ts +111 -58
- package/src/control-plane/docker.ts +70 -25
- package/src/control-plane/env.test.ts +25 -1
- package/src/control-plane/env.ts +84 -1
- package/src/control-plane/home.ts +66 -69
- package/src/control-plane/host-opencode.test.ts +260 -0
- package/src/control-plane/host-opencode.ts +229 -0
- package/src/control-plane/install-edge-cases.test.ts +190 -292
- package/src/control-plane/install-lock.ts +157 -0
- package/src/control-plane/lifecycle.ts +65 -75
- package/src/control-plane/markdown-task.ts +200 -0
- package/src/control-plane/migrate-0110.test.ts +177 -0
- package/src/control-plane/migrate-0110.ts +99 -0
- package/src/control-plane/operator-ids.test.ts +130 -0
- package/src/control-plane/operator-ids.ts +89 -0
- package/src/control-plane/paths.ts +80 -0
- package/src/control-plane/provider-models.ts +154 -0
- package/src/control-plane/registry-components.test.ts +105 -27
- package/src/control-plane/registry.test.ts +247 -51
- package/src/control-plane/registry.ts +404 -54
- package/src/control-plane/rollback.ts +17 -16
- package/src/control-plane/scheduler.ts +75 -262
- package/src/control-plane/secret-mappings.ts +4 -8
- package/src/control-plane/secrets.ts +97 -55
- package/src/control-plane/setup-config.schema.json +5 -17
- package/src/control-plane/setup-status.ts +9 -29
- package/src/control-plane/setup-validation.ts +23 -23
- package/src/control-plane/setup.test.ts +143 -244
- package/src/control-plane/setup.ts +216 -133
- package/src/control-plane/skeleton-guardrail.test.ts +151 -0
- package/src/control-plane/spec-to-env.test.ts +75 -60
- package/src/control-plane/spec-to-env.ts +68 -153
- package/src/control-plane/stack-spec.test.ts +22 -84
- package/src/control-plane/stack-spec.ts +9 -89
- package/src/control-plane/types.ts +9 -29
- package/src/control-plane/ui-assets.ts +385 -0
- package/src/control-plane/validate.ts +44 -79
- package/src/index.ts +102 -56
- package/src/logger.test.ts +228 -0
- package/src/logger.ts +71 -1
- package/src/provider-constants.ts +22 -1
- package/src/control-plane/audit.ts +0 -40
- package/src/control-plane/env-schema-validation.test.ts +0 -118
- package/src/control-plane/lock.test.ts +0 -194
- package/src/control-plane/lock.ts +0 -176
- package/src/control-plane/memory-config.ts +0 -298
- package/src/control-plane/provider-config.ts +0 -34
- package/src/control-plane/redact-schema.ts +0 -50
- package/src/control-plane/secret-backend.test.ts +0 -359
- package/src/control-plane/secret-backend.ts +0 -322
- 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
|
|
15
|
-
*
|
|
14
|
+
* Only config/ system files are included — user-editable config files
|
|
15
|
+
* are never overwritten by lifecycle operations. */
|
|
16
16
|
const SNAPSHOT_FILES = [
|
|
17
|
-
"
|
|
18
|
-
"
|
|
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
|
-
/**
|
|
2
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
80
|
-
|
|
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
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
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
|
-
|
|
259
|
-
|
|
260
|
-
{ env:
|
|
97
|
+
"akm",
|
|
98
|
+
["tasks", "sync"],
|
|
99
|
+
{ env: { ...process.env, ...akmEnv } },
|
|
261
100
|
(error, _stdout, stderr) => {
|
|
262
101
|
if (error) {
|
|
263
|
-
reject(new Error(
|
|
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
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
})
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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
|
|
33
|
-
{ secretKey: 'openpalm/
|
|
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.
|
|
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.
|
|
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
|
}
|