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.
- {forgexa_cli-1.59.2 → forgexa_cli-1.60.0}/PKG-INFO +2 -2
- {forgexa_cli-1.59.2 → forgexa_cli-1.60.0}/README.md +1 -1
- {forgexa_cli-1.59.2 → forgexa_cli-1.60.0}/forgexa_cli/__init__.py +1 -1
- {forgexa_cli-1.59.2 → forgexa_cli-1.60.0}/forgexa_cli/agent_core.py +604 -2
- {forgexa_cli-1.59.2 → forgexa_cli-1.60.0}/forgexa_cli/daemon.py +40 -5
- {forgexa_cli-1.59.2 → forgexa_cli-1.60.0}/forgexa_cli/main.py +16 -1
- {forgexa_cli-1.59.2 → forgexa_cli-1.60.0}/forgexa_cli.egg-info/PKG-INFO +2 -2
- {forgexa_cli-1.59.2 → forgexa_cli-1.60.0}/pyproject.toml +1 -1
- {forgexa_cli-1.59.2 → forgexa_cli-1.60.0}/tests/test_check_command.py +14 -0
- {forgexa_cli-1.59.2 → forgexa_cli-1.60.0}/forgexa_cli/_build_config.py +0 -0
- {forgexa_cli-1.59.2 → forgexa_cli-1.60.0}/forgexa_cli/_local_bind.py +0 -0
- {forgexa_cli-1.59.2 → forgexa_cli-1.60.0}/forgexa_cli/autoupgrade.py +0 -0
- {forgexa_cli-1.59.2 → forgexa_cli-1.60.0}/forgexa_cli/py.typed +0 -0
- {forgexa_cli-1.59.2 → forgexa_cli-1.60.0}/forgexa_cli/runtime_evidence.py +0 -0
- {forgexa_cli-1.59.2 → forgexa_cli-1.60.0}/forgexa_cli/zcode_protocol.py +0 -0
- {forgexa_cli-1.59.2 → forgexa_cli-1.60.0}/forgexa_cli.egg-info/SOURCES.txt +0 -0
- {forgexa_cli-1.59.2 → forgexa_cli-1.60.0}/forgexa_cli.egg-info/dependency_links.txt +0 -0
- {forgexa_cli-1.59.2 → forgexa_cli-1.60.0}/forgexa_cli.egg-info/entry_points.txt +0 -0
- {forgexa_cli-1.59.2 → forgexa_cli-1.60.0}/forgexa_cli.egg-info/requires.txt +0 -0
- {forgexa_cli-1.59.2 → forgexa_cli-1.60.0}/forgexa_cli.egg-info/top_level.txt +0 -0
- {forgexa_cli-1.59.2 → forgexa_cli-1.60.0}/setup.cfg +0 -0
- {forgexa_cli-1.59.2 → forgexa_cli-1.60.0}/tests/test_auth_and_runtime_commands.py +0 -0
- {forgexa_cli-1.59.2 → forgexa_cli-1.60.0}/tests/test_autoupgrade.py +0 -0
- {forgexa_cli-1.59.2 → forgexa_cli-1.60.0}/tests/test_expiry_warnings_and_revoke.py +0 -0
- {forgexa_cli-1.59.2 → forgexa_cli-1.60.0}/tests/test_local_bind_commands.py +0 -0
- {forgexa_cli-1.59.2 → forgexa_cli-1.60.0}/tests/test_runtime_credentials.py +0 -0
- {forgexa_cli-1.59.2 → forgexa_cli-1.60.0}/tests/test_session_credentials.py +0 -0
- {forgexa_cli-1.59.2 → forgexa_cli-1.60.0}/tests/test_silent_install.py +0 -0
- {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.
|
|
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**
|
|
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**
|
|
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.
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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**
|
|
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
|
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|