forgexa-cli 1.59.2__tar.gz → 1.60.1__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.1}/PKG-INFO +2 -2
  2. {forgexa_cli-1.59.2 → forgexa_cli-1.60.1}/README.md +1 -1
  3. {forgexa_cli-1.59.2 → forgexa_cli-1.60.1}/forgexa_cli/__init__.py +1 -1
  4. {forgexa_cli-1.59.2 → forgexa_cli-1.60.1}/forgexa_cli/agent_core.py +633 -2
  5. {forgexa_cli-1.59.2 → forgexa_cli-1.60.1}/forgexa_cli/daemon.py +50 -11
  6. {forgexa_cli-1.59.2 → forgexa_cli-1.60.1}/forgexa_cli/main.py +19 -3
  7. {forgexa_cli-1.59.2 → forgexa_cli-1.60.1}/forgexa_cli.egg-info/PKG-INFO +2 -2
  8. {forgexa_cli-1.59.2 → forgexa_cli-1.60.1}/pyproject.toml +1 -1
  9. {forgexa_cli-1.59.2 → forgexa_cli-1.60.1}/tests/test_check_command.py +29 -0
  10. {forgexa_cli-1.59.2 → forgexa_cli-1.60.1}/forgexa_cli/_build_config.py +0 -0
  11. {forgexa_cli-1.59.2 → forgexa_cli-1.60.1}/forgexa_cli/_local_bind.py +0 -0
  12. {forgexa_cli-1.59.2 → forgexa_cli-1.60.1}/forgexa_cli/autoupgrade.py +0 -0
  13. {forgexa_cli-1.59.2 → forgexa_cli-1.60.1}/forgexa_cli/py.typed +0 -0
  14. {forgexa_cli-1.59.2 → forgexa_cli-1.60.1}/forgexa_cli/runtime_evidence.py +0 -0
  15. {forgexa_cli-1.59.2 → forgexa_cli-1.60.1}/forgexa_cli/zcode_protocol.py +0 -0
  16. {forgexa_cli-1.59.2 → forgexa_cli-1.60.1}/forgexa_cli.egg-info/SOURCES.txt +0 -0
  17. {forgexa_cli-1.59.2 → forgexa_cli-1.60.1}/forgexa_cli.egg-info/dependency_links.txt +0 -0
  18. {forgexa_cli-1.59.2 → forgexa_cli-1.60.1}/forgexa_cli.egg-info/entry_points.txt +0 -0
  19. {forgexa_cli-1.59.2 → forgexa_cli-1.60.1}/forgexa_cli.egg-info/requires.txt +0 -0
  20. {forgexa_cli-1.59.2 → forgexa_cli-1.60.1}/forgexa_cli.egg-info/top_level.txt +0 -0
  21. {forgexa_cli-1.59.2 → forgexa_cli-1.60.1}/setup.cfg +0 -0
  22. {forgexa_cli-1.59.2 → forgexa_cli-1.60.1}/tests/test_auth_and_runtime_commands.py +0 -0
  23. {forgexa_cli-1.59.2 → forgexa_cli-1.60.1}/tests/test_autoupgrade.py +0 -0
  24. {forgexa_cli-1.59.2 → forgexa_cli-1.60.1}/tests/test_expiry_warnings_and_revoke.py +0 -0
  25. {forgexa_cli-1.59.2 → forgexa_cli-1.60.1}/tests/test_local_bind_commands.py +0 -0
  26. {forgexa_cli-1.59.2 → forgexa_cli-1.60.1}/tests/test_runtime_credentials.py +0 -0
  27. {forgexa_cli-1.59.2 → forgexa_cli-1.60.1}/tests/test_session_credentials.py +0 -0
  28. {forgexa_cli-1.59.2 → forgexa_cli-1.60.1}/tests/test_silent_install.py +0 -0
  29. {forgexa_cli-1.59.2 → forgexa_cli-1.60.1}/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.1
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`) 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` records an attestation; `forgexa check --agent deepseek --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`) 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` records an attestation; `forgexa check --agent deepseek --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.1"
@@ -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
 
@@ -529,6 +531,34 @@ def _agent_discovery_verification_issue(agent_id: str, binary: str) -> str | Non
529
531
  return agent_execution_verification_issue(agent_id, binary)
530
532
 
531
533
 
534
+ def canonical_agent_id(agent_id: str | None) -> str | None:
535
+ return "deepseek" if agent_id == "deepseek-harness" else agent_id
536
+
537
+
538
+ def canonical_agent_inventory(agents: list | None) -> list | None:
539
+ if not isinstance(agents, list) or not agents:
540
+ return agents
541
+ return [
542
+ {**entry, "agent_id": "deepseek", "reported_agent_id": "deepseek-harness"}
543
+ if isinstance(entry, dict) and entry.get("agent_id") == "deepseek-harness" else entry
544
+ for entry in agents
545
+ ]
546
+
547
+
548
+ def runtime_agent_wire_id(agent_id: str | None, available_agents: list | None) -> str | None:
549
+ canonical = canonical_agent_id(agent_id)
550
+ if canonical != "deepseek":
551
+ return canonical
552
+ reported = [
553
+ entry.get("reported_agent_id") or entry.get("agent_id")
554
+ for entry in available_agents or []
555
+ if isinstance(entry, dict) and canonical_agent_id(entry.get("agent_id")) == canonical
556
+ ]
557
+ if "deepseek" not in reported and "deepseek-harness" in reported:
558
+ return "deepseek-harness"
559
+ return canonical
560
+
561
+
532
562
  # ── Agent Discovery ──
533
563
 
534
564
 
@@ -582,6 +612,17 @@ class AgentDiscovery:
582
612
  ),
583
613
  "upgrade_hint": "mimo upgrade",
584
614
  },
615
+ "deepseek": {
616
+ "commands": ["dsh"],
617
+ "detect": "dsh --version",
618
+ "invoke_modes": ["cli"],
619
+ "env_path_override": "FACTORY_DEEPSEEK_HARNESS_PATH",
620
+ "env_model_override": "FACTORY_DEEPSEEK_HARNESS_MODEL",
621
+ "compatibility_level": "L2",
622
+ "min_version": (0, 2, 0),
623
+ "strict_min_version": True,
624
+ "upgrade_hint": "npm i -g @deepseek-ai/dsh@0.2.0-rc.2",
625
+ },
585
626
  "qwen": {
586
627
  "commands": ["qwen"],
587
628
  "detect": "qwen --version",
@@ -872,7 +913,7 @@ class AgentDiscovery:
872
913
  )
873
914
  continue
874
915
  min_version_ok, version_warning = _check_agent_min_version(agent_id, version, spec)
