Compare commits

...

20 Commits

Author SHA1 Message Date
1abfcbf59d test: 加固NapCat上游审计Jenkins门禁 2026-06-25 04:03:21 +08:00
f9ce9b6015 fix: 修正NapCat运行时发布确认门禁 2026-06-25 03:51:46 +08:00
36da962bb0 fix: 收紧NapCatQQ自动化Jenkins入口 2026-06-25 03:46:22 +08:00
4fc7c4a776 docs: 增加NapCatQQ自动化Jenkins入口 2026-06-25 03:26:01 +08:00
d53b17f6f1 feat: 增加NAS Codex环境引导工具 2026-06-25 02:20:47 +08:00
30222fd85e fix: 透传NapCat自动化命令覆盖参数 2026-06-25 02:04:45 +08:00
fc750ddcf1 fix: 对齐NapCat自动化入口默认值 2026-06-25 01:52:13 +08:00
6fc5e65986 test: 覆盖NapCat审计artifactRoot解析 2026-06-25 01:31:55 +08:00
b767732fd3 feat: 接入NapCat自动化MCP工具和CLI 2026-06-25 01:24:31 +08:00
c16a867c60 fix: 加固NapCat Codex审计执行器 2026-06-25 01:04:57 +08:00
3f7ed99aec feat: 封装NapCat Codex审计执行器 2026-06-25 00:47:17 +08:00
99408d3cec fix: 补强NapCat审计Windows路径拒绝 2026-06-25 00:30:20 +08:00
7619dd5928 fix: 强化NapCat审计只读与路径校验 2026-06-25 00:22:49 +08:00
1f60506c89 fix: 补齐NapCat上游审计干合并探测 2026-06-25 00:08:05 +08:00
4b18c1bec8 feat: 增加NapCat上游审计采集器 2026-06-25 00:00:59 +08:00
d5e9bf5b01 fix: 收紧NapCat提示词加载边界 2026-06-24 23:47:19 +08:00
e29ce3be86 feat: 增加NapCat自动化提示词包 2026-06-24 23:36:40 +08:00
9052ecbc2a fix: 收紧NapCat审计输出契约 2026-06-24 23:24:56 +08:00
efc6f50199 feat: 定义NapCat自动化审计契约 2026-06-24 23:12:21 +08:00
32d02b5a8f chore: 更新NapCat登录诊断与设备护栏 2026-06-23 20:20:31 +08:00
20 changed files with 3164 additions and 11 deletions

View File

