From d53b17f6f171e058aaf6992e64c32d53b69e1db8 Mon Sep 17 00:00:00 2001 From: sunlei Date: Thu, 25 Jun 2026 02:20:47 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0NAS=20Codex=E7=8E=AF?= =?UTF-8?q?=E5=A2=83=E5=BC=95=E5=AF=BC=E5=B7=A5=E5=85=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 6 +- src/selfTest.ts | 75 +++++++++++++ src/tools/napcatAutomation.ts | 192 +++++++++++++++++++++++++++++----- 3 files changed, 246 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 8720c62..3c1ed44 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ - 生成接口测试计划:接口改动后输出真实调用命令和统一返回结构断言。 - 生成业务链路测试计划:固化 Admin 登录、博客 CRUD、QQBot 扫码/自动回复、更新登录 SSE、FFLogs 命令、系统日志可视化、Web/Playground 回跳。 - 静态检查 NapCat 设备身份护栏:确认 API 仍保留 QQNT 可见 hostname、实体 OUI 风格 MAC、QQNT `machine-info` 写入、持久化 runtime dir 和 `DB_TIMEZONE` 默认值。 -- 生成 NapCatQQ 上游同步与 runtime 发布自动化入口:MCP/CLI 默认 dry-run,覆盖 upstream audit、sync candidate review、runtime readiness、remote dev handoff 和 NAS Codex bootstrap plan;除 upstream audit 在显式 `execute=true` 且 `useCodex=true` 时可运行 Codex 外,其余 Task 5 入口只返回 prompt/context/只读命令骨架。 +- 生成 NapCatQQ 上游同步与 runtime 发布自动化入口:MCP/CLI 默认 dry-run,覆盖 upstream audit、sync candidate review、runtime readiness、remote dev handoff 和 NAS Codex bootstrap plan;upstream audit 在显式 `execute=true` 且 `useCodex=true` 时可运行 Codex,NAS Codex bootstrap 在显式 `execute=true` 时只返回目录准备和 CLI 包检查/安装计划,不复制 secrets、不写 systemd、不触发发布。 - 生成卡点固化记录:把超时、卡进程、远程命令误写、重复失败整理成“问题点 / 稳定解法 / 后续入口 / 验证证据”,避免原样重试。 - 生成改动文档同步计划:按变更文件自动提示需要同步的 README、API、AGENTS、docs、Obsidian、skill 和 ktWorkflow 入口。 - 生成多仓库提交/推送计划:按仓库分组、建议提交信息、列出提交和推送前检查。 @@ -105,7 +105,7 @@ pnpm run admin-login -- --url http://127.0.0.1:5999/#/auth/login | `kt_napcat_sync_candidate_review` | 生成 NapCat sync candidate review 的 prompt/context/只读命令骨架,Task 5 不执行 Codex 或 Git 写操作 | | `kt_napcat_runtime_release_readiness` | 生成 NapCat runtime release readiness 的 prompt/context/只读检查骨架,不自动构建或发布 | | `kt_napcat_remote_dev_handoff` | 生成 NapCat 自动化远程开发 handoff 的 prompt/context 骨架,不创建线程或同步仓库 | -| `kt_nas_codex_bootstrap_plan` | 生成可信 NAS Codex bootstrap dry-run plan,Task 5 仅输出只读检查和占位计划 | +| `kt_nas_codex_bootstrap_plan` | 生成可信 NAS Codex bootstrap dry-run/execute command plan;execute 只规划目录准备和 CLI 包检查/安装,不复制 secrets、不写 systemd | | `kt_napcat_device_profile_check` | 静态检查 API NapCat 设备身份护栏,覆盖 hostname、MAC、`machine-info`、runtime dir 和 DB timezone | | `kt_blocker_resolution` | 生成或写入卡点固化记录,提醒停止原样重试 | | `kt_remote_health_check` | 生成或执行远程只读健康检查命令 | @@ -128,7 +128,7 @@ pnpm run admin-login -- --url http://127.0.0.1:5999/#/auth/login | `pnpm run napcat-sync-candidate-review` | 输出 sync candidate review 的 prompt/context/只读命令骨架,不执行 Codex 或 Git 写操作。 | | `pnpm run napcat-runtime-release-readiness` | 输出 runtime release readiness 的 prompt/context/只读检查骨架,不构建镜像、不调用 kubectl、不发布。 | | `pnpm run napcat-remote-dev-handoff` | 输出远程开发 handoff 的 prompt/context 骨架,不创建线程、不同步仓库、不执行 Codex。 | -| `pnpm run nas-codex-bootstrap` | 输出可信 NAS Codex bootstrap dry-run plan;真实安装、同步和 systemd 接入留给后续实现。 | +| `pnpm run nas-codex-bootstrap` | 输出可信 NAS Codex bootstrap dry-run plan;传 `--execute` 只生成目录准备和 CLI 包检查/安装命令计划,不真实执行 SSH、不复制 secrets、不写 systemd。 | | `pnpm run napcat-device-profile-check` | 静态检查 API NapCat 设备身份护栏,防止回退到未知设备风险配置。 | | `pnpm run deploy-observation` | 默认 dry-run 输出只读 NAS 观测命令;传 `--execute` 时执行 Jenkins/K8s/health/smoke 观测并写入 `.kt-workspace/test-artifacts/deploy-observation`。 | | `pnpm run obsidian-context` | 输出 Obsidian 索引上下文;可传 `--module Admin`、`--query QQBot`、`--max-documents 10`。 | diff --git a/src/selfTest.ts b/src/selfTest.ts index 715d9e8..bb383c0 100644 --- a/src/selfTest.ts +++ b/src/selfTest.ts @@ -209,6 +209,81 @@ export async function runSelfTest(): Promise { ) { throw new Error("NAS Codex bootstrap default alignment self-check failed"); } + const bootstrap = buildNasCodexBootstrapPlan({ execute: false }); + const bootstrapCommandText = bootstrap.commands + .map((item) => item.command) + .join("\n"); + if (!bootstrapCommandText.includes("/vol1/docker/kt-codex/home/.codex")) { + throw new Error("NAS Codex bootstrap CODEX_HOME self-check failed"); + } + if (bootstrapCommandText.includes("C:\\Users")) { + throw new Error("NAS Codex bootstrap Windows path guard self-check failed"); + } + for (const expected of [ + "node --version", + "corepack --version", + "pnpm --version", + "git --version", + "docker --version", + "codex --version", + "test -d /vol1/docker/kt-codex/workspace/KT", + "test -d /vol1/docker/kt-codex/home/.codex", + ]) { + if (!bootstrapCommandText.includes(expected)) { + throw new Error(`NAS Codex bootstrap dry-run command missing: ${expected}`); + } + } + for (const command of bootstrap.commands) { + if (!command.readOnly) { + throw new Error("NAS Codex bootstrap dry-run command must be read-only"); + } + } + const executeBootstrap = buildNasCodexBootstrapPlan({ execute: true }); + const executeBootstrapCommandText = executeBootstrap.commands + .map((item) => item.command) + .join("\n"); + if ( + executeBootstrap.execute !== true || + executeBootstrap.commands.every((command) => command.readOnly) + ) { + throw new Error("NAS Codex bootstrap execute plan self-check failed"); + } + if ( + !executeBootstrapCommandText.includes("install -d -m 700 /vol1/docker/kt-codex/home/.codex") || + !executeBootstrapCommandText.includes("npm install -g @openai/codex") + ) { + throw new Error("NAS Codex bootstrap execute package plan self-check failed"); + } + for (const forbidden of [ + "git push", + "git merge", + "docker build", + "docker pull", + "kubectl", + "systemctl enable", + "systemctl start", + "cp ~/.codex", + "scp ~/.codex", + "C:\\Users", + ]) { + if (executeBootstrapCommandText.includes(forbidden)) { + throw new Error(`NAS Codex bootstrap forbidden command leaked: ${forbidden}`); + } + } + let windowsBootstrapPathRejected = false; + try { + buildNasCodexBootstrapPlan({ + codexHome: "C:\\Users\\example\\.codex", + execute: false, + }); + } catch (error) { + windowsBootstrapPathRejected = String(error).includes( + "Unsafe NAS Codex bootstrap path", + ); + } + if (!windowsBootstrapPathRejected) { + throw new Error("NAS Codex bootstrap unsafe Windows path self-check failed"); + } const napcatCandidateOverride = buildNapcatSyncCandidateReview({ candidateBranch: "custom-candidate", lastAcceptedUpstreamBase: "custom-base", diff --git a/src/tools/napcatAutomation.ts b/src/tools/napcatAutomation.ts index c2eb6bb..fdf6bbc 100644 --- a/src/tools/napcatAutomation.ts +++ b/src/tools/napcatAutomation.ts @@ -333,21 +333,54 @@ export function buildNapcatRemoteDevHandoff( }); } +interface NasCodexBootstrapContext { + codexHome: string; + serviceName: string; + workspaceRoot: string; +} + /** - * Builds a non-mutating NAS Codex bootstrap plan placeholder for later automation tasks. - * @param input - Trusted NAS workspace/Codex-home hints and optional service name; execution is intentionally not implemented in Task 5. - * @returns A dry-run bootstrap checklist and read-only verification commands. + * Builds a safe NAS Codex bootstrap command plan without running SSH locally. + * @param input - Trusted NAS workspace/Codex-home hints, optional service name, and explicit execution intent. + * @returns A dry-run or execute-mode command plan; execute mode only emits bounded NAS preparation commands for a trusted operator to run. */ export function buildNasCodexBootstrapPlan(input: NasCodexBootstrapPlanInput) { const artifactRoot = input.artifactRoot?.trim() || `${napcatAutomationSkeletonArtifactRoot}/nas-codex-bootstrap`; - const context = { - codexHome: input.codexHome ?? napcatAutomationDefaults.codexHome, - serviceName: input.serviceName ?? napcatAutomationDefaults.bootstrapServiceName, - workspaceRoot: input.workspaceRoot ?? napcatAutomationDefaults.workspaceRoot, + const context: NasCodexBootstrapContext = { + codexHome: resolveNasBootstrapPath( + 'codexHome', + input.codexHome, + napcatAutomationDefaults.codexHome, + ), + serviceName: resolveNasBootstrapServiceName( + input.serviceName ?? napcatAutomationDefaults.bootstrapServiceName, + ), + workspaceRoot: resolveNasBootstrapPath( + 'workspaceRoot', + input.workspaceRoot, + napcatAutomationDefaults.workspaceRoot, + ), }; const requestedExecute = input.execute === true; + const commands = [ + { + command: buildNasBootstrapDryRunCommand(context), + description: + 'Run bounded read-only NAS checks for Linux tools, KT workspace, and trusted CODEX_HOME.', + readOnly: true, + }, + ]; + + if (requestedExecute) { + commands.push({ + command: buildNasBootstrapExecuteCommand(context), + description: + 'Create trusted NAS Codex directories and install/check CLI packages without touching business repos, secrets, deploys, or systemd.', + readOnly: false, + }); + } return { artifacts: [ @@ -360,32 +393,143 @@ export function buildNasCodexBootstrapPlan(input: NasCodexBootstrapPlanInput) { type: 'json', }, ], - commands: [ - { - command: 'ssh nas "command -v codex && codex --version"', - description: 'Read the trusted NAS Codex CLI version without installing or changing files.', - readOnly: true, - }, - { - command: - 'ssh nas "test -d /vol1/docker/kt-codex/workspace/KT && git -C /vol1/docker/kt-codex/workspace/KT status --short --branch"', - description: 'Read the trusted NAS KT workspace state without syncing or committing.', - readOnly: true, - }, - ], + commands, context, - execute: false, + execute: requestedExecute, notes: [ - 'Task 5 only exposes a dry-run bootstrap plan. Task 6 must implement real NAS bootstrap logic before any mutation is allowed.', + 'The builder only returns command plans; it does not open SSH sessions or mutate NAS state by itself.', + 'Dry-run mode emits Linux-only version and directory checks and never copies Codex secrets.', requestedExecute - ? 'execute=true was requested, but no bootstrap mutation is performed by this placeholder.' + ? 'execute=true adds a bounded preparation plan that may create trusted directories and install/check Node/Corepack/pnpm/Codex packages, but still does not push, merge, deploy, edit business repos, copy secrets, or write systemd.' : 'execute defaults to false.', ], requestedExecute, - summary: 'NAS Codex bootstrap dry-run plan generated.', + summary: requestedExecute + ? 'NAS Codex bootstrap execute-mode command plan generated.' + : 'NAS Codex bootstrap dry-run plan generated.', }; } +/** + * Resolves and validates a NAS bootstrap Linux path before embedding it in remote shell text. + * @param label - Human-readable input name used in validation errors. + * @param value - Optional caller override for a trusted NAS filesystem path. + * @param fallback - Stabilized default path used when no override is provided. + * @returns A normalized absolute Linux path under `/vol1/docker/kt-codex`. + */ +function resolveNasBootstrapPath( + label: string, + value: string | undefined, + fallback: string, +): string { + const resolved = (value?.trim() || fallback).replace(/\/+$/, ''); + if ( + /^[a-zA-Z]:[\\/]/.test(resolved) || + resolved.includes('\\') || + resolved.includes('~') || + resolved.includes('$') || + /[\r\n]/.test(resolved) || + !/^\/[A-Za-z0-9._/-]+$/.test(resolved) || + !resolved.startsWith('/vol1/docker/kt-codex/') || + hasParentPathSegment(resolved) + ) { + throw new Error(`Unsafe NAS Codex bootstrap path "${label}"`); + } + return resolved; +} + +/** + * Validates the future service label while Task 6 deliberately avoids writing systemd units. + * @param value - Service name from CLI/MCP input or the stabilized default. + * @returns A compact service label safe to carry in JSON context. + */ +function resolveNasBootstrapServiceName(value: string): string { + const resolved = value.trim(); + if (!/^[A-Za-z0-9._-]+$/.test(resolved)) { + throw new Error('Unsafe NAS Codex bootstrap serviceName'); + } + return resolved; +} + +/** + * Builds the bounded PowerShell-to-NAS here-string wrapper used by bootstrap plans. + * @param bodyLines - Linux shell commands that will run on NAS only when a human executes the returned plan. + * @param timeoutSeconds - Remote `timeout` budget that prevents hanging checks or installs. + * @returns A copy-pastable PowerShell command using the KT NAS CRLF-safe SSH pattern. + */ +function buildNasBootstrapSshPlanCommand( + bodyLines: string[], + timeoutSeconds: number, +): string { + return [ + "$remoteScript = @'", + 'set -euo pipefail', + ...bodyLines, + "'@", + `$remoteScript | ssh nas "tr -d '\\015' | timeout ${timeoutSeconds}s bash -s"`, + ].join('\n'); +} + +/** + * Builds the read-only NAS Codex bootstrap dry-run command required by Task 6. + * @param context - Trusted NAS workspace and CODEX_HOME paths. + * @returns A bounded SSH here-string containing Linux-only version and directory checks. + */ +function buildNasBootstrapDryRunCommand( + context: NasCodexBootstrapContext, +): string { + return buildNasBootstrapSshPlanCommand( + [ + "printf '%s\\n' '[ktWorkflow] NAS Codex bootstrap dry-run checks'", + 'node --version', + 'corepack --version', + 'pnpm --version', + 'git --version', + 'docker --version', + 'codex --version', + `test -d ${context.workspaceRoot}`, + `test -d ${context.codexHome}`, + `git -C ${context.workspaceRoot} status --short --branch`, + ], + 120, + ); +} + +/** + * Builds the explicit execute-mode NAS preparation plan without running it locally. + * @param context - Trusted NAS workspace, Codex home, and service label context. + * @returns A bounded SSH here-string that creates only kt-codex directories and checks/installs CLI packages. + */ +function buildNasBootstrapExecuteCommand( + context: NasCodexBootstrapContext, +): string { + const workspaceParent = path.posix.dirname(context.workspaceRoot); + + return buildNasBootstrapSshPlanCommand( + [ + "printf '%s\\n' '[ktWorkflow] NAS Codex bootstrap execute plan'", + `install -d -m 755 ${workspaceParent}`, + `install -d -m 700 ${context.codexHome}`, + `install -d -m 755 ${napcatNasCodexArtifactRoot}`, + 'node --version', + 'corepack --version', + 'corepack enable', + 'corepack prepare pnpm@10.28.2 --activate', + 'pnpm --version', + 'git --version', + 'docker --version', + 'if ! command -v codex >/dev/null 2>&1; then', + ' npm install -g @openai/codex', + 'fi', + 'codex --version', + `test -d ${context.workspaceRoot}`, + `test -d ${context.codexHome}`, + `git -C ${context.workspaceRoot} status --short --branch`, + ], + 300, + ); +} + /** * Quotes a POSIX shell path argument for NAS-side Codex automation commands. * @param value - Absolute or repo-relative filesystem path that will be interpreted by a Linux shell.