875
- verification_issue = _agent_discovery_verification_issue(agent_id, resolved)
916
+ verification_issue = await asyncio.to_thread(_agent_discovery_verification_issue, agent_id, resolved)
876
917
  if verification_issue:
877
918
  min_version_ok = False
878
919
  version_warning = verification_issue
@@ -916,6 +957,8 @@ class AgentDiscovery:
916
957
  parts[0] = resolved
917
958
  parts = _prepare_agent_command(parts, preserve_argv=False)
918
959
  env = None
960
+ if detect_cmd == "dsh --version":
961
+ env = deepseek_harness_probe_env()
919
962
  if detect_cmd in ("opencode --version", "mimo --version"):
920
963
  probe_dir = tempfile.TemporaryDirectory(prefix=f"forgexa-{detect_cmd.split()[0]}-probe-")
921
964
  env = os.environ.copy()
@@ -2111,6 +2154,591 @@ def build_mimo_command(
2111
2154
  return cmd
2112
2155
 
2113
2156
 
2157
+ DEEPSEEK_HARNESS_VERSION = "0.2.0-rc.2"
2158
+ DEEPSEEK_HARNESS_MAX_OUTPUT_BYTES = 4 * 1024 * 1024
2159
+ DEEPSEEK_HARNESS_APPROVAL_FILE = Path.home() / ".forgexa" / "deepseek-harness-approved.json"
2160
+
2161
+
2162
+ def _reject_nonfinite_json(value: str) -> None:
2163
+ raise ValueError("Non-finite JSON number")
2164
+
2165
+
2166
+ @dataclass
2167
+ class DeepSeekHarnessRunState:
2168
+ """Validate one headless invocation independently of retained log tails."""
2169
+
2170
+ expected_cwd: str | None = None
2171
+ session_id: str | None = None
2172
+ text: str = ""
2173
+ reason: dict | None = None
2174
+ error: str | None = None
2175
+ failure_code: str | None = None
2176
+ started: bool = False
2177
+ saw_final: bool = False
2178
+ usage_incomplete: bool = False
2179
+ usage_steps: dict = field(default_factory=dict)
2180
+
2181
+ def fail(self, message: str, code: str = "deepseek_harness_protocol_error") -> None:
2182
+ if self.error is None:
2183
+ self.error = message[:2000]
2184
+ self.failure_code = code
2185
+
2186
+ def feed_line(self, line: str) -> dict | None:
2187
+ if len(line.encode("utf-8")) > DEEPSEEK_HARNESS_MAX_OUTPUT_BYTES:
2188
+ self.fail("Harness output exceeds the allowed size", "deepseek_harness_output_limit")
2189
+ return None
2190
+ try:
2191
+ event = json.loads(line, parse_constant=_reject_nonfinite_json)
2192
+ except (ValueError, RecursionError):
2193
+ self.fail("Harness stdout must contain valid JSONL events")
2194
+ return None
2195
+ if not isinstance(event, dict):
2196
+ self.fail("Harness event must be an object")
2197
+ return None
2198
+ self.feed(event)
2199
+ return event
2200
+
2201
+ def feed(self, event: dict) -> None:
2202
+ event_type = event.get("type")
2203
+ if self.saw_final:
2204
+ self.fail("Harness emitted data after its final event")
2205
+ return
2206
+ if event_type == "error":
2207
+ message = event.get("message")
2208
+ if not isinstance(message, str):
2209
+ self.fail("Harness error event is missing its message")
2210
+ return
2211
+ missing = not self.started and re.search(
2212
+ r'^session ".+" does not exist; omit --session-id', message,
2213
+ )
2214
+ self.fail(message, "native_session_not_found" if missing else "deepseek_harness_error")
2215
+ return
2216
+ if event_type == "session":
2217
+ session_id, cwd = event.get("sessionId"), event.get("cwd")
2218
+ if (
2219
+ self.session_id is not None or event.get("truncated")
2220
+ or not isinstance(session_id, str) or not session_id.strip()
2221
+ or len(session_id) > 256 or "\0" in session_id
2222
+ or not isinstance(cwd, str) or not cwd or "\0" in cwd
2223
+ ):
2224
+ self.fail("Harness session identity is missing, duplicated or truncated")
2225
+ return
2226
+ if self.expected_cwd and Path(cwd).resolve() != Path(self.expected_cwd).resolve():
2227
+ self.fail("Harness session belongs to another workspace", "deepseek_harness_session_incompatible")
2228
+ return
2229
+ self.session_id = session_id
2230
+ return
2231
+ if self.session_id is None:
2232
+ self.fail("Harness output did not begin with a session event")
2233
+ return
2234
+ if event_type == "status":
2235
+ phase = event.get("phase")
2236
+ if phase not in {"turn_start", "step_start", "step_end", "turn_end"}:
2237
+ self.fail("Harness emitted an unsupported status phase")
2238
+ elif phase == "turn_start":
2239
+ if self.started:
2240
+ self.fail("Harness emitted more than one turn")
2241
+ self.started = True
2242
+ elif not self.started or self.reason is not None:
2243
+ self.fail("Harness emitted an out-of-order turn status")
2244
+ elif phase == "turn_end":
2245
+ reason = event.get("reason")
2246
+ if (
2247
+ not isinstance(reason, dict) or not isinstance(reason.get("kind"), str)
2248
+ or ("error" in reason and not isinstance(reason["error"], dict))
2249
+ or event.get("truncated")
2250
+ ):
2251
+ self.fail("Harness terminal reason is missing or truncated")
2252
+ else:
2253
+ self.reason = reason
2254
+ elif phase == "step_end" and "usage" in event:
2255
+ usage = event["usage"]
2256
+ if (
2257
+ not isinstance(usage, dict)
2258
+ or any(not isinstance(usage.get(key), int) or isinstance(usage.get(key), bool) or usage[key] < 0 for key in ("inputTokens", "outputTokens"))
2259
+ or any(not isinstance(value, int) or isinstance(value, bool) or value < 0 for value in usage.values())
2260
+ ):
2261
+ self.fail("Harness usage sample is invalid")
2262
+ else:
2263
+ self.usage_steps[(str(event.get("turn")), str(event.get("step")))] = usage
2264
+ elif phase == "step_end":
2265
+ self.usage_incomplete = True
2266
+ return
2267
+ if event_type == "final":
2268
+ final_text = event.get("text")
2269
+ if not isinstance(final_text, str) or self.reason is None or event.get("truncated"):
2270
+ self.fail("Harness final answer is missing or arrived before its terminal reason")
2271
+ elif len(final_text.encode("utf-8")) > DEEPSEEK_HARNESS_MAX_OUTPUT_BYTES:
2272
+ self.fail("Harness final answer exceeds the allowed size", "deepseek_harness_output_limit")
2273
+ else:
2274
+ self.text = final_text
2275
+ self.saw_final = True
2276
+ return
2277
+ if event_type in {"text", "thinking"}:
2278
+ if not isinstance(event.get("text"), str) and not event.get("truncated"):
2279
+ self.fail("Harness text event is missing its text")
2280
+ elif event_type in {"tool_call", "tool_result"}:
2281
+ call_id = event.get("callId")
2282
+ if not isinstance(call_id, str) or not call_id.strip() or len(call_id) > 256 or "\0" in call_id:
2283
+ self.fail("Harness tool event is missing a valid native callId")
2284
+ elif event_type == "tool_call" and not isinstance(event.get("tool"), str):
2285
+ self.fail("Harness tool call is missing its tool name")
2286
+ elif event_type == "tool_result" and event.get("status") not in {"completed", "error"}:
2287
+ self.fail("Harness tool result has an unsupported status")
2288
+ else:
2289
+ self.fail("Harness emitted an unsupported event type")
2290
+
2291
+ def finish(self, exit_code: int, *, cancelled: bool = False) -> dict:
2292
+ if not cancelled and self.error is None:
2293
+ if not self.started or self.reason is None or not self.saw_final:
2294
+ self.fail("Harness exited without a complete turn and final answer")
2295
+ elif self.reason.get("kind") != "completed":
2296
+ detail = self.reason.get("error") or {}
2297
+ self.fail(
2298
+ str(detail.get("message") or f"Harness turn ended: {self.reason['kind']}"),
2299
+ str(detail.get("code") or "deepseek_harness_turn_failed"),
2300
+ )
2301
+ elif exit_code != 0:
2302
+ self.fail(f"Harness exited with code {exit_code}", "agent_exit_code")
2303
+ metrics = {}
2304
+ for native, normalized in (
2305
+ ("inputTokens", "input_tokens"), ("outputTokens", "output_tokens"),
2306
+ ("cacheReadTokens", "cache_read_tokens"), ("cacheWriteTokens", "cache_write_tokens"),
2307
+ ("reasoningTokens", "reasoning_tokens"),
2308
+ ):
2309
+ if not self.usage_incomplete and self.usage_steps and all(native in sample for sample in self.usage_steps.values()):
2310
+ metrics[normalized] = sum(sample[native] for sample in self.usage_steps.values())
2311
+ return {
2312
+ "status": "cancelled" if cancelled else "failed" if self.error else "success",
2313
+ "text": self.text, "summary": self.text[-2000:],
2314
+ "session_id": self.session_id,
2315
+ "error": None if cancelled else self.error,
2316
+ "failure_code": None if cancelled else self.failure_code,
2317
+ "is_error": not cancelled and self.error is not None,
2318
+ "metrics": metrics,
2319
+ }
2320
+
2321
+
2322
+ def build_deepseek_harness_command(
2323
+ binary: str,
2324
+ *,
2325
+ overlay_path: str | Path,
2326
+ session_id: str | None = None,
2327
+ ) -> list[str]:
2328
+ """Build a headless JSONL invocation whose prompt arrives through stdin."""
2329
+ command = [
2330
+ binary, "--profile", "headless", "--patch", str(overlay_path), "--json",
2331
+ ]
2332
+ if session_id is not None:
2333
+ if not session_id.strip():
2334
+ raise ValueError("DeepSeek Harness session ID must not be blank")
2335
+ command += ["--session-id", session_id]
2336
+ return command
2337
+
2338
+
2339
+ def _deepseek_harness_package_roots(binary: str) -> list[Path]:
2340
+ resolved = Path(binary).resolve()
2341
+ roots = []
2342
+ for name in ("dsh", "dsh-headless", "dsh-base"):
2343
+ candidates = []
2344
+ for parent in (resolved.parent, *list(resolved.parents)[:6]):
2345
+ candidates.extend((parent, parent / "node_modules" / "@deepseek-ai" / name, parent / "@deepseek-ai" / name))
2346
+ for candidate in candidates:
2347
+ manifest = candidate / "package.json"
2348
+ try:
2349
+ metadata = json.loads(manifest.read_text(encoding="utf-8"))
2350
+ except (OSError, ValueError):
2351
+ continue
2352
+ if isinstance(metadata, dict) and metadata.get("name") == f"@deepseek-ai/{name}":
2353
+ if metadata.get("version") != DEEPSEEK_HARNESS_VERSION:
2354
+ raise RuntimeError(f"DeepSeek Harness requires the verified {DEEPSEEK_HARNESS_VERSION} package set")
2355
+ roots.append(candidate.resolve())
2356
+ break
2357
+ else:
2358
+ raise RuntimeError(f"Could not locate the installed @deepseek-ai/{name} package")
2359
+ return roots
2360
+
2361
+
2362
+ def deepseek_harness_probe_env() -> dict[str, str]:
2363
+ allowed = {"PATH", "HOME", "USERPROFILE", "SYSTEMROOT", "WINDIR", "COMSPEC", "PATHEXT", "TEMP", "TMP", "LANG", "LC_ALL"}
2364
+ return {key: value for key, value in os.environ.items() if key.upper() in allowed}
2365
+
2366
+
2367
+ def deepseek_harness_build_info(binary: str) -> dict:
2368
+ roots = _deepseek_harness_package_roots(binary)
2369
+ node = shutil.which("node")
2370
+ if not node:
2371
+ raise RuntimeError("DeepSeek Harness requires Node.js")
2372
+ result = subprocess.run([node, "-p", "process.versions.node"], capture_output=True, text=True, timeout=10, env=deepseek_harness_probe_env())
2373
+ node_version = result.stdout.strip()
2374
+ parsed = _parse_semver(node_version)
2375
+ 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):
2376
+ raise RuntimeError("DeepSeek Harness requires Node 22.19+ or Node 24+")
2377
+ digest = hashlib.sha256()
2378
+ digest.update(f"{sys.platform}:{platform.machine()}:{platform.release()}:{node_version}:harness-policy-2".encode())
2379
+ extensions = [manifest.parent for manifest in (roots[0] / "node_modules" / "@deepseek-ai").glob("dsh*/package.json")]
2380
+ for root in sorted(set([*roots, *extensions])):
2381
+ files = [root / "package.json"] + sorted((root / "lib").rglob("*.js"))
2382
+ if (root / "cordis.patch.yml").is_file():
2383
+ files.append(root / "cordis.patch.yml")
2384
+ for filename in files:
2385
+ digest.update(str(filename.relative_to(root)).encode())
2386
+ digest.update(filename.read_bytes())
2387
+ return {"version": DEEPSEEK_HARNESS_VERSION, "fingerprint": digest.hexdigest(), "node_version": node_version, "package_root": str(roots[0])}
2388
+
2389
+
2390
+ def deepseek_harness_verification_issue(binary: str) -> str | None:
2391
+ try:
2392
+ information = deepseek_harness_build_info(binary)
2393
+ if sys.platform not in {"darwin", "linux"}:
2394
+ return "DeepSeek Harness outer isolation is not verified on this platform"
2395
+ required = "sandbox-exec" if sys.platform == "darwin" else "bwrap"
2396
+ if not shutil.which(required):
2397
+ return f"DeepSeek Harness requires the {required} outer sandbox"
2398
+ approved = json.loads(DEEPSEEK_HARNESS_APPROVAL_FILE.read_text(encoding="utf-8"))
2399
+ if (
2400
+ isinstance(approved, dict) and approved.get("version") == 1
2401
+ and isinstance(approved.get("builds"), list)
2402
+ and information["fingerprint"] in approved["builds"]
2403
+ ):
2404
+ return None
2405
+ except (OSError, ValueError, RuntimeError, subprocess.SubprocessError):
2406
+ pass
2407
+ return (
2408
+ "DeepSeek Harness requires live start/resume/cancel/isolation acceptance "
2409
+ "for this installed build; then run `forgexa agent approve-live deepseek`"
2410
+ )
2411
+
2412
+
2413
+ def approve_deepseek_harness_build(binary: str) -> dict:
2414
+ information = deepseek_harness_build_info(binary)
2415
+ DEEPSEEK_HARNESS_APPROVAL_FILE.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
2416
+ document = {"version": 1, "builds": [information["fingerprint"]], "runtime_version": information["version"]}
2417
+ with tempfile.NamedTemporaryFile("w", encoding="utf-8", dir=DEEPSEEK_HARNESS_APPROVAL_FILE.parent, delete=False) as temporary:
2418
+ json.dump(document, temporary)
2419
+ temporary_path = Path(temporary.name)
2420
+ try:
2421
+ if sys.platform != "win32":
2422
+ temporary_path.chmod(0o600)
2423
+ os.replace(temporary_path, DEEPSEEK_HARNESS_APPROVAL_FILE)
2424
+ finally:
2425
+ temporary_path.unlink(missing_ok=True)
2426
+ return information
2427
+
2428
+
2429
+ def deepseek_harness_base_env(env: dict[str, str], *, home: str | Path, temporary_root: str | Path) -> dict[str, str]:
2430
+ allowed = {
2431
+ "PATH", "HOME", "USER", "LOGNAME", "USERPROFILE", "HOMEDRIVE", "HOMEPATH",
2432
+ "SYSTEMROOT", "WINDIR", "COMSPEC", "APPDATA", "LOCALAPPDATA", "LANG", "LC_ALL",
2433
+ "LC_CTYPE", "TERM", "HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY", "NO_PROXY",
2434
+ "NODE_EXTRA_CA_CERTS", "SSL_CERT_FILE", "SSL_CERT_DIR", "DEEPSEEK_API_KEY",
2435
+ }
2436
+ controlled = {name: value for name, value in env.items() if name.upper() in allowed}
2437
+ controlled.update({
2438
+ "DSH_HOME": str(home), "DSH_PERMISSION_MODE": "workspace-write",
2439
+ "DSH_TELEMETRY_DISABLED": "1", "DSH_TOOLS_MODE": "native",
2440
+ "TMPDIR": str(temporary_root), "TMP": str(temporary_root), "TEMP": str(temporary_root),
2441
+ "DEEPSEEK_BASE_URL": "https://api.deepseek.com/anthropic",
2442
+ "GIT_CONFIG_GLOBAL": os.devnull, "GIT_CONFIG_NOSYSTEM": "1",
2443
+ "GIT_TERMINAL_PROMPT": "0", "GIT_OPTIONAL_LOCKS": "0",
2444
+ })
2445
+ return controlled
2446
+
2447
+
2448
+ def deepseek_harness_configuration(binary: str, credentials_path: Path, env: dict[str, str]) -> dict:
2449
+ package_root = _deepseek_harness_package_roots(binary)[0]
2450
+ script = """
2451
+ const fs = require('node:fs');
2452
+ const path = require('node:path');
2453
+ const {createRequire} = require('node:module');
2454
+ try {
2455
+ const localRequire = createRequire(path.join(process.argv[1], 'package.json'));
2456
+ const yaml = localRequire('js-yaml');
2457
+ const document = fs.existsSync(process.argv[2]) ? yaml.load(fs.readFileSync(process.argv[2], 'utf8')) : {};
2458
+ const apiKey = Boolean(process.env.DEEPSEEK_API_KEY || document?.refs?.DEEPSEEK_API_KEY);
2459
+ const account = Object.entries(document?.records || {}).some(([name, entry]) => name.includes('deepseek') && entry?.kind === 'grant');
2460
+ console.log(JSON.stringify({provider: apiKey ? 'deepseek-official' : account ? 'deepseek-account' : null}));
2461
+ } catch {
2462
+ console.log(JSON.stringify({error: 'Harness credentials could not be validated'}));
2463
+ }
2464
+ """
2465
+ result = subprocess.run(
2466
+ [shutil.which("node") or "node", "-e", script, str(package_root), str(credentials_path)],
2467
+ capture_output=True, text=True, timeout=10, env=env,
2468
+ )
2469
+ if result.returncode or len(result.stdout) > 4096:
2470
+ raise RuntimeError("Harness credential inspection failed")
2471
+ metadata = json.loads(result.stdout)
2472
+ if metadata.get("error") or metadata.get("provider") not in {"deepseek-official", "deepseek-account"}:
2473
+ raise RuntimeError("DeepSeek Harness requires an API key or an existing DeepSeek account login")
2474
+ return metadata
2475
+
2476
+
2477
+ def deepseek_harness_home(workspace_path: str | Path) -> Path:
2478
+ root = str(Path(workspace_path).expanduser().resolve())
2479
+ key = hashlib.sha256(root.encode("utf-8")).hexdigest()
2480
+ return Path.home() / ".forgexa" / "agents" / "deepseek-harness" / key
2481
+
2482
+
2483
+ def deepseek_harness_profile_lock_path(workspace_path: str | Path) -> Path:
2484
+ return deepseek_harness_home(workspace_path) / "profile.lock"
2485
+
2486
+
2487
+ def deepseek_harness_overlay(*, cwd: Path, credentials_path: Path, provider: str, model: str) -> list[dict]:
2488
+ if not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._:/-]{0,199}", model):
2489
+ raise ValueError("Invalid DeepSeek Harness model ID")
2490
+ rows = [
2491
+ {"id": "agent-default-model", "config": {"provider": provider, "model": model, "reasoningEffort": "high"}},
2492
+ {"id": "credentials", "config": {"path": str(credentials_path), "watch": False}},
2493
+ {"id": "approval", "config": {"policy": "never"}},
2494
+ {"id": "permission", "config": {
2495
+ "presets": {"forgexa-workspace": {"sandbox": "workspace-write", "approval": "never"}},
2496
+ "defaultPreset": "forgexa-workspace",
2497
+ }},
2498
+ {"id": "sandbox-policy", "config": {"mode": "workspace-write", "workspaceRoot": str(cwd)}},
2499
+ {"id": "fs-sandbox", "config": {"cwd": str(cwd)}},
2500
+ {"id": "tools", "config": {"mode": "native"}},
2501
+ {"id": "session-log-deepseek", "config": {"enabled": False}},
2502
+ {"id": "plugin-package-inventory-deepseek", "config": {"enabled": False}},
2503
+ ]
2504
+ disabled = (
2505
+ "session-telemetry-otel", "hmr", "plugin-manager", "tool-plugin-manager",
2506
+ "llm-pi-ai", "skill-filesystem", "tool-skill", "tool-subagent", "tool-subagent-fork",
2507
+ "tool-subagent-control", "tool-subagent-list-agents", "tool-workflow",
2508
+ "goal-round-driver", "command-goal", "tool-goal", "command-feedback", "tool-web",
2509
+ )
2510
+ rows.extend({"id": name, "disabled": True} for name in disabled)
2511
+ rows.extend([
2512
+ {"id": "deepseek-account", "disabled": provider != "deepseek-account"},
2513
+ {"id": "llm-deepseek-account", "disabled": provider != "deepseek-account"},
2514
+ {"id": "llm-deepseek", "disabled": provider != "deepseek-official", "config": {
2515
+ "apiKeyEnv": "DEEPSEEK_API_KEY", "baseURL": "https://api.deepseek.com/anthropic",
2516
+ "reasoningEffort": "high", "maxTokens": 32768,
2517
+ }},
2518
+ ])
2519
+ return rows
2520
+
2521
+
2522
+ def _deepseek_harness_git_roots(cwd: Path) -> list[Path]:
2523
+ roots = [cwd / ".git"]
2524
+ if roots[0].is_file():
2525
+ content = roots[0].read_text(encoding="utf-8")[:4096]
2526
+ if content.startswith("gitdir:"):
2527
+ git_dir = (cwd / content[7:].strip()).resolve()
2528
+ roots.append(git_dir)
2529
+ common = git_dir / "commondir"
2530
+ if common.is_file():
2531
+ roots.append((git_dir / common.read_text(encoding="utf-8").strip()).resolve())
2532
+ return roots
2533
+
2534
+
2535
+ def deepseek_harness_sandbox_command(command: list[str], *, cwd: Path, home: Path, temporary_root: Path, credentials_path: Path | None = None) -> list[str]:
2536
+ git_roots = [*_deepseek_harness_git_roots(cwd), home / "forgexa-session-bindings"]
2537
+ if credentials_path is not None:
2538
+ git_roots.append(credentials_path)
2539
+ if sys.platform == "darwin":
2540
+ sandbox = shutil.which("sandbox-exec")
2541
+ if not sandbox:
2542
+ raise RuntimeError("DeepSeek Harness outer sandbox is unavailable")
2543
+ writable = " ".join(f"(subpath {json.dumps(str(root.resolve()))})" for root in (cwd, home, temporary_root))
2544
+ protected = " ".join(f"(subpath {json.dumps(str(root.resolve()))})" for root in git_roots)
2545
+ profile = f'(version 1) (allow default) (deny file-write*) (allow file-write* {writable} (literal "/dev/null")) (deny file-write* {protected})'
2546
+ return [sandbox, "-p", profile, *command]
2547
+ if sys.platform == "linux":
2548
+ sandbox = shutil.which("bwrap")
2549
+ if not sandbox:
2550
+ raise RuntimeError("DeepSeek Harness requires a working bubblewrap outer sandbox")
2551
+ wrapped = [sandbox, "--die-with-parent", "--unshare-pid", "--ro-bind", "/", "/", "--proc", "/proc", "--dev", "/dev"]
2552
+ for root in (cwd, home, temporary_root):
2553
+ wrapped += ["--bind", str(root), str(root)]
2554
+ for root in git_roots:
2555
+ if root.exists():
2556
+ wrapped += ["--ro-bind", str(root.resolve()), str(root.resolve())]
2557
+ return [*wrapped, "--chdir", str(cwd), "--", *command]
2558
+ raise RuntimeError("DeepSeek Harness outer isolation has not been verified on this platform")
2559
+
2560
+
2561
+ @contextmanager
2562
+ def deepseek_harness_runtime(binary: str, workspace_path: str | Path, *, model: str | None = None, session_id: str | None = None):
2563
+ binary = os.path.abspath(shutil.which(binary) or os.path.expanduser(binary))
2564
+ cwd = Path(workspace_path).expanduser().resolve()
2565
+ home = deepseek_harness_home(cwd)
2566
+ home.mkdir(parents=True, exist_ok=True, mode=0o700)
2567
+ if sys.platform != "win32":
2568
+ home.chmod(0o700)
2569
+ lock = acquire_file_lock(deepseek_harness_profile_lock_path(cwd))
2570
+ if lock is None:
2571
+ raise RuntimeError("DeepSeek Harness profile is busy with another local turn")
2572
+ try:
2573
+ bindings = home / "forgexa-session-bindings"
2574
+ bindings.mkdir(exist_ok=True, mode=0o700)
2575
+ profile = home / "profiles" / "headless"
2576
+ for untrusted in (home / "cordis.patch.yml", profile / "node_modules", home / "profiles" / "node_modules"):
2577
+ if untrusted.exists() or untrusted.is_symlink():
2578
+ raise RuntimeError("The managed Harness home must not contain user patches or plugin overrides")
2579
+ patch = profile / "cordis.patch.yml"
2580
+ if patch.is_file():
2581
+ effective_lines = [
2582
+ line.strip() for line in patch.read_text(encoding="utf-8").splitlines()
2583
+ if line.strip() and not line.lstrip().startswith("#")
2584
+ ]
2585
+ if effective_lines not in ([], ["[]"]):
2586
+ raise RuntimeError("The managed Harness profile must not contain user patches")
2587
+ manifest = profile / "package.json"
2588
+ if manifest.is_file():
2589
+ bundles = json.loads(manifest.read_text(encoding="utf-8")).get("dsh", {}).get("profile", {}).get("bundles")
2590
+ if bundles != ["@deepseek-ai/dsh-base", "@deepseek-ai/dsh-headless"]:
2591
+ raise RuntimeError("The managed Harness profile contains an unsupported bundle")
2592
+ with tempfile.TemporaryDirectory(prefix="run-", dir=home) as run_dir:
2593
+ temporary_root = Path(run_dir).resolve()
2594
+ env = deepseek_harness_base_env(os.environ.copy(), home=home, temporary_root=temporary_root)
2595
+ source_home = Path(os.environ.get("DSH_HOME", str(Path.home() / ".dsh"))).expanduser().resolve()
2596
+ credentials = Path(os.environ.get("FACTORY_DEEPSEEK_HARNESS_CREDENTIALS", str(source_home / ".credentials.yaml"))).expanduser().resolve()
2597
+ configuration = deepseek_harness_configuration(binary, credentials, env)
2598
+ selected_model = model or os.environ.get("FACTORY_DEEPSEEK_HARNESS_MODEL", "").strip() or "deepseek-flash"
2599
+ overlay = temporary_root / "run.patch.json"
2600
+ composition = deepseek_harness_overlay(cwd=cwd, credentials_path=credentials, provider=configuration["provider"], model=selected_model)
2601
+ identity = hashlib.sha256(json.dumps({"build": deepseek_harness_build_info(binary)["fingerprint"], "overlay": composition}, sort_keys=True).encode()).hexdigest()
2602
+ env["FORGEXA_DEEPSEEK_HARNESS_COMPOSITION"] = identity
2603
+ if session_id:
2604
+ binding = bindings / (hashlib.sha256(session_id.encode()).hexdigest() + ".json")
2605
+ if not binding.is_file() or json.loads(binding.read_text(encoding="utf-8")).get("composition") != identity:
2606
+ raise RuntimeError("Native Harness session configuration changed or is not owned by Forgexa; start a new session")
2607
+ overlay.write_text(json.dumps(composition), encoding="utf-8")
2608
+ command = build_deepseek_harness_command(binary, overlay_path=overlay, session_id=session_id)
2609
+ yield deepseek_harness_sandbox_command(command, cwd=cwd, home=home, temporary_root=temporary_root, credentials_path=credentials), env
2610
+ finally:
2611
+ release_file_lock(lock)
2612
+
2613
+
2614
+ async def run_deepseek_harness(
2615
+ binary: str,
2616
+ prompt: str,
2617
+ workspace_path: str | Path,
2618
+ *,
2619
+ timeout: float | None = 300,
2620
+ model: str | None = None,
2621
+ session_id: str | None = None,
2622
+ on_event: Any = None,
2623
+ on_started: Any = None,
2624
+ on_chunk: Any = None,
2625
+ cancel_requested: Any = None,
2626
+ ) -> dict:
2627
+ """Execute and validate a supervised one-shot process with bounded output."""
2628
+ process = None
2629
+ state = DeepSeekHarnessRunState(expected_cwd=str(workspace_path))
2630
+ raw_lines = []
2631
+ raw_bytes = 0
2632
+ stderr_text = ""
2633
+ runtime = deepseek_harness_runtime(binary, workspace_path, model=model, session_id=session_id)
2634
+ entered = False
2635
+ try:
2636
+ command, env = await asyncio.to_thread(runtime.__enter__)
2637
+ entered = True
2638
+ process = await asyncio.create_subprocess_exec(
2639
+ *_prepare_agent_command(command), cwd=str(workspace_path), env=env,
2640
+ stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE,
2641
+ stderr=asyncio.subprocess.PIPE, limit=DEEPSEEK_HARNESS_MAX_OUTPUT_BYTES,
2642
+ **_agent_process_group_kwargs(),
2643
+ )
2644
+ if on_started is not None:
2645
+ on_started(process)
2646
+ if cancel_requested is not None and cancel_requested():
2647
+ _kill_proc(process)
2648
+
2649
+ async def read_stderr() -> str:
2650
+ tail = ""
2651
+ while True:
2652
+ chunk = await process.stderr.read(4096)
2653
+ if not chunk:
2654
+ return tail
2655
+ tail = (tail + chunk.decode("utf-8", errors="replace"))[-10000:]
2656
+
2657
+ async def collect() -> str:
2658
+ nonlocal raw_bytes
2659
+ stderr_task = asyncio.create_task(read_stderr())
2660
+ try:
2661
+ process.stdin.write(prompt.encode("utf-8"))
2662
+ await process.stdin.drain()
2663
+ process.stdin.close()
2664
+ while True:
2665
+ try:
2666
+ encoded = await process.stdout.readline()
2667
+ except (ValueError, asyncio.LimitOverrunError):
2668
+ state.fail("Harness output exceeds the allowed size", "deepseek_harness_output_limit")
2669
+ _kill_proc(process)
2670
+ break
2671
+ if not encoded:
2672
+ break
2673
+ raw_bytes += len(encoded)
2674
+ if raw_bytes > DEEPSEEK_HARNESS_MAX_OUTPUT_BYTES:
2675
+ state.fail("Harness output exceeds the allowed size", "deepseek_harness_output_limit")
2676
+ _kill_proc(process)
2677
+ break
2678
+ try:
2679
+ line = encoded.decode("utf-8").strip()
2680
+ except UnicodeDecodeError:
2681
+ state.fail("Harness output contains invalid UTF-8")
2682
+ _kill_proc(process)
2683
+ break
2684
+ if not line:
2685
+ continue
2686
+ raw_lines.append(line[-10000:])
2687
+ raw_lines[:] = raw_lines[-20:]
2688
+ event = state.feed_line(line)
2689
+ if state.error:
2690
+ _kill_proc(process)
2691
+ break
2692
+ if event is not None and event.get("type") == "session" and env.get("FORGEXA_DEEPSEEK_HARNESS_COMPOSITION"):
2693
+ binding = deepseek_harness_home(workspace_path) / "forgexa-session-bindings" / (hashlib.sha256(state.session_id.encode()).hexdigest() + ".json")
2694
+ with tempfile.NamedTemporaryFile("w", encoding="utf-8", dir=binding.parent, delete=False) as temporary:
2695
+ json.dump({"composition": env["FORGEXA_DEEPSEEK_HARNESS_COMPOSITION"]}, temporary)
2696
+ temporary_path = Path(temporary.name)
2697
+ try:
2698
+ os.replace(temporary_path, binding)
2699
+ finally:
2700
+ temporary_path.unlink(missing_ok=True)
2701
+ if on_chunk is not None:
2702
+ await on_chunk(line)
2703
+ if event is not None and on_event is not None:
2704
+ await on_event(event)
2705
+ await process.wait()
2706
+ return await stderr_task
2707
+ finally:
2708
+ if not stderr_task.done():
2709
+ stderr_task.cancel()
2710
+ await asyncio.gather(stderr_task, return_exceptions=True)
2711
+
2712
+ try:
2713
+ stderr_text = await asyncio.wait_for(collect(), timeout=timeout)
2714
+ except asyncio.TimeoutError:
2715
+ state.fail(f"Harness timed out after {timeout:g}s", "agent_timeout")
2716
+ except (BrokenPipeError, ConnectionResetError):
2717
+ state.fail("Harness closed the input pipe before accepting its task")
2718
+ if process.returncode is None:
2719
+ _kill_proc(process)
2720
+ await process.wait()
2721
+ except asyncio.CancelledError:
2722
+ if process is not None and process.returncode is None:
2723
+ _kill_proc(process)
2724
+ await process.wait()
2725
+ raise
2726
+ except Exception as error:
2727
+ message = str(error)
2728
+ code = "deepseek_harness_profile_busy" if "profile is busy" in message else "deepseek_harness_startup_failed"
2729
+ state.fail(message, code)
2730
+ finally:
2731
+ if process is not None and process.returncode is None:
2732
+ _kill_proc(process)
2733
+ await process.wait()
2734
+ if entered:
2735
+ await asyncio.to_thread(runtime.__exit__, None, None, None)
2736
+ cancelled = bool(cancel_requested is not None and cancel_requested())
2737
+ result = state.finish(process.returncode if process is not None else -1, cancelled=cancelled)
2738
+ return {**result, "exit_code": process.returncode if process is not None else -1,
2739
+ "stderr": stderr_text, "raw_tail": "\n".join(raw_lines)}
2740
+
2741
+
2114
2742
  def mimo_base_env(env: dict[str, str]) -> dict[str, str]:
