feat: tighten code queue supervision cli
This commit is contained in:
@@ -2,6 +2,7 @@ ARG CODE_QUEUE_BASE_IMAGE=oven/bun:1-debian
|
||||
FROM ${CODE_QUEUE_BASE_IMAGE}
|
||||
|
||||
ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
|
||||
ENV UNIDESK_SKILLS_PATH=/root/.agents/skills
|
||||
|
||||
RUN (command -v codex >/dev/null 2>&1 && command -v opencode >/dev/null 2>&1 && command -v docker >/dev/null 2>&1 && command -v rg >/dev/null 2>&1 && command -v cargo >/dev/null 2>&1 && command -v rustc >/dev/null 2>&1 && command -v rustfmt >/dev/null 2>&1) \
|
||||
|| (apt-get update \
|
||||
@@ -57,6 +58,7 @@ COPY src/components/shared /app/src/components/shared
|
||||
WORKDIR /app/src/components/microservices/code-queue
|
||||
COPY src/components/microservices/code-queue/tsconfig.json ./tsconfig.json
|
||||
COPY src/components/microservices/code-queue/src ./src
|
||||
RUN mkdir -p /root/.agents/skills
|
||||
|
||||
EXPOSE 4222
|
||||
ENTRYPOINT ["tini", "--"]
|
||||
|
||||
@@ -23,6 +23,7 @@ services:
|
||||
CODE_QUEUE_SCHEDULER_ENABLED: "${CODE_QUEUE_SCHEDULER_ENABLED:-true}"
|
||||
CODE_QUEUE_STARTUP_OA_BACKFILL_ENABLED: "${CODE_QUEUE_STARTUP_OA_BACKFILL_ENABLED:-false}"
|
||||
CODE_QUEUE_WORKDIR: "${CODE_QUEUE_WORKDIR:-/workspace}"
|
||||
UNIDESK_SKILLS_PATH: "${UNIDESK_SKILLS_PATH:-/root/.agents/skills}"
|
||||
CODE_QUEUE_CODEX_HOME: "/var/lib/unidesk/code-queue/codex-home"
|
||||
CODE_QUEUE_OPENCODE_XDG_DIR: "/var/lib/unidesk/code-queue/opencode-xdg"
|
||||
CODE_QUEUE_SOURCE_CODEX_CONFIG: "/root/.codex/config.toml"
|
||||
@@ -81,6 +82,7 @@ services:
|
||||
- ${CODE_QUEUE_WORKSPACE_PATH:-/home/ubuntu}:/workspace
|
||||
- ${CODE_QUEUE_CODEX_CONFIG_PATH:-/home/ubuntu/.codex/config.toml}:/root/.codex/config.toml:ro
|
||||
- ${CODE_QUEUE_CODEX_AUTH_PATH:-/home/ubuntu/.codex/auth.json}:/root/.codex/auth.json:ro
|
||||
- ${CODE_QUEUE_SKILLS_PATH:-/home/ubuntu/.agents/skills}:/root/.agents/skills:ro
|
||||
- ${CODE_QUEUE_SSH_DIR:-/home/ubuntu/.ssh}:/root/.ssh:ro
|
||||
- ${CODE_QUEUE_LOG_DIR:-../../../../.state/code-queue/logs}:/var/log/unidesk
|
||||
- ${CODE_QUEUE_STATE_DIR:-../../../../.state/code-queue}:/var/lib/unidesk/code-queue
|
||||
|
||||
@@ -384,6 +384,7 @@ function readConfig(): RuntimeConfig {
|
||||
remoteDefaultWorkdir,
|
||||
executionProviderIds,
|
||||
remoteCodexEnvKeys: envList("CODE_QUEUE_REMOTE_CODEX_ENV_KEYS", ["OPENAI_API_KEY", "CRS_OAI_KEY", "OPENAI_BASE_URL", "OPENAI_API_BASE", "MINIMAX_API_KEY", "MINIMAX_API_BASE", "MINIMAX_MODEL"]),
|
||||
skillsPath: envString("UNIDESK_SKILLS_PATH", "/root/.agents/skills"),
|
||||
codexHome: envString("CODE_QUEUE_CODEX_HOME", "/var/lib/unidesk/code-queue/codex-home"),
|
||||
opencodeXdgDir: envString("CODE_QUEUE_OPENCODE_XDG_DIR", resolve(dataDir, "opencode-xdg")),
|
||||
sourceCodexConfig: envString("CODE_QUEUE_SOURCE_CODEX_CONFIG", "/root/.codex/config.toml"),
|
||||
@@ -2413,6 +2414,74 @@ function runProbe(command: string, args: string[], timeout = 3_000): { ok: boole
|
||||
return { ok: result.status === 0, output };
|
||||
}
|
||||
|
||||
function decodeMountInfoPath(value: string): string {
|
||||
return value.replace(/\\([0-7]{3})/gu, (_match, octal: string) => String.fromCharCode(Number.parseInt(octal, 8)));
|
||||
}
|
||||
|
||||
function mountInfoForPath(path: string): { mountPoint: string | null; readonly: boolean | null } {
|
||||
try {
|
||||
const target = resolve(path);
|
||||
let best: { mountPoint: string; readonly: boolean } | null = null;
|
||||
for (const line of readFileSync("/proc/self/mountinfo", "utf8").split(/\r?\n/u)) {
|
||||
if (line.trim().length === 0) continue;
|
||||
const fields = line.split(" ");
|
||||
const mountPoint = decodeMountInfoPath(fields[4] ?? "");
|
||||
const options = (fields[5] ?? "").split(",");
|
||||
if (mountPoint.length === 0) continue;
|
||||
const matches = target === mountPoint || target.startsWith(mountPoint.endsWith("/") ? mountPoint : `${mountPoint}/`);
|
||||
if (!matches) continue;
|
||||
if (best === null || mountPoint.length > best.mountPoint.length) best = { mountPoint, readonly: options.includes("ro") };
|
||||
}
|
||||
return best ?? { mountPoint: null, readonly: null };
|
||||
} catch {
|
||||
return { mountPoint: null, readonly: null };
|
||||
}
|
||||
}
|
||||
|
||||
function collectSkillsStatus(): JsonValue {
|
||||
const path = config.skillsPath;
|
||||
const exists = existsSync(path);
|
||||
const mountInfo = mountInfoForPath(path);
|
||||
let directory = false;
|
||||
let skillCount = 0;
|
||||
let cliSpecAvailable = false;
|
||||
let readonly = mountInfo.readonly === true;
|
||||
let error: string | null = null;
|
||||
if (exists) {
|
||||
try {
|
||||
const stat = statSync(path);
|
||||
directory = stat.isDirectory();
|
||||
if (directory) {
|
||||
const entries = readdirSync(path, { withFileTypes: true });
|
||||
skillCount = entries.filter((entry) => entry.isDirectory()).length;
|
||||
cliSpecAvailable = existsSync(resolve(path, "cli-spec", "SKILL.md"));
|
||||
if (mountInfo.readonly === null) {
|
||||
const writeProbe = runProbe("sh", ["-lc", `test ! -w ${shellQuote(path)}`], 2_000);
|
||||
readonly = writeProbe.ok;
|
||||
}
|
||||
}
|
||||
} catch (probeError) {
|
||||
error = probeError instanceof Error ? probeError.message : String(probeError);
|
||||
}
|
||||
}
|
||||
const available = exists && directory;
|
||||
return {
|
||||
path,
|
||||
mountPoint: mountInfo.mountPoint,
|
||||
exists,
|
||||
directory,
|
||||
available,
|
||||
readonly,
|
||||
skillCount,
|
||||
cliSpecAvailable,
|
||||
expectedMount: "host ~/.agents/skills mounted read-only to UNIDESK_SKILLS_PATH",
|
||||
repairHint: available && readonly && cliSpecAvailable
|
||||
? null
|
||||
: "DEV code-queue should mount /home/ubuntu/.agents/skills read-only at /root/.agents/skills and set UNIDESK_SKILLS_PATH=/root/.agents/skills.",
|
||||
error,
|
||||
} as unknown as JsonValue;
|
||||
}
|
||||
|
||||
function collectDevReady(): JsonValue {
|
||||
const now = Date.now();
|
||||
if (devReadyCache !== null && now - devReadyCache.checkedAtMs < 30_000) return devReadyCache.value;
|
||||
@@ -2457,7 +2526,9 @@ function collectDevReady(): JsonValue {
|
||||
const sshKeyProbe = runProbe("sh", ["-lc", "test -d /root/.ssh && find /root/.ssh -maxdepth 1 -type f \\( -name 'id_*' ! -name '*.pub' \\) -perm -400 -print -quit"]);
|
||||
const githubKnownHostProbe = runProbe("ssh-keygen", ["-F", "github.com", "-f", "/root/.ssh/known_hosts"]);
|
||||
const sshSharedReady = existsSync("/root/.ssh") && sshKeyProbe.ok && sshKeyProbe.output.trim().length > 0;
|
||||
const ok = missingTools.length === 0 && dockerProbe.ok && composeProbe.ok && workdirExists && dockerSocketExists && codexConfigReady && sshSharedReady;
|
||||
const skills = collectSkillsStatus() as Record<string, JsonValue>;
|
||||
const skillsReady = skills.available === true && skills.readonly === true && skills.cliSpecAvailable === true;
|
||||
const ok = missingTools.length === 0 && dockerProbe.ok && composeProbe.ok && workdirExists && dockerSocketExists && codexConfigReady && sshSharedReady && skillsReady;
|
||||
const value: JsonValue = {
|
||||
ok,
|
||||
missingTools,
|
||||
@@ -2489,6 +2560,7 @@ function collectDevReady(): JsonValue {
|
||||
githubKnownHostPresent: githubKnownHostProbe.ok,
|
||||
ready: sshSharedReady,
|
||||
},
|
||||
skills,
|
||||
};
|
||||
devReadyCache = { checkedAtMs: now, value };
|
||||
return value;
|
||||
@@ -5174,6 +5246,7 @@ async function route(req: Request): Promise<Response> {
|
||||
status: "starting",
|
||||
databaseReady,
|
||||
databaseLastError,
|
||||
skills: collectSkillsStatus(),
|
||||
startedAt: serviceStartedAt,
|
||||
}, 503);
|
||||
return jsonResponse({
|
||||
@@ -5190,6 +5263,7 @@ async function route(req: Request): Promise<Response> {
|
||||
schedulerPollIntervalMs: config.schedulerPollIntervalMs,
|
||||
queue: queueSummary(false, state.tasks),
|
||||
executionDiagnostics: executionDiagnosticsForTasks(state.tasks),
|
||||
skills: collectSkillsStatus(),
|
||||
egressProxy: await providerGatewayEgressProxyStatus(),
|
||||
oaEventPublisher: oaEventPublisherStatus(),
|
||||
startedAt: serviceStartedAt,
|
||||
|
||||
@@ -8,6 +8,7 @@ export const codeQueueEnvironmentHintTitle = "# Code Queue 运行环境提示";
|
||||
export const codeQueueEnvironmentHint = [
|
||||
codeQueueEnvironmentHintTitle,
|
||||
"如果当前 Code Queue Docker 容器缺少完成任务所需的环境、系统包或语言依赖,可以先在容器内临时安装以推进当前任务;同时必须把该依赖补到 `src/components/microservices/code-queue/Dockerfile`,让后续任务重建镜像后可直接使用。",
|
||||
"任务可通过 `UNIDESK_SKILLS_PATH`(默认 `/root/.agents/skills`)读取注入的宿主 skills;若需要确认注入状态,查询 Code Queue `/api/dev-ready` 的 `devReady.skills`,缺失时报告该字段的修复建议,不要读取或输出宿主 token/auth 配置。",
|
||||
].join("\n");
|
||||
|
||||
export function stripAutoReferenceHint(prompt: string): string {
|
||||
|
||||
@@ -123,6 +123,7 @@ export interface RuntimeConfig {
|
||||
remoteDefaultWorkdir: string;
|
||||
executionProviderIds: string[];
|
||||
remoteCodexEnvKeys: string[];
|
||||
skillsPath: string;
|
||||
codexHome: string;
|
||||
opencodeXdgDir: string;
|
||||
sourceCodexConfig: string;
|
||||
|
||||
@@ -329,6 +329,8 @@ spec:
|
||||
value: danger-full-access
|
||||
- name: CODE_QUEUE_APPROVAL_POLICY
|
||||
value: never
|
||||
- name: UNIDESK_SKILLS_PATH
|
||||
value: /root/.agents/skills
|
||||
- name: CODE_QUEUE_EGRESS_PROXY_ENABLED
|
||||
value: "true"
|
||||
- name: CODE_QUEUE_EGRESS_PROXY_URL
|
||||
@@ -377,6 +379,9 @@ spec:
|
||||
- name: codex-auth
|
||||
mountPath: /root/.codex/auth.json
|
||||
readOnly: true
|
||||
- name: skills-dir
|
||||
mountPath: /root/.agents/skills
|
||||
readOnly: true
|
||||
- name: ssh-dir
|
||||
mountPath: /root/.ssh
|
||||
readOnly: true
|
||||
@@ -433,6 +438,10 @@ spec:
|
||||
hostPath:
|
||||
path: /home/ubuntu/.codex/auth.json
|
||||
type: File
|
||||
- name: skills-dir
|
||||
hostPath:
|
||||
path: /home/ubuntu/.agents/skills
|
||||
type: Directory
|
||||
- name: ssh-dir
|
||||
hostPath:
|
||||
path: /home/ubuntu/.ssh
|
||||
@@ -531,6 +540,8 @@ spec:
|
||||
value: "2"
|
||||
- name: CODE_QUEUE_EGRESS_PROXY_ENABLED
|
||||
value: "false"
|
||||
- name: UNIDESK_SKILLS_PATH
|
||||
value: /root/.agents/skills
|
||||
- name: CODE_QUEUE_NOTIFY_CLAUDEQQ_ENABLED
|
||||
value: "false"
|
||||
- name: CODE_QUEUE_CODEX_SQLITE_LOG_EXPORT_ENABLED
|
||||
@@ -543,6 +554,9 @@ spec:
|
||||
- name: repo
|
||||
mountPath: /root/unidesk
|
||||
readOnly: true
|
||||
- name: skills-dir
|
||||
mountPath: /root/.agents/skills
|
||||
readOnly: true
|
||||
- name: logs
|
||||
mountPath: /var/log/unidesk-dev
|
||||
- name: state
|
||||
@@ -580,6 +594,10 @@ spec:
|
||||
hostPath:
|
||||
path: /home/ubuntu/unidesk-dev-code-queue-deploy/code-queue
|
||||
type: Directory
|
||||
- name: skills-dir
|
||||
hostPath:
|
||||
path: /home/ubuntu/.agents/skills
|
||||
type: Directory
|
||||
- name: logs
|
||||
hostPath:
|
||||
path: /home/ubuntu/unidesk-dev-code-queue-deploy/state/logs
|
||||
@@ -674,6 +692,8 @@ spec:
|
||||
value: "2"
|
||||
- name: CODE_QUEUE_EGRESS_PROXY_ENABLED
|
||||
value: "false"
|
||||
- name: UNIDESK_SKILLS_PATH
|
||||
value: /root/.agents/skills
|
||||
- name: CODE_QUEUE_NOTIFY_CLAUDEQQ_ENABLED
|
||||
value: "false"
|
||||
- name: CODE_QUEUE_CODEX_SQLITE_LOG_EXPORT_ENABLED
|
||||
@@ -686,6 +706,9 @@ spec:
|
||||
- name: repo
|
||||
mountPath: /root/unidesk
|
||||
readOnly: true
|
||||
- name: skills-dir
|
||||
mountPath: /root/.agents/skills
|
||||
readOnly: true
|
||||
- name: logs
|
||||
mountPath: /var/log/unidesk-dev
|
||||
- name: state
|
||||
@@ -723,6 +746,10 @@ spec:
|
||||
hostPath:
|
||||
path: /home/ubuntu/unidesk-dev-code-queue-deploy/code-queue
|
||||
type: Directory
|
||||
- name: skills-dir
|
||||
hostPath:
|
||||
path: /home/ubuntu/.agents/skills
|
||||
type: Directory
|
||||
- name: logs
|
||||
hostPath:
|
||||
path: /home/ubuntu/unidesk-dev-code-queue-deploy/state/logs
|
||||
|
||||
Reference in New Issue
Block a user