forgexa-cli 1.48.3__tar.gz → 1.49.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.48.3 → forgexa_cli-1.49.0}/PKG-INFO +1 -1
- {forgexa_cli-1.48.3 → forgexa_cli-1.49.0}/forgexa_cli/__init__.py +1 -1
- {forgexa_cli-1.48.3 → forgexa_cli-1.49.0}/forgexa_cli/agent_core.py +96 -0
- {forgexa_cli-1.48.3 → forgexa_cli-1.49.0}/forgexa_cli/daemon.py +111 -24
- {forgexa_cli-1.48.3 → forgexa_cli-1.49.0}/forgexa_cli.egg-info/PKG-INFO +1 -1
- {forgexa_cli-1.48.3 → forgexa_cli-1.49.0}/pyproject.toml +1 -1
- {forgexa_cli-1.48.3 → forgexa_cli-1.49.0}/README.md +0 -0
- {forgexa_cli-1.48.3 → forgexa_cli-1.49.0}/forgexa_cli/_build_config.py +0 -0
- {forgexa_cli-1.48.3 → forgexa_cli-1.49.0}/forgexa_cli/_local_bind.py +0 -0
- {forgexa_cli-1.48.3 → forgexa_cli-1.49.0}/forgexa_cli/autoupgrade.py +0 -0
- {forgexa_cli-1.48.3 → forgexa_cli-1.49.0}/forgexa_cli/main.py +0 -0
- {forgexa_cli-1.48.3 → forgexa_cli-1.49.0}/forgexa_cli/py.typed +0 -0
- {forgexa_cli-1.48.3 → forgexa_cli-1.49.0}/forgexa_cli/runtime_evidence.py +0 -0
- {forgexa_cli-1.48.3 → forgexa_cli-1.49.0}/forgexa_cli.egg-info/SOURCES.txt +0 -0
- {forgexa_cli-1.48.3 → forgexa_cli-1.49.0}/forgexa_cli.egg-info/dependency_links.txt +0 -0
- {forgexa_cli-1.48.3 → forgexa_cli-1.49.0}/forgexa_cli.egg-info/entry_points.txt +0 -0
- {forgexa_cli-1.48.3 → forgexa_cli-1.49.0}/forgexa_cli.egg-info/requires.txt +0 -0
- {forgexa_cli-1.48.3 → forgexa_cli-1.49.0}/forgexa_cli.egg-info/top_level.txt +0 -0
- {forgexa_cli-1.48.3 → forgexa_cli-1.49.0}/setup.cfg +0 -0
- {forgexa_cli-1.48.3 → forgexa_cli-1.49.0}/tests/test_auth_and_runtime_commands.py +0 -0
- {forgexa_cli-1.48.3 → forgexa_cli-1.49.0}/tests/test_autoupgrade.py +0 -0
- {forgexa_cli-1.48.3 → forgexa_cli-1.49.0}/tests/test_check_command.py +0 -0
- {forgexa_cli-1.48.3 → forgexa_cli-1.49.0}/tests/test_expiry_warnings_and_revoke.py +0 -0
- {forgexa_cli-1.48.3 → forgexa_cli-1.49.0}/tests/test_local_bind_commands.py +0 -0
- {forgexa_cli-1.48.3 → forgexa_cli-1.49.0}/tests/test_runtime_credentials.py +0 -0
- {forgexa_cli-1.48.3 → forgexa_cli-1.49.0}/tests/test_session_credentials.py +0 -0
- {forgexa_cli-1.48.3 → forgexa_cli-1.49.0}/tests/test_silent_install.py +0 -0
- {forgexa_cli-1.48.3 → forgexa_cli-1.49.0}/tests/test_upgrade_observability.py +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""forgexa-cli — Forgexa command-line client."""
|
|
2
|
-
__version__ = "1.
|
|
2
|
+
__version__ = "1.49.0"
|
|
@@ -319,6 +319,21 @@ class AgentDiscovery:
|
|
|
319
319
|
),
|
|
320
320
|
"upgrade_hint": "opencode upgrade",
|
|
321
321
|
},
|
|
322
|
+
"mimo": {
|
|
323
|
+
"commands": ["mimo"],
|
|
324
|
+
"detect": "mimo --version",
|
|
325
|
+
"invoke_modes": ["cli"],
|
|
326
|
+
"env_path_override": "FACTORY_MIMO_PATH",
|
|
327
|
+
"env_model_override": "FACTORY_MIMO_MODEL",
|
|
328
|
+
"compatibility_level": "L2",
|
|
329
|
+
"min_version": (0, 1, 14),
|
|
330
|
+
"min_version_reason": (
|
|
331
|
+
"The Runtime Daemon relies on the verified `mimo run --format json` "
|
|
332
|
+
"protocol and non-interactive permission flag introduced by MiMo Code "
|
|
333
|
+
"v0.1.14."
|
|
334
|
+
),
|
|
335
|
+
"upgrade_hint": "mimo upgrade",
|
|
336
|
+
},
|
|
322
337
|
"gemini": {
|
|
323
338
|
"commands": ["gemini"],
|
|
324
339
|
"detect": "gemini --version",
|
|
@@ -380,6 +395,7 @@ class AgentDiscovery:
|
|
|
380
395
|
extra_dirs += [
|
|
381
396
|
appdata / "npm", # npm -g installs
|
|
382
397
|
home / ".opencode" / "bin",
|
|
398
|
+
home / ".mimocode" / "bin",
|
|
383
399
|
home / ".cargo" / "bin",
|
|
384
400
|
home / ".bun" / "bin",
|
|
385
401
|
home / "scoop" / "shims", # scoop package manager
|
|
@@ -422,6 +438,7 @@ class AgentDiscovery:
|
|
|
422
438
|
extra_dirs += [
|
|
423
439
|
home / ".local" / "bin", # pip / pipx installs, Claude Code
|
|
424
440
|
home / ".opencode" / "bin", # opencode
|
|
441
|
+
home / ".mimocode" / "bin", # MiMo Code
|
|
425
442
|
home / ".cargo" / "bin", # Rust / cargo installs
|
|
426
443
|
home / ".bun" / "bin", # bun
|
|
427
444
|
home / ".volta" / "bin", # volta (node version manager)
|
|
@@ -658,6 +675,14 @@ class AgentDiscovery:
|
|
|
658
675
|
# ── Advisory file locks ──
|
|
659
676
|
|
|
660
677
|
|
|
678
|
+
def mimo_profile_lock_path() -> Path:
|
|
679
|
+
"""Return the machine-local lock shared by all MiMo Code processes."""
|
|
680
|
+
override = os.environ.get("FORGEXA_MIMO_PROFILE_LOCK", "").strip()
|
|
681
|
+
if override:
|
|
682
|
+
return Path(override)
|
|
683
|
+
return Path.home() / ".forgexa" / "locks" / "mimo-profile.lock"
|
|
684
|
+
|
|
685
|
+
|
|
661
686
|
def acquire_file_lock(
|
|
662
687
|
path: str | Path,
|
|
663
688
|
*,
|
|
@@ -1616,6 +1641,77 @@ def build_opencode_command(
|
|
|
1616
1641
|
return cmd
|
|
1617
1642
|
|
|
1618
1643
|
|
|
1644
|
+
def build_mimo_command(
|
|
1645
|
+
binary: str,
|
|
1646
|
+
*,
|
|
1647
|
+
cwd: str | Path,
|
|
1648
|
+
model: str | None = None,
|
|
1649
|
+
prompt_file: str | Path | None = None,
|
|
1650
|
+
session_id: str | None = None,
|
|
1651
|
+
prompt: str | None = None,
|
|
1652
|
+
) -> list[str]:
|
|
1653
|
+
"""MiMo Code CLI non-interactive invocation for the local AgentHost."""
|
|
1654
|
+
cmd = [
|
|
1655
|
+
binary, "run",
|
|
1656
|
+
"--format", "json",
|
|
1657
|
+
"--dangerously-skip-permissions",
|
|
1658
|
+
"--dir", str(cwd),
|
|
1659
|
+
]
|
|
1660
|
+
if session_id:
|
|
1661
|
+
cmd += ["--session", session_id]
|
|
1662
|
+
if model:
|
|
1663
|
+
cmd += ["--model", model]
|
|
1664
|
+
if prompt_file is not None:
|
|
1665
|
+
cmd += [
|
|
1666
|
+
"--file", str(prompt_file),
|
|
1667
|
+
"--", "Read and follow the attached instructions.",
|
|
1668
|
+
]
|
|
1669
|
+
elif prompt is not None:
|
|
1670
|
+
cmd += ["--", prompt]
|
|
1671
|
+
return cmd
|
|
1672
|
+
|
|
1673
|
+
|
|
1674
|
+
def mimo_base_env(env: dict[str, str]) -> dict[str, str]:
|
|
1675
|
+
"""Return the constrained MiMo Code environment used by AgentHost turns."""
|
|
1676
|
+
env = dict(env)
|
|
1677
|
+
env.update({
|
|
1678
|
+
"MIMOCODE_DISABLE_SHARE": "true",
|
|
1679
|
+
"MIMOCODE_ENABLE_ANALYSIS": "false",
|
|
1680
|
+
"MIMOCODE_PURE": "true",
|
|
1681
|
+
"MIMOCODE_DISABLE_DEFAULT_PLUGINS": "true",
|
|
1682
|
+
"MIMOCODE_DISABLE_PROJECT_CONFIG": "true",
|
|
1683
|
+
"MIMOCODE_DISABLE_EXTERNAL_SKILLS": "true",
|
|
1684
|
+
"MIMOCODE_DISABLE_LSP_DOWNLOAD": "true",
|
|
1685
|
+
"MIMOCODE_DISABLE_CLAUDE_CODE_MCP": "true",
|
|
1686
|
+
"MIMOCODE_DISABLE_AUTOUPDATE": "true",
|
|
1687
|
+
"MIMOCODE_MIMO_ONLY": "true",
|
|
1688
|
+
"MIMOCODE_PERMISSION": json.dumps({
|
|
1689
|
+
"*": "deny",
|
|
1690
|
+
"read": "allow",
|
|
1691
|
+
"edit": "allow",
|
|
1692
|
+
"glob": "allow",
|
|
1693
|
+
"grep": "allow",
|
|
1694
|
+
"lsp": "allow",
|
|
1695
|
+
"bash": {
|
|
1696
|
+
"*": "allow",
|
|
1697
|
+
"git commit*": "deny",
|
|
1698
|
+
"git * commit*": "deny",
|
|
1699
|
+
"git push*": "deny",
|
|
1700
|
+
"git * push*": "deny",
|
|
1701
|
+
"git reset --hard*": "deny",
|
|
1702
|
+
"git * reset --hard*": "deny",
|
|
1703
|
+
},
|
|
1704
|
+
"external_directory": "deny",
|
|
1705
|
+
"task": "deny",
|
|
1706
|
+
"skill": "deny",
|
|
1707
|
+
"webfetch": "deny",
|
|
1708
|
+
"websearch": "deny",
|
|
1709
|
+
"codesearch": "deny",
|
|
1710
|
+
}, separators=(",", ":")),
|
|
1711
|
+
})
|
|
1712
|
+
return env
|
|
1713
|
+
|
|
1714
|
+
|
|
1619
1715
|
def opencode_base_env(
|
|
1620
1716
|
env: dict[str, str], *, data_home: str | Path, require_windows_shell: bool = False,
|
|
1621
1717
|
) -> dict[str, str]:
|
|
@@ -1120,7 +1120,7 @@ except (ImportError, ModuleNotFoundError):
|
|
|
1120
1120
|
# DAEMON_VERSION is the protocol/logic version of the daemon code.
|
|
1121
1121
|
# Kept in sync with pyproject.toml version via bump-version.sh.
|
|
1122
1122
|
# CLIENT_TYPE identifies which packaging/distribution this daemon runs in.
|
|
1123
|
-
DAEMON_VERSION = "1.
|
|
1123
|
+
DAEMON_VERSION = "1.49.0"
|
|
1124
1124
|
|
|
1125
1125
|
|
|
1126
1126
|
def _detect_client_type() -> str:
|
|
@@ -1934,7 +1934,7 @@ def _daemon_extract_agent_output(agent_id: str, raw: str) -> str:
|
|
|
1934
1934
|
return _daemon_extract_copilot_output(raw)
|
|
1935
1935
|
if agent_id == "codex":
|
|
1936
1936
|
return _daemon_extract_codex_output(raw)
|
|
1937
|
-
if agent_id
|
|
1937
|
+
if agent_id in ("opencode", "mimo"):
|
|
1938
1938
|
return _daemon_extract_opencode_output(raw)
|
|
1939
1939
|
return raw
|
|
1940
1940
|
|
|
@@ -5640,6 +5640,10 @@ class ProcessManager:
|
|
|
5640
5640
|
result = await self._run_codex(agent, prompt, workspace_path, timeout, task.task_id, on_chunk)
|
|
5641
5641
|
elif agent.agent_id == "opencode":
|
|
5642
5642
|
result = await self._run_opencode(agent, prompt, workspace_path, timeout, task.task_id, on_chunk)
|
|
5643
|
+
elif agent.agent_id == "mimo":
|
|
5644
|
+
result = await self._run_mimo(
|
|
5645
|
+
agent, prompt, workspace_path, timeout, task.task_id, on_chunk,
|
|
5646
|
+
)
|
|
5643
5647
|
elif agent.agent_id == "gemini":
|
|
5644
5648
|
result = await self._run_gemini(agent, prompt, workspace_path, timeout, task.task_id, on_chunk)
|
|
5645
5649
|
elif agent.agent_id == "kimi":
|
|
@@ -6379,6 +6383,53 @@ class ProcessManager:
|
|
|
6379
6383
|
result.metrics.update(parsed_metrics)
|
|
6380
6384
|
return result
|
|
6381
6385
|
|
|
6386
|
+
async def _run_mimo(
|
|
6387
|
+
self, agent: DiscoveredAgent, prompt: str, cwd: Path, timeout: int, task_id: str,
|
|
6388
|
+
on_chunk: Any = None,
|
|
6389
|
+
) -> TaskResult:
|
|
6390
|
+
"""Run MiMo Code with its user-managed profile serialized machine-wide.
|
|
6391
|
+
|
|
6392
|
+
MiMo's native sessions, memory and credentials belong to its own profile.
|
|
6393
|
+
Unlike OpenCode, do not redirect XDG/MIMOCODE_HOME or copy auth files per
|
|
6394
|
+
task: that would break its state model and risk credential handling bugs.
|
|
6395
|
+
The shared lock covers AgentHost and Daemon processes using that profile.
|
|
6396
|
+
"""
|
|
6397
|
+
profile_lock = agent_core.acquire_file_lock(agent_core.mimo_profile_lock_path())
|
|
6398
|
+
if profile_lock is None:
|
|
6399
|
+
return TaskResult(
|
|
6400
|
+
status="failed",
|
|
6401
|
+
exit_code=-1,
|
|
6402
|
+
stdout="",
|
|
6403
|
+
stderr="",
|
|
6404
|
+
error=(
|
|
6405
|
+
"MiMo Code profile is busy with another local turn; retry after it "
|
|
6406
|
+
"finishes"
|
|
6407
|
+
),
|
|
6408
|
+
failure_code="mimo_profile_busy",
|
|
6409
|
+
)
|
|
6410
|
+
|
|
6411
|
+
tmp_root = tempfile.mkdtemp(prefix=f"mimo-{task_id[:8]}-")
|
|
6412
|
+
try:
|
|
6413
|
+
prompt_file = Path(tmp_root) / "instructions.md"
|
|
6414
|
+
prompt_file.write_text(prompt, encoding="utf-8")
|
|
6415
|
+
cmd = agent_core.build_mimo_command(
|
|
6416
|
+
agent.command,
|
|
6417
|
+
cwd=cwd,
|
|
6418
|
+
model=os.environ.get("FACTORY_MIMO_MODEL", "").strip() or None,
|
|
6419
|
+
prompt_file=prompt_file,
|
|
6420
|
+
)
|
|
6421
|
+
env = agent_core.mimo_base_env(os.environ.copy())
|
|
6422
|
+
result = await self._run_cli(
|
|
6423
|
+
cmd, cwd, timeout, task_id, on_chunk=on_chunk, env=env,
|
|
6424
|
+
)
|
|
6425
|
+
finally:
|
|
6426
|
+
agent_core.release_file_lock(profile_lock)
|
|
6427
|
+
shutil.rmtree(tmp_root, ignore_errors=True)
|
|
6428
|
+
|
|
6429
|
+
parsed_metrics = self._parse_agent_jsonl_output(result.stdout)
|
|
6430
|
+
result.metrics.update(parsed_metrics)
|
|
6431
|
+
return result
|
|
6432
|
+
|
|
6382
6433
|
async def _run_gemini(
|
|
6383
6434
|
self, agent: DiscoveredAgent, prompt: str, cwd: Path, timeout: int, task_id: str,
|
|
6384
6435
|
on_chunk: Any = None,
|
|
@@ -7830,7 +7881,7 @@ class HeartbeatService:
|
|
|
7830
7881
|
|
|
7831
7882
|
def update(self, active_tasks: int, agents: list[dict]):
|
|
7832
7883
|
self._active_tasks = active_tasks
|
|
7833
|
-
self._agents = agents
|
|
7884
|
+
self._agents = [dict(agent) for agent in agents]
|
|
7834
7885
|
|
|
7835
7886
|
async def _sync_local_workspace_bindings(self) -> None:
|
|
7836
7887
|
try:
|
|
@@ -8143,6 +8194,8 @@ class ServerConnection:
|
|
|
8143
8194
|
self.poller: TaskPoller | None = None
|
|
8144
8195
|
self.reporter: ProgressReporter | None = None
|
|
8145
8196
|
self.log_uploader: LogUploader | None = None
|
|
8197
|
+
self._agents: list[DiscoveredAgent] = []
|
|
8198
|
+
self._max_concurrent = 5
|
|
8146
8199
|
self._auth_failures = 0 # Consecutive auth failure count
|
|
8147
8200
|
self._max_auth_failures = 3 # Trigger re-registration after this many
|
|
8148
8201
|
# Set when the server demands a runtime credential (compat window
|
|
@@ -8568,7 +8621,19 @@ class ServerConnection:
|
|
|
8568
8621
|
self._re_register_lock = asyncio.Lock()
|
|
8569
8622
|
return self._re_register_lock
|
|
8570
8623
|
|
|
8571
|
-
|
|
8624
|
+
def update_agent_inventory(
|
|
8625
|
+
self, active_tasks: int, agents: list[DiscoveredAgent],
|
|
8626
|
+
) -> None:
|
|
8627
|
+
"""Keep heartbeat and re-registration on one current agent snapshot."""
|
|
8628
|
+
self._agents = list(agents)
|
|
8629
|
+
if self.heartbeat:
|
|
8630
|
+
self.heartbeat.update(active_tasks, _agent_dicts_for_wire(self._agents))
|
|
8631
|
+
|
|
8632
|
+
async def re_register(
|
|
8633
|
+
self,
|
|
8634
|
+
agents: list[DiscoveredAgent] | None = None,
|
|
8635
|
+
max_concurrent: int | None = None,
|
|
8636
|
+
):
|
|
8572
8637
|
"""Refresh token and re-register with the server.
|
|
8573
8638
|
|
|
8574
8639
|
Called when persistent auth failures indicate stale token/hash.
|
|
@@ -8578,6 +8643,10 @@ class ServerConnection:
|
|
|
8578
8643
|
Single-flight: concurrent triggers (heartbeat/poller/poll_ai_jobs all
|
|
8579
8644
|
share the auth-failure callback) collapse into one execution.
|
|
8580
8645
|
"""
|
|
8646
|
+
if agents is not None:
|
|
8647
|
+
self._agents = list(agents)
|
|
8648
|
+
if max_concurrent is not None:
|
|
8649
|
+
self._max_concurrent = max_concurrent
|
|
8581
8650
|
lock = self._get_re_register_lock()
|
|
8582
8651
|
if lock.locked():
|
|
8583
8652
|
logger.debug(
|
|
@@ -8588,7 +8657,7 @@ class ServerConnection:
|
|
|
8588
8657
|
async with lock:
|
|
8589
8658
|
await self.refresh_access_token()
|
|
8590
8659
|
try:
|
|
8591
|
-
await self.register(
|
|
8660
|
+
await self.register(self._agents, self._max_concurrent)
|
|
8592
8661
|
# Sync runtime_id to all services (may change after re-registration)
|
|
8593
8662
|
if self.heartbeat and self.runtime_id:
|
|
8594
8663
|
self.heartbeat.runtime_id = self.runtime_id
|
|
@@ -8610,9 +8679,10 @@ class ServerConnection:
|
|
|
8610
8679
|
|
|
8611
8680
|
async def register(self, agents: list[DiscoveredAgent], max_concurrent: int):
|
|
8612
8681
|
"""Register this daemon with the server."""
|
|
8613
|
-
self._agents = agents
|
|
8682
|
+
self._agents = list(agents)
|
|
8683
|
+
self._max_concurrent = max_concurrent
|
|
8614
8684
|
self._max_concurrent_cache = max_concurrent
|
|
8615
|
-
agent_dicts = _agent_dicts_for_wire(
|
|
8685
|
+
agent_dicts = _agent_dicts_for_wire(self._agents)
|
|
8616
8686
|
async def _register_once():
|
|
8617
8687
|
payload = {
|
|
8618
8688
|
"daemon_id": self.daemon_id,
|
|
@@ -8685,7 +8755,7 @@ class ServerConnection:
|
|
|
8685
8755
|
preflight_snapshot_provider: Callable[[], dict | None] | None = None,
|
|
8686
8756
|
):
|
|
8687
8757
|
"""Initialize heartbeat, poller, and reporter services."""
|
|
8688
|
-
self._agents = agents
|
|
8758
|
+
self._agents = list(agents)
|
|
8689
8759
|
self._max_concurrent = getattr(self, '_max_concurrent_cache', 5)
|
|
8690
8760
|
|
|
8691
8761
|
def _on_auth_success():
|
|
@@ -8708,7 +8778,7 @@ class ServerConnection:
|
|
|
8708
8778
|
try:
|
|
8709
8779
|
loop = asyncio.get_event_loop()
|
|
8710
8780
|
if loop.is_running():
|
|
8711
|
-
loop.create_task(self.re_register(
|
|
8781
|
+
loop.create_task(self.re_register())
|
|
8712
8782
|
except Exception:
|
|
8713
8783
|
pass
|
|
8714
8784
|
|
|
@@ -8719,7 +8789,7 @@ class ServerConnection:
|
|
|
8719
8789
|
)
|
|
8720
8790
|
self.heartbeat._on_success = _on_heartbeat_success
|
|
8721
8791
|
self.heartbeat._on_auth_failure = _on_auth_failure
|
|
8722
|
-
self.
|
|
8792
|
+
self.update_agent_inventory(0, self._agents)
|
|
8723
8793
|
self.poller = TaskPoller(
|
|
8724
8794
|
self.client, self.server_url, self.runtime_id, poll_interval,
|
|
8725
8795
|
)
|
|
@@ -9354,10 +9424,8 @@ class RuntimeDaemon:
|
|
|
9354
9424
|
logger.info("Refreshed agent inventory: %s -> %s", previous_agents, current_agents)
|
|
9355
9425
|
|
|
9356
9426
|
active_tasks = len(self.active_tasks)
|
|
9357
|
-
agent_dicts = self._agents_as_dicts()
|
|
9358
9427
|
for connection in self.connections:
|
|
9359
|
-
|
|
9360
|
-
connection.heartbeat.update(active_tasks, agent_dicts)
|
|
9428
|
+
connection.update_agent_inventory(active_tasks, refreshed_agents)
|
|
9361
9429
|
|
|
9362
9430
|
async def _agent_refresh_loop(self) -> None:
|
|
9363
9431
|
while not self._shutdown:
|
|
@@ -9493,6 +9561,11 @@ class RuntimeDaemon:
|
|
|
9493
9561
|
for field, value in attachment.items():
|
|
9494
9562
|
if value and not existing.get(field):
|
|
9495
9563
|
existing[field] = value
|
|
9564
|
+
if download_url and download_url != existing.get("download_url"):
|
|
9565
|
+
aliases = list(existing.get("_download_url_aliases") or [])
|
|
9566
|
+
if download_url not in aliases:
|
|
9567
|
+
aliases.append(download_url)
|
|
9568
|
+
existing["_download_url_aliases"] = aliases
|
|
9496
9569
|
existing_artifact_id = str(
|
|
9497
9570
|
existing.get("artifact_id") or existing.get("id") or ""
|
|
9498
9571
|
).strip()
|
|
@@ -9537,6 +9610,12 @@ class RuntimeDaemon:
|
|
|
9537
9610
|
for field, value in attachment.items():
|
|
9538
9611
|
if value and not identified.get(field):
|
|
9539
9612
|
identified[field] = value
|
|
9613
|
+
legacy_url = attachment.get("download_url")
|
|
9614
|
+
if isinstance(legacy_url, str) and legacy_url and legacy_url != identified.get("download_url"):
|
|
9615
|
+
aliases = list(identified.get("_download_url_aliases") or [])
|
|
9616
|
+
if legacy_url not in aliases:
|
|
9617
|
+
aliases.append(legacy_url)
|
|
9618
|
+
identified["_download_url_aliases"] = aliases
|
|
9540
9619
|
return deduplicated
|
|
9541
9620
|
|
|
9542
9621
|
@staticmethod
|
|
@@ -9551,14 +9630,14 @@ class RuntimeDaemon:
|
|
|
9551
9630
|
@staticmethod
|
|
9552
9631
|
def _rewrite_staged_attachment_references(
|
|
9553
9632
|
prompt: str,
|
|
9554
|
-
staged: list[tuple[str, str, str]],
|
|
9633
|
+
staged: list[tuple[str, list[str], str]],
|
|
9555
9634
|
) -> str:
|
|
9556
9635
|
if not staged:
|
|
9557
9636
|
return prompt
|
|
9558
9637
|
|
|
9559
9638
|
local_files = []
|
|
9560
|
-
for file_name,
|
|
9561
|
-
|
|
9639
|
+
for file_name, download_urls, local_path in staged:
|
|
9640
|
+
for download_url in download_urls:
|
|
9562
9641
|
command_pattern = r"`curl -L '" + re.escape(download_url) + r"' -o /tmp/[^`]+`"
|
|
9563
9642
|
prompt = re.sub(command_pattern, lambda _match, path=local_path: f"`{path}`", prompt)
|
|
9564
9643
|
prompt = prompt.replace(f"Download: {download_url}", f"Staged local file: `{local_path}`")
|
|
@@ -9685,7 +9764,7 @@ class RuntimeDaemon:
|
|
|
9685
9764
|
)
|
|
9686
9765
|
|
|
9687
9766
|
staging_dir = Path(tempfile.mkdtemp(prefix=f"forgexa-attachments-{task.task_id[:8]}-"))
|
|
9688
|
-
staged: list[tuple[str, str, str]] = []
|
|
9767
|
+
staged: list[tuple[str, list[str], str]] = []
|
|
9689
9768
|
deadline = time.monotonic() + self._TASK_ATTACHMENT_DOWNLOAD_TIMEOUT_SECONDS
|
|
9690
9769
|
remaining_bytes = self._TASK_ATTACHMENT_MAX_TOTAL_BYTES
|
|
9691
9770
|
try:
|
|
@@ -9694,6 +9773,10 @@ class RuntimeDaemon:
|
|
|
9694
9773
|
attachment.get("file_path") or attachment.get("file_name") or "attachment"
|
|
9695
9774
|
)
|
|
9696
9775
|
download_url = str(attachment.get("download_url") or "")
|
|
9776
|
+
download_url_aliases = [download_url] if download_url else []
|
|
9777
|
+
for alias in attachment.get("_download_url_aliases") or []:
|
|
9778
|
+
if isinstance(alias, str) and alias and alias not in download_url_aliases:
|
|
9779
|
+
download_url_aliases.append(alias)
|
|
9697
9780
|
destination = staging_dir / self._task_attachment_filename(attachment, position)
|
|
9698
9781
|
try:
|
|
9699
9782
|
expected_size = int(attachment.get("size_bytes") or 0)
|
|
@@ -9735,7 +9818,7 @@ class RuntimeDaemon:
|
|
|
9735
9818
|
timeout_seconds=remaining_seconds,
|
|
9736
9819
|
)
|
|
9737
9820
|
remaining_bytes -= destination.stat().st_size
|
|
9738
|
-
staged.append((file_name,
|
|
9821
|
+
staged.append((file_name, download_url_aliases, str(destination)))
|
|
9739
9822
|
except Exception:
|
|
9740
9823
|
shutil.rmtree(staging_dir, ignore_errors=True)
|
|
9741
9824
|
raise
|
|
@@ -10223,8 +10306,7 @@ class RuntimeDaemon:
|
|
|
10223
10306
|
# Update heartbeat on all connections with total active count
|
|
10224
10307
|
total_active = len(self.active_tasks)
|
|
10225
10308
|
for conn in self.connections:
|
|
10226
|
-
|
|
10227
|
-
conn.heartbeat.update(total_active, self._agents_as_dicts())
|
|
10309
|
+
conn.update_agent_inventory(total_active, self.agents)
|
|
10228
10310
|
|
|
10229
10311
|
# Poll from all connected servers
|
|
10230
10312
|
for conn in self.connections:
|
|
@@ -10622,6 +10704,7 @@ class RuntimeDaemon:
|
|
|
10622
10704
|
"claude": "npm install -g @anthropic-ai/claude-code",
|
|
10623
10705
|
"codex": "npm install -g @openai/codex",
|
|
10624
10706
|
"opencode": "curl -sSL https://opencode.ai/install | sh",
|
|
10707
|
+
"mimo": "curl -fsSL https://mimo.xiaomi.com/install | bash",
|
|
10625
10708
|
"gemini": "npm install -g @google/gemini-cli",
|
|
10626
10709
|
"kimi": "curl -fsSL https://code.kimi.com/kimi-code/install.sh | bash",
|
|
10627
10710
|
"copilot": (
|
|
@@ -11668,9 +11751,11 @@ class RuntimeDaemon:
|
|
|
11668
11751
|
for agent in self.agents:
|
|
11669
11752
|
if agent.agent_id == preferred_id:
|
|
11670
11753
|
return agent
|
|
11671
|
-
# 3. Any remaining agent
|
|
11754
|
+
# 3. Any remaining agent. MiMo Code requires explicit selection or an
|
|
11755
|
+
# explicit policy fallback entry; it must never become an implicit
|
|
11756
|
+
# catch-all because its profile state is shared machine-wide.
|
|
11672
11757
|
for agent in self.agents:
|
|
11673
|
-
if agent.agent_id not in tried:
|
|
11758
|
+
if agent.agent_id not in tried and agent.agent_id != "mimo":
|
|
11674
11759
|
return agent
|
|
11675
11760
|
|
|
11676
11761
|
return None
|
|
@@ -11713,8 +11798,9 @@ class RuntimeDaemon:
|
|
|
11713
11798
|
for agent in self.agents:
|
|
11714
11799
|
if agent.compatibility_level == "L3":
|
|
11715
11800
|
return agent
|
|
11716
|
-
# 5. Any agent (last resort for unrecognized types)
|
|
11717
|
-
|
|
11801
|
+
# 5. Any agent (last resort for unrecognized types), excluding the
|
|
11802
|
+
# explicit-only MiMo Code driver.
|
|
11803
|
+
return next((agent for agent in self.agents if agent.agent_id != "mimo"), None)
|
|
11718
11804
|
|
|
11719
11805
|
# ── Layer 2: Validation Gate ──
|
|
11720
11806
|
|
|
@@ -13028,6 +13114,7 @@ class RuntimeDaemon:
|
|
|
13028
13114
|
"claude": "npm install -g @anthropic-ai/claude-code",
|
|
13029
13115
|
"codex": "npm install -g @openai/codex",
|
|
13030
13116
|
"opencode": "curl -sSL https://opencode.ai/install | sh",
|
|
13117
|
+
"mimo": "curl -fsSL https://mimo.xiaomi.com/install | bash",
|
|
13031
13118
|
"gemini": "npm install -g @google/gemini-cli",
|
|
13032
13119
|
"kimi": "curl -fsSL https://code.kimi.com/kimi-code/install.sh | bash",
|
|
13033
13120
|
"copilot": (
|
|
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
|
|
File without changes
|
|
File without changes
|