2115
2743
  """Return the constrained MiMo Code environment used by AgentHost turns."""
2116
2744
  env = dict(env)
@@ -2506,6 +3134,9 @@ def agent_live_gate_approved(agent_id: str, env: dict[str, str] | None = None) -
2506
3134
 
2507
3135
  def agent_execution_verification_issue(agent_id: str, binary: str) -> str | None:
2508
3136
  """Return a fail-closed build/live-acceptance gate for local agents."""
3137
+ agent_id = canonical_agent_id(agent_id)
3138
+ if agent_id == "deepseek":
3139
+ return deepseek_harness_verification_issue(binary)
2509
3140
  if agent_id == "zcode":
2510
3141
  build_issue = zcode_build_verification_issue(binary)
2511
3142
  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.1"
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_core.canonical_agent_id(agent.agent_id) == "deepseek":
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_core.canonical_agent_id(agent.agent_id) == "deepseek" 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": "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",
@@ -13133,13 +13167,15 @@ class RuntimeDaemon:
13133
13167
  self, current_agent_id: str, fallback_chain: list[str] | None, tried: set[str]
13134
13168
  ) -> DiscoveredAgent | None:
13135
13169
  """Find the next untried agent from fallback chain or available agents."""
13170
+ tried = {agent_core.canonical_agent_id(agent_id) for agent_id in tried}
13136
13171
  # 1. Try fallback chain
