mirror of
https://github.com/openclaw/clawhub.git
synced 2026-08-14 00:47:57 +00:00
fix: gate local Codex workers (#2472)
This commit is contained in:
@@ -103,6 +103,24 @@ CLAWHUB_WORKTREE_SOURCE=/path/to/source/worktree bun run setup:worktree
|
||||
|
||||
The detached server writes runtime state under `.codex/runtime/`. Stop it with `wt --yes stop` before removing the worktree.
|
||||
|
||||
### Local Codex workers
|
||||
|
||||
Local dev does not start Codex-backed workers by default, so `dev:worktree` does
|
||||
not spend Codex quota.
|
||||
|
||||
To process local ClawScan or Skill Card jobs, opt in for that shell:
|
||||
|
||||
```bash
|
||||
CLAWHUB_ALLOW_LOCAL_CODEX_SCAN=1 bun run dev:workers -- --workers security-scan --once
|
||||
CLAWHUB_ALLOW_LOCAL_CODEX_SCAN=1 bun run dev:workers -- --workers skill-card --once
|
||||
```
|
||||
|
||||
Opted-in local runs use an ignored worktree-local `CODEX_HOME` unless you provide
|
||||
one.
|
||||
|
||||
Without those workers, local ClawScan and Skill Card jobs stay pending until you
|
||||
opt in, seed/mock results, or use the production workflows.
|
||||
|
||||
### Seed the database
|
||||
|
||||
Populate local QA fixtures and the committed public corpus so the UI isn't empty:
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
export const LOCAL_CODEX_WORKER_OPT_IN = "CLAWHUB_ALLOW_LOCAL_CODEX_SCAN";
|
||||
|
||||
export function isGitHubActionsRunner(env: NodeJS.ProcessEnv) {
|
||||
return (
|
||||
env.GITHUB_ACTIONS === "true" &&
|
||||
env.CI === "true" &&
|
||||
Boolean(env.GITHUB_RUN_ID?.trim()) &&
|
||||
Boolean(env.GITHUB_REPOSITORY?.trim())
|
||||
);
|
||||
}
|
||||
|
||||
export function isCodexWorkerExecutionAllowed(env: NodeJS.ProcessEnv) {
|
||||
return env[LOCAL_CODEX_WORKER_OPT_IN] === "1" || isGitHubActionsRunner(env);
|
||||
}
|
||||
|
||||
export function localCodexWorkerOptInReason() {
|
||||
return `set ${LOCAL_CODEX_WORKER_OPT_IN}=1 to run Codex workers locally`;
|
||||
}
|
||||
|
||||
export function assertCodexWorkerExecutionAllowed(env: NodeJS.ProcessEnv) {
|
||||
if (isCodexWorkerExecutionAllowed(env)) return;
|
||||
throw new Error(`Refusing to run local Codex workers without ${LOCAL_CODEX_WORKER_OPT_IN}=1`);
|
||||
}
|
||||
|
||||
export function resolveCodexWorkerHome(env: NodeJS.ProcessEnv, fallbackLocalHome: string) {
|
||||
const explicitHome = env.CODEX_HOME?.trim();
|
||||
if (explicitHome) return explicitHome;
|
||||
if (isGitHubActionsRunner(env)) return undefined;
|
||||
if (env[LOCAL_CODEX_WORKER_OPT_IN] === "1") return fallbackLocalHome;
|
||||
return undefined;
|
||||
}
|
||||
@@ -110,15 +110,42 @@ describe("dev-workers", () => {
|
||||
env: {},
|
||||
});
|
||||
|
||||
expect(resolved.workers.map((worker) => worker.id)).toEqual(["security-scan"]);
|
||||
expect(resolved.workers.map((worker) => worker.id)).toEqual([]);
|
||||
expect(resolved.skipped).toEqual([
|
||||
expect.objectContaining({
|
||||
workerId: "security-scan",
|
||||
reason: expect.stringContaining("CLAWHUB_ALLOW_LOCAL_CODEX_SCAN=1"),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
workerId: "skill-card",
|
||||
reason: expect.stringContaining("NVIDIA Skill Card automation checkout"),
|
||||
reason: expect.stringContaining("CLAWHUB_ALLOW_LOCAL_CODEX_SCAN=1"),
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps Codex workers with explicit local opt-in", async () => {
|
||||
const root = await tempDir();
|
||||
const toolDir = join(root, "nvidia-tooling");
|
||||
await mkdir(join(toolDir, "AI Transparency Card Automation", "scripts"), { recursive: true });
|
||||
await writeFile(
|
||||
join(toolDir, "AI Transparency Card Automation", "scripts", "render_card.py"),
|
||||
"print('render')\n",
|
||||
"utf8",
|
||||
);
|
||||
const selected = resolveEnabledWorkers({
|
||||
workers: [],
|
||||
skip: [],
|
||||
});
|
||||
|
||||
const resolved = resolveRunnableWorkers(selected, parseArgs(["--nvidia-tool-dir", toolDir]), {
|
||||
cwd: root,
|
||||
env: { CLAWHUB_ALLOW_LOCAL_CODEX_SCAN: "1" },
|
||||
});
|
||||
|
||||
expect(resolved.workers.map((worker) => worker.id)).toEqual(["security-scan", "skill-card"]);
|
||||
expect(resolved.skipped).toEqual([]);
|
||||
});
|
||||
|
||||
it("keeps the Skill Card worker when an explicit NVIDIA tool checkout exists", async () => {
|
||||
const root = await tempDir();
|
||||
const toolDir = join(root, "nvidia-tooling");
|
||||
@@ -136,7 +163,7 @@ describe("dev-workers", () => {
|
||||
const resolved = resolveRunnableWorkers(
|
||||
selected,
|
||||
parseArgs(["--workers", "skill-card", "--nvidia-tool-dir", toolDir]),
|
||||
{ cwd: root, env: {} },
|
||||
{ cwd: root, env: { CLAWHUB_ALLOW_LOCAL_CODEX_SCAN: "1" } },
|
||||
);
|
||||
|
||||
expect(resolved.workers.map((worker) => worker.id)).toEqual(["skill-card"]);
|
||||
|
||||
@@ -4,6 +4,7 @@ import { existsSync } from "node:fs";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { basename, join, resolve } from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { isCodexWorkerExecutionAllowed, localCodexWorkerOptInReason } from "./codex-worker-guard";
|
||||
|
||||
type DevWorkerId = "security-scan" | "skill-card";
|
||||
|
||||
@@ -13,6 +14,7 @@ type WorkerDefinition = {
|
||||
script: string;
|
||||
requiredEnv: string[];
|
||||
requiredAnyEnv?: string[];
|
||||
requiresCodexCli: boolean;
|
||||
productionWorkflow: string;
|
||||
};
|
||||
|
||||
@@ -36,6 +38,7 @@ export const WORKERS: WorkerDefinition[] = [
|
||||
script: "scripts/security/run-codex-scan-worker.ts",
|
||||
// Shared Convex worker credential used by security and Skill Card workers.
|
||||
requiredEnv: ["CONVEX_URL", "SECURITY_SCAN_WORKER_TOKEN"],
|
||||
requiresCodexCli: true,
|
||||
productionWorkflow: ".github/workflows/security-scan-codex.yml",
|
||||
},
|
||||
{
|
||||
@@ -44,6 +47,7 @@ export const WORKERS: WorkerDefinition[] = [
|
||||
script: "scripts/skill-cards/run-skill-card-worker.ts",
|
||||
// Shared Convex worker credential used by security and Skill Card workers.
|
||||
requiredEnv: ["CONVEX_URL", "SECURITY_SCAN_WORKER_TOKEN"],
|
||||
requiresCodexCli: true,
|
||||
productionWorkflow: ".github/workflows/skill-card-worker.yml",
|
||||
},
|
||||
];
|
||||
@@ -204,6 +208,13 @@ export function resolveRunnableWorkers(
|
||||
const runnable: WorkerDefinition[] = [];
|
||||
const skipped: Array<{ workerId: DevWorkerId; reason: string }> = [];
|
||||
for (const worker of workers) {
|
||||
if (worker.requiresCodexCli && !isCodexWorkerExecutionAllowed(context.env)) {
|
||||
skipped.push({
|
||||
workerId: worker.id,
|
||||
reason: localCodexWorkerOptInReason(),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (worker.id !== "skill-card" || hasNvidiaSkillCardTooling(options, context)) {
|
||||
runnable.push(worker);
|
||||
continue;
|
||||
|
||||
@@ -3,6 +3,12 @@ import { mkdir, mkdtemp, readFile, readdir, rm, writeFile } from "node:fs/promis
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
assertCodexWorkerExecutionAllowed,
|
||||
isCodexWorkerExecutionAllowed,
|
||||
LOCAL_CODEX_WORKER_OPT_IN,
|
||||
resolveCodexWorkerHome,
|
||||
} from "../codex-worker-guard";
|
||||
import {
|
||||
buildPrompt,
|
||||
normalizeSkillSpectorAnalysis,
|
||||
@@ -24,6 +30,54 @@ async function tempDir() {
|
||||
}
|
||||
|
||||
describe("run-codex-scan-worker diagnostics", () => {
|
||||
it("blocks direct local Codex security worker runs without opt-in", () => {
|
||||
expect(isCodexWorkerExecutionAllowed({})).toBe(false);
|
||||
expect(() => assertCodexWorkerExecutionAllowed({})).toThrow(
|
||||
`Refusing to run local Codex workers without ${LOCAL_CODEX_WORKER_OPT_IN}=1`,
|
||||
);
|
||||
});
|
||||
|
||||
it("does not treat a bare GITHUB_ACTIONS flag as CI authorization", () => {
|
||||
expect(isCodexWorkerExecutionAllowed({ GITHUB_ACTIONS: "true" })).toBe(false);
|
||||
expect(() => assertCodexWorkerExecutionAllowed({ GITHUB_ACTIONS: "true" })).toThrow(
|
||||
`Refusing to run local Codex workers without ${LOCAL_CODEX_WORKER_OPT_IN}=1`,
|
||||
);
|
||||
});
|
||||
|
||||
it("allows direct Codex security worker runs in GitHub Actions", () => {
|
||||
const env = {
|
||||
CI: "true",
|
||||
GITHUB_ACTIONS: "true",
|
||||
GITHUB_REPOSITORY: "openclaw/clawhub",
|
||||
GITHUB_RUN_ID: "123",
|
||||
};
|
||||
|
||||
expect(isCodexWorkerExecutionAllowed(env)).toBe(true);
|
||||
expect(() => assertCodexWorkerExecutionAllowed(env)).not.toThrow();
|
||||
});
|
||||
|
||||
it("allows direct local Codex security worker runs with explicit opt-in", () => {
|
||||
expect(isCodexWorkerExecutionAllowed({ [LOCAL_CODEX_WORKER_OPT_IN]: "1" })).toBe(true);
|
||||
expect(() =>
|
||||
assertCodexWorkerExecutionAllowed({ [LOCAL_CODEX_WORKER_OPT_IN]: "1" }),
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
it("uses an isolated local Codex home for opted-in local workers by default", () => {
|
||||
expect(
|
||||
resolveCodexWorkerHome(
|
||||
{ [LOCAL_CODEX_WORKER_OPT_IN]: "1" },
|
||||
"/repo/.codex/runtime/codex-workers/security-scan",
|
||||
),
|
||||
).toBe("/repo/.codex/runtime/codex-workers/security-scan");
|
||||
expect(
|
||||
resolveCodexWorkerHome(
|
||||
{ [LOCAL_CODEX_WORKER_OPT_IN]: "1", CODEX_HOME: "/tmp/custom-codex-home" },
|
||||
"/repo/.codex/runtime/codex-workers/security-scan",
|
||||
),
|
||||
).toBe("/tmp/custom-codex-home");
|
||||
});
|
||||
|
||||
it("frames workspace inspection as discretionary Codex research", () => {
|
||||
const prompt = buildPrompt(
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import { mkdirSync } from "node:fs";
|
||||
import { mkdir, mkdtemp, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { basename, dirname, join, resolve } from "node:path";
|
||||
@@ -12,6 +13,7 @@ import {
|
||||
type LlmEvalDimension,
|
||||
SKILL_SECURITY_EVALUATOR_SYSTEM_PROMPT,
|
||||
} from "../../convex/lib/securityPrompt";
|
||||
import { assertCodexWorkerExecutionAllowed, resolveCodexWorkerHome } from "../codex-worker-guard";
|
||||
|
||||
type ClaimedJob = {
|
||||
job: {
|
||||
@@ -113,6 +115,7 @@ const DEFAULT_DIAGNOSTICS_ROOT = join(
|
||||
".artifacts/codex-security-scan",
|
||||
process.env.GITHUB_RUN_ID ?? `local-${process.pid}`,
|
||||
);
|
||||
const LOCAL_CODEX_HOME = join(root, ".codex/runtime/codex-workers/security-scan");
|
||||
const ARTIFACT_SIGNAL_FILE_EXTENSIONS = new Set([
|
||||
".cjs",
|
||||
".css",
|
||||
@@ -601,6 +604,11 @@ Return the required JSON object only.`;
|
||||
|
||||
function codexEnv() {
|
||||
const env = { ...process.env };
|
||||
const codexHome = resolveCodexWorkerHome(process.env, LOCAL_CODEX_HOME);
|
||||
if (codexHome) {
|
||||
mkdirSync(codexHome, { recursive: true });
|
||||
env.CODEX_HOME = codexHome;
|
||||
}
|
||||
delete env.GH_TOKEN;
|
||||
delete env.GITHUB_TOKEN;
|
||||
delete env.CONVEX_DEPLOY_KEY;
|
||||
@@ -1081,6 +1089,7 @@ async function processJob(
|
||||
|
||||
async function main() {
|
||||
const { batchLimit, maxJobs, maxRuntimeMs, leaseMs, diagnosticsRoot } = parseArgs();
|
||||
assertCodexWorkerExecutionAllowed(process.env);
|
||||
const convexUrl = process.env.CONVEX_URL ?? process.env.VITE_CONVEX_URL;
|
||||
if (!convexUrl) throw new Error("CONVEX_URL or VITE_CONVEX_URL is required");
|
||||
const token = requireEnv("SECURITY_SCAN_WORKER_TOKEN");
|
||||
|
||||
@@ -3,6 +3,11 @@ import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
assertCodexWorkerExecutionAllowed,
|
||||
isCodexWorkerExecutionAllowed,
|
||||
LOCAL_CODEX_WORKER_OPT_IN,
|
||||
} from "../codex-worker-guard";
|
||||
import {
|
||||
applyServerPublisherToContext,
|
||||
assertPublicSkillCardMarkdown,
|
||||
@@ -29,6 +34,13 @@ async function tempDir() {
|
||||
}
|
||||
|
||||
describe("run-skill-card-worker Codex skill setup", () => {
|
||||
it("blocks direct local Skill Card worker runs without Codex opt-in", () => {
|
||||
expect(isCodexWorkerExecutionAllowed({})).toBe(false);
|
||||
expect(() => assertCodexWorkerExecutionAllowed({})).toThrow(
|
||||
`Refusing to run local Codex workers without ${LOCAL_CODEX_WORKER_OPT_IN}=1`,
|
||||
);
|
||||
});
|
||||
|
||||
it("uses the same batch, runtime, and lease defaults as the security worker", () => {
|
||||
expect(DEFAULT_BATCH_LIMIT).toBe(6);
|
||||
expect(DEFAULT_MAX_RUNTIME_MS).toBe(40 * 60 * 1000);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import { mkdirSync } from "node:fs";
|
||||
import { cp, mkdir, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { basename, dirname, join, resolve } from "node:path";
|
||||
@@ -6,6 +7,7 @@ import { pathToFileURL } from "node:url";
|
||||
import { ConvexHttpClient } from "convex/browser";
|
||||
import { api } from "../../convex/_generated/api";
|
||||
import type { Id } from "../../convex/_generated/dataModel";
|
||||
import { assertCodexWorkerExecutionAllowed, resolveCodexWorkerHome } from "../codex-worker-guard";
|
||||
|
||||
type ClaimedSkillCardJob = {
|
||||
job: {
|
||||
@@ -44,6 +46,7 @@ const NVIDIA_AUTOMATION_DIR = "AI Transparency Card Automation";
|
||||
const NVIDIA_SKILL_DIR = "nvidia-skill-card-generator";
|
||||
const SKILL_CARD_CONTEXT_FILE = "skill-card.context.json";
|
||||
const SKILL_CARD_OUTPUT_FILE = "skill-card.md";
|
||||
const LOCAL_CODEX_HOME = join(root, ".codex/runtime/codex-workers/skill-card");
|
||||
const NVIDIA_ONLY_PUBLIC_CARD_PATTERNS = [
|
||||
"NVIDIA believes",
|
||||
"For Release on NVIDIA Platforms Only",
|
||||
@@ -137,6 +140,11 @@ async function download(url: string) {
|
||||
|
||||
function codexEnv() {
|
||||
const env = { ...process.env };
|
||||
const codexHome = resolveCodexWorkerHome(process.env, LOCAL_CODEX_HOME);
|
||||
if (codexHome) {
|
||||
mkdirSync(codexHome, { recursive: true });
|
||||
env.CODEX_HOME = codexHome;
|
||||
}
|
||||
delete env.GH_TOKEN;
|
||||
delete env.GITHUB_TOKEN;
|
||||
delete env.CONVEX_DEPLOY_KEY;
|
||||
@@ -442,6 +450,7 @@ async function processJob(
|
||||
|
||||
async function main() {
|
||||
const { batchLimit, maxJobs, maxRuntimeMs, leaseMs, toolDir } = parseArgs();
|
||||
assertCodexWorkerExecutionAllowed(process.env);
|
||||
const convexUrl = process.env.CONVEX_URL ?? process.env.VITE_CONVEX_URL;
|
||||
if (!convexUrl) throw new Error("CONVEX_URL or VITE_CONVEX_URL is required");
|
||||
const token = workerToken();
|
||||
|
||||
@@ -65,6 +65,14 @@ The copy step is best effort. If `.convex` is already a symlink to the source wo
|
||||
|
||||
Use `wt --yes stop` before removing or recreating a worktree. If a stale pid blocks startup, stop the service and inspect the runtime log before deleting files by hand.
|
||||
|
||||
Local worktree startup must not consume the developer's Codex account
|
||||
implicitly. Workers that invoke Codex CLI, including ClawScan and Skill Card
|
||||
generation, are disabled in local dev unless the process has
|
||||
`CLAWHUB_ALLOW_LOCAL_CODEX_SCAN=1`; GitHub Actions workers remain allowed. When
|
||||
local Codex workers are explicitly enabled, they must default to an ignored
|
||||
worktree-local `CODEX_HOME` under `.codex/runtime/codex-workers/` unless the
|
||||
operator provides `CODEX_HOME`.
|
||||
|
||||
## Seeding Contract
|
||||
|
||||
`bun run seed:dev` uses the same worktree setup helper and the same local Convex readiness checks as the detached dev server. It must remain the documented default seed command. Lower-level Convex calls and `seed:public-corpus` are recovery or fixture-authoring tools, not the first-run path.
|
||||
|
||||
Reference in New Issue
Block a user