forgexa-cli 1.59.2__tar.gz → 1.60.0__tar.gz

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.
Files changed (29) hide show
  1. {forgexa_cli-1.59.2 → forgexa_cli-1.60.0}/PKG-INFO +2 -2
  2. {forgexa_cli-1.59.2 → forgexa_cli-1.60.0}/README.md +1 -1
  3. {forgexa_cli-1.59.2 → forgexa_cli-1.60.0}/forgexa_cli/__init__.py +1 -1
  4. {forgexa_cli-1.59.2 → forgexa_cli-1.60.0}/forgexa_cli/agent_core.py +604 -2
  5. {forgexa_cli-1.59.2 → forgexa_cli-1.60.0}/forgexa_cli/daemon.py +40 -5
  6. {forgexa_cli-1.59.2 → forgexa_cli-1.60.0}/forgexa_cli/main.py +16 -1
  7. {forgexa_cli-1.59.2 → forgexa_cli-1.60.0}/forgexa_cli.egg-info/PKG-INFO +2 -2
  8. {forgexa_cli-1.59.2 → forgexa_cli-1.60.0}/pyproject.toml +1 -1
  9. {forgexa_cli-1.59.2 → forgexa_cli-1.60.0}/tests/test_check_command.py +14 -0
  10. {forgexa_cli-1.59.2 → forgexa_cli-1.60.0}/forgexa_cli/_build_config.py +0 -0
  11. {forgexa_cli-1.59.2 → forgexa_cli-1.60.0}/forgexa_cli/_local_bind.py +0 -0
  12. {forgexa_cli-1.59.2 → forgexa_cli-1.60.0}/forgexa_cli/autoupgrade.py +0 -0
  13. {forgexa_cli-1.59.2 → forgexa_cli-1.60.0}/forgexa_cli/py.typed +0 -0
  14. {forgexa_cli-1.59.2 → forgexa_cli-1.60.0}/forgexa_cli/runtime_evidence.py +0 -0
  15. {forgexa_cli-1.59.2 → forgexa_cli-1.60.0}/forgexa_cli/zcode_protocol.py +0 -0
  16. {forgexa_cli-1.59.2 → forgexa_cli-1.60.0}/forgexa_cli.egg-info/SOURCES.txt +0 -0
  17. {forgexa_cli-1.59.2 → forgexa_cli-1.60.0}/forgexa_cli.egg-info/dependency_links.txt +0 -0
  18. {forgexa_cli-1.59.2 → forgexa_cli-1.60.0}/forgexa_cli.egg-info/entry_points.txt +0 -0
  19. {forgexa_cli-1.59.2 → forgexa_cli-1.60.0}/forgexa_cli.egg-info/requires.txt +0 -0
  20. {forgexa_cli-1.59.2 → forgexa_cli-1.60.0}/forgexa_cli.egg-info/top_level.txt +0 -0
  21. {forgexa_cli-1.59.2 → forgexa_cli-1.60.0}/setup.cfg +0 -0
  22. {forgexa_cli-1.59.2 → forgexa_cli-1.60.0}/tests/test_auth_and_runtime_commands.py +0 -0
  23. {forgexa_cli-1.59.2 → forgexa_cli-1.60.0}/tests/test_autoupgrade.py +0 -0
  24. {forgexa_cli-1.59.2 → forgexa_cli-1.60.0}/tests/test_expiry_warnings_and_revoke.py +0 -0
  25. {forgexa_cli-1.59.2 → forgexa_cli-1.60.0}/tests/test_local_bind_commands.py +0 -0
  26. {forgexa_cli-1.59.2 → forgexa_cli-1.60.0}/tests/test_runtime_credentials.py +0 -0
  27. {forgexa_cli-1.59.2 → forgexa_cli-1.60.0}/tests/test_session_credentials.py +0 -0
  28. {forgexa_cli-1.59.2 → forgexa_cli-1.60.0}/tests/test_silent_install.py +0 -0
  29. {forgexa_cli-1.59.2 → forgexa_cli-1.60.0}/tests/test_upgrade_observability.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forgexa-cli
3
- Version: 1.59.2
3
+ Version: 1.60.0
4
4
  Summary: Forgexa CLI — command-line client and AI agent runtime for the Forgexa platform
5
5
  Author-email: Jason Sun <dev.winds@gmail.com>
6
6
  License-Expression: MIT