13137
13172
  if fallback_chain:
13138
13173
  for fallback_id in fallback_chain:
13174
+ fallback_id = agent_core.canonical_agent_id(fallback_id)
13139
13175
  if fallback_id in tried:
13140
13176
  continue
13141
13177
  for agent in self.agents:
13142
- if agent.agent_id == fallback_id:
13178
+ if agent_core.canonical_agent_id(agent.agent_id) == fallback_id:
13143
13179
  return agent
13144
13180
 
13145
13181
  # 2. Try any available agent not yet tried (prefer opencode > copilot > gemini > claude)
@@ -13155,8 +13191,8 @@ class RuntimeDaemon:
13155
13191
  # become implicit catch-alls because their execution profiles are
13156
13192
  # tightly managed.
13157
13193
  for agent in self.agents:
13158
- if agent.agent_id not in tried and agent.agent_id not in {
13159
- "mimo", "qwen", "codebuddy", "qoder", "trae", "zcode",
13194
+ if agent_core.canonical_agent_id(agent.agent_id) not in tried and agent_core.canonical_agent_id(agent.agent_id) not in {
13195
+ "mimo", "qwen", "codebuddy", "qoder", "trae", "zcode", "deepseek",
13160
13196
  }:
13161
13197
  return agent
13162
13198
 
@@ -13174,18 +13210,20 @@ class RuntimeDaemon:
13174
13210
  custom identifier). This prevents silent substitution when the user explicitly
