dsh-pentester 1.0.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -5
- package/docker/kali/TOOL_PROMPT.md +7 -8
- package/lib/build-id.json +1 -0
- package/lib/client.js +3705 -48
- package/lib/client.js.map +4 -4
- package/lib/index.d.ts +0 -12
- package/lib/index.js +2076 -430
- package/lib/index.js.map +1 -1
- package/package.json +13 -13
- package/presets/pentester/agent.cordis.yml +71 -44
- package/presets/pentester/run-state.mjs +99 -90
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-pentester",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "Minimal Root-Orchestrator PTES pentesting plugin for DeepSeek Harness",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
}
|
|
36
36
|
},
|
|
37
37
|
"scripts": {
|
|
38
|
-
"build": "tsdown && node scripts/build-client.mjs",
|
|
38
|
+
"build": "node scripts/build-id.mjs && tsdown && node scripts/build-client.mjs",
|
|
39
39
|
"typecheck": "tsc --noEmit",
|
|
40
40
|
"check": "pnpm run typecheck && pnpm run build",
|
|
41
41
|
"test": "node --import ./test/resolve-ts.mjs --test \"test/*.test.ts\"",
|
|
@@ -60,24 +60,24 @@
|
|
|
60
60
|
"homepage": "https://github.com/fb0sh/dsh-pentester.git",
|
|
61
61
|
"peerDependencies": {
|
|
62
62
|
"@deepseek-ai/cordis": "^4.0.0",
|
|
63
|
-
"@deepseek-ai/schemastery": "^3.18.0",
|
|
64
63
|
"@deepseek-ai/dsh-client-runtime": "*",
|
|
65
|
-
"@deepseek-ai/dsh-client-ui-primitives": "*"
|
|
64
|
+
"@deepseek-ai/dsh-client-ui-primitives": "*",
|
|
65
|
+
"@deepseek-ai/schemastery": "^3.18.0"
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|
|
68
68
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
69
|
+
"@deepseek-ai/dsh-client-runtime": "0.1.0-rc.7",
|
|
70
|
+
"@deepseek-ai/dsh-client-ui-primitives": "0.1.0-rc.7",
|
|
69
71
|
"@deepseek-ai/dsh-typert-protocol": "0.1.0-rc.7",
|
|
70
72
|
"@deepseek-ai/schemastery": "^3.18.1",
|
|
71
73
|
"@types/dockerode": "^3.3.42",
|
|
72
74
|
"@types/node": "^22.20.1",
|
|
73
|
-
"@types/tar-stream": "^3.1.4",
|
|
74
|
-
"tsdown": "^0.22.14",
|
|
75
|
-
"typescript": "~5.9.2",
|
|
76
|
-
"@deepseek-ai/dsh-client-runtime": "0.1.0-rc.7",
|
|
77
|
-
"@deepseek-ai/dsh-client-ui-primitives": "0.1.0-rc.7",
|
|
78
75
|
"@types/react": "18.3.12",
|
|
79
76
|
"@types/react-dom": "18.3.1",
|
|
80
|
-
"
|
|
77
|
+
"@types/tar-stream": "^3.1.4",
|
|
78
|
+
"esbuild": "0.25.0",
|
|
79
|
+
"tsdown": "^0.22.14",
|
|
80
|
+
"typescript": "~5.9.2"
|
|
81
81
|
},
|
|
82
82
|
"exports": {
|
|
83
83
|
".": {
|
|
@@ -93,10 +93,10 @@
|
|
|
93
93
|
},
|
|
94
94
|
"dependencies": {
|
|
95
95
|
"dockerode": "^5.0.1",
|
|
96
|
-
"tar-stream": "^3.2.0",
|
|
97
|
-
"zod": "^4.4.3",
|
|
98
96
|
"react": "18.3.1",
|
|
99
|
-
"react-dom": "18.3.1"
|
|
97
|
+
"react-dom": "18.3.1",
|
|
98
|
+
"tar-stream": "^3.2.0",
|
|
99
|
+
"zod": "^4.4.3"
|
|
100
100
|
},
|
|
101
101
|
"pnpm": {
|
|
102
102
|
"ignoredBuiltDependencies": [
|
|
@@ -28,8 +28,10 @@
|
|
|
28
28
|
with a stage handoff summary; (7) when a stage result needs rework, call
|
|
29
29
|
pentester_rollback_stage with the stage id and a reason.
|
|
30
30
|
|
|
31
|
-
YOUR TOOL SURFACE IS ORCHESTRATION ONLY: you have the
|
|
32
|
-
tools,
|
|
31
|
+
YOUR TOOL SURFACE IS ORCHESTRATION ONLY: you have the five pentester_*
|
|
32
|
+
tools (pentester_start, pentester_delegate, pentester_cancel_delegation,
|
|
33
|
+
pentester_advance_stage, pentester_rollback_stage), the native
|
|
34
|
+
ask_user_question, and the subagent control tools
|
|
33
35
|
(send_message, interrupt_agent, list_agents). You have NO bash, file,
|
|
34
36
|
or container-exec tools — you cannot read workspace files, run commands,
|
|
35
37
|
or inspect workers directly. Information reaches you through the injected
|
|
@@ -62,72 +64,97 @@
|
|
|
62
64
|
can see all workers with list_agents. Workers can report important
|
|
63
65
|
findings to you proactively; you will be notified.
|
|
64
66
|
|
|
65
|
-
|
|
67
|
+
PRE-ENGAGEMENT TOOL-CALL CONTRACT — HARD REQUIREMENT
|
|
66
68
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
2. Build the COMPLETE fixed Pre-engagement question batch — EXACTLY six
|
|
71
|
-
questions in strict order (see below). Do NOT invent your own
|
|
72
|
-
questions, do NOT wrap ask_user_question in another tool, and do NOT
|
|
73
|
-
ask target, scope, exclusions, RoE, language, or confirmation in
|
|
74
|
-
separate calls.
|
|
75
|
-
3. Call ask_user_question EXACTLY ONCE with every required question in
|
|
76
|
-
a single `questions` array of length 6.
|
|
77
|
-
4. Host sends ALL six questions to the user in one UI panel. The user
|
|
78
|
-
answers all of them in one submission.
|
|
79
|
-
5. Read the answers bundled in the single tool result. If ALL are
|
|
80
|
-
confirmed (target accepted, scope accepted, exclusions valid, RoE
|
|
81
|
-
selected, language selected, final_confirm == "确认并开始"), call
|
|
82
|
-
pentester_advance_stage ONCE to bootstrap the run. Do NOT ask a
|
|
83
|
-
second confirmation round.
|
|
84
|
-
6. If ANY answer is not confirmed (target_confirm choice is "目标不正确",
|
|
85
|
-
scope_confirm is "范围不正确", roe_confirm is "需要自定义 RoE", or
|
|
86
|
-
final_confirm is "暂不开始"), do NOT bootstrap. Instead, tell the
|
|
87
|
-
user in natural language which items need correction and why. After
|
|
88
|
-
the user provides corrections, issue a NEW complete batch — still
|
|
89
|
-
ONE ask_user_question call, still 6 questions. Never resume from
|
|
90
|
-
question 3; always the full batch.
|
|
91
|
-
|
|
92
|
-
FIXED PRE-ENGAGEMENT BATCH (6 questions, strict order & ids):
|
|
69
|
+
For a NEW or RESTART assessment, call `ask_user_question` EXACTLY ONCE
|
|
70
|
+
for Pre-engagement. That single invocation MUST contain exactly six
|
|
71
|
+
questions in exactly this order:
|
|
93
72
|
|
|
94
73
|
1. id "target_confirm" — header "🎯 测试目标", question "请确认本次授权渗透测试的 Primary Target。",
|
|
95
74
|
options: [{label: "<parsed target>", description: "使用当前识别的目标"},
|
|
96
|
-
{label: "目标不正确", description: "暂不开始测试,我会重新提供目标"}]
|
|
97
|
-
Replace "<parsed target>" with the target the user already supplied (e.g. "192.168.21.102").
|
|
75
|
+
{label: "目标不正确", description: "暂不开始测试,我会重新提供目标"}]
|
|
98
76
|
|
|
99
77
|
2. id "scope_confirm" — header "📋 授权范围", question "请确认本次允许测试的范围。",
|
|
100
78
|
options: [{label: "<parsed scope>", description: "<scope description>"},
|
|
101
|
-
{label: "范围不正确", description: "暂不开始测试,我会重新提供授权范围"}]
|
|
102
|
-
Replace "<parsed scope>" with what the user supplied (e.g. "仅 192.168.21.102:80").
|
|
79
|
+
{label: "范围不正确", description: "暂不开始测试,我会重新提供授权范围"}]
|
|
103
80
|
|
|
104
81
|
3. id "exclusions_confirm" — header "🚫 排除项", question "除授权范围本身之外,是否还有额外禁止的目标、路径或行为?",
|
|
105
82
|
options: [{label: "无额外排除项", description: "仍严格受上述 Scope 和 RoE 限制"},
|
|
106
|
-
{label: "有额外排除项", description: "暂不开始,我会补充排除项"}]
|
|
83
|
+
{label: "有额外排除项", description: "暂不开始,我会补充排除项"}]
|
|
107
84
|
|
|
108
85
|
4. id "roe_confirm" — header "⚔️ 交战规则", question "请选择本次测试的 Rules of Engagement。",
|
|
109
86
|
options: [{label: "标准 RoE", description: "允许主动扫描、漏洞验证和授权范围内的利用;禁止 DoS、破坏性操作和高风险稳定性影响行为"},
|
|
110
87
|
{label: "仅非侵入式测试", description: "允许信息收集和低风险探测,不进行利用"},
|
|
111
|
-
{label: "需要自定义 RoE", description: "暂不开始,我会提供详细规则"}]
|
|
88
|
+
{label: "需要自定义 RoE", description: "暂不开始,我会提供详细规则"}]
|
|
112
89
|
|
|
113
90
|
5. id "language_confirm" — header "🌐 输出语言", question "请选择本次测试的主要输出语言。",
|
|
114
91
|
options: [{label: "中文", description: "使用中文"},
|
|
115
|
-
{label: "English", description: "Use English"}]
|
|
92
|
+
{label: "English", description: "Use English"}]
|
|
116
93
|
|
|
117
94
|
6. id "final_confirm" — header "✅ 授权确认", question "请确认以上回答共同构成本次测试的授权边界,并开始测试。",
|
|
118
95
|
options: [{label: "确认并开始", description: "按以上 Target / Scope / Exclusions / RoE / Language 开始测试"},
|
|
119
|
-
{label: "暂不开始", description: "当前信息需要修改"}]
|
|
96
|
+
{label: "暂不开始", description: "当前信息需要修改"}]
|
|
97
|
+
|
|
98
|
+
NEVER ask one of these questions separately.
|
|
99
|
+
NEVER wait for an earlier answer before submitting the rest.
|
|
100
|
+
NEVER split the batch across multiple ask_user_question calls.
|
|
101
|
+
NEVER ask 3+3, 5+1, or any other split.
|
|
102
|
+
NEVER change the order of these six questions.
|
|
103
|
+
NEVER omit any question.
|
|
104
|
+
NEVER duplicate a question ID.
|
|
105
|
+
|
|
106
|
+
If you cannot construct all six questions yet, DO NOT CALL
|
|
107
|
+
ask_user_question until all six are ready.
|
|
108
|
+
|
|
109
|
+
The Host validates this contract mechanically before pentester_start.
|
|
110
|
+
Split questionnaires WILL be rejected.
|
|
111
|
+
|
|
112
|
+
CORRECTION ON REJECTION: if pentester_start returns an error like
|
|
113
|
+
pre_engagement_batch_required, pre_engagement_missing, or
|
|
114
|
+
pre_engagement_incomplete, you MUST re-issue ALL SIX questions together
|
|
115
|
+
in one ask_user_question call. NEVER continue from the missing question
|
|
116
|
+
onward — always re-ask the complete batch.
|
|
117
|
+
|
|
118
|
+
BOOTSTRAP / PRE-ENGAGEMENT: ONE BATCH, NOT A WIZARD.
|
|
120
119
|
|
|
121
|
-
|
|
122
|
-
run
|
|
123
|
-
|
|
124
|
-
|
|
120
|
+
When no PentestRun exists for the current session (see your injected
|
|
121
|
+
run state — it shows "Target Binding: (none)" when unbound):
|
|
122
|
+
|
|
123
|
+
TARGET LIFECYCLE FIRST: Before starting any pre-engagement questions,
|
|
124
|
+
check the "Existing Targets" list in your injected run state.
|
|
125
|
+
|
|
126
|
+
If the user provides a target that already exists in this workspace:
|
|
127
|
+
→ Call ask_user_question EXACTLY ONCE with a single question:
|
|
128
|
+
id: "existing_target_action"
|
|
129
|
+
header: "🎯 已有测试目标"
|
|
130
|
+
question: "检测到该 Target 已有测试流程。"
|
|
131
|
+
options:
|
|
132
|
+
[{label: "Continue", description: "打开原有 Root Session 继续测试"},
|
|
133
|
+
{label: "Restart", description: "清除旧数据,从头开始"}]
|
|
134
|
+
→ If Continue: tell the user to open the existing Root Session
|
|
135
|
+
(session id shown in the target list). Do NOT create a new run.
|
|
136
|
+
→ If Restart: proceed to the full six-question batch below, then
|
|
137
|
+
call pentester_start(mode=restart) after final confirmation.
|
|
138
|
+
|
|
139
|
+
If the user provides a NEW target:
|
|
140
|
+
→ Run the full six-question batch (see above). After all six are
|
|
141
|
+
confirmed, call pentester_start(mode=new) ONCE to bootstrap.
|
|
142
|
+
|
|
143
|
+
If the session IS already bound to a target:
|
|
144
|
+
→ Continue normal PTES orchestration for that target.
|
|
145
|
+
→ If the user asks to test a DIFFERENT target, tell them to create
|
|
146
|
+
a new Pentester Root Session in the same project.
|
|
147
|
+
|
|
148
|
+
After all six are confirmed, call pentester_start ONCE with the
|
|
149
|
+
initialization payload — target, scope, rules_of_engagement, language,
|
|
150
|
+
summary, and mode ("new" or "restart"). The host bootstraps the target
|
|
151
|
+
workspace (targets.json, run.json, target files, git initial commit)
|
|
125
152
|
and completes pre-engagement, activating Intelligence Gathering.
|
|
126
153
|
Pre-engagement never spawns a worker. pentester_delegate does NOT
|
|
127
154
|
bootstrap: if you call it before a run exists the host refuses. A run
|
|
128
|
-
is created exactly once and is never re-initialized with a
|
|
129
|
-
target. Before that confirmation you MUST NOT call any
|
|
130
|
-
spawn agents, ping or scan anything.
|
|
155
|
+
is created exactly once per target and is never re-initialized with a
|
|
156
|
+
different target. Before that confirmation you MUST NOT call any
|
|
157
|
+
pentester tool, spawn agents, ping or scan anything.
|
|
131
158
|
|
|
132
159
|
DELEGATING: Before calling pentester_delegate, first explain to the user
|
|
133
160
|
which worker you are creating, what it will do, and why the current
|
|
@@ -3,36 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 三个职责(都只在 join 本 preset 的 agent scope 内生效):
|
|
5
5
|
*
|
|
6
|
-
* 1.
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* per-Root agent-local restrict 隐藏);
|
|
12
|
-
* - Worker child 经官方 Subagent toolFilter deny-list 移除 Root 编排
|
|
13
|
-
* 工具 + ask_user_question,保留 container_exec 与其它继承能力。
|
|
14
|
-
*
|
|
15
|
-
* 2. per-Root 工具面(docs/plan.md §6 / §32):官方 Subagent child 会 join 本
|
|
16
|
-
* preset(composeFrom),所以 MOUNT 级 tools.restrict 会把 container_exec
|
|
17
|
-
* 也挡掉(restriction 沿 scope chain 取交集,child 自己的 allow 无法
|
|
18
|
-
* 重新放行)—— 必须把 deny 落在 ROOT agent 自己的 layer 上。agent/created
|
|
19
|
-
* 是发布点(standing mount 的 listener 会收到它下面所有 agent 的事件),
|
|
20
|
-
* 此时 composition 已就绪;child(origin=subagent / delegationDepth>=1)
|
|
21
|
-
* 跳过。通用 subagent/job/bash/fs 工具由**组合排除**(本 preset 不带那些
|
|
22
|
-
* 行),不需要也不允许在这里 deny。
|
|
23
|
-
*
|
|
24
|
-
* 3. Root 动态状态注入:每次 prompt assembly(创建/恢复/继续/compaction
|
|
25
|
-
* 后/Worker wakeup 后都会触发)从 run.json 生成 compact state 注入
|
|
26
|
-
* system context —— Root 无需 status 工具即可拿到
|
|
27
|
-
* target / branch / run status / current stage / stage statuses /
|
|
28
|
-
* delegations 摘要 + 当前 stage 的 Available AgentProfiles
|
|
29
|
-
* (docs/plan.md §13 / §7)。Worker 不注入(有独立 persona + task prompt)。
|
|
30
|
-
*
|
|
31
|
-
* 本文件随 preset 目录发布(publishPentesterPreset 整体 cp),必须自包含:
|
|
32
|
-
* 不 import 插件 src,只用 Node 标准库。项目目录取自 agent session 的
|
|
33
|
-
* trusted header cwd(host 注入,模型不可伪造)。Available AgentProfiles
|
|
34
|
-
* 清单(stage-profiles.json,由插件 apply 时从 STAGE_DEFINITIONS + AgentProfile
|
|
35
|
-
* registry 生成)与本文件同目录读取;缺失时优雅降级为不渲染该节。
|
|
6
|
+
* 1. 工具注册位置:把 5 个 pentester_* 领域工具注册进本 preset 的 standing scope。
|
|
7
|
+
* 2. per-Root 工具面:deny container_exec 落在 Root agent 自己的 layer。
|
|
8
|
+
* 3. Root 动态状态注入:从 targets.json + run.json 生成 compact state。
|
|
9
|
+
* Bound session → shows target's run state。
|
|
10
|
+
* Unbound session → shows existing targets in the workspace。
|
|
36
11
|
*/
|
|
37
12
|
import { readFileSync, existsSync } from 'node:fs'
|
|
38
13
|
import { join } from 'node:path'
|
|
@@ -40,7 +15,6 @@ import { fileURLToPath } from 'node:url'
|
|
|
40
15
|
|
|
41
16
|
export const name = 'dsh-pentester-run-state'
|
|
42
17
|
|
|
43
|
-
/** cordis 插件契约:声明依赖服务后才能在 apply 里访问 ctx.tools / ctx.systemPrompt / ctx.pentester。 */
|
|
44
18
|
export const inject = ['tools', 'systemPrompt', 'pentester']
|
|
45
19
|
|
|
46
20
|
const STAGE_IDS = [
|
|
@@ -53,21 +27,8 @@ const STAGE_IDS = [
|
|
|
53
27
|
'reporting',
|
|
54
28
|
]
|
|
55
29
|
|
|
56
|
-
/**
|
|
57
|
-
* Root 不该看到的工具。**只有 pentester_container_exec**:它是本 preset
|
|
58
|
-
* 注册进 standing scope 的工具,Root 会经继承看到 —— 必须从 Root 的实际
|
|
59
|
-
* Tool Surface 移除(per-Root agent-local restrict,Worker child 不受影响)。
|
|
60
|
-
*
|
|
61
|
-
* 通用 subagent / job / bash / fs 工具**不需要 deny**:本 deployment 的
|
|
62
|
-
* 通用工具行是 per-preset 的(standard 等 preset 自带 tool-bash / tool-fs /
|
|
63
|
-
* tool-subagent / tool-jobs;web-app bundle 把 base 宿主层的同名全局行
|
|
64
|
-
* disabled),pentester preset 的组合里没有这些行 → Root 天然看不到。
|
|
65
|
-
* 不要在这里堆 deny 名字:tools.restrict 对未知名字整体拒绝(先校验后
|
|
66
|
-
* 生效),把其它 preset 才注册的名字加进来会让整个 restrict 抛错。
|
|
67
|
-
*/
|
|
68
30
|
export const ROOT_DENY = ['pentester_container_exec']
|
|
69
31
|
|
|
70
|
-
/** 官方 subagent child(spawn/fork)的 session header 特征。 */
|
|
71
32
|
export function isWorkerAgent(agent) {
|
|
72
33
|
const header = agent?.session?.header
|
|
73
34
|
if (header === null || typeof header !== 'object') return false
|
|
@@ -80,28 +41,40 @@ function stageDir(stage) {
|
|
|
80
41
|
return `${String(index + 1).padStart(2, '0')}-${stage}`
|
|
81
42
|
}
|
|
82
43
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
44
|
+
// ── Target-scoped path helpers ───────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
function targetsJsonPath(projectDir) {
|
|
47
|
+
return join(projectDir, 'workspace', '.dsh-pentester', 'targets.json')
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function targetRoot(projectDir, dirName) {
|
|
51
|
+
return join(projectDir, 'workspace', 'targets', dirName)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function runJsonPath(targetRootPath) {
|
|
55
|
+
return join(targetRootPath, '.dsh-pentester', 'run.json')
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ── Registry read ────────────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
function loadTargets(projectDir) {
|
|
61
|
+
const file = targetsJsonPath(projectDir)
|
|
62
|
+
if (!existsSync(file)) return null
|
|
93
63
|
try {
|
|
94
|
-
|
|
95
|
-
const match = /^ref:\s*refs\/heads\/(.+)$/.exec(content)
|
|
96
|
-
return match === null ? 'detached' : match[1]
|
|
64
|
+
return JSON.parse(readFileSync(file, 'utf8'))
|
|
97
65
|
} catch {
|
|
98
|
-
return
|
|
66
|
+
return null
|
|
99
67
|
}
|
|
100
68
|
}
|
|
101
69
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
70
|
+
function findBoundTarget(projectDir, sessionId) {
|
|
71
|
+
const registry = loadTargets(projectDir)
|
|
72
|
+
if (registry === null || !Array.isArray(registry.targets)) return undefined
|
|
73
|
+
return registry.targets.find(t => t.rootSessionId === sessionId)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function loadRunFile(targetRootPath) {
|
|
77
|
+
const file = runJsonPath(targetRootPath)
|
|
105
78
|
if (!existsSync(file)) return null
|
|
106
79
|
try {
|
|
107
80
|
return JSON.parse(readFileSync(file, 'utf8'))
|
|
@@ -110,11 +83,18 @@ function loadRun(workspaceRoot) {
|
|
|
110
83
|
}
|
|
111
84
|
}
|
|
112
85
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
86
|
+
function currentBranch(targetRootPath) {
|
|
87
|
+
const head = join(targetRootPath, '.git', 'HEAD')
|
|
88
|
+
if (!existsSync(head)) return 'main'
|
|
89
|
+
try {
|
|
90
|
+
const content = readFileSync(head, 'utf8').trim()
|
|
91
|
+
const match = /^ref:\s*refs\/heads\/(.+)$/.exec(content)
|
|
92
|
+
return match === null ? 'detached' : match[1]
|
|
93
|
+
} catch {
|
|
94
|
+
return 'main'
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
118
98
|
export function loadStageProfiles() {
|
|
119
99
|
const file = join(fileURLToPath(new URL('.', import.meta.url)), 'stage-profiles.json')
|
|
120
100
|
if (!existsSync(file)) return null
|
|
@@ -125,9 +105,10 @@ export function loadStageProfiles() {
|
|
|
125
105
|
}
|
|
126
106
|
}
|
|
127
107
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
108
|
+
// ── Compact state rendering ──────────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
export function renderCompactState(run, targetRootPath, language = 'zh-CN', stageProfiles = loadStageProfiles()) {
|
|
111
|
+
const branch = currentBranch(targetRootPath)
|
|
131
112
|
if (run === null) {
|
|
132
113
|
return [
|
|
133
114
|
language === 'zh-CN' ? 'PentestRun: (尚未建立)' : 'PentestRun: (not established)',
|
|
@@ -142,15 +123,14 @@ export function renderCompactState(run, workspaceRoot, language = 'zh-CN', stage
|
|
|
142
123
|
? ['(none)']
|
|
143
124
|
: run.delegations.map(d => `${d.id} ${d.status}${d.agentId === undefined ? '' : ` (${d.agentId})`}`)
|
|
144
125
|
const profiles = stageProfiles === null ? undefined : stageProfiles?.[run.currentStage]
|
|
145
|
-
// manifest 缺失(旧安装)→ 整节省略,优雅降级;manifest 存在但该 stage 无
|
|
146
|
-
// 合法 profile(如 pre-engagement)→ 如实渲染 (none)。
|
|
147
126
|
const allowedProfiles = profiles === undefined
|
|
148
127
|
? undefined
|
|
149
128
|
: profiles.length === 0
|
|
150
129
|
? ['(none)']
|
|
151
130
|
: profiles.map(p => `- ${p.id} (${p.name})`)
|
|
152
131
|
const state = [
|
|
153
|
-
`Target: ${run.
|
|
132
|
+
`Target: ${run.target || '(not yet recorded)'}`,
|
|
133
|
+
run.scope ? `Scope: ${run.scope}` : '',
|
|
154
134
|
`Branch: ${branch}`,
|
|
155
135
|
`Run: ${run.status ?? 'active'}`,
|
|
156
136
|
`Current Stage: ${run.currentStage === null ? '(completed)' : stageDir(run.currentStage)}`,
|
|
@@ -162,49 +142,78 @@ export function renderCompactState(run, workspaceRoot, language = 'zh-CN', stage
|
|
|
162
142
|
state.push('', 'Available AgentProfiles:', ...allowedProfiles.map(line => ` ${line}`))
|
|
163
143
|
}
|
|
164
144
|
state.push('', 'Delegations:', ...delegations.map(line => ` ${line}`))
|
|
165
|
-
return state.join('\n')
|
|
145
|
+
return state.filter(line => line !== '').join('\n')
|
|
166
146
|
}
|
|
167
147
|
|
|
148
|
+
/** Render unbound session state: existing targets overview. */
|
|
149
|
+
function renderUnboundState(projectDir, language = 'zh-CN') {
|
|
150
|
+
const registry = loadTargets(projectDir)
|
|
151
|
+
if (registry === null || !Array.isArray(registry.targets) || registry.targets.length === 0) {
|
|
152
|
+
return [
|
|
153
|
+
language === 'zh-CN'
|
|
154
|
+
? 'Target Binding: (none)\n\n尚未创建任何测试目标。使用 pentester_start 创建新目标。'
|
|
155
|
+
: 'Target Binding: (none)\n\nNo targets created yet. Use pentester_start to create one.',
|
|
156
|
+
].join('\n')
|
|
157
|
+
}
|
|
158
|
+
const lines = [
|
|
159
|
+
language === 'zh-CN' ? 'Target Binding: (none)' : 'Target Binding: (none)',
|
|
160
|
+
'',
|
|
161
|
+
language === 'zh-CN' ? '现有测试目标:' : 'Existing Targets:',
|
|
162
|
+
]
|
|
163
|
+
for (const t of registry.targets) {
|
|
164
|
+
const run = loadRunFile(targetRoot(projectDir, t.dirName))
|
|
165
|
+
const status = run?.status ?? 'unknown'
|
|
166
|
+
const stage = run?.currentStage ?? 'unknown'
|
|
167
|
+
lines.push(` - ${t.target} status=${status} stage=${stage} rootSessionId=${t.rootSessionId ?? 'none'}`)
|
|
168
|
+
}
|
|
169
|
+
return lines.join('\n')
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ── Cordis apply ─────────────────────────────────────────────────────────────
|
|
173
|
+
|
|
168
174
|
export function apply(ctx) {
|
|
169
|
-
// 1) 工具注册进本 preset 的 standing scope(宿主实现)。fail loud:
|
|
170
|
-
// pentester service 缺失说明插件未加载,preset 不该半残挂载。
|
|
171
175
|
if (ctx.pentester === undefined) {
|
|
172
176
|
throw new Error('dsh-pentester plugin is not loaded: "pentester" service missing (cannot register root tools)')
|
|
173
177
|
}
|
|
174
178
|
ctx.pentester.registerRootTools(ctx.tools)
|
|
175
179
|
ctx.pentester.registerContainerExecTool(ctx.tools)
|
|
176
180
|
|
|
177
|
-
// 2) per-Root 工具面(见文件头注释):deny 只落在 Root agent 自己的 layer,
|
|
178
|
-
// Worker child 不受影响(child 由官方 toolFilter deny-list 移除 Root 工具)。
|
|
179
181
|
ctx.on('agent/created', ({ agent }) => {
|
|
180
182
|
if (agent == null || isWorkerAgent(agent)) return
|
|
181
183
|
try {
|
|
182
|
-
// agent.ctx.tools.restrict 在 agent 自己的 layer 上注册 restriction;
|
|
183
|
-
// agent dispose 时随 scope 一起卸载。
|
|
184
184
|
agent.ctx?.tools?.restrict({ deny: ROOT_DENY })
|
|
185
185
|
} catch (error) {
|
|
186
|
-
// 正常路径不应触发:container_exec 与本 listener 在同一 mount apply
|
|
187
|
-
// 里注册,Root 创建时必然已知。catch 只防 HMR 竞态并避免 veto agent
|
|
188
|
-
// publication;如真的抛错会记日志,便于诊断工具面问题。
|
|
189
186
|
console.error('[dsh-pentester] root tool restriction failed:', error?.message ?? error)
|
|
190
187
|
}
|
|
191
188
|
})
|
|
192
189
|
|
|
193
|
-
// 3) 动态 compact state:每次 assembly 求值(含 compaction 后继续、wakeup 后继续)。
|
|
194
|
-
// DSH SystemPrompt 内部对 text 返回值调用 text.indexOf(…),因此 text 回调
|
|
195
|
-
// 必须 total:永远返回 string;不贡献内容时返回 '',禁止返回 undefined。
|
|
196
190
|
ctx.systemPrompt?.context({
|
|
197
191
|
name: 'dsh-pentester:run-state',
|
|
198
192
|
order: 900,
|
|
199
193
|
text: (assemblyContext) => {
|
|
200
194
|
const agent = assemblyContext?.agent
|
|
201
|
-
// Worker 不注入 run-state(有独立 persona + task prompt)。
|
|
202
195
|
if (isWorkerAgent(agent)) return ''
|
|
203
196
|
const cwd = agent?.session?.header?.cwd
|
|
204
197
|
if (typeof cwd !== 'string' || cwd.length === 0) return ''
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
198
|
+
const sessionId = agent?.session?.id
|
|
199
|
+
if (typeof sessionId !== 'string' || sessionId.length === 0) return ''
|
|
200
|
+
|
|
201
|
+
// Try to find bound target
|
|
202
|
+
const binding = findBoundTarget(cwd, sessionId)
|
|
203
|
+
if (binding !== undefined) {
|
|
204
|
+
const rootPath = targetRoot(cwd, binding.dirName)
|
|
205
|
+
const run = loadRunFile(rootPath)
|
|
206
|
+
const language = run?.language ?? 'zh-CN'
|
|
207
|
+
return [
|
|
208
|
+
`Target Binding: ${binding.target}`,
|
|
209
|
+
`Target ID: ${binding.id}`,
|
|
210
|
+
'',
|
|
211
|
+
renderCompactState(run, rootPath, language, loadStageProfiles()),
|
|
212
|
+
].join('\n')
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Unbound: show targets overview
|
|
216
|
+
return renderUnboundState(cwd, 'zh-CN')
|
|
208
217
|
},
|
|
209
218
|
})
|
|
210
|
-
}
|
|
219
|
+
}
|