@@ -35,7 +35,7 @@ Requires-Dist: socksio>=1.0.0; extra == "daemon"
35
35
  Command-line client and agent runtime for the [Forgexa](https://forgexa.net) platform.
36
36
 
37
37
  Communicates with the Forgexa server via REST API.
38
- Includes a built-in **daemon** that discovers twelve local AI agent integrations (Claude Code, Codex, GitHub Copilot, Gemini CLI, OpenCode, Kimi Code, MiMo Code, Qwen Code, CodeBuddy, Qoder CLI, TraeCode CLI, and ZCode) and executes tasks on behalf of the server. MiMo Code (`mimo`, v0.1.14+), Qwen Code (`qwen`, v0.24.1+), CodeBuddy (`codebuddy`, v2.156.0+), Qoder CLI (`qoder`, v1.1.59+), TraeCode CLI (`trae`), and ZCode (`zcode`) are selected explicitly; automatic routing and generated default fallback chains exclude all six. CodeBuddy and Qoder completed their local AgentHost start/resume acceptance gates. Authentication, endpoint, and model selection remain in each local CLI profile; Forgexa never stores or overrides them.
38
+ Includes a built-in **daemon** with thirteen integrations: Claude Code, Codex, GitHub Copilot, Gemini CLI, OpenCode, Kimi Code, MiMo Code, Qwen Code, CodeBuddy, Qoder CLI, TraeCode CLI, ZCode, and DeepSeek Harness. Six standard candidates are automatically routed; seven require explicit selection. Harness (`deepseek-harness`) is a developer preview for personal Runtimes/Desktop, pinned to 0.2.0-rc.2 and Node 22.19+ within 22.x or 24+. It owns a managed runtime profile with read-only external credentials, without copying personal settings. Shared pools/native Windows are blocked. After exact-build live acceptance, `forgexa agent approve-live deepseek-harness` records an attestation; `forgexa check --agent deepseek-harness --quick` avoids model calls, while normal `check` runs a potentially billable minimal task. See [global/local installation and limits](../documentation/guide/agent-installation.md#deepseek-harness-developer-preview).
39
39
 
40
40
  ## Installation
41
41
 
@@ -3,7 +3,7 @@
3
3
  Command-line client and agent runtime for the [Forgexa](https://forgexa.net) platform.
4
4
 
5
5
  Communicates with the Forgexa server via REST API.
6
- Includes a built-in **daemon** that discovers twelve local AI agent integrations (Claude Code, Codex, GitHub Copilot, Gemini CLI, OpenCode, Kimi Code, MiMo Code, Qwen Code, CodeBuddy, Qoder CLI, TraeCode CLI, and ZCode) and executes tasks on behalf of the server. MiMo Code (`mimo`, v0.1.14+), Qwen Code (`qwen`, v0.24.1+), CodeBuddy (`codebuddy`, v2.156.0+), Qoder CLI (`qoder`, v1.1.59+), TraeCode CLI (`trae`), and ZCode (`zcode`) are selected explicitly; automatic routing and generated default fallback chains exclude all six. CodeBuddy and Qoder completed their local AgentHost start/resume acceptance gates. Authentication, endpoint, and model selection remain in each local CLI profile; Forgexa never stores or overrides them.
6
+ Includes a built-in **daemon** with thirteen integrations: Claude Code, Codex, GitHub Copilot, Gemini CLI, OpenCode, Kimi Code, MiMo Code, Qwen Code, CodeBuddy, Qoder CLI, TraeCode CLI, ZCode, and DeepSeek Harness. Six standard candidates are automatically routed; seven require explicit selection. Harness (`deepseek-harness`) is a developer preview for personal Runtimes/Desktop, pinned to 0.2.0-rc.2 and Node 22.19+ within 22.x or 24+. It owns a managed runtime profile with read-only external credentials, without copying personal settings. Shared pools/native Windows are blocked. After exact-build live acceptance, `forgexa agent approve-live deepseek-harness` records an attestation; `forgexa check --agent deepseek-harness --quick` avoids model calls, while normal `check` runs a potentially billable minimal task. See [global/local installation and limits](../documentation/guide/agent-installation.md#deepseek-harness-developer-preview).
7
7
 
8
8
  ## Installation
9
9
 
@@ -1,2 +1,2 @@
1
1
  """forgexa-cli — Forgexa command-line client."""
2
- __version__ = "1.59.2"
2
+ __version__ = "1.60.0"
@@ -46,12 +46,14 @@ import hashlib
46
46
  import json
47
47
  import logging
48
48
  import os
49
+ import platform
49
50
  import re
50
51
  import shutil
51
52
  import subprocess
52
53
  import tempfile
53
54
  import time
54
- from dataclasses import dataclass
55
+ from contextlib import contextmanager
56
+ from dataclasses import dataclass, field
55
57
  from pathlib import Path
56
58
  from typing import Any
57
59
 
@@ -582,6 +584,17 @@ class AgentDiscovery:
582
584
  ),
583
585
  "upgrade_hint": "mimo upgrade",
584
586
  },
587
+ "deepseek-harness": {
588
+ "commands": ["dsh"],
589
+ "detect": "dsh --version",
590
+ "invoke_modes": ["cli"],
591
+ "env_path_override": "FACTORY_DEEPSEEK_HARNESS_PATH",
592
+ "env_model_override": "FACTORY_DEEPSEEK_HARNESS_MODEL",
593
+ "compatibility_level": "L2",
594
+ "min_version": (0, 2, 0),
595
+ "strict_min_version": True,
596
+ "upgrade_hint": "npm i -g @deepseek-ai/dsh@0.2.0-rc.2",
597
+ },
585
598
  "qwen": {
586
599
  "commands": ["qwen"],
587
600
  "detect": "qwen --version",
@@ -872,7 +885,7 @@ class AgentDiscovery:
872
885
  )
873
886
  continue
874
887
  min_version_ok, version_warning = _check_agent_min_version(agent_id, version, spec)
875
- verification_issue = _agent_discovery_verification_issue(agent_id, resolved)
888
+ verification_issue = await asyncio.to_thread(_agent_discovery_verification_issue, agent_id, resolved)
876
889
  if verification_issue:
877
890
  min_version_ok = False
878
891
  version_warning = verification_issue
@@ -916,6 +929,8 @@ class AgentDiscovery:
916
929
  parts[0] = resolved
917
930
  parts = _prepare_agent_command(parts, preserve_argv=False)
918
931
  env = None
932
+ if detect_cmd == "dsh --version":
933
+ env = deepseek_harness_probe_env()
919
934
  if detect_cmd in ("opencode --version", "mimo --version"):
920
935
  probe_dir = tempfile.TemporaryDirectory(prefix=f"forgexa-{detect_cmd.split()[0]}-probe-")
921
936
  env = os.environ.copy()
@@ -2111,6 +2126,591 @@ def build_mimo_command(
2111
2126
  return cmd
2112
2127
 
2113
2128
 
2129
+ DEEPSEEK_HARNESS_VERSION = "0.2.0-rc.2"
2130
+ DEEPSEEK_HARNESS_MAX_OUTPUT_BYTES = 4 * 1024 * 1024
2131
+ DEEPSEEK_HARNESS_APPROVAL_FILE = Path.home() / ".forgexa" / "deepseek-harness-approved.json"
2132
+
2133
+
2134
+ def _reject_nonfinite_json(value: str) -> None:
2135
+ raise ValueError("Non-finite JSON number")
2136
+
2137
+
2138
+ @dataclass
2139
+ class DeepSeekHarnessRunState:
2140
+ """Validate one headless invocation independently of retained log tails."""
2141
+
2142
+ expected_cwd: str | None = None
2143
+ session_id: str | None = None
2144
+ text: str = ""
2145
+ reason: dict | None = None
2146
+ error: str | None = None
2147
+ failure_code: str | None = None
2148
+ started: bool = False
2149
+ saw_final: bool = False
2150
+ usage_incomplete: bool = False
2151
+ usage_steps: dict = field(default_factory=dict)
2152
+
2153
+ def fail(self, message: str, code: str = "deepseek_harness_protocol_error") -> None:
2154
+ if self.error is None:
2155
+ self.error = message[:2000]
2156
+ self.failure_code = code
2157
+
2158
+ def feed_line(self, line: str) -> dict | None:
2159
+ if len(line.encode("utf-8")) > DEEPSEEK_HARNESS_MAX_OUTPUT_BYTES:
2160
+ self.fail("Harness output exceeds the allowed size", "deepseek_harness_output_limit")
2161
+ return None
2162
+ try:
2163
+ event = json.loads(line, parse_constant=_reject_nonfinite_json)
2164
+ except (ValueError, RecursionError):
2165
+ self.fail("Harness stdout must contain valid JSONL events")
2166
+ return None
2167
+ if not isinstance(event, dict):
2168
+ self.fail("Harness event must be an object")
2169
+ return None
2170
+ self.feed(event)
2171
+ return event
2172
+
2173
+ def feed(self, event: dict) -> None:
2174
+ event_type = event.get("type")
2175
+ if self.saw_final:
2176
+ self.fail("Harness emitted data after its final event")
2177
+ return
2178
+ if event_type == "error":
2179
+ message = event.get("message")
2180
+ if not isinstance(message, str):
2181
+ self.fail("Harness error event is missing its message")
2182
+ return
2183
+ missing = not self.started and re.search(
2184
+ r'^session ".+" does not exist; omit --session-id', message,
2185
+ )
2186
+ self.fail(message, "native_session_not_found" if missing else "deepseek_harness_error")
2187
+ return
2188
+ if event_type == "session":
2189
+ session_id, cwd = event.get("sessionId"), event.get("cwd")
2190
+ if (
2191
+ self.session_id is not None or event.get("truncated")
2192
+ or not isinstance(session_id, str) or not session_id.strip()
2193
+ or len(session_id) > 256 or "\0" in session_id
2194
+ or not isinstance(cwd, str) or not cwd or "\0" in cwd
2195
+ ):
2196
+ self.fail("Harness session identity is missing, duplicated or truncated")
2197
+ return
2198
+ if self.expected_cwd and Path(cwd).resolve() != Path(self.expected_cwd).resolve():
2199
+ self.fail("Harness session belongs to another workspace", "deepseek_harness_session_incompatible")
2200
+ return
2201
+ self.session_id = session_id
2202
+ return
2203
+ if self.session_id is None:
2204
+ self.fail("Harness output did not begin with a session event")
2205
+ return
2206
+ if event_type == "status":
2207
+ phase = event.get("phase")
2208
+ if phase not in {"turn_start", "step_start", "step_end", "turn_end"}:
2209
+ self.fail("Harness emitted an unsupported status phase")
2210
+ elif phase == "turn_start":
2211
+ if self.started:
2212
+ self.fail("Harness emitted more than one turn")
2213
+ self.started = True
2214
+ elif not self.started or self.reason is not None:
2215
+ self.fail("Harness emitted an out-of-order turn status")
2216
+ elif phase == "turn_end":
2217
+ reason = event.get("reason")
2218
+ if (
2219
+ not isinstance(reason, dict) or not isinstance(reason.get("kind"), str)
2220
+ or ("error" in reason and not isinstance(reason["error"], dict))
2221
+ or event.get("truncated")
2222
+ ):
2223
+ self.fail("Harness terminal reason is missing or truncated")
2224
+ else:
2225
+ self.reason = reason
2226
+ elif phase == "step_end" and "usage" in event:
2227
+ usage = event["usage"]
2228
+ if (
2229
+ not isinstance(usage, dict)
2230
+ or any(not isinstance(usage.get(key), int) or isinstance(usage.get(key), bool) or usage[key] < 0 for key in ("inputTokens", "outputTokens"))
2231
+ or any(not isinstance(value, int) or isinstance(value, bool) or value < 0 for value in usage.values())
2232
+ ):
2233
+ self.fail("Harness usage sample is invalid")
2234
+ else:
2235
+ self.usage_steps[(str(event.get("turn")), str(event.get("step")))] = usage
2236
+ elif phase == "step_end":
2237
+ self.usage_incomplete = True
2238
+ return
2239
+ if event_type == "final":
2240
+ final_text = event.get("text")
2241
+ if not isinstance(final_text, str) or self.reason is None or event.get("truncated"):
2242
+ self.fail("Harness final answer is missing or arrived before its terminal reason")
2243
+ elif len(final_text.encode("utf-8")) > DEEPSEEK_HARNESS_MAX_OUTPUT_BYTES:
2244
+ self.fail("Harness final answer exceeds the allowed size", "deepseek_harness_output_limit")
2245
+ else:
2246
+ self.text = final_text
2247
+ self.saw_final = True
2248
+ return
2249
+ if event_type in {"text", "thinking"}:
2250
+ if not isinstance(event.get("text"), str) and not event.get("truncated"):
2251
+ self.fail("Harness text event is missing its text")
2252
+ elif event_type in {"tool_call", "tool_result"}:
2253
+ call_id = event.get("callId")
2254
+ if not isinstance(call_id, str) or not call_id.strip() or len(call_id) > 256 or "\0" in call_id:
2255
+ self.fail("Harness tool event is missing a valid native callId")
2256
+ elif event_type == "tool_call" and not isinstance(event.get("tool"), str):
2257
+ self.fail("Harness tool call is missing its tool name")
2258
+ elif event_type == "tool_result" and event.get("status") not in {"completed", "error"}:
2259
+ self.fail("Harness tool result has an unsupported status")
2260
+ else:
2261
+ self.fail("Harness emitted an unsupported event type")
2262
+
2263
+ def finish(self, exit_code: int, *, cancelled: bool = False) -> dict:
2264
+ if not cancelled and self.error is None:
2265
+ if not self.started or self.reason is None or not self.saw_final:
2266
+ self.fail("Harness exited without a complete turn and final answer")
2267
+ elif self.reason.get("kind") != "completed":
2268
+ detail = self.reason.get("error") or {}
2269
+ self.fail(
2270
+ str(detail.get("message") or f"Harness turn ended: {self.reason['kind']}"),
2271
+ str(detail.get("code") or "deepseek_harness_turn_failed"),
2272
+ )
2273
+ elif exit_code != 0:
2274
+ self.fail(f"Harness exited with code {exit_code}", "agent_exit_code")
2275
+ metrics = {}
2276
+ for native, normalized in (
2277
+ ("inputTokens", "input_tokens"), ("outputTokens", "output_tokens"),
2278
+ ("cacheReadTokens", "cache_read_tokens"), ("cacheWriteTokens", "cache_write_tokens"),
2279
+ ("reasoningTokens", "reasoning_tokens"),
2280
+ ):
2281
+ if not self.usage_incomplete and self.usage_steps and all(native in sample for sample in self.usage_steps.values()):
2282
+ metrics[normalized] = sum(sample[native] for sample in self.usage_steps.values())
2283
+ return {
2284
+ "status": "cancelled" if cancelled else "failed" if self.error else "success",
2285
+ "text": self.text, "summary": self.text[-2000:],
2286
+ "session_id": self.session_id,
2287
+ "error": None if cancelled else self.error,
2288
+ "failure_code": None if cancelled else self.failure_code,
2289
+ "is_error": not cancelled and self.error is not None,
2290
+ "metrics": metrics,
2291
+ }
2292
+
2293
+
2294
+ def build_deepseek_harness_command(
2295
+ binary: str,
2296
+ *,
2297
+ overlay_path: str | Path,
2298
+ session_id: str | None = None,
2299
+ ) -> list[str]:
2300
+ """Build a headless JSONL invocation whose prompt arrives through stdin."""
2301
+ command = [
2302
+ binary, "--profile", "headless", "--patch", str(overlay_path), "--json",
2303
+ ]
2304
+ if session_id is not None:
2305
+ if not session_id.strip():
2306
+ raise ValueError("DeepSeek Harness session ID must not be blank")
2307
+ command += ["--session-id", session_id]
2308
+ return command
2309
+
2310
+
2311
+ def _deepseek_harness_package_roots(binary: str) -> list[Path]:
2312
+ resolved = Path(binary).resolve()
2313
+ roots = []
2314
+ for name in ("dsh", "dsh-headless", "dsh-base"):
2315
+ candidates = []
2316
+ for parent in (resolved.parent, *list(resolved.parents)[:6]):
2317
+ candidates.extend((parent, parent / "node_modules" / "@deepseek-ai" / name, parent / "@deepseek-ai" / name))
2318
+ for candidate in candidates:
2319
+ manifest = candidate / "package.json"
2320
+ try:
2321
+ metadata = json.loads(manifest.read_text(encoding="utf-8"))
2322
+ except (OSError, ValueError):
2323
+ continue
2324
+ if isinstance(metadata, dict) and metadata.get("name") == f"@deepseek-ai/{name}":
2325
+ if metadata.get("version") != DEEPSEEK_HARNESS_VERSION:
2326
+ raise RuntimeError(f"DeepSeek Harness requires the verified {DEEPSEEK_HARNESS_VERSION} package set")
2327
+ roots.append(candidate.resolve())
2328
+ break
2329
+ else:
2330
+ raise RuntimeError(f"Could not locate the installed @deepseek-ai/{name} package")
2331
+ return roots
2332
+
2333
+
2334
+ def deepseek_harness_probe_env() -> dict[str, str]:
2335
+ allowed = {"PATH", "HOME", "USERPROFILE", "SYSTEMROOT", "WINDIR", "COMSPEC", "PATHEXT", "TEMP", "TMP", "LANG", "LC_ALL"}
2336
+ return {key: value for key, value in os.environ.items() if key.upper() in allowed}
2337
+
2338
+
2339
+ def deepseek_harness_build_info(binary: str) -> dict:
2340
+ roots = _deepseek_harness_package_roots(binary)
2341
+ node = shutil.which("node")
2342
+ if not node:
2343
+ raise RuntimeError("DeepSeek Harness requires Node.js")
2344
+ result = subprocess.run([node, "-p", "process.versions.node"], capture_output=True, text=True, timeout=10, env=deepseek_harness_probe_env())
2345
+ node_version = result.stdout.strip()
2346
+ parsed = _parse_semver(node_version)
2347
+ if result.returncode or not re.fullmatch(r"\d+\.\d+\.\d+", node_version) or parsed is None or not ((parsed[0] == 22 and parsed >= (22, 19, 0)) or parsed[0] >= 24):
2348
+ raise RuntimeError("DeepSeek Harness requires Node 22.19+ or Node 24+")
2349
+ digest = hashlib.sha256()
2350
+ digest.update(f"{sys.platform}:{platform.machine()}:{platform.release()}:{node_version}:harness-policy-2".encode())
2351
+ extensions = [manifest.parent for manifest in (roots[0] / "node_modules" / "@deepseek-ai").glob("dsh*/package.json")]
2352
+ for root in sorted(set([*roots, *extensions])):
2353
+ files = [root / "package.json"] + sorted((root / "lib").rglob("*.js"))
2354
+ if (root / "cordis.patch.yml").is_file():
2355
+ files.append(root / "cordis.patch.yml")
2356
+ for filename in files:
2357
+ digest.update(str(filename.relative_to(root)).encode())
2358
+ digest.update(filename.read_bytes())
2359
+ return {"version": DEEPSEEK_HARNESS_VERSION, "fingerprint": digest.hexdigest(), "node_version": node_version, "package_root": str(roots[0])}
2360
+
2361
+
2362
+ def deepseek_harness_verification_issue(binary: str) -> str | None:
2363
+ try:
2364
+ information = deepseek_harness_build_info(binary)
2365
+ if sys.platform not in {"darwin", "linux"}:
2366
+ return "DeepSeek Harness outer isolation is not verified on this platform"
2367
+ required = "sandbox-exec" if sys.platform == "darwin" else "bwrap"
2368
+ if not shutil.which(required):
2369
+ return f"DeepSeek Harness requires the {required} outer sandbox"
2370
+ approved = json.loads(DEEPSEEK_HARNESS_APPROVAL_FILE.read_text(encoding="utf-8"))
2371
+ if (
2372
+ isinstance(approved, dict) and approved.get("version") == 1
2373
+ and isinstance(approved.get("builds"), list)
2374
+ and information["fingerprint"] in approved["builds"]
2375
+ ):
2376
+ return None
2377
+ except (OSError, ValueError, RuntimeError, subprocess.SubprocessError):
2378
+ pass
2379
+ return (
2380
+ "DeepSeek Harness requires live start/resume/cancel/isolation acceptance "
2381
+ "for this installed build; then run `forgexa agent approve-live deepseek-harness`"
2382
+ )
2383
+
2384
+
2385
+ def approve_deepseek_harness_build(binary: str) -> dict:
2386
+ information = deepseek_harness_build_info(binary)
2387
+ DEEPSEEK_HARNESS_APPROVAL_FILE.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
2388
+ document = {"version": 1, "builds": [information["fingerprint"]], "runtime_version": information["version"]}
2389
+ with tempfile.NamedTemporaryFile("w", encoding="utf-8", dir=DEEPSEEK_HARNESS_APPROVAL_FILE.parent, delete=False) as temporary:
2390
+ json.dump(document, temporary)
2391
+ temporary_path = Path(temporary.name)
2392
+ try:
2393
+ if sys.platform != "win32":
2394
+ temporary_path.chmod(0o600)
2395
+ os.replace(temporary_path, DEEPSEEK_HARNESS_APPROVAL_FILE)
2396
+ finally:
2397
+ temporary_path.unlink(missing_ok=True)
2398
+ return information
2399
+
2400
+
2401
+ def deepseek_harness_base_env(env: dict[str, str], *, home: str | Path, temporary_root: str | Path) -> dict[str, str]:
2402
+ allowed = {
2403
+ "PATH", "HOME", "USER", "LOGNAME", "USERPROFILE", "HOMEDRIVE", "HOMEPATH",
2404
+ "SYSTEMROOT", "WINDIR", "COMSPEC", "APPDATA", "LOCALAPPDATA", "LANG", "LC_ALL",
2405
+ "LC_CTYPE", "TERM", "HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY", "NO_PROXY",
2406
+ "NODE_EXTRA_CA_CERTS", "SSL_CERT_FILE", "SSL_CERT_DIR", "DEEPSEEK_API_KEY",
2407
+ }
2408
+ controlled = {name: value for name, value in env.items() if name.upper() in allowed}
2409
+ controlled.update({
2410
+ "DSH_HOME": str(home), "DSH_PERMISSION_MODE": "workspace-write",
2411
+ "DSH_TELEMETRY_DISABLED": "1", "DSH_TOOLS_MODE": "native",
2412
+ "TMPDIR": str(temporary_root), "TMP": str(temporary_root), "TEMP": str(temporary_root),
2413
+ "DEEPSEEK_BASE_URL": "https://api.deepseek.com/anthropic",
2414
+ "GIT_CONFIG_GLOBAL": os.devnull, "GIT_CONFIG_NOSYSTEM": "1",
2415
+ "GIT_TERMINAL_PROMPT": "0", "GIT_OPTIONAL_LOCKS": "0",
2416
+ })
2417
+ return controlled
2418
+
2419
+
2420
+ def deepseek_harness_configuration(binary: str, credentials_path: Path, env: dict[str, str]) -> dict:
2421
+ package_root = _deepseek_harness_package_roots(binary)[0]
2422
+ script = """
2423
+ const fs = require('node:fs');
2424
+ const path = require('node:path');
2425
+ const {createRequire} = require('node:module');
2426
+ try {
2427
+ const localRequire = createRequire(path.join(process.argv[1], 'package.json'));
2428
+ const yaml = localRequire('js-yaml');
2429
+ const document = fs.existsSync(process.argv[2]) ? yaml.load(fs.readFileSync(process.argv[2], 'utf8')) : {};
2430
+ const apiKey = Boolean(process.env.DEEPSEEK_API_KEY || document?.refs?.DEEPSEEK_API_KEY);
2431
+ const account = Object.entries(document?.records || {}).some(([name, entry]) => name.includes('deepseek') && entry?.kind === 'grant');
2432
+ console.log(JSON.stringify({provider: apiKey ? 'deepseek-official' : account ? 'deepseek-account' : null}));
2433
+ } catch {
2434
+ console.log(JSON.stringify({error: 'Harness credentials could not be validated'}));
2435
+ }
2436
+ """
2437
+ result = subprocess.run(
2438
+ [shutil.which("node") or "node", "-e", script, str(package_root), str(credentials_path)],
2439
+ capture_output=True, text=True, timeout=10, env=env,
2440
+ )
2441
+ if result.returncode or len(result.stdout) > 4096:
2442
+ raise RuntimeError("Harness credential inspection failed")
2443
+ metadata = json.loads(result.stdout)
2444
+ if metadata.get("error") or metadata.get("provider") not in {"deepseek-official", "deepseek-account"}:
2445
+ raise RuntimeError("DeepSeek Harness requires an API key or an existing DeepSeek account login")
2446
+ return metadata
2447
+
2448
+
2449
+ def deepseek_harness_home(workspace_path: str | Path) -> Path:
2450
+ root = str(Path(workspace_path).expanduser().resolve())
2451
+ key = hashlib.sha256(root.encode("utf-8")).hexdigest()
2452
+ return Path.home() / ".forgexa" / "agents" / "deepseek-harness" / key
2453
+
2454
+
2455
+ def deepseek_harness_profile_lock_path(workspace_path: str | Path) -> Path:
2456
+ return deepseek_harness_home(workspace_path) / "profile.lock"
2457
+
2458
+
2459
+ def deepseek_harness_overlay(*, cwd: Path, credentials_path: Path, provider: str, model: str) -> list[dict]:
2460
+ if not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._:/-]{0,199}", model):
2461
+ raise ValueError("Invalid DeepSeek Harness model ID")
2462
+ rows = [
2463
+ {"id": "agent-default-model", "config": {"provider": provider, "model": model, "reasoningEffort": "high"}},
2464
+ {"id": "credentials", "config": {"path": str(credentials_path), "watch": False}},
2465
+ {"id": "approval", "config": {"policy": "never"}},
2466
+ {"id": "permission", "config": {
2467
+ "presets": {"forgexa-workspace": {"sandbox": "workspace-write", "approval": "never"}},
2468
+ "defaultPreset": "forgexa-workspace",
2469
+ }},
2470
+ {"id": "sandbox-policy", "config": {"mode": "workspace-write", "workspaceRoot": str(cwd)}},
2471
+ {"id": "fs-sandbox", "config": {"cwd": str(cwd)}},
2472
+ {"id": "tools", "config": {"mode": "native"}},
2473
+ {"id": "session-log-deepseek", "config": {"enabled": False}},
2474
+ {"id": "plugin-package-inventory-deepseek", "config": {"enabled": False}},
2475
+ ]
2476
+ disabled = (
2477
+ "session-telemetry-otel", "hmr", "plugin-manager", "tool-plugin-manager",
2478
+ "llm-pi-ai", "skill-filesystem", "tool-skill", "tool-subagent", "tool-subagent-fork",
2479
+ "tool-subagent-control", "tool-subagent-list-agents", "tool-workflow",
2480
+ "goal-round-driver", "command-goal", "tool-goal", "command-feedback", "tool-web",
2481
+ )
2482
+ rows.extend({"id": name, "disabled": True} for name in disabled)
2483
+ rows.extend([
2484
+ {"id": "deepseek-account", "disabled": provider != "deepseek-account"},
2485
+ {"id": "llm-deepseek-account", "disabled": provider != "deepseek-account"},
2486
+ {"id": "llm-deepseek", "disabled": provider != "deepseek-official", "config": {
2487
+ "apiKeyEnv": "DEEPSEEK_API_KEY", "baseURL": "https://api.deepseek.com/anthropic",
2488
+ "reasoningEffort": "high", "maxTokens": 32768,
2489
+ }},
2490
+ ])
2491
+ return rows
2492
+
2493
+
2494
+ def _deepseek_harness_git_roots(cwd: Path) -> list[Path]:
2495
+ roots = [cwd / ".git"]
2496
+ if roots[0].is_file():
2497
+ content = roots[0].read_text(encoding="utf-8")[:4096]
2498
+ if content.startswith("gitdir:"):
2499
+ git_dir = (cwd / content[7:].strip()).resolve()
2500
+ roots.append(git_dir)
2501
+ common = git_dir / "commondir"
2502
+ if common.is_file():
2503
+ roots.append((git_dir / common.read_text(encoding="utf-8").strip()).resolve())
2504
+ return roots
2505
+
2506
+
2507
+ def deepseek_harness_sandbox_command(command: list[str], *, cwd: Path, home: Path, temporary_root: Path, credentials_path: Path | None = None) -> list[str]:
2508
+ git_roots = [*_deepseek_harness_git_roots(cwd), home / "forgexa-session-bindings"]
2509
+ if credentials_path is not None:
2510
+ git_roots.append(credentials_path)
2511
+ if sys.platform == "darwin":
2512
+ sandbox = shutil.which("sandbox-exec")
2513
+ if not sandbox:
2514
+ raise RuntimeError("DeepSeek Harness outer sandbox is unavailable")
2515
+ writable = " ".join(f"(subpath {json.dumps(str(root.resolve()))})" for root in (cwd, home, temporary_root))
2516
+ protected = " ".join(f"(subpath {json.dumps(str(root.resolve()))})" for root in git_roots)
2517
+ profile = f'(version 1) (allow default) (deny file-write*) (allow file-write* {writable} (literal "/dev/null")) (deny file-write* {protected})'
2518
+ return [sandbox, "-p", profile, *command]
2519
+ if sys.platform == "linux":
2520
+ sandbox = shutil.which("bwrap")
2521
+ if not sandbox:
2522
+ raise RuntimeError("DeepSeek Harness requires a working bubblewrap outer sandbox")
2523
+ wrapped = [sandbox, "--die-with-parent", "--unshare-pid", "--ro-bind", "/", "/", "--proc", "/proc", "--dev", "/dev"]
2524
+ for root in (cwd, home, temporary_root):
2525
+ wrapped += ["--bind", str(root), str(root)]
2526
+ for root in git_roots:
2527
+ if root.exists():
2528
+ wrapped += ["--ro-bind", str(root.resolve()), str(root.resolve())]
2529
+ return [*wrapped, "--chdir", str(cwd), "--", *command]
2530
+ raise RuntimeError("DeepSeek Harness outer isolation has not been verified on this platform")
2531
+
2532
+
2533
+ @contextmanager
2534
+ def deepseek_harness_runtime(binary: str, workspace_path: str | Path, *, model: str | None = None, session_id: str | None = None):
2535
+ binary = os.path.abspath(shutil.which(binary) or os.path.expanduser(binary))
2536
+ cwd = Path(workspace_path).expanduser().resolve()
2537
+ home = deepseek_harness_home(cwd)
2538
+ home.mkdir(parents=True, exist_ok=True, mode=0o700)
2539
+ if sys.platform != "win32":
2540
+ home.chmod(0o700)
2541
+ lock = acquire_file_lock(deepseek_harness_profile_lock_path(cwd))
2542
+ if lock is None:
2543
+ raise RuntimeError("DeepSeek Harness profile is busy with another local turn")
2544
+ try:
2545
+ bindings = home / "forgexa-session-bindings"
2546
+ bindings.mkdir(exist_ok=True, mode=0o700)
2547
+ profile = home / "profiles" / "headless"
2548
+ for untrusted in (home / "cordis.patch.yml", profile / "node_modules", home / "profiles" / "node_modules"):
2549
+ if untrusted.exists() or untrusted.is_symlink():
2550
+ raise RuntimeError("The managed Harness home must not contain user patches or plugin overrides")
2551
+ patch = profile / "cordis.patch.yml"
2552
+ if patch.is_file():
2553
+ effective_lines = [
2554
+ line.strip() for line in patch.read_text(encoding="utf-8").splitlines()
2555
+ if line.strip() and not line.lstrip().startswith("#")
2556
+ ]
2557
+ if effective_lines not in ([], ["[]"]):
2558
+ raise RuntimeError("The managed Harness profile must not contain user patches")
2559
+ manifest = profile / "package.json"
2560
+ if manifest.is_file():
2561
+ bundles = json.loads(manifest.read_text(encoding="utf-8")).get("dsh", {}).get("profile", {}).get("bundles")
2562
+ if bundles != ["@deepseek-ai/dsh-base", "@deepseek-ai/dsh-headless"]:
2563
+ raise RuntimeError("The managed Harness profile contains an unsupported bundle")
2564
+ with tempfile.TemporaryDirectory(prefix="run-", dir=home) as run_dir:
2565
+ temporary_root = Path(run_dir).resolve()
2566
+ env = deepseek_harness_base_env(os.environ.copy(), home=home, temporary_root=temporary_root)
2567
+ source_home = Path(os.environ.get("DSH_HOME", str(Path.home() / ".dsh"))).expanduser().resolve()
2568
+ credentials = Path(os.environ.get("FACTORY_DEEPSEEK_HARNESS_CREDENTIALS", str(source_home / ".credentials.yaml"))).expanduser().resolve()
2569
+ configuration = deepseek_harness_configuration(binary, credentials, env)
2570
+ selected_model = model or os.environ.get("FACTORY_DEEPSEEK_HARNESS_MODEL", "").strip() or "deepseek-flash"
2571
+ overlay = temporary_root / "run.patch.json"
2572
+ composition = deepseek_harness_overlay(cwd=cwd, credentials_path=credentials, provider=configuration["provider"], model=selected_model)
2573
+ identity = hashlib.sha256(json.dumps({"build": deepseek_harness_build_info(binary)["fingerprint"], "overlay": composition}, sort_keys=True).encode()).hexdigest()
2574
+ env["FORGEXA_DEEPSEEK_HARNESS_COMPOSITION"] = identity
2575
+ if session_id:
2576
+ binding = bindings / (hashlib.sha256(session_id.encode()).hexdigest() + ".json")
2577
+ if not binding.is_file() or json.loads(binding.read_text(encoding="utf-8")).get("composition") != identity:
2578
+ raise RuntimeError("Native Harness session configuration changed or is not owned by Forgexa; start a new session")
2579
+ overlay.write_text(json.dumps(composition), encoding="utf-8")
2580
+ command = build_deepseek_harness_command(binary, overlay_path=overlay, session_id=session_id)
2581
+ yield deepseek_harness_sandbox_command(command, cwd=cwd, home=home, temporary_root=temporary_root, credentials_path=credentials), env
2582
+ finally:
2583
+ release_file_lock(lock)
2584
+
2585
+
2586
+ async def run_deepseek_harness(
2587
+ binary: str,
2588
+ prompt: str,
2589
+ workspace_path: str | Path,
2590
+ *,
2591
+ timeout: float | None = 300,
2592
+ model: str | None = None,
2593
+ session_id: str | None = None,
2594
+ on_event: Any = None,
2595
+ on_started: Any = None,
2596
+ on_chunk: Any = None,
2597
+ cancel_requested: Any = None,
2598
+ ) -> dict:
2599
+ """Execute and validate a supervised one-shot process with bounded output."""
2600
+ process = None
2601
+ state = DeepSeekHarnessRunState(expected_cwd=str(workspace_path))
2602
+ raw_lines = []
2603
+ raw_bytes = 0
2604
+ stderr_text = ""
2605
+ runtime = deepseek_harness_runtime(binary, workspace_path, model=model, session_id=session_id)
2606
+ entered = False
2607
+ try:
2608
+ command, env = await asyncio.to_thread(runtime.__enter__)
2609
+ entered = True
2610
+ process = await asyncio.create_subprocess_exec(
2611
+ *_prepare_agent_command(command), cwd=str(workspace_path), env=env,
2612
+ stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE,
2613
+ stderr=asyncio.subprocess.PIPE, limit=DEEPSEEK_HARNESS_MAX_OUTPUT_BYTES,
2614
+ **_agent_process_group_kwargs(),
2615
+ )
2616
+ if on_started is not None:
2617
+ on_started(process)
2618
+ if cancel_requested is not None and cancel_requested():
2619
+ _kill_proc(process)
2620
+
2621
+ async def read_stderr() -> str:
2622
+ tail = ""
2623
+ while True:
2624
+ chunk = await process.stderr.read(4096)
2625
+ if not chunk:
2626
+ return tail
2627
+ tail = (tail + chunk.decode("utf-8", errors="replace"))[-10000:]
2628
+
2629
+ async def collect() -> str:
2630
+ nonlocal raw_bytes
2631
+ stderr_task = asyncio.create_task(read_stderr())
2632
+ try:
2633
+ process.stdin.write(prompt.encode("utf-8"))
2634
+ await process.stdin.drain()
2635
+ process.stdin.close()
2636
+ while True:
2637
+ try:
2638
+ encoded = await process.stdout.readline()
2639
+ except (ValueError, asyncio.LimitOverrunError):
2640
+ state.fail("Harness output exceeds the allowed size", "deepseek_harness_output_limit")
2641
+ _kill_proc(process)
2642
+ break
2643
+ if not encoded:
2644
+ break
2645
+ raw_bytes += len(encoded)
2646
+ if raw_bytes > DEEPSEEK_HARNESS_MAX_OUTPUT_BYTES:
2647
+ state.fail("Harness output exceeds the allowed size", "deepseek_harness_output_limit")
2648
+ _kill_proc(process)
2649
+ break
2650
+ try:
2651
+ line = encoded.decode("utf-8").strip()
2652
+ except UnicodeDecodeError:
2653
+ state.fail("Harness output contains invalid UTF-8")
2654
+ _kill_proc(process)
2655
+ break
2656
+ if not line:
2657
+ continue
2658
+ raw_lines.append(line[-10000:])
2659
+ raw_lines[:] = raw_lines[-20:]
2660
+ event = state.feed_line(line)
2661
+ if state.error:
2662
+ _kill_proc(process)
2663
+ break
2664
+ if event is not None and event.get("type") == "session" and env.get("FORGEXA_DEEPSEEK_HARNESS_COMPOSITION"):
2665
+ binding = deepseek_harness_home(workspace_path) / "forgexa-session-bindings" / (hashlib.sha256(state.session_id.encode()).hexdigest() + ".json")
2666
+ with tempfile.NamedTemporaryFile("w", encoding="utf-8", dir=binding.parent, delete=False) as temporary:
2667
+ json.dump({"composition": env["FORGEXA_DEEPSEEK_HARNESS_COMPOSITION"]}, temporary)
2668
+ temporary_path = Path(temporary.name)
2669
+ try:
2670
+ os.replace(temporary_path, binding)
2671
+ finally:
2672
+ temporary_path.unlink(missing_ok=True)
2673
+ if on_chunk is not None:
2674
+ await on_chunk(line)
2675
+ if event is not None and on_event is not None:
2676
+ await on_event(event)
2677
+ await process.wait()
2678
+ return await stderr_task
2679
+ finally:
2680
+ if not stderr_task.done():
2681
+ stderr_task.cancel()
2682
+ await asyncio.gather(stderr_task, return_exceptions=True)
2683
+
2684
+ try:
2685
+ stderr_text = await asyncio.wait_for(collect(), timeout=timeout)
2686
+ except asyncio.TimeoutError:
2687
+ state.fail(f"Harness timed out after {timeout:g}s", "agent_timeout")
2688
+ except (BrokenPipeError, ConnectionResetError):
2689
+ state.fail("Harness closed the input pipe before accepting its task")
2690
+ if process.returncode is None:
2691
+ _kill_proc(process)
2692
+ await process.wait()
2693
+ except asyncio.CancelledError:
2694
+ if process is not None and process.returncode is None:
2695
+ _kill_proc(process)
2696
+ await process.wait()
2697
+ raise
2698
+ except Exception as error:
2699
+ message = str(error)
2700
+ code = "deepseek_harness_profile_busy" if "profile is busy" in message else "deepseek_harness_startup_failed"
2701
+ state.fail(message, code)
2702
+ finally:
2703
+ if process is not None and process.returncode is None:
2704
+ _kill_proc(process)
2705
+ await process.wait()
2706
+ if entered:
2707
+ await asyncio.to_thread(runtime.__exit__, None, None, None)
2708
+ cancelled = bool(cancel_requested is not None and cancel_requested())
2709
+ result = state.finish(process.returncode if process is not None else -1, cancelled=cancelled)
2710
+ return {**result, "exit_code": process.returncode if process is not None else -1,
2711
+ "stderr": stderr_text, "raw_tail": "\n".join(raw_lines)}
2712
+
2713
+
2114
2714
  def mimo_base_env(env: dict[str, str]) -> dict[str, str]:
2115
2715
  """Return the constrained MiMo Code environment used by AgentHost turns."""
2116
2716
  env = dict(env)
@@ -2506,6 +3106,8 @@ def agent_live_gate_approved(agent_id: str, env: dict[str, str] | None = None) -
2506
3106
 
2507
3107
  def agent_execution_verification_issue(agent_id: str, binary: str) -> str | None:
2508
3108
  """Return a fail-closed build/live-acceptance gate for local agents."""
3109
+ if agent_id == "deepseek-harness":
3110
+ return deepseek_harness_verification_issue(binary)
2509
3111
  if agent_id == "zcode":
2510
3112
  build_issue = zcode_build_verification_issue(binary)
2511
3113
  if build_issue:
@@ -1196,7 +1196,7 @@ except (ImportError, ModuleNotFoundError):
1196
1196
  # DAEMON_VERSION is the protocol/logic version of the daemon code.
1197
1197
  # Kept in sync with pyproject.toml version via bump-version.sh.
1198
1198
  # CLIENT_TYPE identifies which packaging/distribution this daemon runs in.
1199
- DAEMON_VERSION = "1.59.2"
1199
+ DAEMON_VERSION = "1.60.0"
1200
1200
 
1201
1201
 
1202
1202
  def _detect_client_type() -> str:
@@ -5912,6 +5912,7 @@ class ProcessManager:
5912
5912
  def __init__(self):
5913
5913
  self.active_processes: dict[str, asyncio.subprocess.Process] = {}
5914
5914
  self.active_zcode_sessions: dict[str, ZCodeProtocolSession] = {}
5915
+ self._harness_cancel_requests: dict[str, bool] = {}
5915
5916
 
5916
5917
  @staticmethod
5917
5918
  def _has_failure_pattern(text: str) -> str | None:
@@ -6212,7 +6213,7 @@ class ProcessManager:
6212
6213
  preflight={"scope": "runtime", "checks": [check]},
6213
6214
  )
6214
6215
 
6215
- execution_gate_issue = agent_core.agent_execution_verification_issue(
6216
+ execution_gate_issue = await asyncio.to_thread(agent_core.agent_execution_verification_issue,
6216
6217
  agent.agent_id,
6217
6218
  agent.command,
6218
6219
  )
@@ -6265,6 +6266,10 @@ class ProcessManager:
6265
6266
  result = await self._run_mimo(
6266
6267
  agent, prompt, workspace_path, timeout, task.task_id, on_chunk,
6267
6268
  )
6269
+ elif agent.agent_id == "deepseek-harness":
6270
+ result = await self._run_deepseek_harness(
6271
+ agent, prompt, workspace_path, timeout, task.task_id, on_chunk,
6272
+ )
6268
6273
  elif agent.agent_id == "qwen":
6269
6274
  result = await self._run_qwen(
6270
6275
  agent, prompt, workspace_path, timeout, task, on_chunk,
@@ -6311,7 +6316,7 @@ class ProcessManager:
6311
6316
  result.metrics["duration_seconds"] = round(elapsed, 2)
6312
6317
 
6313
6318
  # Detect agent-level failures despite exit code 0
6314
- output_failure = self._detect_agent_output_failure(result, agent.agent_id)
6319
+ output_failure = None if agent.agent_id == "deepseek-harness" else self._detect_agent_output_failure(result, agent.agent_id)
6315
6320
  if output_failure:
6316
6321
  logger.warning(
6317
6322
  "Agent '%s' exited 0 but output indicates failure: %s",
@@ -7079,6 +7084,32 @@ class ProcessManager:
7079
7084
  result.metrics.update(parsed_metrics)
7080
7085
  return result
7081
7086
 
7087
+ async def _run_deepseek_harness(
7088
+ self, agent: DiscoveredAgent, prompt: str, cwd: Path, timeout: int,
7089
+ task_id: str, on_chunk: Any = None,
7090
+ ) -> TaskResult:
7091
+ self._harness_cancel_requests[task_id] = False
7092
+
7093
+ def on_started(proc: asyncio.subprocess.Process) -> None:
7094
+ self.active_processes[task_id] = proc
7095
+
7096
+ try:
7097
+ result = await agent_core.run_deepseek_harness(
7098
+ agent.command, prompt, cwd, timeout=timeout,
7099
+ on_started=on_started, on_chunk=on_chunk,
7100
+ cancel_requested=lambda: self._harness_cancel_requests.get(task_id, False),
7101
+ )
7102
+ return TaskResult(
7103
+ status=result["status"], exit_code=result.get("exit_code", -1),
7104
+ stdout=result.get("text", ""), stderr=result.get("stderr", ""),
7105
+ error=result.get("error") or "", failure_code=result.get("failure_code") or "",
7106
+ metrics=result.get("metrics", {}),
7107
+ has_verified_completion=result["status"] == "success",
7108
+ )
7109
+ finally:
7110
+ self.active_processes.pop(task_id, None)
7111
+ self._harness_cancel_requests.pop(task_id, None)
7112
+
7082
7113
  async def _run_qwen(
7083
7114
  self,
7084
7115
  agent: DiscoveredAgent,
@@ -8446,6 +8477,8 @@ class ProcessManager:
8446
8477
  return info
8447
8478
 
8448
8479
  async def cancel(self, task_id: str):
8480
+ if task_id in self._harness_cancel_requests:
8481
+ self._harness_cancel_requests[task_id] = True
8449
8482
  zcode_session = self.active_zcode_sessions.pop(task_id, None)
8450
8483
  if zcode_session is not None:
8451
8484
  try:
@@ -12071,6 +12104,7 @@ class RuntimeDaemon:
12071
12104
  "codex": "npm install -g @openai/codex",
12072
12105
  "opencode": "curl -sSL https://opencode.ai/install | sh",
12073
12106
  "mimo": "curl -fsSL https://mimo.xiaomi.com/install | bash",
12107
+ "deepseek-harness": "npm i -g @deepseek-ai/dsh@0.2.0-rc.2",
12074
12108
  "qwen": "npm install -g @qwen-code/qwen-code",
12075
12109
  "codebuddy": "npm install -g @tencent-ai/codebuddy-code",
12076
12110
  "qoder": "curl -fsSL https://qoder.com/install | bash",
@@ -13156,7 +13190,7 @@ class RuntimeDaemon:
13156
13190
  # tightly managed.
13157
13191
  for agent in self.agents:
13158
13192
  if agent.agent_id not in tried and agent.agent_id not in {
13159
- "mimo", "qwen", "codebuddy", "qoder", "trae", "zcode",
13193
+ "mimo", "qwen", "codebuddy", "qoder", "trae", "zcode", "deepseek-harness",
13160
13194
  }:
13161
13195
  return agent
13162
13196
 
@@ -13206,7 +13240,7 @@ class RuntimeDaemon:
13206
13240
  (
13207
13241
  agent for agent in self.agents
13208
13242
  if agent.agent_id not in {
13209
- "mimo", "qwen", "codebuddy", "qoder", "trae", "zcode",
13243
+ "mimo", "qwen", "codebuddy", "qoder", "trae", "zcode", "deepseek-harness",
13210
13244
  }
13211
13245
  ),
13212
13246
  None,
@@ -14598,6 +14632,7 @@ class RuntimeDaemon:
14598
14632
  "codex": "npm install -g @openai/codex",
14599
14633
  "opencode": "curl -sSL https://opencode.ai/install | sh",
14600
14634
  "mimo": "curl -fsSL https://mimo.xiaomi.com/install | bash",
14635
+ "deepseek-harness": "npm i -g @deepseek-ai/dsh@0.2.0-rc.2",
14601
14636
  "qwen": "npm install -g @qwen-code/qwen-code",
14602
14637
  "codebuddy": "npm install -g @tencent-ai/codebuddy-code",
14603
14638
  "qoder": "curl -fsSL https://qoder.com/install | bash",
@@ -3687,6 +3687,20 @@ def cmd_agent_approve_live(args: argparse.Namespace) -> None:
3687
3687
  from . import agent_core
3688
3688
 
3689
3689
  agent_id = args.agent_id.strip().lower()
3690
+ if agent_id == "deepseek-harness":
3691
+ binary = getattr(args, "binary", None) or os.environ.get("FACTORY_DEEPSEEK_HARNESS_PATH") or shutil.which("dsh")
3692
+ if not binary:
3693
+ print("error: dsh not found; install @deepseek-ai/dsh@0.2.0-rc.2 globally", file=sys.stderr)
3694
+ return
3695
+ try:
3696
+ information = agent_core.approve_deepseek_harness_build(binary)
3697
+ except (OSError, ValueError, RuntimeError, subprocess.SubprocessError) as exc:
3698
+ print(f"error: {exc}", file=sys.stderr)
3699
+ return
3700
+ print(f"DeepSeek Harness build approved: {information['fingerprint']}")
3701
+ print(f"runtime version: {information['version']}; Node: {information['node_version']}")
3702
+ print("This records your attestation, not an automatic live test. Only approve after start/resume/cancel/isolation acceptance.")
3703
+ return
3690
3704
  gated = set(agent_core._UNVERIFIED_AGENT_LIVE_GATE_REASONS)
3691
3705
  if agent_id not in gated:
3692
3706
  print(
@@ -3962,8 +3976,9 @@ def main() -> None:
3962
3976
  )
3963
3977
  agent_approve_live_p.add_argument(
3964
3978
  "agent_id", metavar="AGENT_ID",
3965
- help="Gated agent id (e.g. trae, zcode)",
3979
+ help="Gated agent id (e.g. trae, zcode, deepseek-harness)",
3966
3980
  )
3981
+ agent_approve_live_p.add_argument("--binary", default=None, metavar="PATH", help="Installed Harness binary for build-scoped acceptance")
3967
3982
 
3968
3983
  # runtimes
3969
3984
  rt_p = sub.add_parser("runtimes", help="Runtime management")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forgexa-cli
3
- Version: 1.59.2
3
+ Version: 1.60.0
4
4
  Summary: Forgexa CLI — command-line client and AI agent runtime for the Forgexa platform
5
5
  Author-email: Jason Sun <dev.winds@gmail.com>
6
6
  License-Expression: MIT
@@ -35,7 +35,7 @@ Requires-Dist: socksio>=1.0.0; extra == "daemon"
35
35
  Command-line client and agent runtime for the [Forgexa](https://forgexa.net) platform.
36
36
 
37
37
  Communicates with the Forgexa server via REST API.
38
- Includes a built-in **daemon** that discovers twelve local AI agent integrations (Claude Code, Codex, GitHub Copilot, Gemini CLI, OpenCode, Kimi Code, MiMo Code, Qwen Code, CodeBuddy, Qoder CLI, TraeCode CLI, and ZCode) and executes tasks on behalf of the server. MiMo Code (`mimo`, v0.1.14+), Qwen Code (`qwen`, v0.24.1+), CodeBuddy (`codebuddy`, v2.156.0+), Qoder CLI (`qoder`, v1.1.59+), TraeCode CLI (`trae`), and ZCode (`zcode`) are selected explicitly; automatic routing and generated default fallback chains exclude all six. CodeBuddy and Qoder completed their local AgentHost start/resume acceptance gates. Authentication, endpoint, and model selection remain in each local CLI profile; Forgexa never stores or overrides them.
38
+ Includes a built-in **daemon** with thirteen integrations: Claude Code, Codex, GitHub Copilot, Gemini CLI, OpenCode, Kimi Code, MiMo Code, Qwen Code, CodeBuddy, Qoder CLI, TraeCode CLI, ZCode, and DeepSeek Harness. Six standard candidates are automatically routed; seven require explicit selection. Harness (`deepseek-harness`) is a developer preview for personal Runtimes/Desktop, pinned to 0.2.0-rc.2 and Node 22.19+ within 22.x or 24+. It owns a managed runtime profile with read-only external credentials, without copying personal settings. Shared pools/native Windows are blocked. After exact-build live acceptance, `forgexa agent approve-live deepseek-harness` records an attestation; `forgexa check --agent deepseek-harness --quick` avoids model calls, while normal `check` runs a potentially billable minimal task. See [global/local installation and limits](../documentation/guide/agent-installation.md#deepseek-harness-developer-preview).
39
39
 
40
40
  ## Installation
41
41
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "forgexa-cli"
3
- version = "1.59.2"
3
+ version = "1.60.0"
4
4
  description = "Forgexa CLI — command-line client and AI agent runtime for the Forgexa platform"
5
5
  requires-python = ">=3.9"
6
6
  license = "MIT"
@@ -30,6 +30,20 @@ def _agent(agent_id: str, version: str = "1.0.0") -> daemon.DiscoveredAgent:
30
30
  )
31
31
 
32
32
 
33
+ def test_deepseek_harness_live_approval_is_build_scoped(tmp_path, monkeypatch, capsys):
34
+ from forgexa_cli import agent_core
35
+
36
+ approval = tmp_path / "build-approved.json"
37
+ old_approval = tmp_path / "agent-approved"
38
+ monkeypatch.setattr(agent_core, "DEEPSEEK_HARNESS_APPROVAL_FILE", approval)
39
+ monkeypatch.setattr(agent_core, "AGENT_LIVE_VERIFIED_FILE", old_approval)
40
+ monkeypatch.setattr(agent_core, "deepseek_harness_build_info", lambda binary: {"fingerprint": "test-build", "version": "0.2.0-rc.2", "node_version": "25.2.1"})
41
+ main.cmd_agent_approve_live(SimpleNamespace(agent_id="deepseek-harness", binary="/fake/dsh"))
42
+ assert json.loads(approval.read_text())["builds"] == ["test-build"]
43
+ assert not old_approval.exists()
44
+ assert "attestation" in capsys.readouterr().out
45
+
46
+
33
47
  def test_check_quick_reports_installed_and_missing_agents(
34
48
  monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str],
35
49
  ) -> None:
File without changes