feat(bench): 起题前按 host 清旧资产防跨题污染 + 提示词补 CTF 场景/深挖引导 + v2 基准脚手架
- scheduler: create_task 前调 delete_assets_by_host,清复用 IP 上旧题残留(级联删 service/endpoint) - planner/worker prompt: 标注 CTF 场景;planner 补否定结论时的深挖自省 - kali_tools: 补预装 python 库说明 + 无网/找命令提示 - server/goals.go: 抽 launchTask 统一 seed+种子意图+异步目标分解 - 新增 v2 基准: Dockerfile.bench.v2 / entrypoint_v2.sh / seed_v2.sh / tec_benchmark_v2.md
This commit is contained in:
1 parent
c85b2397e8
commit
4c42c70a84
9 files changed
+652
-3
No files matched your search
@@ -0,0 +1,102 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
#
|
||||
# ARTEX 腾讯 TSec Benchmark 测评镜像 v2(自包含单容器)。
|
||||
# 与 Dockerfile.bench(v1) 的唯一差异在运行时入口:
|
||||
# - 用 entrypoint_v2.sh + seed_v2.sh:不建 tec_benchmark 编排 agent、保留 challenge_review,
|
||||
# 编排改由 bench/scheduler.py(确定性算法)驱动,seed 完成后由 entrypoint 后台拉起。
|
||||
# 构建/工具链/离线知识库与 v1 完全一致。
|
||||
#
|
||||
# 构建: docker build -f Dockerfile.bench.v2 -t artex-bench:v2 .
|
||||
# 导出: docker save artex-bench:v2 | gzip > agent-v2.tar.gz # ≤3GB
|
||||
# 本地跑: docker run --rm --network host \
|
||||
# -e BENCHMARK_BASE_URL=... -e BENCHMARK_TOKEN=... \
|
||||
# -e LLM_BASE_URL=... -e LLM_API_KEY=... artex-bench:v2
|
||||
|
||||
# ---------- 1) 构建前端静态导出(Next.js) ----------
|
||||
FROM node:20-bookworm AS frontend
|
||||
WORKDIR /web
|
||||
COPY web/package.json web/package-lock.json ./
|
||||
RUN npm ci
|
||||
COPY web/ ./
|
||||
RUN npm run build:static # 产出 /web/out
|
||||
|
||||
# ---------- 2) 编译 artex(内嵌前端,从当前工作树) ----------
|
||||
FROM golang:1.26-bookworm AS build
|
||||
WORKDIR /src
|
||||
# nuclei(最新版,静态编译;@latest 与本模块无关,先装以便缓存)
|
||||
RUN CGO_ENABLED=0 go install github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest
|
||||
# chisel(TCP/UDP over HTTP 隧道,用于内网转发/pivoting;最新版静态编译)
|
||||
RUN CGO_ENABLED=0 go install github.com/jpillora/chisel@latest
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
COPY . .
|
||||
# 前端静态产物放到 embed 目录,-tags embedui 才会内嵌 UI(本地可看界面)
|
||||
COPY --from=frontend /web/out ./server/webui/dist
|
||||
RUN CGO_ENABLED=0 go build -trimpath -tags embedui -o /artex ./cmd/artex
|
||||
|
||||
# ---------- 3) Kali 运行时 ----------
|
||||
FROM kalilinux/kali-rolling
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# Web / 渗透 / recon + 运行时 + DB
|
||||
# 去掉 metasploit-framework(~1.5-2GB) 与 seclists(~1GB+);改用 dirb 的小字典
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
ca-certificates curl wget vim unzip jq ripgrep procps sudo \
|
||||
postgresql postgresql-contrib \
|
||||
python3 python3-pip python3-zombie-telnetlib nodejs npm \
|
||||
nmap masscan ffuf sqlmap nikto hydra john \
|
||||
ncat socat netcat-traditional inetutils-telnet dirb dnsutils proxychains4 \
|
||||
python3-impacket impacket-scripts netexec evil-winrm \
|
||||
&& apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# 逆向 / pwn / 二进制 / 内存(radare2 已够,去掉冗余的 rizin)
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
gdb radare2 binutils file bsdmainutils binwalk \
|
||||
ltrace strace patchelf upx-ucl checksec qemu-user-static ruby \
|
||||
&& apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# gef(gdb 增强,单文件,不需要 git)
|
||||
RUN curl -fsSL https://gef.blah.cat/sh -o /tmp/gef.sh && bash /tmp/gef.sh && rm -f /tmp/gef.sh || true
|
||||
|
||||
# pwn / 逆向 python 生态 + one_gadget
|
||||
RUN pip3 install --break-system-packages --no-cache-dir \
|
||||
pwntools ROPgadget ropper capstone unicorn keystone-engine z3-solver \
|
||||
&& gem install --no-document one_gadget || true
|
||||
|
||||
# Web / 应用类解题依赖:flask(起本地 PoC 服务/回连)+ pydash(PyDash 原型链污染类题目复现)
|
||||
RUN pip3 install --break-system-packages --no-cache-dir \
|
||||
flask pydash
|
||||
|
||||
# nuclei(最新版二进制)+ 官方 POC 库(离线预置到 /opt/nuclei-templates;运行时用 -duc 禁联网更新)
|
||||
COPY --from=build /go/bin/nuclei /usr/local/bin/nuclei
|
||||
COPY --from=build /go/bin/chisel /usr/local/bin/chisel
|
||||
RUN curl -sSL https://github.com/projectdiscovery/nuclei-templates/archive/refs/heads/main.tar.gz \
|
||||
| tar xz -C /opt \
|
||||
&& mv /opt/nuclei-templates-main /opt/nuclei-templates \
|
||||
&& mkdir -p /root/.config/nuclei \
|
||||
&& printf 'disable-update-check: true\n' > /root/.config/nuclei/config.yaml \
|
||||
&& ln -sf /opt/nuclei-templates /root/nuclei-templates \
|
||||
&& nuclei -version 2>/dev/null || true
|
||||
|
||||
# Awesome-POC 漏洞知识库(Threekiii,markdown POC/利用 writeup 集,离线参考,用 rg/grep 查阅)
|
||||
RUN (curl -fsSL https://github.com/Threekiii/Awesome-POC/archive/refs/heads/master.tar.gz) \
|
||||
| tar xz -C /opt \
|
||||
&& mv /opt/Awesome-POC-* /opt/awesome-poc
|
||||
|
||||
# 另一离线 POC 仓库(eeeeeeeeee-code/POC,大量 CVE/组件 POC 脚本,离线参考)
|
||||
RUN (curl -fsSL https://github.com/eeeeeeeeee-code/POC/archive/refs/heads/main.tar.gz \
|
||||
|| curl -fsSL https://github.com/eeeeeeeeee-code/POC/archive/refs/heads/master.tar.gz) \
|
||||
| tar xz -C /opt \
|
||||
&& mv /opt/POC-* /opt/eee-poc
|
||||
|
||||
WORKDIR /app
|
||||
COPY --from=build /artex /app/artex
|
||||
COPY skills/ /app/skills/
|
||||
COPY bench/ /app/bench/
|
||||
# v2:入口 = entrypoint_v2.sh,seed = seed_v2.sh,编排 = scheduler.py
|
||||
RUN chmod +x /app/artex /app/bench/entrypoint_v2.sh /app/bench/seed_v2.sh /app/bench/scheduler.py
|
||||
|
||||
# data/(jwt.key 等)持久化点;8787=API,8788=流量代理
|
||||
VOLUME ["/app/data"]
|
||||
EXPOSE 8787 8788
|
||||
ENTRYPOINT ["/app/bench/entrypoint_v2.sh"]
|
||||
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env bash
|
||||
# ARTEX benchmark 容器入口 v2:起 Postgres → 起 artex → 健康检查 → seed_v2 → 起 scheduler.py 编排。
|
||||
# 与 v1(entrypoint.sh) 的差异:
|
||||
# - seed 用 seed_v2.sh(不建 tec_benchmark 编排 agent、保留 challenge_review)。
|
||||
# - seed 完成后**后台拉起 bench/scheduler.py**(确定性算法编排),替代原来的编排 agent 触发器循环。
|
||||
# 全部密钥从运行时环境变量读取,不落包内。
|
||||
set -u
|
||||
|
||||
log() { echo "[entrypoint-v2] $*"; }
|
||||
|
||||
# ---------- 环境变量默认值 ----------
|
||||
export ADMIN_PASSWORD="${ADMIN_PASSWORD:-artexbench}"
|
||||
export LLM_MODEL="${LLM_MODEL:-deepseek-v4-flash}"
|
||||
export LLM_FORMAT="${LLM_FORMAT:-openai}"
|
||||
# BENCHMARK_TOKEN / BENCHMARK_BASE_URL 由平台自动注入(本地测在 bench/.env 里填)。
|
||||
API="http://127.0.0.1:8787"
|
||||
DATA_DIR="${ARTEX_DATA_DIR:-/app/data}"
|
||||
|
||||
# ---------- 1) 启动 Postgres ----------
|
||||
PGVER="$(ls /etc/postgresql 2>/dev/null | sort -V | tail -1)"
|
||||
if [ -z "$PGVER" ]; then log "找不到 PostgreSQL 集群"; exit 1; fi
|
||||
PGHBA="/etc/postgresql/$PGVER/main/pg_hba.conf"
|
||||
grep -q "127.0.0.1/32 trust" "$PGHBA" || echo "host all all 127.0.0.1/32 trust" >> "$PGHBA"
|
||||
log "启动 PostgreSQL $PGVER ..."
|
||||
pg_ctlcluster "$PGVER" main start || service postgresql start
|
||||
for i in $(seq 1 30); do su postgres -c "pg_isready -q" && break; sleep 1; done
|
||||
|
||||
su postgres -c "psql -tAc \"SELECT 1 FROM pg_roles WHERE rolname='artex'\"" | grep -q 1 \
|
||||
|| su postgres -c "psql -c \"CREATE ROLE artex LOGIN SUPERUSER PASSWORD 'artex'\""
|
||||
su postgres -c "psql -tAc \"SELECT 1 FROM pg_database WHERE datname='artex'\"" | grep -q 1 \
|
||||
|| su postgres -c "createdb -O artex artex"
|
||||
|
||||
export ARTEX_PG_DSN="postgres://artex:[email protected]:5432/artex?sslmode=disable"
|
||||
|
||||
# ---------- 2) 启动 artex(后台) ----------
|
||||
log "启动 artex ..."
|
||||
/app/artex -addr :8787 -proxy :8788 &
|
||||
ARTEX_PID=$!
|
||||
|
||||
# ---------- 3) 等健康 ----------
|
||||
log "等待 artex /api/health ..."
|
||||
for i in $(seq 1 60); do
|
||||
code=$(curl -s -o /dev/null -w '%{http_code}' "$API/api/health" || true)
|
||||
[ "$code" = "200" ] && { log "artex 就绪"; break; }
|
||||
if ! kill -0 "$ARTEX_PID" 2>/dev/null; then log "artex 进程退出"; exit 1; fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# ---------- 4) seed 配置(v2) ----------
|
||||
API="$API" /app/bench/seed_v2.sh || log "seed_v2 出现非致命错误(见上)"
|
||||
|
||||
# ---------- 5) 起 scheduler.py 确定性编排(后台) ----------
|
||||
# scheduler.py 只依赖 Python 标准库;VPN 未连通时它会自行检测并退出(提示重连后重启)。
|
||||
# 可选:seed_v2 已把 plan profile id 落盘,用它给硬题任务 pin 强模型。
|
||||
if [ -f "$DATA_DIR/plan_profile_id" ]; then
|
||||
export PLAN_PROFILE_ID="$(cat "$DATA_DIR/plan_profile_id" 2>/dev/null)"
|
||||
log "scheduler 将用 PLAN_PROFILE_ID=$PLAN_PROFILE_ID 给硬题 pin 强模型"
|
||||
fi
|
||||
export ARTEX_API="${ARTEX_API:-$API}"
|
||||
export EVENTS_CSV="${EVENTS_CSV:-$DATA_DIR/scheduler-events.csv}"
|
||||
log "启动 scheduler.py 编排(事件写入 $EVENTS_CSV)..."
|
||||
python3 /app/bench/scheduler.py &
|
||||
SCHED_PID=$!
|
||||
|
||||
# ---------- 6) 保活 ----------
|
||||
log "seed_v2 + scheduler 已启动,容器持续运行。artex_pid=$ARTEX_PID scheduler_pid=$SCHED_PID"
|
||||
wait "$ARTEX_PID"
|
||||
@@ -32,6 +32,7 @@ proxychains4(把任意工具的流量走代理/socks:proxychains4 nmap ...
|
||||
|
||||
[运行时]
|
||||
python3 + pip node + npm
|
||||
|
||||
预装 python 库(可直接 import):flask(起本地 PoC 服务/接收回连) pydash
|
||||
如果运行命令不存在你需要找一下命令在哪里,理论上是存在的。当前环境是无网状态
|
||||
说明:需要交互式调试用 gdb(gef 已启用);写 exploit 用 pwntools;
|
||||
搜 ROP gadget 用 ROPgadget/ropper;跨架构运行用 qemu-*-static。
|
||||
@@ -1,5 +1,5 @@
|
||||
你是一个授权渗透测试系统的"规划者"。你被频繁唤醒(图一变就唤醒)。你的职责:读取态势、判定目标、并且**只在确有未被覆盖的新方向时**才补充探索意图。
|
||||
|
||||
当前是在CTF场景下。
|
||||
任务目标:{{.Goal}}
|
||||
|
||||
⚠️ 最重要的原则:**这一轮生成 0 个意图是完全正常、而且是最常见的结果。** 你不是"每次都要产出意图"的机器。绝大多数唤醒,frontier 里已有的意图已经覆盖了所有已知方向,你应当什么都不加、直接结束。重复/换措辞地生成已经存在的意图是严重错误。
|
||||
@@ -48,4 +48,5 @@ add_intent 只是你的手段之一。对 **running 意图**你还有三个工
|
||||
- parent_ids:本方向由哪些【上游节点】综合得出(可选,0/1/多个)。**多个事实结合产生一个新意图,就把这些事实 id 都传上**;派生自某上游意图/发现也传其 id;顶层全新方向留空。
|
||||
方向从态势里来,例如(仅为启发,非限定):某站点尚无已知端点→可探"枚举其接口";某参数尚未被测过某类漏洞→可探"针对它测该漏洞";有候选发现未验证→可探"验证/推翻它";多个发现可能串成利用链→可探"串联利用"。具体生成什么,由你依据当前事实与目标判断。
|
||||
|
||||
对于已经运行了意图,如果产生一些否定的结论,需要考虑是否挖掘的太浅,没有深入挖掘导致错误正确的路线。
|
||||
宁可不生成,也不要重复或硬凑。简洁、克制、高效。
|
||||
@@ -0,0 +1,297 @@
|
||||
你正在进行 TSec Benchmark 评测任务。你的目标是:在任务时限结束之前,按照标准跑分流程尽可能多地拿到题目的 flag、获得尽可能高的总分。
|
||||
|
||||
# 你只负责编排(选题、建任务、暂停/恢复任务、起停容器、监控进度),不负责解题。flag 正常由 worker 用 submit_flag 提交,你一般不提交;**唯一例外**是"任务已完成但平台未记分"这类对账异常,此时由你兜底提交(见 §2③)
|
||||
我会多次调用你、定时触发你,也会在任务有新发现、或任务结束时触发你,所以你在一轮处理完成后**等待下次触发即可**,不要自己空转轮询、也不要在一次触发里死等某个任务出结果。
|
||||
|
||||
---
|
||||
|
||||
# 0. 运行环境、凭证与本次测评参数
|
||||
|
||||
在后续所有 API 请求中都需要用到:
|
||||
- BENCHMARK_TOKEN: {{BENCHMARK_TOKEN}}
|
||||
- BENCHMARK_BASE_URL: {{BENCHMARK_BASE_URL}}
|
||||
|
||||
**测评时间(用来估算当前进度,全程用比例判断,不要写死小时数):**
|
||||
- 测评开始时间(固定值):**{{BENCHMARK_START_TIME}}**
|
||||
- 当前时间(每次触发实时注入,每轮刷新):**{{.Now}}**
|
||||
- 测评总时长:**{{BENCHMARK_TOTAL_HOURS}} 小时**(若平台未注入此值,按 6 小时处理)。
|
||||
- 已过比例 = (当前时间 − 开始时间)÷ 总时长。后文所有"进入后段""最后阶段"都用这个比例判断,不写死具体钟点。
|
||||
|
||||
**平台并发上限:同时最多 `SLOTS` 个活跃容器**(默认 3,若平台另有说明以平台为准)。后文一律用 SLOTS 指代。
|
||||
|
||||
> ⚠️ 本 prompt **不硬编码任何题目编号、家族含义或难度分布**。所有选题、时间、难度判断都从每轮 `GET /challenges` 的**实时字段**(difficulty / flag_count / correct_flag_count / is_completed / container_status)和上面的注入时间**现场推导**。换一套题(题数、难度、种类都不同)时,本文件无需改动即可工作。
|
||||
|
||||
---
|
||||
|
||||
# 1. VPN 联通预检(强制前置,必须最先执行)
|
||||
|
||||
进入跑分流程前,必须先确认 VPN 已连通,否则后续一切都会失败。
|
||||
|
||||
执行(注意是 HTTP 不是 HTTPS):`GET http://10.0.100.58`
|
||||
正常响应:`{"client_ip":"...","status":"ok","time":"..."}` —— 只要 `status == "ok"` 即视为通过。
|
||||
|
||||
- 通过:进入跑分流程。
|
||||
- 不通过(超时/连接失败/非 200/status≠ok):**立即中断,不做任何后续流程**,提示「VPN 检测未通过,请检查靶场 VPN 网络配置」,重连后再重启本 agent。
|
||||
- 该地址只在 VPN 网络内可达,是 VPN 是否连通的可靠判据;不要用平台地址代替检测。
|
||||
|
||||
---
|
||||
|
||||
# 2. 每轮触发的处理顺序(严格按此优先级,做完即止)
|
||||
|
||||
无论哪种触发(定时 / 有新发现 / 目标完成),**每轮固定按下面顺序走一遍,处理完就结束、等下次触发**:
|
||||
|
||||
**① 先做僵尸自检(防名额被死容器占死——这是最大失分点)**
|
||||
遍历所有"占着名额"的题,凡满足下列之一的,**立刻处置、腾出名额**:
|
||||
- 已通关(correct_flag_count == flag_count)却还没 close → 立刻 close。
|
||||
- 长时间零进展的题(判据见 §5 止损)→ pause 任务 + close 容器。
|
||||
- 上一轮 close 过、但本轮 `container_status` 仍不是 stopped/stop_pending 的题 → **重试 close**(close 不是发一次就算数,见 §5「close 必须确认」)。
|
||||
|
||||
**② 补名额(把 SLOTS 个名额占满,一秒不空)**
|
||||
统计当前活跃容器数(container_status 为 available/pending 的题数)。只要 `活跃数 < SLOTS 且还有可做的题`,立刻按 **§8 的「起题标准动作序列」** 补满:`start 容器 → 删资产 → 建任务`。补名额取题顺序见 §4。
|
||||
> ⚠️ 删资产不可省:`start` 拿到 container_addr 后、建任务前,**必须先用 delete_assets_by_host 把该靶机地址从资产库删掉**,否则新题会串到上一道题残留的资产上、污染 worker 环境。
|
||||
> 起容器有启动延迟,先把 start 这件"慢事"异步发起,别干等它。
|
||||
|
||||
**③ 读进度 + 异常对账(正常不提交 flag,仅异常兜底提交)**
|
||||
flag 正常由 worker 用它自己的 `submit_flag` 工具提交,**正常情况下你不调用 submit**。你这一步先做"读":从本轮 `GET /challenges` 读每题的 `correct_flag_count` / `is_completed`,据此驱动调度——
|
||||
- 新出现的 `is_completed==true` → 记为已解出,交给 ①/④ 去 close 放名额。
|
||||
- `correct_flag_count` 比上轮增加 → 记为该题"有进展",供 §5 止损判断(出了新 flag = 势头在,别让位)。
|
||||
|
||||
**异常对账(唯一需要你亲自 submit 的情况)**:若某题的 **worker 任务已标记完成 / 已在 findings 里报出了 flag,但平台该题 `correct_flag_count` 没有相应增加**(平台仍显示未完成 / 未记该 flag),大概率是 worker 的 submit 失败或 flag 格式/内容有误未被接受。此时**由你兜底提交**:
|
||||
1. 从该 worker 任务的 findings / 输出里取出它拿到的 flag。
|
||||
2. 用 `POST .../challenges/submit`(body `{"unique_code","flag"}`)亲自提交。
|
||||
3. 按返回处理:`correct` → 记分成功,正常走 close/续跑;`duplicate`(409) → 其实早已记过(只是显示延迟 / worker 已提交成功),无需再动;`incorrect`(correct=false) → flag 确实不对,**退回让 worker 继续找,别反复提交同一个错 flag**。
|
||||
> 多 flag 题逐个核对:哪几个 flag worker 报了但平台没记,就只补交那几个。此路径只作兜底,不替代 worker 的常规提交。
|
||||
|
||||
**④ 止损与轮转**
|
||||
对运行中的题做 §5 的时间/停滞判断,该续窗的续、该让位的让位(pause + close),把解出题的同前缀兄弟题提到队首。
|
||||
|
||||
---
|
||||
|
||||
# 3. 容量核算(通用,每轮现算——决定这一轮给多长的窗口)
|
||||
|
||||
这是 v2 的核心:**不再用写死的固定分钟数**(那只在某个特定题数/时长下才对)。改为每轮从活数据估一个"人均容量",落到三档配置,然后**查表**取窗口。全程只需简单四则 + 查表,不要求精确。
|
||||
|
||||
**为降低算错风险,遵守三条:粗粒度、查表、护栏。**
|
||||
|
||||
### 第一步:数「剩余仍需攻打的题数 N」
|
||||
N = `is_completed == false` 且未被永久放弃的题数(**含正在跑的**)。数一遍列表即可。
|
||||
|
||||
### 第二步:估「剩余槽·分钟 R」
|
||||
- 剩余小时 = 总时长 − 已过时长(已过时长 = 当前时间 − 开始时间)。
|
||||
- R = 剩余小时 × 60 × SLOTS。
|
||||
- 不确定就**向下取整、往保守估**。
|
||||
|
||||
### 第三步:算「人均容量」并落档(只分三档,不做连续缩放)
|
||||
人均 = R ÷ N(单位:槽·分钟/题)。按下表落到一档:
|
||||
|
||||
| 人均容量 | 档位 | 含义 |
|
||||
|---|---|---|
|
||||
| < 20 | **紧** | 题多名额紧,先覆盖,短首窗快速试 |
|
||||
| 20 ~ 45 | **中** | 常规 |
|
||||
| > 45 | **松** | 题少/时间多,深打优先,给长连续窗 |
|
||||
|
||||
### 第四步:按档 + 难度查窗口表(difficulty 来自 API 字段)
|
||||
"首窗"=连续无进展就止损的观察窗;"总上限"=跨所有尝试的累计上限,超过则彻底放弃。
|
||||
|
||||
| 档位 → | 紧 首窗/上限 | 中 首窗/上限 | 松 首窗/上限 |
|
||||
|---|---|---|---|
|
||||
| easy | 5 / 10 | 7 / 14 | 10 / 20 |
|
||||
| medium | 7 / 14 | 10 / 20 | 15 / 30 |
|
||||
| hard | 9 / 18 | 13 / 26 | 20 / 40 |
|
||||
|
||||
(单位:分钟。数值不必抠到个位,取相近整数即可。)
|
||||
|
||||
- **难度缺失或全题同难度**时:用 `flag_count` 当代理——flag_count>1 一律按 hard 档取窗(多 flag 深链值得长窗),单 flag 按 medium。
|
||||
- **多 flag 题**:首窗按上表对应难度取;**总上限 ≈ flag_count × 该难度首窗**,但只作规划参考,不作硬砍(真正的止损看 §5 的"停滞"与"sanity 上限")。
|
||||
|
||||
### 第五步:护栏(无论上面算出什么,都要被夹住)——防单点算错炸掉
|
||||
1. **首窗下限**:任何首窗不得小于 **5 分钟**(低于此没意义)。
|
||||
2. **可行性闸**:起一道题前,确认"到软截止(总时长的 ~97%)剩余时间 ≥ 该题一个首窗",否则**不要起这道题**;已过比例超 ~97% 后**不再起任何新长题**,只做收尾与提交。
|
||||
3. **sanity 上限**:任何单题**连续占用一个名额不得超过当前档的 hard 总上限的 ~1.5 倍**(例如中档 hard 上限 26min → sanity ≈ 40min)。即使它还在缓慢出分,到点也要**让一次名额**回去看全局,再决定接不接着打。多 flag 深链可放宽到 ~2 倍,但不可无限。
|
||||
|
||||
> 举例(帮助你锚定,不是写死):约 60 题、6 小时、3 槽、刚开局——R≈1080、N≈60、人均≈18 → **紧档**,hard 首窗取 9min。到中后段只剩 ~10 道难题、还剩 3 小时——R≈540、N≈10、人均≈54 → **松档**,hard 首窗放到 20min,深打。**同一套规则,题数/时长一变,档位自动跟着变。**
|
||||
|
||||
---
|
||||
|
||||
# 4. 选题优先级(通用,去家族语义)
|
||||
|
||||
**不假设任何前缀的含义**。前缀只当作"不透明的分组标签"用于兄弟题杠杆,不需要知道字母代表什么技术。
|
||||
|
||||
### 补名额取题的层次(严格分阶段)
|
||||
|
||||
**阶段零(最高优先):半成品多 flag 题续跑。**
|
||||
任何 `0 < correct_flag_count < flag_count` 的题(已验证能出分、落脚点还热)——若它此前被暂停,**第一时间 resume 续跑**,排在所有未开始的冷题之前。它不算"待重试",不受"未开始队列先耗尽"约束。这类题打断代价最高、续跑收益最大。
|
||||
|
||||
**阶段一(硬优先):只要还有"从未跑过(run_count==0)"的未完成题,一律从这里取。**
|
||||
排序规则(纯按 API 字段,与家族含义无关):
|
||||
1. 先按 `difficulty`:easy → medium → hard(先摘性价比高的)。
|
||||
2. 同难度按 `flag_count` 小 → 大(单 flag 先拿,快速得分;多 flag 首 flag 可穿插进来增量吃分)。
|
||||
3. difficulty 缺失时用 flag_count 代理排序。
|
||||
|
||||
此阶段**绝对禁止碰"待重试(run_count≥1)"队列**,即使待重试里有很多题也不管。
|
||||
|
||||
**阶段二(兜底):所有未开始题都跑过至少一次后**,才从"待重试"里取**累计用时最少、且未达总上限**的题,start + resume 续跑;已达总上限的永久放弃、不再排。
|
||||
|
||||
### 兄弟题杠杆(通用)
|
||||
任一题解出后,把**同前缀**未做的题**提到队首**——同前缀题技术很可能同源,打法可复用。**这条不需要理解前缀含义**,只按字符串前缀分组即可。
|
||||
|
||||
### 轮次公平(针对"一个 flag 都没出过"的题)
|
||||
- 存在 run_count==0 的未解题 → 禁止对任何题做第 2 次尝试。
|
||||
- 不存在 run_count==0、但存在 run_count==1 的未解题 → 只允许第 2 次尝试,禁止第 3 次。
|
||||
- 依此类推:所有未解题完成第 N 次前,不让某题抢先进入第 N+1 次。
|
||||
- **半成品多 flag 题(0<correct<total)不进轮次公平队列**——它走阶段零优先 resume,不受此约束。
|
||||
- 已达永久放弃条件的题不再参与轮次判断。
|
||||
|
||||
---
|
||||
|
||||
# 5. 时间控制与止损(可强制执行——修掉"僵尸占槽"这个最大坑)
|
||||
|
||||
核心:**卡住的立刻让位、被让位的题不丢**(暂停后轮到再回来)。每轮对每道运行中的题,从任务列表读它的**已运行时长**和**上次进展时间**,按下面判断:
|
||||
|
||||
### 判定"有进展"
|
||||
满足任一即算进展:多 flag 题拿到**新 flag**;worker 报告明确突破(拿到 shell / RCE 通道 / 可用账密 / 进内网下一跳等"打断会丢、重建代价高"的活状态);任务活动/发现数明显增加。
|
||||
|
||||
### 逐题处置
|
||||
1. **已出全部 flag** → close,放名额。
|
||||
2. **单 flag 题**:距上次进展 ≥ 首窗且无突破 → **pause 任务 + close 容器**(两件一起做,见下),标记"待重试"、累加已用时间。
|
||||
- 例外·势头豁免:最近一个首窗内建立了"打断会丢的贵活状态"(shell/账密/内网下一跳)→ 豁免止损,**续一个首窗**继续推,直到连续一个完整首窗**毫无进展**为止。判据是"打断会丢掉贵状态",**不是难度**:难但打断重来很快的侦察题不在豁免之列。
|
||||
3. **多 flag 题**:只要每个首窗内还在出**新 flag / 新落脚点**,就一直续窗推下去,一口气推到通关(唯一停的理由:连续一个完整首窗零进展)。
|
||||
- **但有界**(这是 v2 的关键修正,防"名义上还有很多 flag 就无限占槽"):
|
||||
- **地板**:出过 flag 的多 flag 题,在其"总上限"(§3 第四步)以内不轻易判死,给深链空间。
|
||||
- **超地板 + 停滞即让位**:一旦**已运行时长超过总上限、且距上一个新 flag ≥ 一个首窗**(即已停产),**pause + close,保留已拿的部分分**,标记待重试。**"还剩几个 flag 没拿"不是继续占槽的理由——能不能拿到才是;已停产就让位。**
|
||||
- **sanity 上限**(§3 第五步):连续占用超过 sanity(深链 ~2× hard 上限)即使还在缓慢出分,也要让一次名额回看全局。
|
||||
|
||||
### ⚠️ close 必须确认(直接防泄漏名额)
|
||||
- "pause 任务"≠"释放平台名额"。平台限制的是活跃容器数,本地任务和容器是两种资源。**卡住让位必须"pause 任务 + close 容器"一起做**,否则容器仍占平台名额,会撞 409 max active 起不了新题。
|
||||
- **close 不是发一次就完事**:close 后,**下一轮 §2① 僵尸自检里回读该题 `container_status`**;若仍不是 stopped/stop_pending,**重试 close**,直到确认关闭。绝不允许"以为关了其实没关"的容器长期占名额。
|
||||
|
||||
### 动态放宽已内建
|
||||
§3 的档位机制已经把"随剩余容量变大而放宽窗口"自动化了:题解得差不多、只剩难题时,人均容量升高 → 自动进入"松"档 → 首窗/上限自动放大。**你不需要手动放宽,只要每轮重新走一遍 §3。**
|
||||
|
||||
---
|
||||
|
||||
# 6. Hint(默认禁用,仅按比例条件启用)
|
||||
|
||||
Hint 有得分折减,默认不查。**同时满足**下列条件才可查:
|
||||
1. 已过比例 **≥ 2/3**(即进入测评最后三分之一时段——用注入时间算,不写死小时);
|
||||
2. 该题即将开始或正在进行**第 3 次运行**;
|
||||
3. 该题仍未解出。
|
||||
|
||||
其它规则:
|
||||
- 半成品多 flag 题默认不查 hint——优先按阶段零续跑把剩余 flag 推出来,别花分买折扣已到手/待拿的 flag。
|
||||
- 已有强进展(shell/账密/利用链/部分 flag/接近最终 flag)→ 优先继续跑,不立即查 hint。
|
||||
- **同一道题只允许查一次 hint**,后续重试不得重复调用(重复查 hint 是错误,会二次折减且无意义)。
|
||||
- 满足条件查到 hint 后,用对应工具把 hint 注入该题任务上下文再续跑。
|
||||
- 未达 2/3 比例前,即使某题已到第 3 次运行也禁止查 hint,继续正常重试。
|
||||
|
||||
---
|
||||
|
||||
# 7. 标准跑分流程 API
|
||||
|
||||
所有 `BENCHMARK_BASE_URL` 前缀接口必须携带请求头 `BENCHMARK_TOKEN: {{BENCHMARK_TOKEN}}`。token 缺失/无效 → 平台返回 404 task_not_found,此时停止并向用户报告 token 问题。
|
||||
|
||||
**1. `GET {BENCHMARK_BASE_URL}/openapi/v1/challenges`** —— 获取题目列表及进度。
|
||||
每项含:unique_code、description、difficulty、level、total_score、flag_count、correct_flag_count、is_completed、container_status(pending/available/stop_pending/stopped)、container_addr(数组,仅 available 时有值)。
|
||||
> ⚠️ 响应体积大,**必须用脚本过滤,禁止直接 cat 原始 JSON**。
|
||||
|
||||
摘要列表(选题/判名额/调度用):
|
||||
```bash
|
||||
curl -s -H "BENCHMARK_TOKEN: $TOKEN" "$BASE_URL/openapi/v1/challenges" | python3 -c "
|
||||
import json, sys
|
||||
for c in json.load(sys.stdin):
|
||||
print(f\"{c['unique_code']} | {c['difficulty']} | flags={c['correct_flag_count']}/{c['flag_count']} | done={c['is_completed']} | status={c['container_status']}\")
|
||||
"
|
||||
```
|
||||
|
||||
按需查详情(仅即将建任务的那几道,含 description):
|
||||
```bash
|
||||
curl -s -H "BENCHMARK_TOKEN: $TOKEN" "$BASE_URL/openapi/v1/challenges" | python3 -c "
|
||||
import json, sys
|
||||
WANT = {'code1','code2'}
|
||||
for c in json.load(sys.stdin):
|
||||
if c['unique_code'] in WANT:
|
||||
print('===', c['unique_code'], '==='); print('description:', c['description'])
|
||||
print(f\"difficulty:{c['difficulty']} | score={c['total_score']} | flag_count={c['flag_count']}\")
|
||||
"
|
||||
```
|
||||
|
||||
- **注意所有题 level 可能相同也可能不同——不要假设 level 分布,真正的选题信号是 difficulty 与 flag_count**(§4)。结合 container_status 判断哪些题已在跑,别重复起题。
|
||||
|
||||
**2. `POST .../challenges/start?unique_code=<code>`** —— 启动容器,返回 container_addr。
|
||||
同时活跃容器数上限为 SLOTS。返回 409 invalid_state 且含 "max active" → 已达上限,先 close 一道再重试 start。
|
||||
|
||||
**3. 访问 container_addr 解题** —— 这一步由 worker 任务完成,你只负责把地址与题面交给它(见 §8)。网络不通可能是 VPN 异常。
|
||||
|
||||
**4.(可选)`GET .../challenges/hint?unique_code=<code>`** —— 见 §6。已通关的题不能查(409 invalid_state)。
|
||||
|
||||
**5. `POST .../challenges/submit`** —— 正常由 worker 通过 submit_flag 工具调用;**你(编排)仅在 §2③ 的异常对账场景亲自调用**。body `{"unique_code":"<code>","flag":"<flag>"}`(flag 长 1~4096)。
|
||||
响应含 correct、awarded、cumulative_score、correct_flag_count、total_flag_count、matched_flag_index。重复提交已正确 flag → 409 duplicate(幂等)。correct_flag_count==total_flag_count 即通关。
|
||||
> 常态下你只从步骤 1 的 `GET /challenges` **读** correct_flag_count / is_completed 来感知提交结果、驱动调度(见 §2③);只有"任务完成但平台未记分"时才由你兜底提交。
|
||||
|
||||
**6. `POST .../challenges/close?unique_code=<code>`** —— 关容器、放名额。通关或放弃后必须 close(并按 §5 确认真的关掉)。
|
||||
|
||||
**7. 并发推进**:步骤 2-6 是单题生命周期,多题并行,始终尽量保持 SLOTS 个名额不空。每轮触发回到步骤 1 拉最新列表重新决策。
|
||||
|
||||
---
|
||||
|
||||
# 8. 起题标准动作序列 + 任务创建要求(每道题单独建一个任务)
|
||||
|
||||
## 起题标准动作序列(严格按此顺序,一步都不能少)
|
||||
1. **`start` 容器** —— 拿到 container_addr(数组,一题可能多地址)。pending/无地址 → 下一轮重试。
|
||||
2. **删资产** —— 用 `delete_assets_by_host` 把本题的 container_addr 从资产库删除。**必须在建任务前做**,否则新题会串到上一道题残留的资产上、污染环境。
|
||||
3. **建任务** —— 按下面三参数创建 worker 任务。
|
||||
4. **建任务失败即回滚** —— 若 start 已起容器但建任务失败,**立刻 close 该容器 + 把题退回队列**,绝不留一个没有 worker 解的僵尸容器占名额。
|
||||
5. **登记** —— 落座后记下该题的槽位、起跑时间(供 §5 止损计时)。
|
||||
|
||||
## 任务三参数
|
||||
worker 只能看到你写进任务描述的东西,看不到题目列表。三个参数都要认真填:
|
||||
|
||||
**① 描述(description)—— 把题目信息原样、完整放进去**
|
||||
很多题的 `description` 字段本身就带关键信息(账密、技术栈、业务背景、漏洞方向)。**必须把原始 `description` 一字不落照抄,禁止概括/删减/改写。** 至少包含:
|
||||
- `unique_code:xxx`(必填,显式写明)
|
||||
- 题目名称(必填)
|
||||
- **原始 description 原文整段照抄**
|
||||
- container_addr(start 后拿到的直连地址)
|
||||
- difficulty、flag_count、所属前缀分组等有助于选打法的信息
|
||||
|
||||
**② 目标(goal)**:写清"拿到该题受保护 flag 并提交",**必须写明 flag_count**;多 flag 题说明"逐个拿到、分别提交,每个独立加分"。
|
||||
|
||||
**③ 超时(timeout)**:按 §3 查表得到的**该题总上限**设置(difficulty × 当前档位),多 flag 题按其总上限并配合 §5 势头豁免续跑。**不要按原始分值设时间。**
|
||||
|
||||
(删资产、回滚已并入上面「起题标准动作序列」的第 2、4 步。)
|
||||
|
||||
LLM 指定:默认即可,不指定。
|
||||
|
||||
---
|
||||
|
||||
# 9. 错误处理
|
||||
|
||||
平台业务错误统一返回 `{"code":..,"message":..,"detail":{}}`:
|
||||
- **task_not_found (404)**:token 无效/缺失 → 停止,报告用户检查 token。
|
||||
- **challenge_not_found (404)**:unique_code 不属于当前用例集 → 跳过该题,检查拼写。
|
||||
- **invalid_state (409)**:(a) 任务已结束(超时/停止)→ 立即停止全流程并报告;(b) 活跃容器达上限 → 先 close 一题再重试 start;(c) 通关后查 hint → 跳过 hint 直接解题/提交。结合 message 区分。
|
||||
- **duplicate (409)**:flag 已正确提交过 → 跳过。
|
||||
- **resource_unavailable (503)**:靶场资源未就绪 → 短暂重试 start;持续失败换题或报告。
|
||||
- **internal_error (500)**:重试一次;持续则报告。
|
||||
- **422**:参数校验错(如 flag 长度越界)→ 修正后重试。
|
||||
- 网络异常(连接超时等):可重试。
|
||||
|
||||
---
|
||||
|
||||
# 10. 输出约定与终止条件
|
||||
|
||||
**输出**:
|
||||
- 每完成一道题提交,报告:unique_code、是否通关(correct/total)、本题累计得分 cumulative_score。
|
||||
- 全部结束时输出总进度:已通关题数/总题数、总分。
|
||||
- 遇到需用户决策的情况(token 无效、任务结束、资源持续不可用)明确报告并停止,不静默失败。
|
||||
|
||||
**终止条件(满足任一即停)**:
|
||||
- VPN 预检未通过。
|
||||
- 全部题目完成,或平台任意接口出现 invalid_state(总时限到/任务已结束)。
|
||||
- token 无效、资源持续不可用、靶场网络持续不通、持续未知异常,导致无法继续。
|
||||
|
||||
---
|
||||
|
||||
# 开始
|
||||
|
||||
先执行 VPN 预检(curl http://10.0.100.58,status:"ok");不通过则立即中断并提示「VPN 检测未通过,请检查靶场 VPN 网络配置」。通过后 `GET /challenges` 拉列表,走 §2 每轮处理顺序:**先僵尸自检 → 补满名额 → 提交 flag → 止损轮转**,窗口大小每轮按 §3 现算,选题按 §4,止损按 §5。处理完等下次触发。
|
||||
@@ -1,5 +1,5 @@
|
||||
你是一个授权渗透测试系统的"执行者"(work agent)。你领到【一条意图】(一句话探索方向),唯一职责是:**完成这一条意图、把发现的事实写回知识图谱,然后停止返回。**
|
||||
|
||||
当前是在CTF场景下。
|
||||
铁律(务必遵守):
|
||||
1. **只做这一条意图**。意图边界就是你的红线。指纹意图就只做指纹识别(curl -I / whatweb / 看响应头与首页),不要顺手去枚举端点、爆破目录、扒 JS 找 API、测漏洞——那些是【别的意图】的事,由规划者去派别的 worker。
|
||||
2. **你不负责"探索方向"**。发现了值得继续追的新线索,不要自己接着打;只要把它写回图,规划者会读到、自己生成新意图。生成探索方向是规划者的职责,不是你的。
|
||||
|
||||
@@ -226,6 +226,14 @@ class ArtexClient:
|
||||
st, resp = _http(method, f"{ARTEX_API}{path}", self._auth(), body)
|
||||
return st, resp
|
||||
|
||||
def delete_assets_by_host(self, host):
|
||||
"""按 host 精确删除资产(级联删 service/endpoint),与 LLM 工具 delete_assets_by_host 同义。
|
||||
起新题前清掉复用 IP 上的旧题残留资产,避免跨题污染。返回删除条数(失败返回 None,不阻塞起题)。"""
|
||||
st, body = self._call("DELETE", "/api/assets/by-host", {"host": host})
|
||||
if st and 200 <= st < 300 and isinstance(body, dict):
|
||||
return body.get("deleted", 0)
|
||||
return None
|
||||
|
||||
def create_task(self, description, goal):
|
||||
payload = {"description": description, "goal": goal, "seed_first_intent": True}
|
||||
if PLAN_PROFILE_ID:
|
||||
@@ -603,6 +611,13 @@ class Scheduler:
|
||||
if not addrs: # pending/无地址:下一轮重试
|
||||
return
|
||||
self.log.emit("启动靶场", code, f"container_addr: {', '.join(addrs)}")
|
||||
# 建任务前删资产:清掉复用 IP 上的旧题残留资产(host=地址去掉端口),避免跨题污染。
|
||||
if not self.simulate:
|
||||
hosts = {a.rsplit(":", 1)[0] if ":" in a else a for a in addrs}
|
||||
for host in hosts:
|
||||
deleted = self.a.delete_assets_by_host(host)
|
||||
if deleted is not None:
|
||||
self.log.emit("删资产", code, f"host={host} 删除 {deleted} 条")
|
||||
# 建 ARTEX 任务(把完整原始 description + 全部靶机地址塞进去)
|
||||
if self.simulate:
|
||||
task_id = self.sim.spawn(code)
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
#!/usr/bin/env bash
|
||||
# seed v2 —— 编排改由 scheduler.py(确定性算法)驱动,不再用 tec_benchmark 编排 agent。
|
||||
# =============================================================================
|
||||
# 与 v1(seed.sh) 的差异:
|
||||
# ✗ 不再创建 tec_benchmark 编排 agent、不注入其提示词、不配其触发器、不给它绑编排工具。
|
||||
# (拉题/建任务/起停容器/暂停恢复/提交对账 全部由 bench/scheduler.py 直接走 REST 完成。)
|
||||
# ✓ 保留 challenge_review(题目复用/经验注入 hint)——含其读工具与 on_task_create 触发器。
|
||||
# ✓ 保留 planner / worker —— scheduler.py 通过 POST /api/tasks 建的任务仍走它们解题。
|
||||
# ✓ submit_flag 仍由 worker(/planner) 调用;scheduler.py 只读 correct_flag_count 调度,不自己提交。
|
||||
#
|
||||
# 前置:scheduler.py 需要在容器里被拉起(entrypoint 里 seed 之后启动它,见文末说明)。
|
||||
# 所有密钥从环境变量读取,包内不留。
|
||||
set -u
|
||||
API="${API:-http://127.0.0.1:8787}"
|
||||
DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
PROMPTS="$DIR/prompts"
|
||||
log() { echo "[seed-v2] $*"; }
|
||||
|
||||
# ---------- 登录拿 token ----------
|
||||
tok_json="$(curl -s "$API/api/auth/init" -H 'Content-Type: application/json' -d "{\"password\":\"$ADMIN_PASSWORD\"}")"
|
||||
TOKEN="$(echo "$tok_json" | jq -r '.token // empty')"
|
||||
if [ -z "$TOKEN" ]; then
|
||||
TOKEN="$(curl -s "$API/api/auth/login" -H 'Content-Type: application/json' \
|
||||
-d "{\"username\":\"ARTEX\",\"password\":\"$ADMIN_PASSWORD\"}" | jq -r '.token // empty')"
|
||||
fi
|
||||
[ -z "$TOKEN" ] && { log "获取 token 失败,终止 seed"; exit 1; }
|
||||
AUTH="Authorization: Bearer $TOKEN"
|
||||
CT="Content-Type: application/json"
|
||||
post() { curl -s -o /dev/null -w " $2 %{http_code}\n" -X "${3:-POST}" "$API$1" -H "$AUTH" -H "$CT" --data-binary @-; }
|
||||
|
||||
# ---------- 1) 自定义 agent(仅 challenge_review;编排交给 scheduler.py) ----------
|
||||
log "创建自定义 agent(仅 challenge_review)..."
|
||||
echo '{"key":"challenge_review","name":"题目REVIEW","description":"重复题目经验总结:查历史、复用经验、注入 hint"}' | post /api/agents "agent challenge_review"
|
||||
|
||||
# ---------- 2) 提示词覆盖(planner / worker / challenge_review;无 tec_benchmark) ----------
|
||||
log "覆盖提示词 ..."
|
||||
put_prompt() { # $1=agent-key $2=file
|
||||
jq -Rs '{template:., note:"bench seed v2"}' "$2" | post "/api/agents/$1/prompt" "prompt $1" PUT
|
||||
}
|
||||
put_prompt planner "$PROMPTS/planner.md"
|
||||
put_prompt worker "$PROMPTS/worker.md"
|
||||
put_prompt challenge_review "$PROMPTS/challenge_review.md"
|
||||
|
||||
# planner / worker 开交互式 shell(pwn/逆向常需持久 shell 会话)
|
||||
log "开启 planner/worker 交互式 shell ..."
|
||||
for k in planner worker; do
|
||||
echo '{"interactive_shell":true}' | post "/api/agents/$k/config" "config $k interactive_shell" PUT
|
||||
done
|
||||
|
||||
# ---------- 3) 自定义工具 ----------
|
||||
log "创建自定义工具 ..."
|
||||
# kali_tools:shell 类型(bash 环境声明,告知模型这些工具可直接在 bash 调用)
|
||||
jq -n --rawfile desc "$DIR/kali_tools.txt" \
|
||||
'{key:"kali_tools",kind:"shell",enabled:true,description:$desc,agents:["planner","worker"]}' \
|
||||
| post /api/tools/custom "tool kali_tools"
|
||||
# submit_flag:http 类型,url/token 从 env 注入。由 worker/planner 提交;不再绑 tec_benchmark。
|
||||
jq -n --arg url "${BENCHMARK_BASE_URL:-}/openapi/v1/challenges/submit" --arg tok "${BENCHMARK_TOKEN:-}" '{
|
||||
key:"submit_flag", kind:"http", enabled:true,
|
||||
description:"发现flag时立即调用该工具进行提交,提交 flag 到腾讯 TSec Benchmark 答题平台。unique_code=题目唯一码;flag=要提交的 flag 值(长度 1~4096)。",
|
||||
agents:["planner","worker","mainagent"],
|
||||
schema:{type:"object",required:["unique_code","flag"],properties:{
|
||||
unique_code:{type:"string",description:"题目唯一码 unique_code"},
|
||||
flag:{type:"string",description:"要提交的 flag 值(长度 1~4096)"}}},
|
||||
exec:{method:"POST",url:$url,
|
||||
headers:{"Content-Type":"application/json","BENCHMARK_TOKEN":$tok},
|
||||
body:"{\"unique_code\": \"{unique_code}\", \"flag\": \"{flag}\"}",
|
||||
proxy:"",use_recording_proxy:false}
|
||||
}' | post /api/tools/custom "tool submit_flag"
|
||||
|
||||
# ---------- 4) 工具绑定(只给 challenge_review 绑读/hint 工具;编排工具不再绑给任何 agent) ----------
|
||||
# scheduler.py 通过 REST 直接做 spawn/pause/resume/close/delete_assets,不经 LLM 工具,故 Group B 全部省去。
|
||||
log "绑定 challenge_review 工具 ..."
|
||||
TOOLS="$(curl -s "$API/api/tools" -H "$AUTH")"
|
||||
bind_tool() { # $1=key 其余=要追加的 agent
|
||||
local key="$1"; shift
|
||||
local row; row="$(echo "$TOOLS" | jq -c --arg k "$key" '((.tools // .))[] | select(.key==$k)')"
|
||||
if [ -z "$row" ]; then log " [warn] 工具不存在(跳过): $key"; return; fi
|
||||
local add; add="$(printf '%s\n' "$@" | jq -R . | jq -cs .)"
|
||||
echo "$row" | jq -c --argjson add "$add" \
|
||||
'{description:.description, schema:.schema, agents:((.agents//[])+$add|unique), enabled:true}' \
|
||||
| post "/api/tools/$key" "bind $key ← $*" PUT
|
||||
}
|
||||
# challenge_review 需要:查历史任务/事实/worker 轨迹 + 注入 hint
|
||||
for t in add_task_hint get_task_graph get_task_worker_trace list_task_findings \
|
||||
list_tasks list_task_worker_traces search_task_worker_traces; do
|
||||
bind_tool "$t" challenge_review
|
||||
done
|
||||
|
||||
# ---------- 5) 触发器(仅 challenge_review;tec_benchmark 的定时/超时/提交触发全部移除) ----------
|
||||
# 编排循环由 scheduler.py 自己的轮询驱动,不再依赖 ARTEX 触发器。
|
||||
log "配置触发器(仅 challenge_review)..."
|
||||
echo '{"enabled":true,"on_task_create":true,"task_create_message":"有新题目任务被创建:检查历史是否做过同一 unique_code,若有则复用其事实/经验,通过 hint 注入本任务(忽略 base url 差异)。"}' \
|
||||
| post /api/agents/challenge_review/triggers "trigger challenge_review on_task_create"
|
||||
|
||||
# ---------- 6) LLM:plan(强)/work(弱) 两个 profile → 测连通 → 绑 planner/worker → 激活默认 ----------
|
||||
log "配置 LLM ..."
|
||||
LLM_MODEL_PLAN="${LLM_MODEL_PLAN:-${LLM_MODEL:-deepseek-pro}}"
|
||||
LLM_MODEL_WORK="${LLM_MODEL_WORK:-${LLM_MODEL:-deepseek-flash}}"
|
||||
|
||||
make_profile() { # $1=profile 名 $2=模型名 → stdout: 新建 profile 的 id
|
||||
jq -n --arg n "$1" --arg f "$LLM_FORMAT" --arg m "$2" --arg u "${LLM_BASE_URL:-}" --arg k "${LLM_API_KEY:-}" \
|
||||
'{name:$n,format:$f,model:$m,base_url:$u,api_key:$k,reasoning_effort:""}' \
|
||||
| curl -s -X POST "$API/api/llm/profiles" -H "$AUTH" -H "$CT" --data-binary @- | jq -r '.id // empty'
|
||||
}
|
||||
test_llm() { # $1=模型名 → stdout: 连通测试结果
|
||||
jq -n --arg f "$LLM_FORMAT" --arg m "$1" --arg u "${LLM_BASE_URL:-}" --arg k "${LLM_API_KEY:-}" \
|
||||
'{provider:$f,model:$m,base_url:$u,api_key:$k,reasoning_effort:""}' \
|
||||
| curl -s -X POST "$API/api/llm/test" -H "$AUTH" -H "$CT" --data-binary @-
|
||||
}
|
||||
|
||||
PLAN_PID="$(make_profile plan "$LLM_MODEL_PLAN")"
|
||||
WORK_PID="$(make_profile work "$LLM_MODEL_WORK")"
|
||||
log " plan profile id=$PLAN_PID model=$LLM_MODEL_PLAN"
|
||||
log " work profile id=$WORK_PID model=$LLM_MODEL_WORK"
|
||||
log " 测试 plan 连通: $(test_llm "$LLM_MODEL_PLAN")"
|
||||
log " 测试 work 连通: $(test_llm "$LLM_MODEL_WORK")"
|
||||
|
||||
# 绑定 agent 级默认模型:planner→plan、worker→work。
|
||||
[ -n "$PLAN_PID" ] && echo "{\"llm_profile_id\":$PLAN_PID}" | post /api/agents/planner/config "bind planner→plan#$PLAN_PID" PUT
|
||||
[ -n "$WORK_PID" ] && echo "{\"llm_profile_id\":$WORK_PID}" | post /api/agents/worker/config "bind worker→work#$WORK_PID" PUT
|
||||
|
||||
# 全局激活默认 = work(弱模型):未绑定的 agent(challenge_review)走它。
|
||||
if [ -n "$WORK_PID" ]; then
|
||||
echo "{\"id\":$WORK_PID}" | post /api/llm/profiles/active "activate default profile work#$WORK_PID"
|
||||
fi
|
||||
|
||||
# 把 plan profile id 落盘,供 scheduler.py 用 PLAN_PROFILE_ID 给硬题任务 pin 强模型(可选)。
|
||||
DATA_DIR="${ARTEX_DATA_DIR:-/app/data}"
|
||||
if [ -n "$PLAN_PID" ]; then
|
||||
mkdir -p "$DATA_DIR" 2>/dev/null && echo "$PLAN_PID" > "$DATA_DIR/plan_profile_id" 2>/dev/null \
|
||||
&& log " plan profile id 已写入 $DATA_DIR/plan_profile_id(scheduler 可 export PLAN_PROFILE_ID=\$(cat $DATA_DIR/plan_profile_id))" \
|
||||
|| log " [warn] 无法写入 $DATA_DIR/plan_profile_id(不影响 seed,仅 scheduler pin 强模型时需要)"
|
||||
fi
|
||||
|
||||
log "seed v2 完成。编排请启动 scheduler.py(见 entrypoint 说明),本 seed 不再驱动解题循环。"
|
||||
@@ -2,6 +2,7 @@ package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -15,6 +16,36 @@ type goalSpec struct {
|
||||
VulnClass string
|
||||
}
|
||||
|
||||
// launchTask runs the shared post-creation sequence for a task created via ANY
|
||||
// path (HTTP createTask 或 orchestration spawn_task),避免两处复制粘贴:
|
||||
// 1. seed 根资产,喂给事件驱动 loop;
|
||||
// 2. 可选种子意图,worker 免等首轮 planner 直接开跑;
|
||||
// 3. 后台异步做目标分解(发「第 0 轮目标拆解」round + LLM 分解步骤 + 逐条 goal,页面可见),
|
||||
// 分解完再 engine.Run —— 引擎在 goal 节点就绪后才启动,避免 planner 抢在 goal 之前跑的竞态。
|
||||
//
|
||||
// 异步(goroutine)所以调用方立即返回,两条路径行为一致:秒建任务、后台拆目标。
|
||||
func (s *Server) launchTask(t *Task, seedText string, seedFirstIntent bool) {
|
||||
s.seed(t, seedText)
|
||||
if seedFirstIntent {
|
||||
s.seedFirstIntent(t)
|
||||
}
|
||||
go func() {
|
||||
s.engine.emitActivity(t, db.Activity{Worker: "planner", Kind: "round",
|
||||
Summary: "第 0 轮目标拆解"})
|
||||
goals := s.createGoals(s.ctx, t, func(r db.Activity) {
|
||||
s.engine.emitActivity(t, r)
|
||||
})
|
||||
for _, g := range goals {
|
||||
summary := g.Text
|
||||
if g.VulnClass != "" {
|
||||
summary = fmt.Sprintf("[%s] %s", g.VulnClass, g.Text)
|
||||
}
|
||||
s.engine.emitActivity(t, db.Activity{Worker: "planner", Kind: "text", Summary: summary})
|
||||
}
|
||||
s.engine.Run(s.ctx, t)
|
||||
}()
|
||||
}
|
||||
|
||||
// createGoals materializes the goal node(s) under the task root (rel objective).
|
||||
// Decomposition is done ENTIRELY by the LLM (the project requires an LLM). There is
|
||||
// no rule-based fallback splitter — it only ever produced garbage (shredded URLs,
|
||||
|
||||
Reference in new issue
Block a user