@ -15,7 +15,8 @@
- 生成页面测试用例:内置“先写用例、可视化证据、事不过三”的测试闭环。
- 生成接口测试计划:接口改动后输出真实调用命令和统一返回结构断言。
- 生成业务链路测试计划:固化 Admin 登录、博客 CRUD、QQBot 扫码/自动回复、更新登录 SSE、FFLogs 命令、系统日志可视化、Web/Playground 回跳。
- 静态检查 NapCat 设备身份护栏:确认 API 仍保留 QQNT 可见 hostname、稳定 `02:42:*` MAC、QQNT `machine-info` 写入、持久化 runtime dir 和 `DB_TIMEZONE` 默认值。
- 静态检查 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 planupstream audit 在显式 `execute=true``useCodex=true` 时可运行 CodexNAS Codex bootstrap 在显式 `execute=true` 时只返回目录准备和 CLI 包检查/安装计划,不复制 secrets、不写 systemd、不触发发布。
- 生成卡点固化记录:把超时、卡进程、远程命令误写、重复失败整理成“问题点 / 稳定解法 / 后续入口 / 验证证据”,避免原样重试。
- 生成改动文档同步计划:按变更文件自动提示需要同步的 README、API、AGENTS、docs、Obsidian、skill 和 ktWorkflow 入口。
- 生成多仓库提交/推送计划:按仓库分组、建议提交信息、列出提交和推送前检查。
@ -40,6 +41,11 @@ pnpm run obsidian-context -- --module ktWorkflow
pnpm run obsidian-validate
pnpm run obsidian-sync
pnpm run workstream-closeout -- --title "发布闭环" --verification "Jenkins SUCCESS" --doc-sync "无需文档更新" --cleanup "cleanup-history dry-run deleted=0" --cleanup-final-deleted 0 --review "global-review findings=0" --superpowers-review "Superpowers reviewer completed; no Critical/Important findings" --problem "无新卡点" --solution "无新增稳定解法"
pnpm run napcat-upstream-audit -- --artifact-root .kt-workspace/test-artifacts/napcat-upstream-sync
pnpm run napcat-sync-candidate-review
pnpm run napcat-runtime-release-readiness
pnpm run napcat-remote-dev-handoff
pnpm run nas-codex-bootstrap
pnpm run napcat-device-profile-check
pnpm run cleanup-history -- --dry-run
pnpm run cleanup-history -- --execute
@ -49,6 +55,15 @@ pnpm run admin-login -- --url http://127.0.0.1:5999/#/auth/login
如果本机 Node/npm/pnpm 版本不对,先执行 `nvm ls` 查看已安装版本,再用 `nvm use <version>` 切换到目标版本;不要先扫盘找 `node.exe` 路径。
## Jenkins 入口模板
- `ci/jenkins/KT-NapCatQQ-Upstream-Sync.Jenkinsfile` 是 NapCatQQ 上游审计入口,默认只调用 ktWorkflow 的 `napcat-upstream-audit` dry-run保留人工审查边界。
- Codex 上游审计必须同时设置 `RUN_CODEX_AUDIT=true` 并填写 `CONFIRM_CODEX_WORKSPACE_WRITE=RUN_CODEX_WORKSPACE_WRITE_AUDIT`;这是因为 Codex runner 使用 workspace-write sandbox不能作为定时任务默认行为。
- `ci/jenkins/KT-NapCatQQ-Runtime-Release.Jenkinsfile` 是运行时发布入口,先调用 `napcat-runtime-release-readiness`,再通过显式参数触发运行时镜像构建和 API promotion下游 job 使用白名单 choice避免任意 Jenkins job 名被传入。
- 生产 API promotion 除了 `PROMOTE_API_RUNTIME=true` 和明确 image/profile 参数外,还必须填写 `CONFIRM_API_PROMOTION_TARGET=PROMOTE_KT_TEMPLATE_API_MAIN`
- 两个模板默认使用 NAS 工作区 `/vol1/docker/kt-codex/workspace/KT`,运行证据落在 `/vol1/docker/kt-codex/artifacts` 下。
- 模板只保存入口和参数形状,不记录 Jenkins credential、Token、SSH key 或其他真实凭据。
## MCP 客户端配置
把下面配置加入支持 stdio MCP 的客户端配置里:
@ -95,6 +110,11 @@ pnpm run admin-login -- --url http://127.0.0.1:5999/#/auth/login
| `kt_commit_plan` | 生成多仓库提交计划、建议 commit message 和检查项 |
| `kt_push_plan` | 生成多仓库推送计划和远程异常提醒 |
| `kt_business_test_plan` | 生成固化业务链路测试计划,包含 QQBot SSE、FFLogs 和系统日志 |
| `kt_napcat_upstream_audit` | 生成或执行 NapCatQQ upstream release 审计,默认 dry-run显式 `execute/useCodex` 后才运行 Codex |
| `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/execute command planexecute 只规划目录准备和 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` | 生成或执行远程只读健康检查命令 |
@ -113,6 +133,11 @@ pnpm run admin-login -- --url http://127.0.0.1:5999/#/auth/login
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `pnpm run admin-login` | 使用可见 Edge 打开 Admin 登录页,填写账号密码,拖动滑块,保存登录态和截图。默认账号来自初始化数据 `admin/123456`,生产或个人账号用 `KT_ADMIN_USERNAME` / `KT_ADMIN_PASSWORD` 或 CLI 参数覆盖。 |
| `pnpm run global-review` | 对 KT 全部子仓库做只读全局 CodeReview 扫描,默认仅对变更文件做内容深扫,输出 JSON 复审报告,并校验 `TASKS.md` 最近记录只保留范围、关键词、验证字段、NapCat `latest` 漂移、QQBot 重型服务单测导入风险、QQBot 插件 registry/controller smoke 未 mock 具体插件实现风险、OneBot/QQ 登录态边界、NapCat 密码登录验证码 wait 阶段、日志窗口锚点、验证码状态早于日志 URL 的短轮询、验证码 pending 被状态轮询误清理、API Pod SSH 日志读取超时及清理前 pending 回归,以及普通处理中状态误吃旧验证码日志回归;明显测试占位 token 不报凭据风险,确认误报时优先升级 `src/tools/review.ts`,不要把误报沉积到上下文。 |
| `pnpm run napcat-upstream-audit` | 默认 dry-run 输出 NapCatQQ 上游 release 审计分类、只读 Git 命令和 artifact 路径;显式 `--execute --use-codex` 才运行 Codex。 |
| `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`--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`。 |
@ -154,6 +179,7 @@ pnpm run admin-login -- --url http://127.0.0.1:5999/#/auth/login
- 大方向结束前调用 `kt_workstream_closeout``pnpm run workstream-closeout`;必须分别传入 `--review` 的 KT 全局审查证据和 `--superpowers-review` 的 Superpowers code review 证据;如果识别到测试流、卡点、误报、清理、部署观测或命令模板,要先升级对应 ktWorkflow 规则再报告完成。
- 文件改动完成并验证后调用 `kt_global_code_review`,或运行 `pnpm run global-review`;它只读扫描,不删除文件、不提交代码。`findings` 先判定真实风险或工具误报,真实风险修业务代码,误报修 `mcp/ktWorkflow/src/tools/review.ts` 并复跑。
- QQBot NapCat 登录 review 会检查验证码 pending、Docker 日志窗口、API Pod 读取超时,以及 `needNewDevice/jumpUrl` 是否进入 `GetNewDeviceQRCode -> PollNewDeviceQR -> NewDeviceLogin` 或兼容的 `deviceVerifyUrl` pending 状态。
- QQBot NapCat 二维码刷新卡在生成阶段时,优先计数当前容器近 5-20 分钟日志里的“重置已失效登录服务后重新生成二维码”、`Login Error,ErrType: 7 ErrCode: 4` 和“二维码已保存/二维码解码URL”如果 reset/error 数远高于二维码产出,按 `docs/qqbot-nas-runtime.md` 的 v5 稳定解法排查 NapCat native login service reset 风暴,先修 fork/镜像,再看 API SSE。
- 代码或配置改动后调用 `kt_change_doc_sync`,把需要同步的 README/API/AGENTS/docs/Obsidian/skill/ktWorkflow 入口补齐;无需同步时把原因写进收尾证据。
- 报告非平凡任务完成前调用 `kt_workflow_loop_audit`,确认测试证据、文档同步、历史清理最终 `deleted=0`、问题固化、ktWorkflow 升级、KT 全局 review 和 Superpowers code review 都过门;清理证据必须显式传入,不能依赖缺省值。
- 要提交或推送时先调用 `kt_commit_plan` / `kt_push_plan`,按仓库分组确认范围。

View File

@ -0,0 +1,104 @@
pipeline {
agent any
options {
skipDefaultCheckout(true)
timestamps()
timeout(time: 60, unit: 'MINUTES')
}
parameters {
string(name: 'NAPCAT_FORK_REF', defaultValue: 'codex/qr-refresh-login-state', description: 'Reviewed NapCatQQ fork branch, tag, or commit.')
string(name: 'UPSTREAM_RELEASE_TAG', defaultValue: '', description: 'Reviewed upstream release tag, when applicable.')
string(name: 'QQBOT_NAPCAT_IMAGE_OVERRIDE', defaultValue: '', description: 'Explicit runtime image tag to build or promote, for example kt-napcat-desktop-cn:desktop-cn-v9.')
string(name: 'QQBOT_NAPCAT_DESKTOP_PROFILE_VERSION_OVERRIDE', defaultValue: '', description: 'Explicit API desktop profile version override.')
booleanParam(name: 'BUILD_RUNTIME_IMAGE', defaultValue: false, description: 'When true, trigger the reviewed Jenkins runtime image build/publish job.')
choice(name: 'RUNTIME_BUILD_JOB', choices: ['KT-NapCatQQ/Runtime-Image-Build'], description: 'Whitelisted Jenkins job that performs the reviewed runtime image build and publish.')
booleanParam(name: 'PROMOTE_API_RUNTIME', defaultValue: false, description: 'When true, trigger API promotion with explicit image/profile overrides.')
choice(name: 'API_PROMOTION_JOB', choices: ['KT-Template/KT-Template-API/main'], description: 'Whitelisted API Jenkins job that accepts NapCat runtime override parameters.')
string(name: 'CONFIRM_API_PROMOTION_TARGET', defaultValue: '', description: 'Type PROMOTE_KT_TEMPLATE_API_MAIN before triggering the production API promotion job.')
}
environment {
KT_WORKSPACE_ROOT = '/vol1/docker/kt-codex/workspace/KT'
KT_WORKFLOW_DIR = '/vol1/docker/kt-codex/workspace/KT/mcp/ktWorkflow'
KT_ARTIFACT_ROOT = '/vol1/docker/kt-codex/artifacts'
}
stages {
stage('ktWorkflow readiness') {
steps {
sh '''
set -e
pnpm --dir "$KT_WORKFLOW_DIR" run napcat-runtime-release-readiness -- \
--artifact-root "$KT_ARTIFACT_ROOT/napcat-runtime-release" \
--fork-branch "$NAPCAT_FORK_REF" \
--upstream-release-tag "$UPSTREAM_RELEASE_TAG" \
--napcat-image-tag "$QQBOT_NAPCAT_IMAGE_OVERRIDE" \
--profile "$QQBOT_NAPCAT_DESKTOP_PROFILE_VERSION_OVERRIDE"
'''
}
}
stage('Runtime image build and publish') {
when {
expression { return params.BUILD_RUNTIME_IMAGE }
}
steps {
script {
requireRuntimeOverrides(
params.QQBOT_NAPCAT_IMAGE_OVERRIDE,
params.QQBOT_NAPCAT_DESKTOP_PROFILE_VERSION_OVERRIDE
)
build job: params.RUNTIME_BUILD_JOB,
wait: true,
parameters: [
string(name: 'NAPCAT_FORK_REF', value: params.NAPCAT_FORK_REF),
string(name: 'UPSTREAM_RELEASE_TAG', value: params.UPSTREAM_RELEASE_TAG),
string(name: 'QQBOT_NAPCAT_IMAGE_OVERRIDE', value: params.QQBOT_NAPCAT_IMAGE_OVERRIDE),
string(name: 'QQBOT_NAPCAT_DESKTOP_PROFILE_VERSION_OVERRIDE', value: params.QQBOT_NAPCAT_DESKTOP_PROFILE_VERSION_OVERRIDE),
string(name: 'KT_ARTIFACT_ROOT', value: "${env.KT_ARTIFACT_ROOT}/napcat-runtime-release")
]
}
}
}
stage('API promotion') {
when {
expression { return params.PROMOTE_API_RUNTIME }
}
steps {
script {
if (params.CONFIRM_API_PROMOTION_TARGET != 'PROMOTE_KT_TEMPLATE_API_MAIN') {
error('CONFIRM_API_PROMOTION_TARGET must equal PROMOTE_KT_TEMPLATE_API_MAIN before API promotion runs.')
}
requireRuntimeOverrides(
params.QQBOT_NAPCAT_IMAGE_OVERRIDE,
params.QQBOT_NAPCAT_DESKTOP_PROFILE_VERSION_OVERRIDE
)
build job: params.API_PROMOTION_JOB,
wait: true,
parameters: [
string(name: 'QQBOT_NAPCAT_IMAGE_OVERRIDE', value: params.QQBOT_NAPCAT_IMAGE_OVERRIDE),
string(name: 'QQBOT_NAPCAT_DESKTOP_PROFILE_VERSION_OVERRIDE', value: params.QQBOT_NAPCAT_DESKTOP_PROFILE_VERSION_OVERRIDE)
]
}
}
}
}
}
/**
* Stops runtime build or promotion unless both release override values are explicit.
* @param imageTag Reviewed NapCat runtime image tag supplied by the operator.
* @param profile Reviewed desktop profile version supplied by the operator.
* @return Nothing; aborts this pipeline through `error` when a required value is missing.
*/
void requireRuntimeOverrides(String imageTag, String profile) {
if (!imageTag?.trim()) {
error('QQBOT_NAPCAT_IMAGE_OVERRIDE is required before build or promotion.')
}
if (!profile?.trim()) {
error('QQBOT_NAPCAT_DESKTOP_PROFILE_VERSION_OVERRIDE is required before build or promotion.')
}
}

View File

@ -0,0 +1,51 @@
pipeline {
agent any
options {
skipDefaultCheckout(true)
timestamps()
timeout(time: 30, unit: 'MINUTES')
}
parameters {
booleanParam(name: 'RUN_CODEX_AUDIT', defaultValue: false, description: 'When true, allow the optional Codex workspace-write audit after the default dry-run audit.')
string(name: 'CONFIRM_CODEX_WORKSPACE_WRITE', defaultValue: '', description: 'Type RUN_CODEX_WORKSPACE_WRITE_AUDIT to confirm Codex may run with a workspace-write sandbox.')
}
environment {
KT_WORKFLOW_DIR = '/vol1/docker/kt-codex/workspace/KT/mcp/ktWorkflow'
KT_ARTIFACT_ROOT = '/vol1/docker/kt-codex/artifacts'
}
stages {
stage('ktWorkflow upstream audit dry-run') {
steps {
sh '''
set -e
pnpm --dir "$KT_WORKFLOW_DIR" run napcat-upstream-audit -- \
--artifact-root "$KT_ARTIFACT_ROOT/napcat-upstream-sync/dry-run"
'''
}
}
stage('ktWorkflow upstream audit with Codex') {
when {
expression { return params.RUN_CODEX_AUDIT }
}
steps {
script {
if (params.CONFIRM_CODEX_WORKSPACE_WRITE != 'RUN_CODEX_WORKSPACE_WRITE_AUDIT') {
error('CONFIRM_CODEX_WORKSPACE_WRITE must equal RUN_CODEX_WORKSPACE_WRITE_AUDIT before Codex workspace-write audit runs.')
}
}
sh '''
set -e
pnpm --dir "$KT_WORKFLOW_DIR" run napcat-upstream-audit -- \
--execute \
--use-codex \
--artifact-root "$KT_ARTIFACT_ROOT/napcat-upstream-sync/codex"
'''
}
}
}
}

View File

@ -10,6 +10,11 @@
"deploy-observation": "node --import ./node_modules/tsx/dist/loader.mjs src/server.ts --deploy-observation",
"global-review": "node --import ./node_modules/tsx/dist/loader.mjs src/server.ts --global-review",
"napcat-device-profile-check": "node --import ./node_modules/tsx/dist/loader.mjs src/server.ts --napcat-device-profile-check",
"napcat-remote-dev-handoff": "node --import ./node_modules/tsx/dist/loader.mjs src/server.ts --napcat-remote-dev-handoff",
"napcat-runtime-release-readiness": "node --import ./node_modules/tsx/dist/loader.mjs src/server.ts --napcat-runtime-release-readiness",
"napcat-sync-candidate-review": "node --import ./node_modules/tsx/dist/loader.mjs src/server.ts --napcat-sync-candidate-review",
"napcat-upstream-audit": "node --import ./node_modules/tsx/dist/loader.mjs src/server.ts --napcat-upstream-audit",
"nas-codex-bootstrap": "node --import ./node_modules/tsx/dist/loader.mjs src/server.ts --nas-codex-bootstrap",
"obsidian-context": "node --import ./node_modules/tsx/dist/loader.mjs src/server.ts --obsidian-context",
"obsidian-sync": "node --import ./node_modules/tsx/dist/loader.mjs src/server.ts --obsidian-sync",
"obsidian-validate": "node --import ./node_modules/tsx/dist/loader.mjs src/server.ts --obsidian-validate",

View File

@ -0,0 +1,41 @@
# NapCat Remote Dev Handoff
You are summarizing a supplied remote development context packet for the next human-controlled step.
Do not edit files.
Do not run merge, commit, push, deploy, or Docker mutation commands.
Use only the supplied context packet.
Return JSON that matches the requested schema.
## Handoff Scope
Summarize only packet evidence for:
- repository branches and dirty status
- artifact directories and notable generated outputs
- Jenkins status, build numbers, commit/image pointers, and unknown deployment state
- blockers, missing evidence, or unsafe assumptions
- safe next commands that are read-only or dry-run oriented
Do not ask Codex to modify files. Do not include merge, commit, push, deploy, Docker mutation, or destructive cleanup commands as next commands.
## Requested Schema
Return only this JSON object:
```json
{
"repoStates": [
{
"repo": "Repository path or label.",
"branch": "Current branch or unknown.",
"dirtyStatus": "clean | dirty | unknown",
"head": "Commit hash or unknown."
}
],
"artifactDirectories": ["Artifact directory or missing evidence note."],
"jenkinsStatus": "Jenkins status summary or missing evidence note.",
"blockers": ["Blocker or missing evidence note."],
"safeNextCommands": ["Read-only or dry-run command."]
}
```

View File

@ -0,0 +1,34 @@
# NapCat Runtime Release Readiness
You are reviewing a supplied NapCat runtime release packet before a human promotes an image/profile into the API runtime.
Do not edit files.
Do not run merge, commit, push, deploy, or Docker mutation commands.
Use only the supplied context packet.
Return JSON that matches the requested schema.
## Readiness Checks
Base every section on packet evidence:
- source validation: branch, commit, upstream base, fork patch marker, and dirty status
- image validation: image tag, image inspect/build evidence, marker evidence, and runtime asset presence
- API promotion readiness: explicit image/profile values, configuration alignment, and Jenkins/API promotion prerequisites
- online smoke readiness: QQBot/NapCat login, WebUI, command smoke, and rollback smoke expectations
- rollback pointer: last known good branch, commit, image tag, profile, and API runtime values
If any section lacks evidence, state the missing evidence plainly.
## Requested Schema
Return only this JSON object:
```json
{
"sourceValidation": "Evidence-backed source validation status.",
"imageValidation": "Evidence-backed image validation status.",
"apiPromotionReadiness": "Evidence-backed API promotion readiness status.",
"onlineSmokeReadiness": "Evidence-backed online smoke readiness status.",
"rollbackPointer": "Specific rollback pointer or the missing evidence needed to name one."
}
```

View File

@ -0,0 +1,34 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"additionalProperties": false,
"required": [
"sourceValidation",
"imageValidation",
"apiPromotionReadiness",
"onlineSmokeReadiness",
"rollbackPointer"
],
"properties": {
"sourceValidation": {
"type": "string",
"minLength": 1
},
"imageValidation": {
"type": "string",
"minLength": 1
},
"apiPromotionReadiness": {
"type": "string",
"minLength": 1
},
"onlineSmokeReadiness": {
"type": "string",
"minLength": 1
},
"rollbackPointer": {
"type": "string",
"minLength": 1
}
}
}

View File

@ -0,0 +1,33 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"additionalProperties": false,
"required": ["classification", "reasonCodes", "recommendedAction", "summary"],
"properties": {
"classification": {
"enum": ["safe-candidate", "manual-review", "blocked"]
},
"reasonCodes": {
"type": "array",
"minItems": 1,
"items": {
"enum": [
"NO_UPSTREAM_CHANGE",
"HOT_ZONE_CHANGED",
"FORK_PATCH_OVERLAP",
"DRY_MERGE_CONFLICT",
"BUILD_GRAPH_CHANGED",
"METADATA_UNAVAILABLE",
"CODEX_SCHEMA_INVALID"
]
}
},
"recommendedAction": {
"enum": ["no-op", "create-candidate-branch", "request-human-review", "block"]
},
"summary": {
"type": "string",
"minLength": 1
}
}
}

View File

@ -0,0 +1,43 @@
# NapCat Sync Candidate Review
You are reviewing a supplied NapCat candidate branch context packet before a human decides whether to continue the sync.
Do not edit files.
Do not run merge, commit, push, deploy, or Docker mutation commands.
Use only the supplied context packet.
Return JSON that matches the requested schema.
## Review Scope
Review only evidence present in the packet. Cover these areas:
- login service reset behavior and stale native login service state
- QR refresh, stale QR prevention, QR expiry, and scan confirmation state
- WebUI login runtime state, reconciled QQ online state, and false online signals
- captcha and new-device boundaries, including pending state preservation
- package, shell, framework, webui build output, and runtime asset layout
- test gaps, missing smoke evidence, and unverified online behavior
## Finding Rules
Flag only actionable risks grounded in packet evidence. If evidence is missing, report it as a test gap or blocked review item instead of guessing.
## Requested Schema
Return only this JSON object:
```json
{
"decision": "approve | manual-review | block",
"summary": "Short review summary.",
"findings": [
{
"severity": "critical | important | minor",
"area": "login-reset | qr-refresh | webui-runtime | captcha-new-device | package-build | test-gap",
"evidence": "Packet evidence for the finding.",
"requiredAction": "Human-readable action before promotion."
}
],
"testGaps": ["Missing validation evidence."]
}
```

View File

@ -0,0 +1,53 @@
# NapCat Upstream Audit
You are reviewing a supplied NapCat upstream release context packet for KT automation. Decide whether the upstream release can become a candidate branch, needs manual review, or is blocked.
Do not edit files.
Do not run merge, commit, push, deploy, or Docker mutation commands.
Use only the supplied context packet.
Return JSON that matches the requested schema.
## Inputs
Use the packet sections for upstream release metadata, fork patch range, changed files, dry-merge result, build graph diff, and Codex parser status. Treat missing packet sections as unavailable metadata.
## Classification Rules
Choose exactly one `classification`:
- `safe-candidate`: no upstream change exists, or upstream changes are outside hot zones, do not overlap fork patches, dry merge is clean, build graph is stable, and metadata is available.
- `manual-review`: upstream changed login/runtime/build-adjacent areas or fork-adjacent code, but the packet does not prove a hard blocker.
- `blocked`: dry merge conflicts, build graph changes that require maintainer action, missing required metadata, or invalid Codex output schema.
Use one or more `reasonCodes` from this exact list:
- `NO_UPSTREAM_CHANGE`
- `HOT_ZONE_CHANGED`
- `FORK_PATCH_OVERLAP`
- `DRY_MERGE_CONFLICT`
- `BUILD_GRAPH_CHANGED`
- `METADATA_UNAVAILABLE`
- `CODEX_SCHEMA_INVALID`
## Hot Zones
Treat these as NapCat login/runtime hot zones:
- login service reset and native login service state
- QR refresh, stale QR, QR expiry, and scan confirmation state
- WebUI login runtime state and reconciled QQ online state
- captcha and new-device verification boundaries
- package, shell, framework, webui build graph or output layout
## Requested Schema
Return only this JSON object:
```json
{
"classification": "safe-candidate | manual-review | blocked",
"reasonCodes": ["NO_UPSTREAM_CHANGE"],
"recommendedAction": "no-op | create-candidate-branch | request-human-review | block",
"summary": "Short evidence-based decision."
}
```

View File

@ -1,6 +1,10 @@
import type {
DeployObservationInput,
GlobalCodeReviewInput,
NapcatRemoteDevHandoffInput,
NapcatRuntimeReleaseReadinessInput,
NapcatSyncCandidateReviewInput,
NasCodexBootstrapPlanInput,
ObsidianContextInput,
ObsidianSyncInput,
ObsidianValidateInput,
@ -8,6 +12,7 @@ import type {
WorkstreamReusablePattern,
} from '../types.js';
import { workstreamReusablePatternValues } from './constants.js';
import type { NapcatUpstreamAuditInput } from '../tools/napcatAutomation.types.js';
function parseContentScanMode(argv: string[]): 'all' | 'changed' {
if (argv.includes('--content-scan-all') || argv.includes('--contentScanMode=all')) {
@ -62,6 +67,16 @@ function readOptionList(argv: string[], names: string[]): string[] {
.filter(Boolean);
}
/**
* Checks whether any supported boolean CLI flag is present in the raw argv list.
* @param argv - Full process argv including node, script, mode flag, and user options.
* @param names - Flag names to accept in dashed or camelCase form.
* @returns True when the caller provided one of the named flags.
*/
function hasFlag(argv: string[], names: string[]): boolean {
return names.some((name) => argv.includes(name));
}
export function parseWorkstreamCloseoutCliArgs(argv: string[]): WorkstreamCloseoutInput {
const patternSet = new Set<string>(workstreamReusablePatternValues);
const patterns = readOptionList(argv, ['--pattern', '--reusable-pattern'])
@ -123,6 +138,135 @@ export function parseDeployObservationCliArgs(argv: string[]): DeployObservation
};
}
/**
* Parses the NapCat upstream audit CLI mode into the deterministic audit builder input.
* @param argv - Full process argv from `src/server.ts`; `--execute` and `--use-codex` must both be explicit before Codex can run.
* @returns Upstream audit input with dry-run defaults for all optional metadata lists.
*/
export function parseNapcatUpstreamAuditCliArgs(
argv: string[],
): NapcatUpstreamAuditInput {
return {
artifactRoot: readOption(argv, ['--artifact-root', '--artifactRoot']),
codexHome: readOption(argv, ['--codex-home', '--codexHome']),
createCandidateBranch: hasFlag(argv, ['--create-candidate-branch', '--createCandidateBranch']),
dryMergeConflict: hasFlag(argv, ['--dry-merge-conflict', '--dryMergeConflict']),
execute: hasFlag(argv, ['--execute']),
forkBranch: readOption(argv, ['--fork-branch', '--forkBranch']),
forkPatchFiles: readOptionList(argv, [
'--fork-patch-file',
'--fork-patch-files',
'--forkPatchFile',
'--forkPatchFiles',
]),
forkRepo: readOption(argv, ['--fork-repo', '--forkRepo']),
lastAcceptedUpstreamBase: readOption(argv, [
'--last-accepted-upstream-base',
'--lastAcceptedUpstreamBase',
]),
outputSchemaPath: readOption(argv, ['--output-schema', '--outputSchemaPath']),
upstreamChangedFiles: readOptionList(argv, [
'--upstream-changed-file',
'--upstream-changed-files',
'--upstreamChangedFile',
'--upstreamChangedFiles',
]),
upstreamReleaseTag: readOption(argv, [
'--upstream-release-tag',
'--upstream-release-ref',
'--upstreamReleaseTag',
]),
upstreamRepo: readOption(argv, ['--upstream-repo', '--upstreamRepo']),
useCodex: hasFlag(argv, ['--use-codex', '--useCodex']),
workspaceRoot: readOption(argv, ['--workspace-root', '--workspaceRoot']),
};
}
/**
* Parses common dry-run NapCat prompt tool options shared by candidate review, readiness, and handoff modes.
* @param argv - Full process argv from a NapCat automation CLI script.
* @returns Prompt skeleton input with `execute` false unless explicitly requested by the caller.
*/
function parseNapcatPromptToolCliArgs(argv: string[]): NapcatSyncCandidateReviewInput {
return {
artifactRoot: readOption(argv, ['--artifact-root', '--artifactRoot']),
candidateBranch: readOption(argv, ['--candidate-branch', '--candidateBranch']),
execute: hasFlag(argv, ['--execute']),
forkBranch: readOption(argv, ['--fork-branch', '--forkBranch']),
forkRepo: readOption(argv, ['--fork-repo', '--forkRepo']),
lastAcceptedUpstreamBase: readOption(argv, [
'--last-accepted-upstream-base',
'--lastAcceptedUpstreamBase',
]),
upstreamReleaseTag: readOption(argv, [
'--upstream-release-tag',
'--upstream-release-ref',
'--upstreamReleaseTag',
]),
upstreamRepo: readOption(argv, ['--upstream-repo', '--upstreamRepo']),
useCodex: hasFlag(argv, ['--use-codex', '--useCodex']),
workspaceRoot: readOption(argv, ['--workspace-root', '--workspaceRoot']),
};
}
/**
* Parses the NapCat sync-candidate review CLI mode.
* @param argv - Full process argv from `pnpm run napcat-sync-candidate-review`.
* @returns Dry-run prompt skeleton input for human/Codex candidate review handoff.
*/
export function parseNapcatSyncCandidateReviewCliArgs(
argv: string[],
): NapcatSyncCandidateReviewInput {
return parseNapcatPromptToolCliArgs(argv);
}
/**
* Parses the NapCat runtime release readiness CLI mode.
* @param argv - Full process argv from `pnpm run napcat-runtime-release-readiness`.
* @returns Dry-run prompt skeleton input with image/profile hints and no default mutation.
*/
export function parseNapcatRuntimeReleaseReadinessCliArgs(
argv: string[],
): NapcatRuntimeReleaseReadinessInput {
return {
...parseNapcatPromptToolCliArgs(argv),
apiImageTag: readOption(argv, ['--api-image-tag', '--apiImageTag']),
napcatImageTag: readOption(argv, ['--napcat-image-tag', '--napcatImageTag']),
profile: readOption(argv, ['--profile']),
};
}
/**
* Parses the NapCat remote development handoff CLI mode.
* @param argv - Full process argv from `src/server.ts --napcat-remote-dev-handoff`.
* @returns Dry-run prompt skeleton input for a remote workspace handoff packet.
*/
export function parseNapcatRemoteDevHandoffCliArgs(
argv: string[],
): NapcatRemoteDevHandoffInput {
return {
...parseNapcatPromptToolCliArgs(argv),
targetHost: readOption(argv, ['--target-host', '--targetHost']),
};
}
/**
* Parses the NAS Codex bootstrap planning CLI mode.
* @param argv - Full process argv from `pnpm run nas-codex-bootstrap`.
* @returns Dry-run bootstrap plan input; `execute` is false unless the caller explicitly opts in.
*/
export function parseNasCodexBootstrapCliArgs(
argv: string[],
): NasCodexBootstrapPlanInput {
return {
artifactRoot: readOption(argv, ['--artifact-root', '--artifactRoot']),
codexHome: readOption(argv, ['--codex-home', '--codexHome']),
execute: hasFlag(argv, ['--execute']),
serviceName: readOption(argv, ['--service-name', '--serviceName']),
workspaceRoot: readOption(argv, ['--workspace-root', '--workspaceRoot']),
};
}
export function parseObsidianCliArgs(argv: string[]): ObsidianValidateInput {
return {
checkLayout: !argv.includes('--no-layout'),

View File

@ -69,6 +69,11 @@ export const registeredToolNames = [
'kt_commit_plan',
'kt_push_plan',
'kt_business_test_plan',
'kt_napcat_upstream_audit',
'kt_napcat_sync_candidate_review',
'kt_napcat_runtime_release_readiness',
'kt_napcat_remote_dev_handoff',
'kt_nas_codex_bootstrap_plan',
'kt_napcat_device_profile_check',
'kt_blocker_resolution',
'kt_remote_health_check',

View File

@ -22,6 +22,14 @@ import { buildVerificationPlan } from './tools/verification.js';
import { buildWorkstreamCloseout } from './tools/closeout.js';
import { buildChangeDocSync } from './tools/docSync.js';
import { buildDeployObservation } from './tools/deployObservation.js';
import {
buildNapcatRemoteDevHandoff,
buildNapcatRuntimeReleaseReadiness,
buildNapcatSyncCandidateReview,
buildNapcatUpstreamAudit,
buildNasCodexBootstrapPlan,
napcatAutomationDefaults,
} from './tools/napcatAutomation.js';
import { buildCommitPlan, buildComponentWorkflow, buildDbSyncPlan, buildFinishTask, buildPushPlan, buildRemoteHealthCheck } from './tools/workflow.js';
import { workstreamReusablePatternValues } from './core/constants.js';
export function response(data: unknown): McpTextResponse {
@ -35,6 +43,11 @@ export function response(data: unknown): McpTextResponse {
};
}
/**
* Registers every ktWorkflow MCP tool against the provided MCP server instance.
* @param server - MCP SDK server that owns tool names, schemas, descriptions, and async handlers.
* @returns Nothing; handlers are attached to `server` for later stdio transport use.
*/
export function registerTools(server: McpServer): void {
server.registerTool(
'kt_read_context',
@ -370,11 +383,137 @@ export function registerTools(server: McpServer): void {
async (input) => response(buildBusinessTestPlan(input)),
);
server.registerTool(
'kt_napcat_upstream_audit',
{
description:
'生成或执行 NapCatQQ 上游 release 审计:默认 dry-run只读输出 hot zone、fork patch overlap、dry merge 和 Codex 审计入口execute/useCodex 均需显式开启才会运行 Codex。',
inputSchema: {
artifactRoot: z
.string()
.default('.kt-workspace/test-artifacts/napcat-upstream-audit'),
codexHome: z.string().optional(),
createCandidateBranch: z.boolean().default(false),
dryMergeConflict: z.boolean().default(false),
execute: z.boolean().default(false),
forkBranch: z.string().default(napcatAutomationDefaults.forkBranch),
forkPatchFiles: z.array(z.string()).default([]),
forkRepo: z.string().default(napcatAutomationDefaults.forkRepo),
lastAcceptedUpstreamBase: z
.string()
.default(napcatAutomationDefaults.lastAcceptedUpstreamBase),
outputSchemaPath: z.string().optional(),
upstreamChangedFiles: z.array(z.string()).default([]),
upstreamReleaseTag: z.string().default(napcatAutomationDefaults.upstreamReleaseRef),
upstreamRepo: z.string().default(napcatAutomationDefaults.upstreamRepo),
useCodex: z.boolean().default(false),
workspaceRoot: z.string().default(napcatAutomationDefaults.workspaceRoot),
},
title: 'KT NapCat Upstream Audit',
},
async (input) => response(buildNapcatUpstreamAudit(input)),
);
server.registerTool(
'kt_napcat_sync_candidate_review',
{
description:
'生成 NapCatQQ sync candidate 人工/Codex 复审的 dry-run prompt、上下文和只读命令骨架Task 5 不执行 Codex 或 Git 写操作。',
inputSchema: {
artifactRoot: z
.string()
.default('.kt-workspace/test-artifacts/napcat-automation/sync-candidate-review'),
candidateBranch: z.string().default(napcatAutomationDefaults.candidateBranch),
execute: z.boolean().default(false),
forkBranch: z.string().default(napcatAutomationDefaults.forkBranch),
forkRepo: z.string().default(napcatAutomationDefaults.forkRepo),
lastAcceptedUpstreamBase: z
.string()
.default(napcatAutomationDefaults.lastAcceptedUpstreamBase),
upstreamReleaseTag: z.string().default(napcatAutomationDefaults.upstreamReleaseRef),
upstreamRepo: z.string().default(napcatAutomationDefaults.upstreamRepo),
useCodex: z.boolean().default(false),
workspaceRoot: z.string().default(napcatAutomationDefaults.workspaceRoot),
},
title: 'KT NapCat Sync Candidate Review',
},
async (input) => response(buildNapcatSyncCandidateReview(input)),
);
server.registerTool(
'kt_napcat_runtime_release_readiness',
{
description:
'生成 NapCat runtime release readiness dry-run prompt、上下文和只读检查骨架不自动构建镜像、不调用 kubectl、不发布。',
inputSchema: {
apiImageTag: z.string().default(napcatAutomationDefaults.apiImageTag),
artifactRoot: z
.string()
.default('.kt-workspace/test-artifacts/napcat-automation/runtime-release-readiness'),
candidateBranch: z.string().default(napcatAutomationDefaults.candidateBranch),
execute: z.boolean().default(false),
forkBranch: z.string().default(napcatAutomationDefaults.forkBranch),
forkRepo: z.string().default(napcatAutomationDefaults.forkRepo),
napcatImageTag: z.string().default(napcatAutomationDefaults.runtimeImageTag),
profile: z.string().default(napcatAutomationDefaults.runtimeProfile),
upstreamReleaseTag: z.string().default(napcatAutomationDefaults.upstreamReleaseRef),
upstreamRepo: z.string().default(napcatAutomationDefaults.upstreamRepo),
useCodex: z.boolean().default(false),
workspaceRoot: z.string().default(napcatAutomationDefaults.workspaceRoot),
},
title: 'KT NapCat Runtime Release Readiness',
},
async (input) => response(buildNapcatRuntimeReleaseReadiness(input)),
);
server.registerTool(
'kt_napcat_remote_dev_handoff',
{
description:
'生成 NapCat 自动化远程开发 handoff 的 dry-run prompt 和上下文骨架;不创建线程、不执行 Codex、不同步仓库。',
inputSchema: {
artifactRoot: z
.string()
.default('.kt-workspace/test-artifacts/napcat-automation/remote-dev-handoff'),
candidateBranch: z.string().default(napcatAutomationDefaults.candidateBranch),
execute: z.boolean().default(false),
forkBranch: z.string().default(napcatAutomationDefaults.forkBranch),
forkRepo: z.string().default(napcatAutomationDefaults.forkRepo),
targetHost: z.string().default(napcatAutomationDefaults.targetHost),
upstreamReleaseTag: z.string().default(napcatAutomationDefaults.upstreamReleaseRef),
upstreamRepo: z.string().default(napcatAutomationDefaults.upstreamRepo),
useCodex: z.boolean().default(false),
workspaceRoot: z.string().default(napcatAutomationDefaults.workspaceRoot),
},
title: 'KT NapCat Remote Dev Handoff',
},
async (input) => response(buildNapcatRemoteDevHandoff(input)),
);
server.registerTool(
'kt_nas_codex_bootstrap_plan',
{
description:
'生成可信 NAS Codex 自动化环境 bootstrap 的 dry-run planTask 5 仅输出只读检查和占位计划,不安装、不同步、不写 systemd。',
inputSchema: {
artifactRoot: z
.string()
.default('.kt-workspace/test-artifacts/napcat-automation/nas-codex-bootstrap'),
codexHome: z.string().default(napcatAutomationDefaults.codexHome),
execute: z.boolean().default(false),
serviceName: z.string().default(napcatAutomationDefaults.bootstrapServiceName),
workspaceRoot: z.string().default(napcatAutomationDefaults.workspaceRoot),
},
title: 'KT NAS Codex Bootstrap Plan',
},
async (input) => response(buildNasCodexBootstrapPlan(input)),
);
server.registerTool(
'kt_napcat_device_profile_check',
{
description:
'静态检查 API NapCat 设备身份护栏QQNT 可见 hostname、02:42 MAC、machine-info、runtime dir 和 DB_TIMEZONE。',
'静态检查 API NapCat 设备身份护栏QQNT 可见 hostname、实体 OUI MAC、machine-info、runtime dir 和 DB_TIMEZONE。',
inputSchema: {
project: z.string().default('api'),
},

View File

@ -1,9 +1,14 @@
import { mkdirSync, writeFileSync } from "node:fs";
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import path from "node:path";
import {
parseNasCodexBootstrapCliArgs,
parseDeployObservationCliArgs,
parseGlobalReviewCliArgs,
parseNapcatRemoteDevHandoffCliArgs,
parseNapcatRuntimeReleaseReadinessCliArgs,
parseNapcatSyncCandidateReviewCliArgs,
parseNapcatUpstreamAuditCliArgs,
parseWorkstreamCloseoutCliArgs,
} from "./core/cli.js";
import { registeredToolNames } from "./core/constants.js";
@ -35,6 +40,27 @@ import {
findTaskRecordGovernanceFindings,
isBenignCredentialReviewValue,
} from "./tools/review.js";
import {
napcatAutomationReasonCodes,
parseNapcatAutomationClassification,
upstreamAuditOutputSchema,
} from "./tools/napcatAutomation.types.js";
import {
buildNapcatCodexExecCommand,
buildNapcatRemoteDevHandoff,
buildNapcatRuntimeReleaseReadiness,
buildNapcatSyncCandidateReview,
buildNapcatUpstreamAudit,
buildNasCodexBootstrapPlan,
classifyNapcatUpstreamAudit,
napcatAutomationDefaults,
} from "./tools/napcatAutomation.js";
import { registerTools } from "./registerTools.js";
import {
loadNapcatAutomationPrompt,
type NapcatAutomationPromptName,
napcatAutomationPromptNames,
} from "./tools/napcatAutomation.prompts.js";
import {
buildBusinessTestPlan,
buildNapcatDeviceProfileCheck,
@ -54,7 +80,788 @@ import {
buildPushPlan,
buildRemoteHealthCheck,
} from "./tools/workflow.js";
/**
* Runs ktWorkflow guardrail smoke checks from the CLI self-test entry; writes local evidence under the KT workspace and throws on any broken workflow contract.
* @returns A promise that resolves after all contract checks and evidence writing complete.
*/
export async function runSelfTest(): Promise<void> {
const napcatAuditSchemaSmoke = upstreamAuditOutputSchema.safeParse({
classification: "manual-review",
reasonCodes: ["HOT_ZONE_CHANGED"],
recommendedAction: "request-human-review",
summary: "Login hot-zone changed.",
});
if (!napcatAuditSchemaSmoke.success) {
throw new Error("NapCat upstream audit schema self-check failed");
}
const napcatAuditExtraPropertySmoke = upstreamAuditOutputSchema.safeParse({
classification: "manual-review",
reasonCodes: ["HOT_ZONE_CHANGED"],
recommendedAction: "request-human-review",
summary: "Login hot-zone changed.",
unexpectedField: "Codex output schema rejects this.",
});
if (napcatAuditExtraPropertySmoke.success) {
throw new Error("NapCat upstream audit schema strict self-check failed");
}
if (!napcatAutomationReasonCodes.includes("HOT_ZONE_CHANGED")) {
throw new Error("NapCat reason-code list self-check failed");
}
const napcatCli = parseNapcatUpstreamAuditCliArgs([
"node",
"server",
"--napcat-upstream-audit",
"--execute",
"--use-codex",
"--artifact-root",
"/vol1/docker/kt-codex/artifacts/napcat-upstream-sync",
]);
if (!napcatCli.execute || !napcatCli.useCodex) {
throw new Error("NapCat upstream audit CLI parser self-check failed");
}
if (
napcatCli.artifactRoot !==
"/vol1/docker/kt-codex/artifacts/napcat-upstream-sync"
) {
throw new Error("NapCat upstream audit artifactRoot CLI parser self-check failed");
}
const napcatCandidateCli = parseNapcatSyncCandidateReviewCliArgs([
"node",
"server",
"--napcat-sync-candidate-review",
]);
const napcatRuntimeCli = parseNapcatRuntimeReleaseReadinessCliArgs([
"node",
"server",
"--napcat-runtime-release-readiness",
]);
const napcatRemoteHandoffCli = parseNapcatRemoteDevHandoffCliArgs([
"node",
"server",
"--napcat-remote-dev-handoff",
]);
const nasBootstrapCli = parseNasCodexBootstrapCliArgs([
"node",
"server",
"--nas-codex-bootstrap",
]);
if (
napcatCandidateCli.execute ||
napcatRuntimeCli.execute ||
napcatRemoteHandoffCli.execute ||
nasBootstrapCli.execute
) {
throw new Error("NapCat auxiliary CLI dry-run default self-check failed");
}
const napcatCandidateDefaults = buildNapcatSyncCandidateReview({}).context;
const napcatRuntimeDefaults = buildNapcatRuntimeReleaseReadiness({}).context;
const napcatRemoteDefaults = buildNapcatRemoteDevHandoff({}).context;
const nasBootstrapDefaults = buildNasCodexBootstrapPlan({}).context;
if (
napcatCandidateDefaults.candidateBranch !==
napcatAutomationDefaults.candidateBranch ||
napcatCandidateDefaults.forkBranch !== napcatAutomationDefaults.forkBranch ||
napcatCandidateDefaults.forkRepo !== napcatAutomationDefaults.forkRepo ||
napcatCandidateDefaults.lastAcceptedUpstreamBase !==
napcatAutomationDefaults.lastAcceptedUpstreamBase ||
napcatCandidateDefaults.upstreamReleaseTag !==
napcatAutomationDefaults.upstreamReleaseRef ||
napcatCandidateDefaults.upstreamRepo !== napcatAutomationDefaults.upstreamRepo ||
napcatCandidateDefaults.workspaceRoot !==
napcatAutomationDefaults.workspaceRoot
) {
throw new Error("NapCat candidate review default alignment self-check failed");
}
if (
napcatRuntimeDefaults.apiImageTag !==
napcatAutomationDefaults.apiImageTag ||
napcatRuntimeDefaults.candidateBranch !==
napcatAutomationDefaults.candidateBranch ||
napcatRuntimeDefaults.forkBranch !== napcatAutomationDefaults.forkBranch ||
napcatRuntimeDefaults.forkRepo !== napcatAutomationDefaults.forkRepo ||
napcatRuntimeDefaults.napcatImageTag !==
napcatAutomationDefaults.runtimeImageTag ||
napcatRuntimeDefaults.profile !== napcatAutomationDefaults.runtimeProfile ||
napcatRuntimeDefaults.upstreamReleaseTag !==
napcatAutomationDefaults.upstreamReleaseRef ||
napcatRuntimeDefaults.workspaceRoot !==
napcatAutomationDefaults.workspaceRoot
) {
throw new Error("NapCat runtime readiness default alignment self-check failed");
}
if (
napcatRemoteDefaults.candidateBranch !==
napcatAutomationDefaults.candidateBranch ||
napcatRemoteDefaults.forkBranch !== napcatAutomationDefaults.forkBranch ||
napcatRemoteDefaults.forkRepo !== napcatAutomationDefaults.forkRepo ||
napcatRemoteDefaults.targetHost !== napcatAutomationDefaults.targetHost ||
napcatRemoteDefaults.upstreamReleaseTag !==
napcatAutomationDefaults.upstreamReleaseRef ||
napcatRemoteDefaults.workspaceRoot !== napcatAutomationDefaults.workspaceRoot
) {
throw new Error("NapCat remote handoff default alignment self-check failed");
}
if (
nasBootstrapDefaults.codexHome !== napcatAutomationDefaults.codexHome ||
nasBootstrapDefaults.serviceName !==
napcatAutomationDefaults.bootstrapServiceName ||
nasBootstrapDefaults.workspaceRoot !== napcatAutomationDefaults.workspaceRoot
) {
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",
});
const napcatCandidateOverrideCommandText =
napcatCandidateOverride.commands.map((item) => item.command).join("\n");
if (
!napcatCandidateOverrideCommandText.includes("custom-candidate") ||
!napcatCandidateOverrideCommandText.includes("custom-base") ||
napcatCandidateOverrideCommandText.includes(
napcatAutomationDefaults.candidateBranch,
) ||
napcatCandidateOverrideCommandText.includes(
napcatAutomationDefaults.lastAcceptedUpstreamBase,
)
) {
throw new Error("NapCat candidate command override self-check failed");
}
const napcatRuntimeOverride = buildNapcatRuntimeReleaseReadiness({
napcatImageTag: "custom-image",
});
const napcatRuntimeOverrideCommandText =
napcatRuntimeOverride.commands.map((item) => item.command).join("\n");
if (
!napcatRuntimeOverrideCommandText.includes("custom-image") ||
napcatRuntimeOverrideCommandText.includes(
napcatAutomationDefaults.runtimeImageTag,
)
) {
throw new Error("NapCat runtime command override self-check failed");
}
const napcatAuditOverride = buildNapcatUpstreamAudit({
dryMergeConflict: false,
forkBranch: "custom-fork",
forkPatchFiles: [],
lastAcceptedUpstreamBase: "custom-base",
upstreamChangedFiles: [],
upstreamReleaseTag: "custom-upstream",
});
const napcatAuditOverrideCommandText =
napcatAuditOverride.commands.map((item) => item.command).join("\n");
if (
!napcatAuditOverrideCommandText.includes("custom-fork") ||
!napcatAuditOverrideCommandText.includes("custom-base") ||
!napcatAuditOverrideCommandText.includes("custom-upstream") ||
napcatAuditOverrideCommandText.includes(napcatAutomationDefaults.forkBranch) ||
napcatAuditOverrideCommandText.includes(
napcatAutomationDefaults.lastAcceptedUpstreamBase,
) ||
napcatAuditOverrideCommandText.includes(
napcatAutomationDefaults.upstreamReleaseRef,
)
) {
throw new Error("NapCat upstream audit command override self-check failed");
}
const packageJson = JSON.parse(
readFileSync(resolveInsideRoot("mcp/ktWorkflow/package.json"), "utf8"),
) as { scripts?: Record<string, string> };
if (!packageJson.scripts?.["napcat-remote-dev-handoff"]) {
throw new Error("NapCat remote handoff npm script self-check failed");
}
const registeredByFakeServer: string[] = [];
const fakeMcpServer = {
/**
* Records registered MCP tool names so self-test proves the actual registerTools surface, not only the constants list.
* @param name - MCP tool name passed by `registerTools`.
* @returns Nothing; the name is appended to `registeredByFakeServer`.
*/
registerTool(name: string): void {
registeredByFakeServer.push(name);
},
};
registerTools(fakeMcpServer as never);
for (const toolName of [
"kt_napcat_upstream_audit",
"kt_napcat_sync_candidate_review",
"kt_napcat_runtime_release_readiness",
"kt_napcat_remote_dev_handoff",
"kt_nas_codex_bootstrap_plan",
] as const) {
if (!registeredToolNames.includes(toolName)) {
throw new Error(`NapCat MCP tool registry self-check failed: ${toolName}`);
}
if (!registeredByFakeServer.includes(toolName)) {
throw new Error(`NapCat MCP registerTools self-check failed: ${toolName}`);
}
}
if (parseNapcatAutomationClassification("manual-review") !== "manual-review") {
throw new Error("NapCat classification parser self-check failed");
}
const hotZoneAudit = classifyNapcatUpstreamAudit({
dryMergeConflict: false,
forkPatchFiles: ["packages/napcat-core/login/runtime.ts"],
upstreamChangedFiles: ["packages/napcat-core/login/runtime.ts"],
});
if (hotZoneAudit.classification !== "manual-review") {
throw new Error("NapCat hot-zone audit classification self-check failed");
}
if (!hotZoneAudit.reasonCodes.includes("HOT_ZONE_CHANGED")) {
throw new Error("NapCat hot-zone reason-code self-check failed");
}
const dryRunAudit = buildNapcatUpstreamAudit({
dryMergeConflict: false,
forkPatchFiles: ["packages/napcat-core/runtime/base.ts"],
upstreamChangedFiles: ["packages/napcat-core/runtime/upstream.ts"],
});
if (dryRunAudit.execute !== false || dryRunAudit.artifacts.length === 0) {
throw new Error(
"NapCat upstream audit dry-run artifact plan self-check failed",
);
}
const dryRunAuditDefaultCommands = dryRunAudit.commands.map(
(item) => item.command,
);
if (
dryRunAuditDefaultCommands.some((command) =>
command.includes("FORK_BRANCH:-HEAD"),
) ||
!dryRunAuditDefaultCommands.some((command) =>
command.includes(`FORK_BRANCH:-${napcatAutomationDefaults.forkBranch}`),
) ||
!dryRunAuditDefaultCommands.some((command) =>
command.includes(
`UPSTREAM_RELEASE_REF:-${napcatAutomationDefaults.upstreamReleaseRef}`,
),
)
) {
throw new Error("NapCat upstream audit command default alignment self-check failed");
}
const codexCommand = buildNapcatCodexExecCommand({
artifactRoot: "/vol1/docker/kt-codex/artifacts/smoke",
contextPacketPath:
"/vol1/docker/kt-codex/artifacts/smoke/context-packet.md",
outputSchemaPath:
"/vol1/docker/kt-codex/workspace/KT/mcp/ktWorkflow/prompts/napcat/schemas/upstream-audit.schema.json",
promptName: "upstream-audit",
workspaceRoot: "/vol1/docker/kt-codex/workspace/KT",
});
if (!codexCommand.includes("--ask-for-approval never")) {
throw new Error("NapCat Codex runner approval policy self-check failed");
}
if (!codexCommand.includes("codex --ask-for-approval never exec")) {
throw new Error("NapCat Codex runner top-level approval order self-check failed");
}
if (codexCommand.includes("codex exec") || codexCommand.includes("$(cat")) {
throw new Error("NapCat Codex runner stdin safety self-check failed");
}
for (const expected of [
"CODEX_HOME='/vol1/docker/kt-codex/home/.codex'",
"--cd '/vol1/docker/kt-codex/workspace/KT'",
"--profile-v2 automation",
"--sandbox workspace-write",
"--json",
"--ephemeral",
"--output-schema '/vol1/docker/kt-codex/workspace/KT/mcp/ktWorkflow/prompts/napcat/schemas/upstream-audit.schema.json'",
"--output-last-message '/vol1/docker/kt-codex/artifacts/smoke/codex-upstream-audit.md'",
"- < '/vol1/docker/kt-codex/artifacts/smoke/context-packet.md'",
]) {
if (!codexCommand.includes(expected)) {
throw new Error(`NapCat Codex runner command missing: ${expected}`);
}
}
if (codexCommand.includes("git push") || codexCommand.includes("kubectl")) {
throw new Error("NapCat Codex runner mutation guard self-check failed");
}
const upstreamSyncJenkinsTemplate = readFileSync(
resolveInsideRoot(
"mcp/ktWorkflow/ci/jenkins/KT-NapCatQQ-Upstream-Sync.Jenkinsfile",
),
"utf8",
);
const runtimeReleaseJenkinsTemplate = readFileSync(
resolveInsideRoot(
"mcp/ktWorkflow/ci/jenkins/KT-NapCatQQ-Runtime-Release.Jenkinsfile",
),
"utf8",
);
for (const expected of [
"RUN_CODEX_AUDIT",
"CONFIRM_CODEX_WORKSPACE_WRITE",
"RUN_CODEX_WORKSPACE_WRITE_AUDIT",
]) {
if (!upstreamSyncJenkinsTemplate.includes(expected)) {
throw new Error(`NapCat upstream Jenkins template missing: ${expected}`);
}
}
/**
* Verifies that the default upstream audit Jenkins call stays dry-run before the optional Codex execution stage.
* @param template - Full Jenkinsfile text to inspect.
* @returns Nothing; throws when the default audit call is unsafe.
*/
const assertUpstreamJenkinsDefaultAuditDryRun = (template: string): void => {
const defaultStageStart = template.indexOf(
"stage('ktWorkflow upstream audit dry-run')",
);
const codexStageStart = template.indexOf(
"stage('ktWorkflow upstream audit with Codex')",
);
if (
defaultStageStart === -1 ||
codexStageStart === -1 ||
defaultStageStart >= codexStageStart
) {
throw new Error(
"NapCat upstream Jenkins template default audit stage order self-check failed",
);
}
const defaultUpstreamAuditStage = template.slice(
defaultStageStart,
codexStageStart,
);
if (!defaultUpstreamAuditStage.includes("run napcat-upstream-audit --")) {
throw new Error(
"NapCat upstream Jenkins template default audit stage missing audit command",
);
}
for (const forbiddenFlag of ["--execute", "--use-codex"]) {
if (defaultUpstreamAuditStage.includes(forbiddenFlag)) {
throw new Error(
`NapCat upstream Jenkins template default audit must not include ${forbiddenFlag}`,
);
}
}
};
assertUpstreamJenkinsDefaultAuditDryRun(upstreamSyncJenkinsTemplate);
const unsafeDefaultUpstreamAuditTemplate = [
"stage('ktWorkflow upstream audit dry-run') {",
" pnpm --dir \"$KT_WORKFLOW_DIR\" run napcat-upstream-audit -- \\",
" --execute \\",
" --use-codex",
"}",
"stage('ktWorkflow upstream audit with Codex') {",
" pnpm --dir \"$KT_WORKFLOW_DIR\" run napcat-upstream-audit -- \\",
" --execute \\",
" --use-codex",
"}",
].join("\n");
let unsafeDefaultUpstreamAuditRejected = false;
try {
assertUpstreamJenkinsDefaultAuditDryRun(
unsafeDefaultUpstreamAuditTemplate,
);
} catch (error) {
unsafeDefaultUpstreamAuditRejected = String(error).includes(
"NapCat upstream Jenkins template default audit must not include",
);
}
if (!unsafeDefaultUpstreamAuditRejected) {
throw new Error(
"NapCat upstream Jenkins template unsafe default audit self-check failed",
);
}
for (const forbidden of ["git merge", "git push", "docker build", "kubectl"]) {
if (upstreamSyncJenkinsTemplate.includes(forbidden)) {
throw new Error(
`NapCat upstream Jenkins template forbidden command leaked: ${forbidden}`,
);
}
}
for (const expected of [
"choice(name: 'RUNTIME_BUILD_JOB'",
"choice(name: 'API_PROMOTION_JOB'",
"CONFIRM_API_PROMOTION_TARGET",
"PROMOTE_KT_TEMPLATE_API_MAIN",
]) {
if (!runtimeReleaseJenkinsTemplate.includes(expected)) {
throw new Error(`NapCat runtime Jenkins template missing: ${expected}`);
}
}
for (const forbidden of [
"string(name: 'RUNTIME_BUILD_JOB'",
"string(name: 'API_PROMOTION_JOB'",
"git merge",
"git push",
"docker build",
"kubectl",
]) {
if (runtimeReleaseJenkinsTemplate.includes(forbidden)) {
throw new Error(
`NapCat runtime Jenkins template forbidden text leaked: ${forbidden}`,
);
}
}
const runtimeImageBuildStageStart = runtimeReleaseJenkinsTemplate.indexOf(
"stage('Runtime image build and publish')",
);
const apiPromotionStageStart = runtimeReleaseJenkinsTemplate.indexOf(
"stage('API promotion')",
);
const pipelineEndBeforeRuntimeHelpers = runtimeReleaseJenkinsTemplate.indexOf(
"\n}\n\n/**",
apiPromotionStageStart,
);
if (
runtimeImageBuildStageStart === -1 ||
apiPromotionStageStart === -1 ||
pipelineEndBeforeRuntimeHelpers === -1 ||
runtimeImageBuildStageStart > apiPromotionStageStart
) {
throw new Error("NapCat runtime Jenkins template stage slice self-check failed");
}
const runtimeImageBuildStage = runtimeReleaseJenkinsTemplate.slice(
runtimeImageBuildStageStart,
apiPromotionStageStart,
);
const apiPromotionStage = runtimeReleaseJenkinsTemplate.slice(
apiPromotionStageStart,
pipelineEndBeforeRuntimeHelpers,
);
if (runtimeImageBuildStage.includes("CONFIRM_API_PROMOTION_TARGET")) {
throw new Error(
"NapCat runtime Jenkins runtime image build stage must not check API promotion confirmation",
);
}
if (!apiPromotionStage.includes("CONFIRM_API_PROMOTION_TARGET")) {
throw new Error(
"NapCat runtime Jenkins API promotion stage missing confirmation target check",
);
}
if (!apiPromotionStage.includes("PROMOTE_KT_TEMPLATE_API_MAIN")) {
throw new Error(
"NapCat runtime Jenkins API promotion stage missing production confirmation value",
);
}
const apiPromotionConfirmationIndex = apiPromotionStage.indexOf(
"CONFIRM_API_PROMOTION_TARGET",
);
const apiPromotionBuildIndex = apiPromotionStage.indexOf(
"build job: params.API_PROMOTION_JOB",
);
if (
apiPromotionConfirmationIndex === -1 ||
apiPromotionBuildIndex === -1 ||
apiPromotionConfirmationIndex > apiPromotionBuildIndex
) {
throw new Error(
"NapCat runtime Jenkins API promotion confirmation must run before API build job",
);
}
const codexExecutionArtifactRoot = resolveInsideRoot(
".kt-workspace/test-artifacts/napcat-codex-runner-self-test",
);
const codexInvalidSchemaAudit = buildNapcatUpstreamAudit({
artifactRoot: codexExecutionArtifactRoot,
codexExecutor: () => ({
ok: true,
stderr: "schema warning",
stdout: "{\"classification\":\"surprising\"}\n",
}),
dryMergeConflict: false,
execute: true,
forkPatchFiles: [],
upstreamChangedFiles: [],
useCodex: true,
});
if (
codexInvalidSchemaAudit.classification !== "blocked" ||
!codexInvalidSchemaAudit.reasonCodes.includes("CODEX_SCHEMA_INVALID")
) {
throw new Error("NapCat Codex invalid schema fallback self-check failed");
}
if (
!existsSync(
path.join(codexExecutionArtifactRoot, "codex-upstream-audit.jsonl"),
) ||
!existsSync(path.join(codexExecutionArtifactRoot, "context-packet.md")) ||
readFileSync(
path.join(codexExecutionArtifactRoot, "codex-upstream-audit.stderr.log"),
"utf8",
) !== "schema warning"
) {
throw new Error("NapCat Codex artifact capture self-check failed");
}
const codexItemTextArtifactRoot = resolveInsideRoot(
".kt-workspace/test-artifacts/napcat-codex-runner-success-self-test",
);
const codexItemTextAudit = buildNapcatUpstreamAudit({
artifactRoot: codexItemTextArtifactRoot,
codexExecutor: () => ({
ok: true,
stderr: "",
stdout: `${JSON.stringify({
item: {
text: JSON.stringify({
classification: "safe-candidate",
reasonCodes: ["NO_UPSTREAM_CHANGE"],
recommendedAction: "create-candidate-branch",
summary: "ok",
}),
type: "agent_message",
},
type: "item.completed",
})}\n`,
}),
dryMergeConflict: false,
execute: true,
forkPatchFiles: [],
upstreamChangedFiles: [],
useCodex: true,
});
if (
codexItemTextAudit.classification !== "safe-candidate" ||
!codexItemTextAudit.reasonCodes.includes("NO_UPSTREAM_CHANGE") ||
codexItemTextAudit.recommendedAction !== "create-candidate-branch"
) {
throw new Error("NapCat Codex item.text JSONL success self-check failed");
}
const codexThrowArtifactRoot = resolveInsideRoot(
".kt-workspace/test-artifacts/napcat-codex-runner-throw-self-test",
);
const codexThrowAudit = buildNapcatUpstreamAudit({
artifactRoot: codexThrowArtifactRoot,
codexExecutor: () => {
throw new Error("codex timed out");
},
dryMergeConflict: false,
execute: true,
forkPatchFiles: [],
upstreamChangedFiles: [],
useCodex: true,
});
if (
codexThrowAudit.classification !== "blocked" ||
!readFileSync(
path.join(codexThrowArtifactRoot, "codex-upstream-audit.stderr.log"),
"utf8",
).includes("codex timed out")
) {
throw new Error("NapCat Codex thrown executor stderr self-check failed");
}
const unsafeArtifactRootPath = path.resolve("/tmp/not-kt");
const unsafeArtifactRootExisted = existsSync(unsafeArtifactRootPath);
let unsafeArtifactRootRejected = false;
let unsafeArtifactExecutorCalled = false;
try {
buildNapcatUpstreamAudit({
artifactRoot: "/tmp/not-kt",
codexExecutor: () => {
unsafeArtifactExecutorCalled = true;
return { ok: true, stderr: "", stdout: "{}\n" };
},
dryMergeConflict: false,
execute: true,
forkPatchFiles: [],
upstreamChangedFiles: [],
useCodex: true,
});
} catch (error) {
unsafeArtifactRootRejected = String(error).includes(
"Unsafe NapCat Codex artifactRoot",
);
} finally {
if (!unsafeArtifactRootExisted && existsSync(unsafeArtifactRootPath)) {
rmSync(unsafeArtifactRootPath, { force: true, recursive: true });
}
}
if (!unsafeArtifactRootRejected || unsafeArtifactExecutorCalled) {
throw new Error("NapCat Codex unsafe artifact root self-check failed");
}
const dryRunAuditCommands: string[] = [];
for (const command of dryRunAudit.commands) {
if (!command.readOnly) {
throw new Error("NapCat upstream audit dry-run read-only flag failed");
}
dryRunAuditCommands.push(command.command);
}
const dryRunAuditCommandText = dryRunAuditCommands.join("\n");
for (const expected of [
"git status --short --branch",
"git fetch --dry-run upstream --tags --prune",
"git fetch --dry-run origin --prune",
"git diff --name-only",
"git merge-tree",
]) {
if (!dryRunAuditCommandText.includes(expected)) {
throw new Error(
`NapCat upstream audit dry-run command missing: ${expected}`,
);
}
}
for (const command of dryRunAudit.commands) {
if (
command.readOnly &&
/^git\s+fetch(?:\s|$)/.test(command.command) &&
!/\s--dry-run(?:\s|$)/.test(command.command)
) {
throw new Error(
"NapCat upstream audit read-only fetch must use --dry-run",
);
}
}
for (const forbidden of [
{ label: "git merge", regex: /(^|\n)\s*git\s+merge(?:\s|$)/ },
{ label: "git push", regex: /(^|\n)\s*git\s+push(?:\s|$)/ },
{ label: "docker build", regex: /(^|\n)\s*docker\s+build(?:\s|$)/ },
{ label: "kubectl", regex: /(^|\n)\s*kubectl(?:\s|$)/ },
{ label: "deploy", regex: /(^|\n)\s*deploy(?:\s|$)/ },
]) {
if (forbidden.regex.test(dryRunAuditCommandText)) {
throw new Error(
`NapCat upstream audit dry-run command is not read-only: ${forbidden.label}`,
);
}
}
for (const invalidPathCase of [
{
forkPatchFiles: [],
name: "Windows absolute path",
upstreamChangedFiles: ["D:/repo/packages/napcat-core/login/runtime.ts"],
},
{
forkPatchFiles: [],
name: "Unix absolute path",
upstreamChangedFiles: ["/repo/packages/napcat-core/login/runtime.ts"],
},
{
forkPatchFiles: [],
name: "UNC path",
upstreamChangedFiles: [
"\\\\server\\share\\packages\\napcat-core\\login\\runtime.ts",
],
},
{
forkPatchFiles: [],
name: "Windows rooted path",
upstreamChangedFiles: [
"\\packages\\napcat-core\\login\\runtime.ts",
],
},
{
forkPatchFiles: [],
name: "Windows drive-relative path",
upstreamChangedFiles: ["C:packages/napcat-core/login/runtime.ts"],
},
{
forkPatchFiles: [],
name: "parent segment path",
upstreamChangedFiles: ["packages/../napcat-core/login/runtime.ts"],
},
{
forkPatchFiles: ["packages/../napcat-core/login/runtime.ts"],
name: "fork patch parent segment path",
upstreamChangedFiles: [],
},
]) {
const invalidPathAudit = classifyNapcatUpstreamAudit({
dryMergeConflict: false,
forkPatchFiles: invalidPathCase.forkPatchFiles,
upstreamChangedFiles: invalidPathCase.upstreamChangedFiles,
});
if (
invalidPathAudit.classification !== "blocked" ||
invalidPathAudit.recommendedAction !== "block" ||
!invalidPathAudit.reasonCodes.includes("METADATA_UNAVAILABLE")
) {
throw new Error(
`NapCat invalid audit path self-check failed: ${invalidPathCase.name}`,
);
}
}
for (const promptName of napcatAutomationPromptNames) {
const promptText = loadNapcatAutomationPrompt(promptName);
if (!promptText.includes("Do not edit files")) {
throw new Error(`NapCat prompt safety contract missing: ${promptName}`);
}
}
let traversalPromptRejected = false;
try {
loadNapcatAutomationPrompt("../../README" as NapcatAutomationPromptName);
} catch (error) {
traversalPromptRejected = String(error).includes(
"Unsupported NapCat automation prompt",
);
}
if (!traversalPromptRejected) {
throw new Error("NapCat prompt traversal self-check failed");
}
const reviewClassifier = {
localizedSecretLabelAllowed: isBenignCredentialReviewValue("密钥"),
realTokenRejected: !isBenignCredentialReviewValue(
@ -1026,6 +1833,18 @@ export async function runSelfTest(): Promise<void> {
throw new Error("database migration verification self-check failed");
}
const qqbotAccountScanPlan = buildBusinessTestPlan({
flow: "qqbot-account-scan",
});
const qqbotAccountScanText = JSON.stringify(qqbotAccountScanPlan);
if (
!qqbotAccountScanText.includes("pending sessionId") ||
!qqbotAccountScanText.includes("第一轮 Docker run") ||
!qqbotAccountScanText.includes("不能造脏数据")
) {
throw new Error("QQBot account scan non-dirty smoke self-check failed");
}
const closeoutNeedsUpgrade = buildWorkstreamCloseout({
cleanupEvidence: ["cleanup-history dry-run deleted=0。"],
docSyncEvidence: ["kt_change_doc_sync requiredDocs 已处理。"],

View File

@ -3,8 +3,13 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
parseNasCodexBootstrapCliArgs,
parseDeployObservationCliArgs,
parseGlobalReviewCliArgs,
parseNapcatRemoteDevHandoffCliArgs,
parseNapcatRuntimeReleaseReadinessCliArgs,
parseNapcatSyncCandidateReviewCliArgs,
parseNapcatUpstreamAuditCliArgs,
parseObsidianContextCliArgs,
parseObsidianCliArgs,
parseObsidianSyncCliArgs,
@ -18,6 +23,13 @@ import { buildGlobalCodeReview } from './tools/review.js';
import { registerTools } from './registerTools.js';
import { runSelfTest } from './selfTest.js';
import { buildNapcatDeviceProfileCheck } from './tools/testing.js';
import {
buildNapcatRemoteDevHandoff,
buildNapcatRuntimeReleaseReadiness,
buildNapcatSyncCandidateReview,
buildNapcatUpstreamAudit,
buildNasCodexBootstrapPlan,
} from './tools/napcatAutomation.js';
if (process.argv.includes('--cleanup-history')) {
console.log(JSON.stringify(cleanupHistoryArtifacts(parseCliCleanupArgs(process.argv)), null, 2));
@ -43,6 +55,38 @@ if (process.argv.includes('--cleanup-history')) {
2,
),
);
} else if (process.argv.includes('--napcat-upstream-audit')) {
console.log(
JSON.stringify(buildNapcatUpstreamAudit(parseNapcatUpstreamAuditCliArgs(process.argv)), null, 2),
);
} else if (process.argv.includes('--napcat-sync-candidate-review')) {
console.log(
JSON.stringify(
buildNapcatSyncCandidateReview(parseNapcatSyncCandidateReviewCliArgs(process.argv)),
null,
2,
),
);
} else if (process.argv.includes('--napcat-runtime-release-readiness')) {
console.log(
JSON.stringify(
buildNapcatRuntimeReleaseReadiness(parseNapcatRuntimeReleaseReadinessCliArgs(process.argv)),
null,
2,
),
);
} else if (process.argv.includes('--napcat-remote-dev-handoff')) {
console.log(
JSON.stringify(
buildNapcatRemoteDevHandoff(parseNapcatRemoteDevHandoffCliArgs(process.argv)),
null,
2,
),
);
} else if (process.argv.includes('--nas-codex-bootstrap')) {
console.log(
JSON.stringify(buildNasCodexBootstrapPlan(parseNasCodexBootstrapCliArgs(process.argv)), null, 2),
);
} else if (process.argv.includes('--self-test')) {
await runSelfTest();
} else if (process.argv.includes('--workstream-closeout')) {

View File

@ -0,0 +1,64 @@
import { readFileSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
export const napcatAutomationPromptNames = [
'upstream-audit',
'sync-candidate-review',
'runtime-release-readiness',
'remote-dev-handoff',
] as const;
export type NapcatAutomationPromptName =
(typeof napcatAutomationPromptNames)[number];
const promptsRoot = path.resolve(
fileURLToPath(new URL('../..', import.meta.url)),
'prompts',
'napcat',
);
const promptsRootBoundary = promptsRoot.endsWith(path.sep)
? promptsRoot
: `${promptsRoot}${path.sep}`;
const napcatAutomationPromptNameSet = new Set<string>(
napcatAutomationPromptNames,
);
const napcatAutomationPromptFileNames = {
'upstream-audit': 'upstream-audit.md',
'sync-candidate-review': 'sync-candidate-review.md',
'runtime-release-readiness': 'runtime-release-readiness.md',
'remote-dev-handoff': 'remote-dev-handoff.md',
} satisfies Record<NapcatAutomationPromptName, string>;
/**
* Parses an external prompt name into the source-controlled NapCat prompt enum.
* @param value - Raw prompt name from CLI, JSON, MCP input, or a typed caller; only exact entries in `napcatAutomationPromptNames` are accepted.
* @returns A stable prompt name that can be mapped to a fixed file under `prompts/napcat`.
*/
export function parseNapcatAutomationPromptName(
value: string,
): NapcatAutomationPromptName {
if (napcatAutomationPromptNameSet.has(value)) {
return value as NapcatAutomationPromptName;
}
throw new Error(`Unsupported NapCat automation prompt: ${value}`);
}
/**
* Loads a source-controlled NapCat automation prompt by stable prompt name.
* @param name - Prompt identifier from `napcatAutomationPromptNames`; runtime callers may still pass untyped external strings, so the loader re-validates and resolves only fixed filenames.
* @returns UTF-8 prompt text used by Codex CLI automation.
*/
export function loadNapcatAutomationPrompt(
name: NapcatAutomationPromptName,
): string {
const promptName = parseNapcatAutomationPromptName(String(name));
const promptPath = path.resolve(
promptsRoot,
napcatAutomationPromptFileNames[promptName],
);
if (!promptPath.startsWith(promptsRootBoundary)) {
throw new Error(`NapCat automation prompt escaped prompt root: ${name}`);
}
return readFileSync(promptPath, 'utf8');
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,130 @@
import { z } from 'zod';
export const napcatAutomationClassifications = [
'safe-candidate',
'manual-review',
'blocked',
] as const;
export const napcatAutomationReasonCodes = [
'NO_UPSTREAM_CHANGE',
'HOT_ZONE_CHANGED',
'FORK_PATCH_OVERLAP',
'DRY_MERGE_CONFLICT',
'BUILD_GRAPH_CHANGED',
'METADATA_UNAVAILABLE',
'CODEX_SCHEMA_INVALID',
] as const;
export const napcatAutomationRecommendedActions = [
'no-op',
'create-candidate-branch',
'request-human-review',
'block',
] as const;
export const upstreamAuditOutputSchema = z.object({
classification: z.enum(napcatAutomationClassifications),
reasonCodes: z.array(z.enum(napcatAutomationReasonCodes)).min(1),
recommendedAction: z.enum(napcatAutomationRecommendedActions),
summary: z.string().min(1),
}).strict();
export type NapcatAutomationClassification =
(typeof napcatAutomationClassifications)[number];
export type NapcatAutomationReasonCode =
(typeof napcatAutomationReasonCodes)[number];
export type NapcatAutomationRecommendedAction =
(typeof napcatAutomationRecommendedActions)[number];
export interface NapcatAuditClassificationInput {
dryMergeConflict: boolean;
forkPatchFiles: string[];
upstreamChangedFiles: string[];
}
export interface NapcatAuditClassificationResult {
classification: NapcatAutomationClassification;
reasonCodes: NapcatAutomationReasonCode[];
recommendedAction: NapcatAutomationRecommendedAction;
summary: string;
}
export interface NapcatAuditCommand {
command: string;
description: string;
readOnly: boolean;
}
export interface NapcatCodexExecCommandInput {
artifactRoot: string;
codexHome?: string;
contextPacketPath: string;
outputSchemaPath: string;
promptName: 'upstream-audit';
workspaceRoot: string;
}
export interface NapcatCodexExecResult {
ok: boolean;
stderr: string;
stdout: string;
}
export type NapcatCodexExecutor = (
command: string,
) => NapcatCodexExecResult;
export interface NapcatUpstreamAuditInput
extends NapcatAuditClassificationInput {
artifactRoot?: string;
codexExecutor?: NapcatCodexExecutor;
codexHome?: string;
createCandidateBranch?: boolean;
execute?: boolean;
forkBranch?: string;
forkRepo?: string;
lastAcceptedUpstreamBase?: string;
outputSchemaPath?: string;
promptName?: 'upstream-audit';
upstreamReleaseTag?: string;
upstreamRepo?: string;
useCodex?: boolean;
workspaceRoot?: string;
}
export interface NapcatAutomationArtifact {
path: string;
type: 'context' | 'json' | 'jsonl' | 'log' | 'markdown' | 'script';
}
export interface NapcatAutomationResult {
artifacts: NapcatAutomationArtifact[];
classification: NapcatAutomationClassification;
execute: boolean;
recommendedAction: string;
summary: string;
}
export interface NapcatUpstreamAuditResult
extends NapcatAuditClassificationResult {
artifacts: NapcatAutomationArtifact[];
commands: NapcatAuditCommand[];
execute: boolean;
hotZonePatterns: string[];
}
/**
* Parses the upstream audit classification emitted by deterministic collectors or Codex.
* @param value - Raw classification string from deterministic audit metadata or Codex JSON output; must match the shared NapCat automation taxonomy.
* @returns A supported NapCat automation classification for downstream workflow decisions.
*/
export function parseNapcatAutomationClassification(
value: string,
): NapcatAutomationClassification {
const parsed = z.enum(napcatAutomationClassifications).safeParse(value);
if (!parsed.success) {
throw new Error(`Unsupported NapCat audit classification: ${value}`);
}
return parsed.data;
}

View File

@ -44,7 +44,7 @@ function createNapcatCheck(
/**
* Checks that the API project still contains the NapCat device-profile guardrails needed to avoid unknown QQ devices.
* @param input - Project alias or path to scan; defaults to the API backend.
* @returns Static guardrail report covering hostname, MAC, machine-info, runtime dir, and DB timezone.
* @returns Static guardrail report covering hostname, physical-style MAC, machine-info, runtime dir, and DB timezone.
*/
export function buildNapcatDeviceProfileCheck(
input: NapcatDeviceProfileCheckInput = {},
@ -76,10 +76,12 @@ export function buildNapcatDeviceProfileCheck(
'device identity uses qqnt-visible-hostname-v1 and pc-<8hex>',
),
createNapcatCheck(
'docker-bridge-mac-strategy',
deviceIdentity.includes('docker-bridge-mac-v1') &&
deviceIdentity.includes('02:42:'),
'device identity uses docker-bridge-mac-v1 and stable 02:42:* MACs',
'physical-oui-mac-strategy',
deviceIdentity.includes('physical-oui-mac-v1') &&
deviceIdentity.includes('hasPhysicalOuiMacPrefix') &&
deviceIdentity.includes('isRejectedVirtualMacPrefix') &&
deviceIdentity.includes('NAPCAT_PHYSICAL_OUI_PREFIXES'),
'device identity uses physical-oui-mac-v1 and rejects Docker/QEMU virtual MAC prefixes',
),
createNapcatCheck(
'machine-info-options',
@ -230,13 +232,26 @@ export function buildBusinessTestPlan(input: BusinessTestPlanInput = {}): Record
},
'qqbot-account-scan': {
assertions: [
'扫码创建返回 qrcode/containerName/webuiPort。',
'扫码创建接口先返回 pending sessionId再通过 SSE/status 展示容器创建、二维码生成、等待扫码和终态。',
'新增账号首次容器创建必须在第一轮 Docker run 注入设备身份和中文桌面 runtime profile而不是登录后补 env。',
'取消/过期/失败会清理未绑定 NapCat 容器。',
'若真实接口 smoke 会创建线上 QQBot 账号或 NapCat 容器,必须先确认清理方案;不能造脏数据时用 RED/GREEN、只读线上计数复核和未执行真实造号原因替代。',
'删除账号会二次确认并删除专属容器。',
],
preflight: ['NAS SSH 可达', 'NapCat WebUI 可达', 'API QQBot env 配置完整'],
preflight: [
'NAS SSH 可达',
'NapCat WebUI 可达',
'API QQBot env 配置完整',
'确认本轮是否允许创建真实 QQBot 账号和 NapCat 容器',
],
projects: ['api', 'admin'],
steps: ['打开账号连接页', '点击扫码新增账号', '轮询二维码状态', '确认账号回填', '测试删除联动容器'],
steps: [
'打开账号连接页',
'点击扫码新增账号并确认接口不等待 Docker/WebUI 长耗时',
'订阅 scan/events 或轮询 status 确认中文进度',
'确认账号回填和 runtime/profile 归属',
'测试删除联动容器或记录未造号的替代验证证据',
],
},
'qqbot-login-sse': {
assertions: [

View File

@ -189,6 +189,40 @@ export interface NapcatDeviceProfileCheckInput {
project?: string;
}
export interface NapcatPromptToolInput {
artifactRoot?: string;
candidateBranch?: string;
execute?: boolean;
forkBranch?: string;
forkRepo?: string;
upstreamReleaseTag?: string;
upstreamRepo?: string;
useCodex?: boolean;
workspaceRoot?: string;
}
export interface NapcatSyncCandidateReviewInput extends NapcatPromptToolInput {
lastAcceptedUpstreamBase?: string;
}
export interface NapcatRuntimeReleaseReadinessInput extends NapcatPromptToolInput {
apiImageTag?: string;
napcatImageTag?: string;
profile?: string;
}
export interface NapcatRemoteDevHandoffInput extends NapcatPromptToolInput {
targetHost?: string;
}
export interface NasCodexBootstrapPlanInput {
artifactRoot?: string;
codexHome?: string;
execute?: boolean;
serviceName?: string;
workspaceRoot?: string;
}
export interface BlockerResolutionInput {
attempts?: number;
command?: string;