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.
- {forgexa_cli-1.59.2 → forgexa_cli-1.60.1}/PKG-INFO +2 -2
- {forgexa_cli-1.59.2 → forgexa_cli-1.60.1}/README.md +1 -1
- {forgexa_cli-1.59.2 → forgexa_cli-1.60.1}/forgexa_cli/__init__.py +1 -1
- {forgexa_cli-1.59.2 → forgexa_cli-1.60.1}/forgexa_cli/agent_core.py +633 -2
- {forgexa_cli-1.59.2 → forgexa_cli-1.60.1}/forgexa_cli/daemon.py +50 -11
- {forgexa_cli-1.59.2 → forgexa_cli-1.60.1}/forgexa_cli/main.py +19 -3
- {forgexa_cli-1.59.2 → forgexa_cli-1.60.1}/forgexa_cli.egg-info/PKG-INFO +2 -2
- {forgexa_cli-1.59.2 → forgexa_cli-1.60.1}/pyproject.toml +1 -1
- {forgexa_cli-1.59.2 → forgexa_cli-1.60.1}/tests/test_check_command.py +29 -0
- {forgexa_cli-1.59.2 → forgexa_cli-1.60.1}/forgexa_cli/_build_config.py +0 -0
- {forgexa_cli-1.59.2 → forgexa_cli-1.60.1}/forgexa_cli/_local_bind.py +0 -0
- {forgexa_cli-1.59.2 → forgexa_cli-1.60.1}/forgexa_cli/autoupgrade.py +0 -0
- {forgexa_cli-1.59.2 → forgexa_cli-1.60.1}/forgexa_cli/py.typed +0 -0
- {forgexa_cli-1.59.2 → forgexa_cli-1.60.1}/forgexa_cli/runtime_evidence.py +0 -0
- {forgexa_cli-1.59.2 → forgexa_cli-1.60.1}/forgexa_cli/zcode_protocol.py +0 -0
- {forgexa_cli-1.59.2 → forgexa_cli-1.60.1}/forgexa_cli.egg-info/SOURCES.txt +0 -0
- {forgexa_cli-1.59.2 → forgexa_cli-1.60.1}/forgexa_cli.egg-info/dependency_links.txt +0 -0
- {forgexa_cli-1.59.2 → forgexa_cli-1.60.1}/forgexa_cli.egg-info/entry_points.txt +0 -0
- {forgexa_cli-1.59.2 → forgexa_cli-1.60.1}/forgexa_cli.egg-info/requires.txt +0 -0
- {forgexa_cli-1.59.2 → forgexa_cli-1.60.1}/forgexa_cli.egg-info/top_level.txt +0 -0
- {forgexa_cli-1.59.2 → forgexa_cli-1.60.1}/setup.cfg +0 -0
- {forgexa_cli-1.59.2 → forgexa_cli-1.60.1}/tests/test_auth_and_runtime_commands.py +0 -0
- {forgexa_cli-1.59.2 → forgexa_cli-1.60.1}/tests/test_autoupgrade.py +0 -0
- {forgexa_cli-1.59.2 → forgexa_cli-1.60.1}/tests/test_expiry_warnings_and_revoke.py +0 -0
- {forgexa_cli-1.59.2 → forgexa_cli-1.60.1}/tests/test_local_bind_commands.py +0 -0
- {forgexa_cli-1.59.2 → forgexa_cli-1.60.1}/tests/test_runtime_credentials.py +0 -0
- {forgexa_cli-1.59.2 → forgexa_cli-1.60.1}/tests/test_session_credentials.py +0 -0
- {forgexa_cli-1.59.2 → forgexa_cli-1.60.1}/tests/test_silent_install.py +0 -0
- {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.
|
|
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**
|
|
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**
|
|
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.
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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**
|
|
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
|
|
|
@@ -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
|
|
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
|