agent-bypass 2.18.0 → 2.20.1
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/AGENT_ADDITIVE_CHANGELOG.md +10 -101
- package/README.md +10 -42
- package/SKILL.md +12 -7
- package/bin/install.js +5 -325
- package/package.json +3 -6
- package/references/multi-path-attack.md +100 -0
- package/references/search-engine-pivot.md +109 -0
- package/references/tool-selection.md +4 -0
- package/scripts/env_chain.py +337 -0
- package/scripts/pivot_queries.py +321 -0
- package/tests/test_regressions.py +0 -241
- package/tests/test_tool_inventory.py +1 -1
- package/dsh/README.md +0 -74
- package/dsh/build-template.js +0 -310
- package/dsh/persona.md +0 -127
- package/dsh/preset.template.yml +0 -132
|
@@ -153,246 +153,5 @@ class JsripSameSiteTests(unittest.TestCase):
|
|
|
153
153
|
self.assertFalse(self.js.same_site("https://example.com", "https://wwwexample.com/a.js"))
|
|
154
154
|
|
|
155
155
|
|
|
156
|
-
class DshInstallerTests(unittest.TestCase):
|
|
157
|
-
"""DeepSeek Harness:技能安装 + Ck 模式预设注入(临时 HOME,离线)。"""
|
|
158
|
-
|
|
159
|
-
DSH_ENV = ("DSH_HOME", "DSH_PROFILE", "DSH_AGENTS_HOME", "DSH_APP_PATH", "DSH_ASAR_PATH")
|
|
160
|
-
|
|
161
|
-
def _node(self, *args, home):
|
|
162
|
-
env = dict(os.environ)
|
|
163
|
-
env["HOME"] = home
|
|
164
|
-
for k in self.DSH_ENV:
|
|
165
|
-
env.pop(k, None)
|
|
166
|
-
return _tmp_run(["node", str(ROOT / "bin" / "install.js"), *args], env=env)
|
|
167
|
-
|
|
168
|
-
def _seed(self, home, patch_text="[]\n"):
|
|
169
|
-
prof = Path(home) / ".dsh" / "profiles" / "desktop"
|
|
170
|
-
prof.mkdir(parents=True, exist_ok=True)
|
|
171
|
-
f = prof / "cordis.patch.yml"
|
|
172
|
-
f.write_text(patch_text, encoding="utf-8")
|
|
173
|
-
return f
|
|
174
|
-
|
|
175
|
-
def test_install_idempotent_and_persona_dollar_preserved(self):
|
|
176
|
-
with tempfile.TemporaryDirectory() as home:
|
|
177
|
-
patch = self._seed(home)
|
|
178
|
-
r1 = self._node("--dsh", home=home)
|
|
179
|
-
self.assertEqual(r1.returncode, 0, r1.stdout + r1.stderr)
|
|
180
|
-
skill = Path(home) / ".agents" / "skills" / "agent-bypass" / "SKILL.md"
|
|
181
|
-
self.assertTrue(skill.exists(), "DSH 技能应安装到 ~/.agents/skills")
|
|
182
|
-
text = patch.read_text(encoding="utf-8")
|
|
183
|
-
self.assertEqual(text.count("agent-bypass:dsh-preset-begin"), 1)
|
|
184
|
-
self.assertEqual(text.count("id: preset-ck"), 1)
|
|
185
|
-
self.assertIn("Ck 万能执行员", text)
|
|
186
|
-
# $$ 不能被 String.replace 的替换串语义吃掉(PHP 变量变量)
|
|
187
|
-
self.assertIn("parse_str / $$", text)
|
|
188
|
-
# 幂等:重复安装不重复追加
|
|
189
|
-
r2 = self._node("--dsh", home=home)
|
|
190
|
-
self.assertEqual(r2.returncode, 0, r2.stdout + r2.stderr)
|
|
191
|
-
text2 = patch.read_text(encoding="utf-8")
|
|
192
|
-
self.assertEqual(text2.count("agent-bypass:dsh-preset-begin"), 1)
|
|
193
|
-
self.assertEqual(text2.count("id: preset-ck"), 1)
|
|
194
|
-
|
|
195
|
-
def test_crlf_preserved_and_uninstall_restores(self):
|
|
196
|
-
with tempfile.TemporaryDirectory() as home:
|
|
197
|
-
patch = self._seed(home, '# note\r\n- id: web\r\n name: "@deepseek-ai/dsh-web"\r\n')
|
|
198
|
-
self.assertEqual(self._node("--dsh", home=home).returncode, 0)
|
|
199
|
-
data = patch.read_bytes()
|
|
200
|
-
self.assertIn(b"agent-bypass:dsh-preset-begin", data)
|
|
201
|
-
self.assertGreater(data.count(b"\r\n"), 100, "CRLF 文件应保持 CRLF")
|
|
202
|
-
r = self._node("--dsh", "--uninstall", home=home)
|
|
203
|
-
self.assertEqual(r.returncode, 0, r.stdout + r.stderr)
|
|
204
|
-
data2 = patch.read_bytes()
|
|
205
|
-
self.assertNotIn(b"agent-bypass:dsh-preset-begin", data2)
|
|
206
|
-
self.assertIn(b"# note", data2)
|
|
207
|
-
self.assertIn(b"id: web", data2)
|
|
208
|
-
self.assertFalse((Path(home) / ".agents" / "skills" / "agent-bypass").exists())
|
|
209
|
-
|
|
210
|
-
def test_comment_only_patch_stays_valid_yaml(self):
|
|
211
|
-
with tempfile.TemporaryDirectory() as home:
|
|
212
|
-
patch = self._seed(home, "# user comment\n[]\n")
|
|
213
|
-
self.assertEqual(self._node("--dsh", home=home).returncode, 0)
|
|
214
|
-
self.assertEqual(self._node("--dsh", "--uninstall", home=home).returncode, 0)
|
|
215
|
-
text = patch.read_text(encoding="utf-8")
|
|
216
|
-
self.assertIn("# user comment", text)
|
|
217
|
-
self.assertIn("[]", text, "只留注释时应补回空数组,保证 loader 可解析")
|
|
218
|
-
|
|
219
|
-
def test_default_targets_include_pi_opencode_dsh(self):
|
|
220
|
-
with tempfile.TemporaryDirectory() as home:
|
|
221
|
-
(Path(home) / ".pi").mkdir()
|
|
222
|
-
(Path(home) / ".config" / "opencode").mkdir(parents=True)
|
|
223
|
-
self._seed(home)
|
|
224
|
-
r = self._node(home=home)
|
|
225
|
-
self.assertEqual(r.returncode, 0, r.stdout + r.stderr)
|
|
226
|
-
self.assertTrue((Path(home) / ".pi" / "agent" / "skills" / "agent-bypass" / "SKILL.md").exists())
|
|
227
|
-
self.assertTrue((Path(home) / ".config" / "opencode" / "skills" / "agent-bypass" / "SKILL.md").exists())
|
|
228
|
-
self.assertTrue((Path(home) / ".agents" / "skills" / "agent-bypass" / "SKILL.md").exists())
|
|
229
|
-
patch_text = (Path(home) / ".dsh" / "profiles" / "desktop" / "cordis.patch.yml").read_text(encoding="utf-8")
|
|
230
|
-
self.assertIn("id: preset-ck", patch_text)
|
|
231
|
-
self.assertIn("dsh", r.stdout + r.stderr)
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
class DoctorTests(unittest.TestCase):
|
|
235
|
-
"""--doctor 环境自检:只读、不写任何文件。"""
|
|
236
|
-
|
|
237
|
-
def test_doctor_readonly(self):
|
|
238
|
-
with tempfile.TemporaryDirectory() as home:
|
|
239
|
-
env = dict(os.environ)
|
|
240
|
-
env["HOME"] = home
|
|
241
|
-
for k in ("DSH_HOME", "DSH_PROFILE", "DSH_AGENTS_HOME", "DSH_APP_PATH", "DSH_ASAR_PATH"):
|
|
242
|
-
env.pop(k, None)
|
|
243
|
-
r = _tmp_run(["node", str(ROOT / "bin" / "install.js"), "--doctor"], env=env)
|
|
244
|
-
self.assertEqual(r.returncode, 0, r.stdout + r.stderr)
|
|
245
|
-
out = r.stdout + r.stderr
|
|
246
|
-
self.assertIn("环境自检", out)
|
|
247
|
-
self.assertIn("平台", out)
|
|
248
|
-
self.assertIn("包管理器", out)
|
|
249
|
-
# 只读:临时 HOME 不应产生任何文件
|
|
250
|
-
self.assertEqual(list(Path(home).rglob("*")), [], "doctor 不应写文件")
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
class NetDnsScanOriginTests(unittest.TestCase):
|
|
254
|
-
"""fake-IP/TUN:netdns 判定 + portscan 解析守卫 + origin_trace 解析(全部离线)。"""
|
|
255
|
-
|
|
256
|
-
@classmethod
|
|
257
|
-
def setUpClass(cls):
|
|
258
|
-
sys.path.insert(0, str(SCRIPTS))
|
|
259
|
-
import netdns
|
|
260
|
-
import portscan
|
|
261
|
-
import origin_trace
|
|
262
|
-
cls.nd = netdns
|
|
263
|
-
cls.ps = portscan
|
|
264
|
-
cls.ot = origin_trace
|
|
265
|
-
|
|
266
|
-
def test_fake_ip_ranges(self):
|
|
267
|
-
for ip in ("198.18.0.5", "198.19.255.1", "240.1.2.3", "198.51.100.7", "203.0.113.9"):
|
|
268
|
-
self.assertTrue(self.nd.is_fake_ip(ip), ip)
|
|
269
|
-
for ip in ("1.1.1.1", "8.8.8.8", "192.168.1.1", "10.0.0.1"):
|
|
270
|
-
self.assertFalse(self.nd.is_fake_ip(ip), ip)
|
|
271
|
-
|
|
272
|
-
def test_parse_dns_json(self):
|
|
273
|
-
sample = ('{"Answer":[{"type":1,"data":"93.184.216.34"},'
|
|
274
|
-
'{"type":28,"data":"2606:2800::1"},{"type":1,"data":"198.18.0.1"}]}')
|
|
275
|
-
self.assertEqual(self.nd._parse_dns_json(sample, "A"), ["93.184.216.34", "198.18.0.1"])
|
|
276
|
-
self.assertEqual(self.nd._parse_dns_json(sample, "AAAA"), ["2606:2800::1"])
|
|
277
|
-
self.assertEqual(self.nd._parse_dns_json("not-json", "A"), [])
|
|
278
|
-
|
|
279
|
-
def test_portscan_split_host_fake_guard(self):
|
|
280
|
-
self.assertEqual(self.ps.split_host("https://example.com:8443/x"), ("example.com", 8443))
|
|
281
|
-
self.assertEqual(self.ps.split_host("1.2.3.4"), ("1.2.3.4", None))
|
|
282
|
-
self.assertIsNone(self.ps.split_host("bad host")[0])
|
|
283
|
-
self.assertTrue(self.ps.is_fake_ip("198.18.1.2"))
|
|
284
|
-
ip, src, fake = self.ps.resolve_target("198.18.1.2", mode="auto")
|
|
285
|
-
self.assertEqual((ip, src), ("198.18.1.2", "ip"))
|
|
286
|
-
self.assertEqual(fake, [])
|
|
287
|
-
|
|
288
|
-
def test_origin_trace_parsers(self):
|
|
289
|
-
ct = '[{"name_value":"a.example.com\\n*.example.com\\nother.test"}]'
|
|
290
|
-
self.assertEqual(self.ot.parse_crtsh(ct, "example.com"), ["a.example.com", "example.com"])
|
|
291
|
-
self.assertEqual(self.ot.extract_title("<title> Hi There </title>"), "Hi There")
|
|
292
|
-
self.assertEqual(self.ot.cdn_markers({"cf-ray": "x", "server": "nginx"}), ["cf-ray"])
|
|
293
|
-
base = {"title": "X", "server": "nginx", "status": "HTTP/1.1 200 OK", "cdn": ["cf-ray"]}
|
|
294
|
-
cand = {"title": "X", "server": "nginx", "status": "HTTP/1.1 200 OK", "cdn": [], "cert_domain_ok": True}
|
|
295
|
-
score, reasons = self.ot.score_candidate(base, cand)
|
|
296
|
-
self.assertGreaterEqual(score, 5)
|
|
297
|
-
self.assertIn("title", reasons)
|
|
298
|
-
self.assertLess(self.ot.score_candidate(base, {"cdn": ["cf-ray"]})[0], score)
|
|
299
|
-
|
|
300
|
-
def test_doh_shell_syntax(self):
|
|
301
|
-
sh = shutil.which("sh")
|
|
302
|
-
if not sh:
|
|
303
|
-
self.skipTest("no sh")
|
|
304
|
-
r = _tmp_run([sh, "-n", str(SCRIPTS / "doh_resolve.sh")])
|
|
305
|
-
self.assertEqual(r.returncode, 0, r.stderr)
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
class SkillCompactTests(unittest.TestCase):
|
|
309
|
-
"""SKILL.md 体积/去重回归:防止再次膨胀。"""
|
|
310
|
-
|
|
311
|
-
def test_skill_structure_and_size(self):
|
|
312
|
-
skill = (ROOT / "SKILL.md").read_text(encoding="utf-8")
|
|
313
|
-
self.assertEqual(skill.count("<!-- module:"), 35)
|
|
314
|
-
self.assertNotIn("## 修正对照", skill, "重复的修正对照表已移除,勿再加回")
|
|
315
|
-
self.assertLess(len(skill.encode("utf-8")), 132000, "SKILL.md 体积超限(保持精简,长内容放 references/)")
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
class HangProtectionTests(unittest.TestCase):
|
|
319
|
-
"""防卡死:DNS 硬超时、扫描总时限、溯源预算/同名 CDN 扣分(全部离线)。"""
|
|
320
|
-
|
|
321
|
-
@classmethod
|
|
322
|
-
def setUpClass(cls):
|
|
323
|
-
sys.path.insert(0, str(SCRIPTS))
|
|
324
|
-
import netdns, portscan, origin_trace
|
|
325
|
-
cls.nd = netdns
|
|
326
|
-
cls.ps = portscan
|
|
327
|
-
cls.ot = origin_trace
|
|
328
|
-
|
|
329
|
-
def test_system_resolve_hard_timeout(self):
|
|
330
|
-
import time as _t
|
|
331
|
-
orig = self.nd._system_resolve_impl
|
|
332
|
-
self.nd._system_resolve_impl = lambda name: (_t.sleep(3), ["1.2.3.4"])[1]
|
|
333
|
-
t0 = _t.monotonic()
|
|
334
|
-
try:
|
|
335
|
-
ips = self.nd.system_resolve("example.com", timeout=0.2)
|
|
336
|
-
finally:
|
|
337
|
-
self.nd._system_resolve_impl = orig
|
|
338
|
-
self.assertEqual(ips, [])
|
|
339
|
-
self.assertLess(_t.monotonic() - t0, 1.5, "system_resolve 必须有硬超时,不能挂住")
|
|
340
|
-
|
|
341
|
-
def test_portscan_total_deadline(self):
|
|
342
|
-
import asyncio, time as _t
|
|
343
|
-
found, timed_out = asyncio.run(
|
|
344
|
-
self.ps.scan("127.0.0.1", [9, 10], conc=1, timeout=0.05,
|
|
345
|
-
deadline=_t.monotonic() - 1))
|
|
346
|
-
self.assertEqual(found, [])
|
|
347
|
-
self.assertTrue(timed_out)
|
|
348
|
-
|
|
349
|
-
def test_origin_same_cdn_penalty(self):
|
|
350
|
-
base = {"title": "X", "server": "nginx", "status": "HTTP/1.1 200 OK", "cdn": ["cf-ray"]}
|
|
351
|
-
cand_cdn = {"title": "X", "server": "nginx", "status": "HTTP/1.1 200 OK", "cdn": ["cf-ray"]}
|
|
352
|
-
cand_origin = {"title": "X", "server": "nginx", "status": "HTTP/1.1 200 OK", "cdn": []}
|
|
353
|
-
s_cdn, r_cdn = self.ot.score_candidate(base, cand_cdn)
|
|
354
|
-
s_org, r_org = self.ot.score_candidate(base, cand_origin)
|
|
355
|
-
self.assertIn("same-cdn", r_cdn)
|
|
356
|
-
self.assertLess(s_cdn, s_org)
|
|
357
|
-
|
|
358
|
-
def test_passive_ct_parsers(self):
|
|
359
|
-
cs = '[{"dns_names":["www.example.com","*.example.com","x.test"]}]'
|
|
360
|
-
self.assertEqual(self.ot.parse_certspotter(cs, "example.com"), ["www.example.com", "example.com"])
|
|
361
|
-
otx = ('{"passive_dns":[{"hostname":"mail.example.com","address":"1.2.3.4"},'
|
|
362
|
-
'{"hostname":"x.test","address":"9.9.9.9"},'
|
|
363
|
-
'{"hostname":"cdn.example.com","address":"198.18.0.9"}]}')
|
|
364
|
-
hosts, ips = self.ot.parse_otx(otx, "example.com")
|
|
365
|
-
self.assertIn("mail.example.com", hosts)
|
|
366
|
-
self.assertEqual(ips, ["1.2.3.4"], "被动 DNS 的 fake-IP 应被过滤")
|
|
367
|
-
hosts, ips = self.ot.parse_hackertarget("vpn.example.com,5.6.7.8\nAPI count exceeded", "example.com")
|
|
368
|
-
self.assertEqual(hosts, ["vpn.example.com"])
|
|
369
|
-
self.assertEqual(ips, ["5.6.7.8"])
|
|
370
|
-
self.assertEqual(self.ot.parse_anubis('["a.example.com","b.test"]', "example.com"), ["a.example.com"])
|
|
371
|
-
self.assertEqual(self.ot.parse_rapiddns("<td>oa.example.com</td><td>x.test</td>", "example.com"),
|
|
372
|
-
["oa.example.com"])
|
|
373
|
-
for src in ("crt.sh", "certspotter", "otx", "hackertarget", "anubis", "rapiddns"):
|
|
374
|
-
self.assertIn(src, self.ot.CT_SOURCES)
|
|
375
|
-
|
|
376
|
-
def test_ct_multi_source_fallback(self):
|
|
377
|
-
def fake(name, domain, timeout):
|
|
378
|
-
if name == "crt.sh":
|
|
379
|
-
raise RuntimeError("502 Bad Gateway")
|
|
380
|
-
return (["s1.example.com", "s2.example.com"], ["1.2.3.4"])
|
|
381
|
-
orig = self.ot._ct_fetch
|
|
382
|
-
self.ot._ct_fetch = fake
|
|
383
|
-
try:
|
|
384
|
-
hosts, ips, status = self.ot.fetch_passive_names(
|
|
385
|
-
"example.com", sources=["crt.sh", "otx"], budget=5)
|
|
386
|
-
finally:
|
|
387
|
-
self.ot._ct_fetch = orig
|
|
388
|
-
self.assertEqual(hosts, ["s1.example.com", "s2.example.com"])
|
|
389
|
-
self.assertEqual(ips, ["1.2.3.4"])
|
|
390
|
-
self.assertIn("失败", status["crt.sh"], "单源 502 应被记录并跳过")
|
|
391
|
-
|
|
392
|
-
def test_origin_bad_flag_rejected(self):
|
|
393
|
-
r = _tmp_run([sys.executable, str(SCRIPTS / "origin_trace.py"), "--bogus"])
|
|
394
|
-
self.assertEqual(r.returncode, 2)
|
|
395
|
-
|
|
396
|
-
|
|
397
156
|
if __name__ == "__main__":
|
|
398
157
|
unittest.main()
|
|
@@ -33,7 +33,7 @@ class InventoryTests(unittest.TestCase):
|
|
|
33
33
|
skill = (ROOT / 'SKILL.md').read_text(encoding='utf-8')
|
|
34
34
|
for item in ['agent-routing.md','tool-selection.md','verification-gates.md','framework-comparison-2026.md','tool_inventory.py']:
|
|
35
35
|
self.assertIn(item, skill)
|
|
36
|
-
self.assertEqual(skill.count(
|
|
36
|
+
self.assertEqual(skill.count('<!-- module:'), 35)
|
|
37
37
|
|
|
38
38
|
|
|
39
39
|
if __name__ == '__main__':
|
package/dsh/README.md
DELETED
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
# dsh/ — DeepSeek Harness 集成(Ck 模式)
|
|
2
|
-
|
|
3
|
-
把 agent-bypass 接进 [DeepSeek Harness](https://www.deepseek.com)(DSH)桌面端,两件事一起做:
|
|
4
|
-
|
|
5
|
-
1. **技能可读**:技能文件装到 DSH 的用户技能根 `~/.agents/skills/agent-bypass/`,DSH 的 `skill-filesystem` 会在启动时自动发现并加载(catalog 里出现 `agent-bypass`,命中触发词即可用)。
|
|
6
|
-
2. **Ck 模式**:把一个 `@deepseek-ai/dsh-agent-preset` 声明写进 DSH 的 profile patch,新会话里出现 **「Ck 万能执行员」** Agent 预设——人格来自 `persona.md`,工具/插件行与 DSH 官方 `standard` 预设**逐行一致**。
|
|
7
|
-
|
|
8
|
-
## 文件
|
|
9
|
-
|
|
10
|
-
| 文件 | 作用 |
|
|
11
|
-
|---|---|
|
|
12
|
-
| `persona.md` | Ck 万能执行员人格正文(自包含,不引用外部技能/模块) |
|
|
13
|
-
| `preset.template.yml` | 预设声明模板:官方 standard 预设的插件/工具行 + `__PERSONA__` 占位符 |
|
|
14
|
-
| `build-template.js` | 从本机 DSH 的 `app.asar` 重新派生 `preset.template.yml`(DSH 升级后用) |
|
|
15
|
-
|
|
16
|
-
## 安装 / 卸载
|
|
17
|
-
|
|
18
|
-
`npx agent-bypass`(默认目标已含 DSH)或显式;先自检 `npx agent-bypass --doctor` 可看 DSH 安装探测与 Ck 模式状态:
|
|
19
|
-
|
|
20
|
-
```bash
|
|
21
|
-
npx agent-bypass --dsh # 只装 DeepSeek Harness
|
|
22
|
-
npx agent-bypass --dsh --uninstall # 卸载(技能 + 预设标记块)
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
安装器做的事:
|
|
26
|
-
|
|
27
|
-
- 技能 → `~/.agents/skills/agent-bypass/`(`DSH_AGENTS_HOME` 可改共享根;Windows 为 `%USERPROFILE%\.agents\skills`)。该目录是 Agent Skills 开放标准主副本,与 CC Switch / 其他 harness 共用;如果它是指向 Pi 真身的软链,安装器会穿透到真身更新、保持链接。
|
|
28
|
-
- 预设 → `<DSH_HOME>/profiles/<profile>/cordis.patch.yml` 内写入标记块:
|
|
29
|
-
|
|
30
|
-
```yaml
|
|
31
|
-
# >>> agent-bypass:dsh-preset-begin
|
|
32
|
-
- insert:
|
|
33
|
-
- id: preset-ck
|
|
34
|
-
name: '@deepseek-ai/dsh-agent-preset'
|
|
35
|
-
config: { id: ck, name: 'Ck 万能执行员', order: 0, plugins: [...] }
|
|
36
|
-
# <<< agent-bypass:dsh-preset-end
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
幂等:重复安装只更新块内容,不动用户原有条目/注释;保留文件 CRLF 与 BOM;若检测到已有的 bundle 方式安装(`plugin_manager install_bundle`)或已存在 `preset-ck` 声明,会跳过注入并在输出里提示,避免行 id 冲突。
|
|
40
|
-
|
|
41
|
-
## 路径与自动识别(macOS / Windows / Linux)
|
|
42
|
-
|
|
43
|
-
| 系统 | DSH 安装目录探测顺序 | app.asar |
|
|
44
|
-
|---|---|---|
|
|
45
|
-
| macOS | `/Applications/DeepSeek Harness.app`、`~/Applications/…`;找不到时 `mdfind com.deepseek.dsh` 兜底 | `<app>.app/Contents/Resources/app.asar` |
|
|
46
|
-
| Windows | `%LOCALAPPDATA%\Programs\DeepSeek Harness`、`%LOCALAPPDATA%\DeepSeek Harness`、`%ProgramFiles%\…`、`%ProgramFiles(x86)%\…`;找不到时查注册表 Uninstall 项(InstallLocation / DisplayIcon) | `<安装目录>\resources\app.asar` |
|
|
47
|
-
| Linux | `/opt/DeepSeek Harness`、`/opt/deepseek-harness`、`/usr/lib|share/deepseek-harness` | `<安装目录>/resources/app.asar` |
|
|
48
|
-
|
|
49
|
-
覆盖方式(任选):`DSH_APP_PATH=<安装目录>`、`DSH_ASAR_PATH=<app.asar>`、`node dsh/build-template.js --app <目录> / --asar <路径>`。
|
|
50
|
-
|
|
51
|
-
DSH 自身数据目录:`DSH_HOME`(默认 `~/.dsh`)、profile 名 `DSH_PROFILE`(默认自动选 `desktop`,否则扫 `profiles/*/cordis.patch.yml`)。Windows 下同为 `%USERPROFILE%\.dsh`。
|
|
52
|
-
|
|
53
|
-
`build-template.js` 用**纯 Node 读取 asar**(不依赖 Electron 二进制位置,跨平台一致;找不到安装目录时可显式指定)。
|
|
54
|
-
|
|
55
|
-
## 使用 Ck 模式
|
|
56
|
-
|
|
57
|
-
1. 新建会话(New Session)→ 「Agent 预设」控件 → **「Ck 万能执行员」**(`order: 0` 排最前)。
|
|
58
|
-
2. 想以后默认:设置 → 「Agent 预设」→ 自定义分组 → Ck 卡片 → **「设为新任务默认」**。
|
|
59
|
-
3. 已存在的会话保持其启动时预设,不能中途切换;预设列表没立即出现就重启一次 DSH。
|
|
60
|
-
|
|
61
|
-
## DSH 升级后
|
|
62
|
-
|
|
63
|
-
官方 `standard` 预设的插件行可能变化。在装了 DSH 的机器上重跑:
|
|
64
|
-
|
|
65
|
-
```bash
|
|
66
|
-
node dsh/build-template.js # 自动定位 app.asar,重建 preset.template.yml
|
|
67
|
-
npx agent-bypass --dsh # 重新注入(幂等)
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
## 已知边界
|
|
71
|
-
|
|
72
|
-
- 预设不是安全沙箱(DSH 官方说明:YAML 与插件可执行 Host 代码);本目录只产出声明,不含第三方代码。
|
|
73
|
-
- `persona.md` 会作为系统提示前缀随每次请求发送(约 1.4 万字符),嫌重可裁剪后重跑安装。
|
|
74
|
-
- 不要把 `~/.dsh` 与 `~/.agents` 指向错误的用户目录;Windows 用 `%USERPROFILE%`,macOS/Linux 用 `$HOME`。
|
package/dsh/build-template.js
DELETED
|
@@ -1,310 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* 从本机 DeepSeek Harness 安装目录提取官方 standard 预设,派生 Ck 模式的预设模板。
|
|
6
|
-
*
|
|
7
|
-
* 产物:dsh/preset.template.yml
|
|
8
|
-
* —— 与官方 standard 预设的插件/工具行完全一致,只替换预设元数据与 persona;
|
|
9
|
-
* persona 正文保留 __PERSONA__ 占位符,由安装器在安装时填入 dsh/persona.md。
|
|
10
|
-
*
|
|
11
|
-
* 跨平台提示(macOS / Windows / Linux 自动识别):
|
|
12
|
-
* macOS : /Applications/DeepSeek Harness.app(或 ~/Applications),
|
|
13
|
-
* Info.plist 的 CFBundleIdentifier = com.deepseek.dsh;找不到时用 Spotlight(mdfind) 兜底。
|
|
14
|
-
* app.asar = <app>.app/Contents/Resources/app.asar
|
|
15
|
-
* Windows : %LOCALAPPDATA%\Programs\DeepSeek Harness\(electron-builder 默认按用户安装),
|
|
16
|
-
* 以及 %LOCALAPPDATA% / %ProgramFiles% / %ProgramFiles(x86)% 下的 DeepSeek Harness;
|
|
17
|
-
* 找不到时查注册表 Uninstall 项的 InstallLocation / DisplayIcon 兜底。
|
|
18
|
-
* app.asar = <安装目录>\resources\app.asar
|
|
19
|
-
* Linux : /opt/DeepSeek Harness 等常见路径;app.asar = <安装目录>/resources/app.asar
|
|
20
|
-
*
|
|
21
|
-
* 用法:
|
|
22
|
-
* node dsh/build-template.js # 自动探测(可加 --app / --asar 显式覆盖)
|
|
23
|
-
* node dsh/build-template.js --asar <app.asar 路径>
|
|
24
|
-
* node dsh/build-template.js --app <安装目录或 .app>
|
|
25
|
-
* DSH_APP_PATH=/path node dsh/build-template.js # 环境变量覆盖
|
|
26
|
-
* DSH_ASAR_PATH=/path/app.asar node dsh/build-template.js
|
|
27
|
-
*
|
|
28
|
-
* 何时运行:DeepSeek Harness 升级后(官方 standard 预设的插件行可能变化)在本机重跑一次,
|
|
29
|
-
* 再把新的 dsh/preset.template.yml 提交。普通安装不需要运行本脚本。
|
|
30
|
-
*/
|
|
31
|
-
|
|
32
|
-
const fs = require('fs');
|
|
33
|
-
const path = require('path');
|
|
34
|
-
const os = require('os');
|
|
35
|
-
const { spawnSync } = require('child_process');
|
|
36
|
-
|
|
37
|
-
const ROOT = path.join(__dirname, '..');
|
|
38
|
-
const OUT = path.join(__dirname, 'preset.template.yml');
|
|
39
|
-
const PLACEHOLDER = ' __PERSONA__';
|
|
40
|
-
const STANDARD_RELS = [
|
|
41
|
-
'dsh/node_modules/@deepseek-ai/dsh-web-app/presets/standard.patch.yml',
|
|
42
|
-
'node_modules/@deepseek-ai/dsh-web-app/presets/standard.patch.yml',
|
|
43
|
-
];
|
|
44
|
-
const DSH_VERSION_REL = ['dsh/package.json', 'package.json'];
|
|
45
|
-
|
|
46
|
-
/* ---------------------------------------------------------------- 定位安装目录 */
|
|
47
|
-
|
|
48
|
-
function candidateAppDirs() {
|
|
49
|
-
const H = os.homedir();
|
|
50
|
-
const list = [];
|
|
51
|
-
if (process.platform === 'darwin') {
|
|
52
|
-
list.push('/Applications/DeepSeek Harness.app');
|
|
53
|
-
list.push(path.join(H, 'Applications', 'DeepSeek Harness.app'));
|
|
54
|
-
} else if (process.platform === 'win32') {
|
|
55
|
-
const local = process.env.LOCALAPPDATA || path.join(H, 'AppData', 'Local');
|
|
56
|
-
const pf = process.env.ProgramFiles || 'C:\\Program Files';
|
|
57
|
-
const pf86 = process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)';
|
|
58
|
-
list.push(path.join(local, 'Programs', 'DeepSeek Harness'));
|
|
59
|
-
list.push(path.join(local, 'DeepSeek Harness'));
|
|
60
|
-
list.push(path.join(pf, 'DeepSeek Harness'));
|
|
61
|
-
list.push(path.join(pf86, 'DeepSeek Harness'));
|
|
62
|
-
} else {
|
|
63
|
-
list.push('/opt/DeepSeek Harness');
|
|
64
|
-
list.push('/opt/deepseek-harness');
|
|
65
|
-
list.push('/usr/lib/deepseek-harness');
|
|
66
|
-
list.push('/usr/share/deepseek-harness');
|
|
67
|
-
list.push(path.join(H, '.local', 'share', 'deepseek-harness'));
|
|
68
|
-
}
|
|
69
|
-
return list;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// 显式输入(--app / --asar / 环境变量)→ { asar, app }
|
|
73
|
-
function normalizeInput(input) {
|
|
74
|
-
if (!input) return null;
|
|
75
|
-
const abs = path.resolve(input);
|
|
76
|
-
let st;
|
|
77
|
-
try { st = fs.statSync(abs); } catch (e) { return null; }
|
|
78
|
-
if (st.isFile() && abs.endsWith('.asar')) {
|
|
79
|
-
const isMacBundle = abs.includes(`${path.sep}Contents${path.sep}`);
|
|
80
|
-
const app = isMacBundle
|
|
81
|
-
? path.resolve(abs, '..', '..', '..') // *.app
|
|
82
|
-
: path.resolve(abs, '..', '..'); // <app>/resources/app.asar
|
|
83
|
-
return { asar: abs, app: fs.existsSync(app) ? app : null, input: abs };
|
|
84
|
-
}
|
|
85
|
-
if (st.isDirectory()) {
|
|
86
|
-
const cands = [
|
|
87
|
-
path.join(abs, 'Contents', 'Resources', 'app.asar'),
|
|
88
|
-
path.join(abs, 'resources', 'app.asar'),
|
|
89
|
-
path.join(abs, 'app.asar'),
|
|
90
|
-
];
|
|
91
|
-
for (const asar of cands) {
|
|
92
|
-
if (fs.existsSync(asar)) return { asar, app: abs, input: abs };
|
|
93
|
-
}
|
|
94
|
-
// 开发/免打包目录:<app>/resources/app/...
|
|
95
|
-
const dev = path.join(abs, 'resources', 'app');
|
|
96
|
-
if (fs.existsSync(dev)) return { asar: null, app: abs, devDir: dev, input: abs };
|
|
97
|
-
}
|
|
98
|
-
return null;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function mdfindAppDir() {
|
|
102
|
-
if (process.platform !== 'darwin') return null;
|
|
103
|
-
try {
|
|
104
|
-
const r = spawnSync('mdfind', ['kMDItemCFBundleIdentifier == "com.deepseek.dsh"'], { encoding: 'utf8', timeout: 15000 });
|
|
105
|
-
const hit = String(r.stdout || '').split('\n').map((s) => s.trim()).filter(Boolean)[0];
|
|
106
|
-
return hit || null;
|
|
107
|
-
} catch (e) { return null; }
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function registryAppDir() {
|
|
111
|
-
if (process.platform !== 'win32') return null;
|
|
112
|
-
const roots = [
|
|
113
|
-
'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall',
|
|
114
|
-
'HKLM\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall',
|
|
115
|
-
'HKLM\\Software\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall',
|
|
116
|
-
];
|
|
117
|
-
for (const root of roots) {
|
|
118
|
-
let r;
|
|
119
|
-
try {
|
|
120
|
-
r = spawnSync('reg', ['query', root, '/s', '/f', 'DeepSeek Harness', '/d'], { encoding: 'utf8', timeout: 20000 });
|
|
121
|
-
} catch (e) { continue; }
|
|
122
|
-
const out = String(r.stdout || '');
|
|
123
|
-
const loc = out.match(/InstallLocation\s+REG_[A-Z_]+\s+(.+)/i);
|
|
124
|
-
if (loc) {
|
|
125
|
-
const p = loc[1].trim().replace(/^"|"$/g, '');
|
|
126
|
-
if (fs.existsSync(p)) return p;
|
|
127
|
-
}
|
|
128
|
-
const icon = out.match(/DisplayIcon\s+REG_[A-Z_]+\s+"?([^"\r\n,]+\.exe)/i);
|
|
129
|
-
if (icon && fs.existsSync(icon[1])) return path.dirname(icon[1].trim());
|
|
130
|
-
}
|
|
131
|
-
return null;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function locate() {
|
|
135
|
-
const tried = [];
|
|
136
|
-
const push = (v) => { if (v) tried.push(v); };
|
|
137
|
-
|
|
138
|
-
// 1) 环境变量 / 显式参数
|
|
139
|
-
for (const envName of ['DSH_ASAR_PATH', 'DSH_APP_PATH']) {
|
|
140
|
-
const v = process.env[envName];
|
|
141
|
-
if (!v) continue;
|
|
142
|
-
const got = normalizeInput(v);
|
|
143
|
-
if (got) return { ...got, source: envName };
|
|
144
|
-
push(`${envName}=${v}`);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// 2) 常见安装目录
|
|
148
|
-
for (const dir of candidateAppDirs()) {
|
|
149
|
-
push(dir);
|
|
150
|
-
const got = normalizeInput(dir);
|
|
151
|
-
if (got) return { ...got, source: 'candidate' };
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// 3) 系统兜底:macOS Spotlight / Windows 注册表
|
|
155
|
-
const sys = process.platform === 'darwin' ? mdfindAppDir() : registryAppDir();
|
|
156
|
-
if (sys) {
|
|
157
|
-
push(sys);
|
|
158
|
-
const got = normalizeInput(sys);
|
|
159
|
-
if (got) return { ...got, source: process.platform === 'darwin' ? 'mdfind' : 'registry' };
|
|
160
|
-
}
|
|
161
|
-
return { failed: true, tried };
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/* ---------------------------------------------------------------- 读 app.asar */
|
|
165
|
-
|
|
166
|
-
// 纯 Node 读取 asar 内文件(不依赖 Electron;asm 格式:8B 前置 + header pickle + 数据区)
|
|
167
|
-
function readAsarFile(asarPath, relPath) {
|
|
168
|
-
const fd = fs.openSync(asarPath, 'r');
|
|
169
|
-
try {
|
|
170
|
-
const fstat = fs.fstatSync(fd);
|
|
171
|
-
const pre = Buffer.alloc(8);
|
|
172
|
-
if (fs.readSync(fd, pre, 0, 8, 0) !== 8) throw new Error('读取 asar 前置头失败');
|
|
173
|
-
const headerSize = pre.readUInt32LE(4);
|
|
174
|
-
if (!headerSize || headerSize > 64 * 1024 * 1024) throw new Error(`asar 头部大小异常: ${headerSize}`);
|
|
175
|
-
const hp = Buffer.alloc(headerSize);
|
|
176
|
-
if (fs.readSync(fd, hp, 0, headerSize, 8) !== headerSize) throw new Error('asar 头部被截断');
|
|
177
|
-
const jsonLen = hp.readUInt32LE(4);
|
|
178
|
-
let header;
|
|
179
|
-
try {
|
|
180
|
-
header = JSON.parse(hp.subarray(8, 8 + jsonLen).toString('utf8'));
|
|
181
|
-
} catch (e) {
|
|
182
|
-
throw new Error(`asar 头部不是合法 JSON(文件可能损坏或格式已变化): ${e.message}`);
|
|
183
|
-
}
|
|
184
|
-
const dataOffset = 8 + headerSize;
|
|
185
|
-
|
|
186
|
-
let node = header;
|
|
187
|
-
for (const seg of String(relPath).split('/').filter(Boolean)) {
|
|
188
|
-
node = node && node.files && node.files[seg];
|
|
189
|
-
if (!node) return null;
|
|
190
|
-
}
|
|
191
|
-
if (node.files) throw new Error(`${relPath} 是目录`);
|
|
192
|
-
if (node.unpacked) throw new Error(`${relPath} 位于 app.asar.unpacked,请检查 DSH 安装完整性`);
|
|
193
|
-
const off = dataOffset + Number(node.offset);
|
|
194
|
-
const size = Number(node.size);
|
|
195
|
-
if (!Number.isFinite(off) || !Number.isFinite(size) || size < 0 || off + size > fstat.size) {
|
|
196
|
-
throw new Error(`${relPath} 的 asar 条目偏移异常`);
|
|
197
|
-
}
|
|
198
|
-
const buf = Buffer.alloc(size);
|
|
199
|
-
if (fs.readSync(fd, buf, 0, size, off) !== size) throw new Error(`${relPath} 数据被截断`);
|
|
200
|
-
return buf.toString('utf8');
|
|
201
|
-
} finally {
|
|
202
|
-
fs.closeSync(fd);
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
function readAppFile(loc, relPath) {
|
|
207
|
-
if (loc.asar) return readAsarFile(loc.asar, relPath);
|
|
208
|
-
const p = path.join(loc.devDir, relPath);
|
|
209
|
-
return fs.existsSync(p) ? fs.readFileSync(p, 'utf8') : null;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
function readStandardPatch(loc) {
|
|
213
|
-
const errors = [];
|
|
214
|
-
for (const rel of STANDARD_RELS) {
|
|
215
|
-
try {
|
|
216
|
-
const text = readAppFile(loc, rel);
|
|
217
|
-
if (text) return { text, rel };
|
|
218
|
-
} catch (e) { errors.push(`${rel}: ${e.message}`); }
|
|
219
|
-
}
|
|
220
|
-
throw new Error(`在 DSH 安装内找不到 standard.patch.yml\n ${errors.join('\n ')}`);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
function readDshVersion(loc) {
|
|
224
|
-
for (const rel of DSH_VERSION_REL) {
|
|
225
|
-
try {
|
|
226
|
-
const text = readAppFile(loc, rel);
|
|
227
|
-
if (!text) continue;
|
|
228
|
-
const pkg = JSON.parse(text);
|
|
229
|
-
if (pkg && pkg.version) return `${pkg.name || 'dsh'}@${pkg.version}`;
|
|
230
|
-
} catch (e) { /* 忽略 */ }
|
|
231
|
-
}
|
|
232
|
-
return 'unknown';
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
/* ---------------------------------------------------------------- 生成模板 */
|
|
236
|
-
|
|
237
|
-
function build(standard) {
|
|
238
|
-
if (!standard.includes('preset-standard')) throw new Error('standard.patch.yml 不含 preset-standard(DSH 版本可能已变化)');
|
|
239
|
-
let text = standard.replace(' - id: preset-standard', ' - id: preset-ck');
|
|
240
|
-
if (!text.includes(' - id: preset-ck')) throw new Error('预设行重命名失败(standard.patch.yml 格式变化)');
|
|
241
|
-
|
|
242
|
-
const idLine = ' id: standard\n order: 1\n';
|
|
243
|
-
if (!text.includes(idLine)) throw new Error('standard.patch.yml 预设元数据格式变化(id/order 段)');
|
|
244
|
-
const meta = [
|
|
245
|
-
' id: ck',
|
|
246
|
-
" name: 'Ck 万能执行员'",
|
|
247
|
-
" description: '万能执行员:渗透 / 逆向 / Pwn / 协议 / 密码 / 移动 / 取证 / 内存 / 漏洞研究 / 红队 / 自动化 / 开发 / 数据 / 调研,点啥干啥。'",
|
|
248
|
-
' order: 0',
|
|
249
|
-
'',
|
|
250
|
-
].join('\n');
|
|
251
|
-
text = text.replace(idLine, meta);
|
|
252
|
-
|
|
253
|
-
const personaLine = ' prefix: You are a coding agent powered by the {{model}} model.';
|
|
254
|
-
if (!text.includes(personaLine)) throw new Error('standard.patch.yml persona 行格式变化(prefix 单行)');
|
|
255
|
-
text = text.replace(
|
|
256
|
-
" - id: persona\n name: '@deepseek-ai/dsh-persona'\n config:\n" + personaLine,
|
|
257
|
-
" - id: persona\n name: '@deepseek-ai/dsh-persona'\n config:\n prefix: |\n" + PLACEHOLDER
|
|
258
|
-
);
|
|
259
|
-
|
|
260
|
-
// 去掉官方文件头注释,换成仓库口径
|
|
261
|
-
const lines = text.split('\n');
|
|
262
|
-
while (lines.length && lines[0].startsWith('#')) lines.shift();
|
|
263
|
-
const header = [
|
|
264
|
-
'# Ck 万能执行员 — DeepSeek Harness Agent 预设声明(agent-bypass · dsh/)',
|
|
265
|
-
'# 由 dsh/build-template.js 从本机 DSH 官方 standard 预设派生:插件/工具列表与 standard 逐行一致,',
|
|
266
|
-
'# 仅替换 persona 与预设元数据。安装时 npx agent-bypass --dsh 把 __PERSONA__ 替换为 dsh/persona.md 的正文。',
|
|
267
|
-
'# DSH 升级后请在本机重跑:node dsh/build-template.js',
|
|
268
|
-
'',
|
|
269
|
-
].join('\n');
|
|
270
|
-
return header + lines.join('\n');
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
/* ---------------------------------------------------------------- 主流程 */
|
|
274
|
-
|
|
275
|
-
function main() {
|
|
276
|
-
const args = process.argv.slice(2);
|
|
277
|
-
const getArg = (name) => { const i = args.indexOf(name); return i >= 0 ? args[i + 1] : null; };
|
|
278
|
-
const explicit = getArg('--asar') || getArg('--app');
|
|
279
|
-
if (explicit) process.env[String(explicit).endsWith('.asar') ? 'DSH_ASAR_PATH' : 'DSH_APP_PATH'] = explicit;
|
|
280
|
-
|
|
281
|
-
const loc = locate();
|
|
282
|
-
if (loc.failed) {
|
|
283
|
-
console.error('✗ 未找到 DeepSeek Harness 安装目录。已探测:');
|
|
284
|
-
for (const t of loc.tried || []) console.error(` · ${t}`);
|
|
285
|
-
console.error('可显式指定:');
|
|
286
|
-
console.error(' node dsh/build-template.js --app "/Applications/DeepSeek Harness.app"');
|
|
287
|
-
console.error(' node dsh/build-template.js --asar "<app.asar 路径>"');
|
|
288
|
-
console.error(' DSH_APP_PATH / DSH_ASAR_PATH 环境变量同样可用');
|
|
289
|
-
process.exit(1);
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
const standard = readStandardPatch(loc).text;
|
|
293
|
-
const version = readDshVersion(loc);
|
|
294
|
-
const persona = fs.readFileSync(path.join(__dirname, 'persona.md'), 'utf8').replace(/\r\n/g, '\n').trimEnd();
|
|
295
|
-
if (!persona) throw new Error('dsh/persona.md 为空');
|
|
296
|
-
if (/\{\{/.test(persona)) throw new Error('dsh/persona.md 不允许包含 {{…}} 模板变量(persona 是严格模板)');
|
|
297
|
-
|
|
298
|
-
const out = build(standard);
|
|
299
|
-
if (!out.includes(PLACEHOLDER)) throw new Error('生成结果缺少 persona 占位符');
|
|
300
|
-
if (!out.includes(' - id: preset-ck')) throw new Error('生成结果缺少 preset-ck 声明');
|
|
301
|
-
if (/\{\{/.test(out)) throw new Error('生成结果仍含 {{…}}(官方 persona 行可能未替换成功)');
|
|
302
|
-
|
|
303
|
-
fs.writeFileSync(OUT, out, 'utf8');
|
|
304
|
-
console.log(`✓ 模板已生成:${path.relative(ROOT, OUT)}(${Buffer.byteLength(out)} 字节)`);
|
|
305
|
-
console.log(` DSH 版本:${version}`);
|
|
306
|
-
console.log(` 安装目录:${loc.app || loc.input || loc.source}`);
|
|
307
|
-
console.log(` 读取来源:${loc.asar ? 'app.asar' : '未打包目录'}${loc.source ? `(${loc.source})` : ''}`);
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
main();
|