@useorgx/openclaw-plugin 0.4.1 → 0.4.3

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 CHANGED
@@ -142,9 +142,13 @@ npm run job:dispatch -- \
142
142
 
143
143
  Key behavior:
144
144
  - Pulls tasks from OrgX for selected workstreams
145
+ - Runs `orgx_spawn_check` preflight per task before dispatch
146
+ - Injects required OrgX skill context (for example `orgx-engineering-agent`) into worker prompts
147
+ - Applies the same spawn-guard + skill-policy enforcement to manual launch, restart, and Next Up fallback dispatch paths
145
148
  - Spawns parallel Codex workers per task
146
149
  - Retries failures with backoff up to `--max_attempts`
147
150
  - Emits activity and task status transitions into OrgX DB
151
+ - Auto-creates a blocking decision when a task exhausts retries (disable with `--decision_on_block=false`)
148
152
  - Persists resumable state to `.orgx-codex-jobs/<job-id>/job-state.json`
149
153
 
150
154
  Notes:
@@ -1,14 +1,76 @@
1
- import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
2
- import { getOrgxPluginConfigDir, getOrgxPluginConfigPath } from "./paths.js";
1
+ import { chmodSync, existsSync, mkdirSync, readFileSync, statSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { getOpenClawDir } from "./paths.js";
3
4
  import { backupCorruptFileSync, writeJsonFileAtomicSync } from "./fs-utils.js";
4
- function configDir() {
5
- return getOrgxPluginConfigDir();
5
+ const PROVIDER_PROFILE_MAP = {
6
+ openaiApiKey: { profileId: "openai-codex", provider: "openai-codex" },
7
+ anthropicApiKey: { profileId: "anthropic", provider: "anthropic" },
8
+ openrouterApiKey: { profileId: "openrouter", provider: "openrouter" },
9
+ };
10
+ function isSafePathSegment(value) {
11
+ const normalized = value.trim();
12
+ if (!normalized || normalized === "." || normalized === "..")
13
+ return false;
14
+ if (normalized.includes("/") || normalized.includes("\\") || normalized.includes("\0")) {
15
+ return false;
16
+ }
17
+ if (normalized.includes(".."))
18
+ return false;
19
+ return true;
20
+ }
21
+ function parseJson(value) {
22
+ try {
23
+ return JSON.parse(value);
24
+ }
25
+ catch {
26
+ return null;
27
+ }
6
28
  }
7
- function byokFile() {
8
- return getOrgxPluginConfigPath("byok.json");
29
+ function readObject(value) {
30
+ return value && typeof value === "object" && !Array.isArray(value)
31
+ ? value
32
+ : {};
9
33
  }
10
- function ensureConfigDir() {
11
- const dir = configDir();
34
+ function resolveDefaultAgentId() {
35
+ try {
36
+ const configPath = join(getOpenClawDir(), "openclaw.json");
37
+ if (!existsSync(configPath))
38
+ return "main";
39
+ const raw = parseJson(readFileSync(configPath, "utf8"));
40
+ const agents = readObject(raw?.agents);
41
+ const list = Array.isArray(agents.list) ? agents.list : [];
42
+ for (const entry of list) {
43
+ if (!entry || typeof entry !== "object")
44
+ continue;
45
+ const row = entry;
46
+ if (row.default !== true)
47
+ continue;
48
+ const id = typeof row.id === "string" ? row.id.trim() : "";
49
+ if (id && isSafePathSegment(id))
50
+ return id;
51
+ }
52
+ for (const entry of list) {
53
+ if (!entry || typeof entry !== "object")
54
+ continue;
55
+ const row = entry;
56
+ const id = typeof row.id === "string" ? row.id.trim() : "";
57
+ if (id && isSafePathSegment(id))
58
+ return id;
59
+ }
60
+ }
61
+ catch {
62
+ // fall through
63
+ }
64
+ return "main";
65
+ }
66
+ function authProfilesDir() {
67
+ return join(getOpenClawDir(), "agents", resolveDefaultAgentId(), "agent");
68
+ }
69
+ function authProfilesFile() {
70
+ return join(authProfilesDir(), "auth-profiles.json");
71
+ }
72
+ function ensureAuthProfilesDir() {
73
+ const dir = authProfilesDir();
12
74
  mkdirSync(dir, { recursive: true, mode: 0o700 });
13
75
  try {
14
76
  chmodSync(dir, 0o700);
@@ -17,12 +79,56 @@ function ensureConfigDir() {
17
79
  // best effort
18
80
  }
19
81
  }
20
- function parseJson(value) {
82
+ function normalizeAuthProfileEntry(value) {
83
+ if (!value || typeof value !== "object")
84
+ return null;
85
+ const row = value;
86
+ const type = typeof row.type === "string" ? row.type.trim() : "";
87
+ const provider = typeof row.provider === "string" ? row.provider.trim() : "";
88
+ const key = typeof row.key === "string" ? row.key.trim() : "";
89
+ if (!type || !provider || !key)
90
+ return null;
91
+ return { type, provider, key };
92
+ }
93
+ function readAuthProfiles() {
94
+ const file = authProfilesFile();
21
95
  try {
22
- return JSON.parse(value);
96
+ if (!existsSync(file))
97
+ return { file, parsed: null };
98
+ const raw = readFileSync(file, "utf8");
99
+ const parsed = parseJson(raw);
100
+ if (!parsed || typeof parsed !== "object") {
101
+ backupCorruptFileSync(file);
102
+ return { file, parsed: null };
103
+ }
104
+ const profilesRaw = parsed.profiles && typeof parsed.profiles === "object"
105
+ ? parsed.profiles
106
+ : {};
107
+ const profiles = {};
108
+ for (const [profileId, entry] of Object.entries(profilesRaw)) {
109
+ const normalized = normalizeAuthProfileEntry(entry);
110
+ if (!normalized)
111
+ continue;
112
+ profiles[profileId] = normalized;
113
+ }
114
+ return {
115
+ file,
116
+ parsed: {
117
+ version: typeof parsed.version === "number" && Number.isFinite(parsed.version)
118
+ ? Math.floor(parsed.version)
119
+ : 1,
120
+ profiles,
121
+ lastGood: parsed.lastGood && typeof parsed.lastGood === "object"
122
+ ? parsed.lastGood
123
+ : undefined,
124
+ usageStats: parsed.usageStats && typeof parsed.usageStats === "object"
125
+ ? parsed.usageStats
126
+ : undefined,
127
+ },
128
+ };
23
129
  }
24
130
  catch {
25
- return null;
131
+ return { file, parsed: null };
26
132
  }
27
133
  }
28
134
  function normalizeKey(value) {
@@ -33,29 +139,29 @@ function normalizeKey(value) {
33
139
  return null;
34
140
  return trimmed;
35
141
  }
142
+ function findProfileKey(profiles, provider) {
143
+ const entries = Object.entries(profiles);
144
+ if (provider === "openai") {
145
+ const codex = entries.find(([, entry]) => entry.provider === "openai-codex");
146
+ if (codex)
147
+ return codex[1].key;
148
+ const openai = entries.find(([, entry]) => entry.provider === "openai");
149
+ return openai?.[1].key ?? null;
150
+ }
151
+ return entries.find(([, entry]) => entry.provider === provider)?.[1].key ?? null;
152
+ }
36
153
  export function readByokKeys() {
37
- const file = byokFile();
154
+ const { file, parsed } = readAuthProfiles();
38
155
  try {
39
- if (!existsSync(file))
40
- return null;
41
- const raw = readFileSync(file, "utf8");
42
- const parsed = parseJson(raw);
43
- if (!parsed) {
44
- backupCorruptFileSync(file);
45
- return null;
46
- }
47
- if (!parsed || typeof parsed !== "object")
156
+ if (!parsed)
48
157
  return null;
49
- const createdAt = typeof parsed.createdAt === "string" && parsed.createdAt.trim().length > 0
50
- ? parsed.createdAt
51
- : new Date().toISOString();
52
- const updatedAt = typeof parsed.updatedAt === "string" && parsed.updatedAt.trim().length > 0
53
- ? parsed.updatedAt
54
- : createdAt;
158
+ const stats = statSync(file);
159
+ const createdAt = stats.birthtime.toISOString();
160
+ const updatedAt = stats.mtime.toISOString();
55
161
  return {
56
- openaiApiKey: normalizeKey(parsed.openaiApiKey) ?? null,
57
- anthropicApiKey: normalizeKey(parsed.anthropicApiKey) ?? null,
58
- openrouterApiKey: normalizeKey(parsed.openrouterApiKey) ?? null,
162
+ openaiApiKey: normalizeKey(findProfileKey(parsed.profiles, "openai")) ?? null,
163
+ anthropicApiKey: normalizeKey(findProfileKey(parsed.profiles, "anthropic")) ?? null,
164
+ openrouterApiKey: normalizeKey(findProfileKey(parsed.profiles, "openrouter")) ?? null,
59
165
  createdAt,
60
166
  updatedAt,
61
167
  };
@@ -65,33 +171,54 @@ export function readByokKeys() {
65
171
  }
66
172
  }
67
173
  export function writeByokKeys(input) {
68
- ensureConfigDir();
69
- const now = new Date().toISOString();
70
- const existing = readByokKeys();
174
+ ensureAuthProfilesDir();
175
+ const existingParsed = readAuthProfiles().parsed;
176
+ const next = existingParsed ?? {
177
+ version: 1,
178
+ profiles: {},
179
+ };
71
180
  const has = (key) => Object.prototype.hasOwnProperty.call(input, key);
72
- const next = {
73
- openaiApiKey: has("openaiApiKey")
74
- ? normalizeKey(input.openaiApiKey)
75
- : existing?.openaiApiKey ?? null,
76
- anthropicApiKey: has("anthropicApiKey")
77
- ? normalizeKey(input.anthropicApiKey)
78
- : existing?.anthropicApiKey ?? null,
79
- openrouterApiKey: has("openrouterApiKey")
80
- ? normalizeKey(input.openrouterApiKey)
81
- : existing?.openrouterApiKey ?? null,
82
- createdAt: existing?.createdAt ?? now,
83
- updatedAt: now,
181
+ const applyKey = (field, value) => {
182
+ const mapped = PROVIDER_PROFILE_MAP[field];
183
+ const normalized = normalizeKey(value);
184
+ if (normalized) {
185
+ next.profiles[mapped.profileId] = {
186
+ type: "api_key",
187
+ provider: mapped.provider,
188
+ key: normalized,
189
+ };
190
+ return;
191
+ }
192
+ delete next.profiles[mapped.profileId];
193
+ if (next.lastGood)
194
+ delete next.lastGood[mapped.profileId];
195
+ if (next.usageStats)
196
+ delete next.usageStats[mapped.profileId];
84
197
  };
85
- const file = byokFile();
198
+ if (has("openaiApiKey"))
199
+ applyKey("openaiApiKey", input.openaiApiKey);
200
+ if (has("anthropicApiKey"))
201
+ applyKey("anthropicApiKey", input.anthropicApiKey);
202
+ if (has("openrouterApiKey"))
203
+ applyKey("openrouterApiKey", input.openrouterApiKey);
204
+ const file = authProfilesFile();
86
205
  writeJsonFileAtomicSync(file, next, 0o600);
87
- return next;
206
+ const updated = readByokKeys();
207
+ if (updated)
208
+ return updated;
209
+ const now = new Date().toISOString();
210
+ return {
211
+ openaiApiKey: null,
212
+ anthropicApiKey: null,
213
+ openrouterApiKey: null,
214
+ createdAt: now,
215
+ updatedAt: now,
216
+ };
88
217
  }
89
218
  export function clearByokKeys() {
90
- const file = byokFile();
91
- try {
92
- rmSync(file, { force: true });
93
- }
94
- catch {
95
- // best effort
96
- }
219
+ writeByokKeys({
220
+ openaiApiKey: null,
221
+ anthropicApiKey: null,
222
+ openrouterApiKey: null,
223
+ });
97
224
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,6 @@
1
+ import { runGatewayWatchdogDaemon } from "./gateway-watchdog.js";
2
+ void runGatewayWatchdogDaemon().catch((err) => {
3
+ const message = err instanceof Error ? err.message : String(err);
4
+ console.error(`[orgx] gateway-watchdog crashed: ${message}`);
5
+ process.exit(1);
6
+ });
@@ -0,0 +1,11 @@
1
+ type Logger = {
2
+ info?: (message: string, meta?: Record<string, unknown>) => void;
3
+ warn?: (message: string, meta?: Record<string, unknown>) => void;
4
+ debug?: (message: string, meta?: Record<string, unknown>) => void;
5
+ };
6
+ export declare function runGatewayWatchdogDaemon(logger?: Logger): Promise<void>;
7
+ export declare function ensureGatewayWatchdog(logger: Logger): {
8
+ started: boolean;
9
+ pid: number | null;
10
+ };
11
+ export {};
@@ -0,0 +1,221 @@
1
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import { spawn } from "node:child_process";
3
+ import { join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { getOpenClawDir } from "./paths.js";
6
+ import { readOpenClawGatewayPort, readOpenClawSettingsSnapshot } from "./openclaw-settings.js";
7
+ const DEFAULT_MONITOR_INTERVAL_MS = 30_000;
8
+ const DEFAULT_FAILURES_BEFORE_RESTART = 2;
9
+ const DEFAULT_PROBE_TIMEOUT_MS = 2_500;
10
+ const WATCHDOG_PID_FILE = join(getOpenClawDir(), "orgx-gateway-watchdog.pid");
11
+ function readEnvNumber(name, fallback, min) {
12
+ const raw = (process.env[name] ?? "").trim();
13
+ if (!raw)
14
+ return fallback;
15
+ const parsed = Number.parseInt(raw, 10);
16
+ if (!Number.isFinite(parsed) || parsed < min)
17
+ return fallback;
18
+ return parsed;
19
+ }
20
+ function isPidAlive(pid) {
21
+ if (!Number.isFinite(pid) || pid <= 0)
22
+ return false;
23
+ try {
24
+ process.kill(pid, 0);
25
+ return true;
26
+ }
27
+ catch {
28
+ return false;
29
+ }
30
+ }
31
+ function readWatchdogPid() {
32
+ try {
33
+ if (!existsSync(WATCHDOG_PID_FILE))
34
+ return null;
35
+ const raw = readFileSync(WATCHDOG_PID_FILE, "utf8").trim();
36
+ const parsed = Number.parseInt(raw, 10);
37
+ if (!Number.isFinite(parsed) || parsed <= 0)
38
+ return null;
39
+ return parsed;
40
+ }
41
+ catch {
42
+ return null;
43
+ }
44
+ }
45
+ function writeWatchdogPid(pid) {
46
+ const dir = getOpenClawDir();
47
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
48
+ writeFileSync(WATCHDOG_PID_FILE, `${pid}\n`, { mode: 0o600 });
49
+ }
50
+ function clearWatchdogPid() {
51
+ try {
52
+ rmSync(WATCHDOG_PID_FILE, { force: true });
53
+ }
54
+ catch {
55
+ // best effort
56
+ }
57
+ }
58
+ async function runCommandCollect(input) {
59
+ const timeoutMs = input.timeoutMs ?? 10_000;
60
+ return await new Promise((resolve, reject) => {
61
+ const child = spawn(input.command, input.args, {
62
+ env: process.env,
63
+ stdio: ["ignore", "pipe", "pipe"],
64
+ });
65
+ let stdout = "";
66
+ let stderr = "";
67
+ const timer = timeoutMs
68
+ ? setTimeout(() => {
69
+ try {
70
+ child.kill("SIGKILL");
71
+ }
72
+ catch {
73
+ // best effort
74
+ }
75
+ reject(new Error(`Command timed out after ${timeoutMs}ms`));
76
+ }, timeoutMs)
77
+ : null;
78
+ child.stdout?.on("data", (chunk) => {
79
+ stdout += chunk.toString("utf8");
80
+ });
81
+ child.stderr?.on("data", (chunk) => {
82
+ stderr += chunk.toString("utf8");
83
+ });
84
+ child.on("error", (err) => {
85
+ if (timer)
86
+ clearTimeout(timer);
87
+ reject(err);
88
+ });
89
+ child.on("close", (code) => {
90
+ if (timer)
91
+ clearTimeout(timer);
92
+ resolve({ stdout, stderr, exitCode: typeof code === "number" ? code : null });
93
+ });
94
+ });
95
+ }
96
+ async function probeGateway(port, timeoutMs) {
97
+ const controller = new AbortController();
98
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
99
+ try {
100
+ // Any HTTP response (including 404) means the gateway port is reachable.
101
+ await fetch(`http://127.0.0.1:${port}/`, {
102
+ method: "GET",
103
+ signal: controller.signal,
104
+ headers: {
105
+ "cache-control": "no-cache",
106
+ },
107
+ });
108
+ return true;
109
+ }
110
+ catch {
111
+ return false;
112
+ }
113
+ finally {
114
+ clearTimeout(timeout);
115
+ }
116
+ }
117
+ async function restartGateway(logger) {
118
+ const restart = await runCommandCollect({
119
+ command: "openclaw",
120
+ args: ["gateway", "restart", "--json"],
121
+ timeoutMs: 30_000,
122
+ });
123
+ if (restart.exitCode === 0) {
124
+ logger.warn?.("[orgx] Gateway watchdog restarted OpenClaw gateway service");
125
+ return;
126
+ }
127
+ const start = await runCommandCollect({
128
+ command: "openclaw",
129
+ args: ["gateway", "start", "--json"],
130
+ timeoutMs: 30_000,
131
+ });
132
+ if (start.exitCode !== 0) {
133
+ throw new Error(start.stderr.trim() || restart.stderr.trim() || "Failed to restart gateway");
134
+ }
135
+ logger.warn?.("[orgx] Gateway watchdog started OpenClaw gateway service");
136
+ }
137
+ export async function runGatewayWatchdogDaemon(logger = console) {
138
+ const monitorIntervalMs = readEnvNumber("ORGX_GATEWAY_WATCHDOG_INTERVAL_MS", DEFAULT_MONITOR_INTERVAL_MS, 5_000);
139
+ const failuresBeforeRestart = readEnvNumber("ORGX_GATEWAY_WATCHDOG_FAILURES", DEFAULT_FAILURES_BEFORE_RESTART, 1);
140
+ const probeTimeoutMs = readEnvNumber("ORGX_GATEWAY_WATCHDOG_TIMEOUT_MS", DEFAULT_PROBE_TIMEOUT_MS, 500);
141
+ let consecutiveFailures = 0;
142
+ let restartInFlight = false;
143
+ const cleanup = () => {
144
+ const pid = readWatchdogPid();
145
+ if (pid === process.pid) {
146
+ clearWatchdogPid();
147
+ }
148
+ };
149
+ process.on("SIGTERM", () => {
150
+ cleanup();
151
+ process.exit(0);
152
+ });
153
+ process.on("SIGINT", () => {
154
+ cleanup();
155
+ process.exit(0);
156
+ });
157
+ process.on("exit", cleanup);
158
+ writeWatchdogPid(process.pid);
159
+ logger.info?.("[orgx] Gateway watchdog daemon started", {
160
+ intervalMs: monitorIntervalMs,
161
+ failuresBeforeRestart,
162
+ });
163
+ const tick = async () => {
164
+ if (restartInFlight)
165
+ return;
166
+ const snapshot = readOpenClawSettingsSnapshot();
167
+ const port = readOpenClawGatewayPort(snapshot.raw);
168
+ const healthy = await probeGateway(port, probeTimeoutMs);
169
+ if (healthy) {
170
+ consecutiveFailures = 0;
171
+ return;
172
+ }
173
+ consecutiveFailures += 1;
174
+ logger.warn?.("[orgx] Gateway watchdog probe failed", {
175
+ port,
176
+ consecutiveFailures,
177
+ threshold: failuresBeforeRestart,
178
+ });
179
+ if (consecutiveFailures < failuresBeforeRestart) {
180
+ return;
181
+ }
182
+ restartInFlight = true;
183
+ try {
184
+ await restartGateway(logger);
185
+ consecutiveFailures = 0;
186
+ }
187
+ catch (err) {
188
+ logger.warn?.("[orgx] Gateway watchdog failed to restart gateway", {
189
+ error: err instanceof Error ? err.message : String(err),
190
+ });
191
+ }
192
+ finally {
193
+ restartInFlight = false;
194
+ }
195
+ };
196
+ await tick();
197
+ setInterval(() => {
198
+ void tick();
199
+ }, monitorIntervalMs);
200
+ }
201
+ export function ensureGatewayWatchdog(logger) {
202
+ if (process.env.ORGX_DISABLE_GATEWAY_WATCHDOG === "1") {
203
+ logger.debug?.("[orgx] Gateway watchdog disabled via ORGX_DISABLE_GATEWAY_WATCHDOG=1");
204
+ return { started: false, pid: null };
205
+ }
206
+ const existing = readWatchdogPid();
207
+ if (existing && isPidAlive(existing)) {
208
+ return { started: false, pid: existing };
209
+ }
210
+ if (existing && !isPidAlive(existing)) {
211
+ clearWatchdogPid();
212
+ }
213
+ const runnerPath = fileURLToPath(new URL("./gateway-watchdog-runner.js", import.meta.url));
214
+ const child = spawn(process.execPath, [runnerPath], {
215
+ env: process.env,
216
+ stdio: "ignore",
217
+ detached: true,
218
+ });
219
+ child.unref();
220
+ return { started: true, pid: child.pid ?? null };
221
+ }