13175
13211
  selected an agent that is just not installed locally.
13176
13212
  """
13213
+ agent_type = agent_core.canonical_agent_id(agent_type)
13177
13214
  # 1. Exact match
13178
13215
  for agent in self.agents:
13179
- if agent.agent_id == agent_type:
13216
+ if agent_core.canonical_agent_id(agent.agent_id) == agent_type:
13180
13217
  return agent
13181
13218
 
13182
13219
  # 2. Try fallback chain from policy
13183
13220
  if fallback_chain:
13184
13221
  for fallback_id in fallback_chain:
13222
+ fallback_id = agent_core.canonical_agent_id(fallback_id)
13185
13223
  if fallback_id == agent_type:
13186
13224
  continue
13187
13225
  for agent in self.agents:
13188
- if agent.agent_id == fallback_id:
13226
+ if agent_core.canonical_agent_id(agent.agent_id) == fallback_id:
13189
13227
  logger.info("Using fallback agent '%s' (requested '%s')", fallback_id, agent_type)
13190
13228
  return agent
13191
13229
 
@@ -13205,8 +13243,8 @@ class RuntimeDaemon:
13205
13243
  return next(
13206
13244
  (
13207
13245
  agent for agent in self.agents
13208
- if agent.agent_id not in {
13209
- "mimo", "qwen", "codebuddy", "qoder", "trae", "zcode",
13246
+ if agent_core.canonical_agent_id(agent.agent_id) not in {
13247
+ "mimo", "qwen", "codebuddy", "qoder", "trae", "zcode", "deepseek",
13210
13248
  }
13211
13249
  ),
13212
13250
  None,
@@ -14590,7 +14628,7 @@ class RuntimeDaemon:
14590
14628
  # string so _select_agent falls through to its "any available L3
14591
14629
  # agent" logic instead of hard-failing on the 'claude' default.
14592
14630
  _AGENT_ALIASES = {"claude": "claude", "kimi": "kimi"}
14593
- agent_type = _AGENT_ALIASES.get(agent_override or "", agent_override or "")
14631
+ agent_type = agent_core.canonical_agent_id(_AGENT_ALIASES.get(agent_override or "", agent_override or ""))
14594
14632
  agent = self._select_agent(agent_type, [])
14595
14633
  if not agent:
14596
14634
  _INSTALL_HINTS = {
@@ -14598,6 +14636,7 @@ class RuntimeDaemon:
14598
14636
  "codex": "npm install -g @openai/codex",
14599
14637
  "opencode": "curl -sSL https://opencode.ai/install | sh",
14600
14638
  "mimo": "curl -fsSL https://mimo.xiaomi.com/install | bash",
14639
+ "deepseek": "npm i -g @deepseek-ai/dsh@0.2.0-rc.2",
14601
14640
  "qwen": "npm install -g @qwen-code/qwen-code",
14602
14641
  "codebuddy": "npm install -g @tencent-ai/codebuddy-code",
14603
14642
  "qoder": "curl -fsSL https://qoder.com/install | bash",
@@ -3018,8 +3018,9 @@ def cmd_check(args: argparse.Namespace) -> None:
3018
3018
 
3019
3019
  os.environ.setdefault("DAEMON_NO_CONSOLE_LOG", "1")
3020
3020
  from forgexa_cli.daemon import AgentDiscovery
3021
+ from forgexa_cli.agent_core import canonical_agent_id
3021
3022
 
3022
- only_agent = getattr(args, "agent", None)
3023
+ only_agent = canonical_agent_id(getattr(args, "agent", None))
3023
3024
  quick = bool(getattr(args, "quick", False))
3024
3025
  environment_requested = bool(getattr(args, "environment", False))
3025
3026
  timeout = getattr(args, "timeout", None) or 90
@@ -3686,7 +3687,21 @@ def cmd_agent_approve_live(args: argparse.Namespace) -> None:
3686
3687
 
3687
3688
  from . import agent_core
3688
3689
 
3689
- agent_id = args.agent_id.strip().lower()
3690
+ agent_id = agent_core.canonical_agent_id(args.agent_id.strip().lower())
3691
+ if agent_id == "deepseek":
3692
+ binary = getattr(args, "binary", None) or os.environ.get("FACTORY_DEEPSEEK_HARNESS_PATH") or shutil.which("dsh")
3693
+ if not binary:
3694
+ print("error: dsh not found; install @deepseek-ai/dsh@0.2.0-rc.2 globally", file=sys.stderr)
3695
+ return
3696
+ try:
3697
+ information = agent_core.approve_deepseek_harness_build(binary)
3698
+ except (OSError, ValueError, RuntimeError, subprocess.SubprocessError) as exc:
3699
+ print(f"error: {exc}", file=sys.stderr)
3700
+ return
3701
+ print(f"DeepSeek Harness build approved: {information['fingerprint']}")
3702
+ print(f"runtime version: {information['version']}; Node: {information['node_version']}")
3703
+ print("This records your attestation, not an automatic live test. Only approve after start/resume/cancel/isolation acceptance.")
3704
+ return
3690
3705
  gated = set(agent_core._UNVERIFIED_AGENT_LIVE_GATE_REASONS)
3691
3706
  if agent_id not in gated:
3692
3707
  print(
@@ -3962,8 +3977,9 @@ def main() -> None:
3962
3977
  )
3963
3978
  agent_approve_live_p.add_argument(
3964
3979
  "agent_id", metavar="AGENT_ID",
3965
- help="Gated agent id (e.g. trae, zcode)",
3980
+ help="Gated agent id (e.g. trae, zcode, deepseek; legacy deepseek-harness accepted)",
3966
3981
  )
3982
+ agent_approve_live_p.add_argument("--binary", default=None, metavar="PATH", help="Installed Harness binary for build-scoped acceptance")
3967
3983
 
3968
3984
  # runtimes
3969
3985
  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.1
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`) 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` records an attestation; `forgexa check --agent deepseek --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.1"
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,35 @@ def _agent(agent_id: str, version: str = "1.0.0") -> daemon.DiscoveredAgent:
30
30
  )
31
31
 
32
32
 
33
+ @pytest.mark.parametrize("agent_id", ["deepseek", "deepseek-harness"])
34
+ def test_deepseek_harness_live_approval_is_build_scoped(tmp_path, monkeypatch, capsys, agent_id):
35
+ from forgexa_cli import agent_core
36
+
37
+ approval = tmp_path / "build-approved.json"
38
+ old_approval = tmp_path / "agent-approved"
39
+ monkeypatch.setattr(agent_core, "DEEPSEEK_HARNESS_APPROVAL_FILE", approval)
40
+ monkeypatch.setattr(agent_core, "AGENT_LIVE_VERIFIED_FILE", old_approval)
41
+ monkeypatch.setattr(agent_core, "deepseek_harness_build_info", lambda binary: {"fingerprint": "test-build", "version": "0.2.0-rc.2", "node_version": "25.2.1"})
42
+ main.cmd_agent_approve_live(SimpleNamespace(agent_id=agent_id, binary="/fake/dsh"))
43
+ assert json.loads(approval.read_text())["builds"] == ["test-build"]
44
+ assert not old_approval.exists()
45
+ assert "attestation" in capsys.readouterr().out
46
+
47
+
48
+ @pytest.mark.parametrize("agent_id", ["deepseek", "deepseek-harness"])
49
+ def test_deepseek_harness_quick_check_accepts_legacy_id(monkeypatch, capsys, agent_id):
50
+ monkeypatch.setattr(daemon.AgentDiscovery, "AGENT_REGISTRY", {"deepseek": {"commands": ["dsh"], "detect": "dsh --version"}})
51
+ monkeypatch.setattr(daemon.AgentDiscovery, "discover", AsyncMock(return_value=[_agent("deepseek", "0.2.0-rc.2")]))
52
+ execute = AsyncMock()
53
+ monkeypatch.setattr(daemon.ProcessManager, "run_agent", execute)
54
+ main.cmd_check(SimpleNamespace(agent=agent_id, quick=True, timeout=90))
55
+ output = capsys.readouterr().out
56
+ assert "deepseek" in output and "v0.2.0-rc.2" in output
57
+ assert "1/1 agent(s) available" in output
58
+ assert "deepseek-harness" not in output
59
+ execute.assert_not_called()
60
+
61
+
33
62
  def test_check_quick_reports_installed_and_missing_agents(
34
63
  monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str],
35
64
  ) -> None:
File without changes