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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-pentester",
3
- "version": "1.0.0",
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
- "esbuild": "0.25.0"
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 four pentester_*
32
- tools, the native ask_user_question, and the subagent control 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
- BOOTSTRAP / PRE-ENGAGEMENT: ONE BATCH, NOT A WIZARD.
67
+ PRE-ENGAGEMENT TOOL-CALL CONTRACT HARD REQUIREMENT
66
68
 
67
- When no PentestRun exists in the current session:
68
-
69
- 1. Parse any Target / Scope information already supplied by the user.
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
- After all six are confirmed, call pentester_advance_stage ONCE with the
122
- run initialization payload run.targets, run.rules_of_engagement and
123
- run.language — plus a summary of the confirmed pre-engagement. The host
124
- bootstraps the workspace (run.json, target files, git initial commit)
120
+ When no PentestRun exists for the current session (see your injected
121
+ run stateit 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 different
129
- target. Before that confirmation you MUST NOT call any pentester tool,
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. 工具注册位置(docs/plan.md §7 / §32):把 5 个 pentester_* 领域工具
7
- * 注册进本 preset standing scope(实现由宿主 `pentester` service 提供)。
8
- * 结果:
9
- * - standard Session 完全看不到 pentester_*(宿主不再全局注册);
10
- * - Root Session 看到 4 pentester 编排工具(container_exec 由下方
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
- * 从 workspace git 仓库读当前分支。
85
- * 直接读 `.git/HEAD`(`ref: refs/heads/<branch>`),不 spawn git 子进程:
86
- * 本函数在每次 prompt assembly 的同步 text 回调里执行,spawn 会被 DSH
87
- * sandbox 拦截或向 stderr 泄漏 fatal 输出;纯 fs 读取无副作用且不会抛错。
88
- * 非 git 仓库 / 读取失败 → 回退 'main'。
89
- */
90
- function currentBranch(workspaceRoot) {
91
- const head = join(workspaceRoot, '.git', 'HEAD')
92
- if (!existsSync(head)) return 'main'
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
- const content = readFileSync(head, 'utf8').trim()
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 'main'
66
+ return null
99
67
  }
100
68
  }
101
69
 
102
- /** run.json;不存在(尚未 bootstrap)返回 null。 */
103
- function loadRun(workspaceRoot) {
104
- const file = join(workspaceRoot, '.dsh-pentester', 'run.json')
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
- * 读取与 preset 同目录发布的 stage → AgentProfile 清单
115
- * (插件 apply 时由 STAGE_DEFINITIONS + AgentProfile registry 生成)。
116
- * 文件缺失(旧安装 / 测试)返回 null,渲染时优雅降级。
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
- /** 生成 compact Root state(绝不注入完整 run.json)。@returns 多行文本。 */
129
- export function renderCompactState(run, workspaceRoot, language = 'zh-CN', stageProfiles = loadStageProfiles()) {
130
- const branch = currentBranch(workspaceRoot)
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.targets.join(', ') || '(not yet recorded)'}`,
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 run = loadRun(cwd)
206
- const language = run?.language ?? 'zh-CN'
207
- return renderCompactState(run, cwd, language, loadStageProfiles())
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
+ }