cybersecured-agent-cli 2.0.0__py3-none-any.whl
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.
- cybersecured_agent/__init__.py +12 -0
- cybersecured_agent/__main__.py +6 -0
- cybersecured_agent/agent_frameworks/__init__.py +23 -0
- cybersecured_agent/agent_frameworks/_types.py +17 -0
- cybersecured_agent/agent_frameworks/hermes.py +108 -0
- cybersecured_agent/agent_frameworks/kimi_cli.py +108 -0
- cybersecured_agent/agent_frameworks/openclaw.py +117 -0
- cybersecured_agent/agent_frameworks/opencode.py +111 -0
- cybersecured_agent/api_client.py +251 -0
- cybersecured_agent/cli.py +73 -0
- cybersecured_agent/commands/__init__.py +25 -0
- cybersecured_agent/commands/advisory.py +52 -0
- cybersecured_agent/commands/agent.py +395 -0
- cybersecured_agent/commands/application.py +100 -0
- cybersecured_agent/commands/assessment.py +82 -0
- cybersecured_agent/commands/auth.py +78 -0
- cybersecured_agent/commands/config.py +50 -0
- cybersecured_agent/commands/coverage.py +46 -0
- cybersecured_agent/commands/diagnose.py +24 -0
- cybersecured_agent/commands/incident.py +234 -0
- cybersecured_agent/commands/scan.py +164 -0
- cybersecured_agent/commands/status.py +33 -0
- cybersecured_agent/config.py +114 -0
- cybersecured_agent/constants.py +98 -0
- cybersecured_agent/exceptions.py +25 -0
- cybersecured_agent/fingerprint/__init__.py +20 -0
- cybersecured_agent/fingerprint/core.py +126 -0
- cybersecured_agent/fingerprint/dump.py +139 -0
- cybersecured_agent/fingerprint/hashing.py +55 -0
- cybersecured_agent/fingerprint/versions.py +9 -0
- cybersecured_agent/infra/__init__.py +4 -0
- cybersecured_agent/infra/cloud_detect.py +111 -0
- cybersecured_agent/infra/config_files.py +73 -0
- cybersecured_agent/infra/desensitize.py +163 -0
- cybersecured_agent/infra/machine_id.py +173 -0
- cybersecured_agent/infra/process_chain.py +165 -0
- cybersecured_agent/machine_id.py +13 -0
- cybersecured_agent/output.py +91 -0
- cybersecured_agent/risk_factors.py +43 -0
- cybersecured_agent/workspace.py +31 -0
- cybersecured_agent_cli-2.0.0.dist-info/METADATA +76 -0
- cybersecured_agent_cli-2.0.0.dist-info/RECORD +45 -0
- cybersecured_agent_cli-2.0.0.dist-info/WHEEL +4 -0
- cybersecured_agent_cli-2.0.0.dist-info/entry_points.txt +2 -0
- cybersecured_agent_cli-2.0.0.dist-info/licenses/LICENSE +674 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Cybersecured (厚锋科技) Agent CLI - 龙行无忧 AI智能体风险管家服务命令行工具。
|
|
2
|
+
|
|
3
|
+
A cross-platform CLI tool for AI智能体 risk assessment and advisory services.
|
|
4
|
+
Supports Windows, Linux, macOS, cloud platforms, and Docker containers.
|
|
5
|
+
|
|
6
|
+
License: GPL-3.0-or-later
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
__version__ = "2.0.0"
|
|
10
|
+
__author__ = "Cybersecured Technology (厚锋科技)"
|
|
11
|
+
__email__ = "developer@cybersecured.cn"
|
|
12
|
+
__license__ = "GPL-3.0-or-later"
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Agent framework modules registry.
|
|
2
|
+
|
|
3
|
+
Related design: docs/41.tools-designs/design-cybersecured-agent-cli.md
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from . import openclaw, hermes, kimi_cli, opencode
|
|
7
|
+
from ..fingerprint.versions import FP_CURRENT_VERSION
|
|
8
|
+
|
|
9
|
+
# Order = detection priority (highest first)
|
|
10
|
+
SUPPORTED_FRAMEWORKS = (openclaw, hermes, kimi_cli, opencode)
|
|
11
|
+
|
|
12
|
+
_REQUIRED_ATTRS = ("NAME", "detect", "collect_environment", "STABLE_IDENTITY_BY_VERSION")
|
|
13
|
+
|
|
14
|
+
for _mod in SUPPORTED_FRAMEWORKS:
|
|
15
|
+
for _attr in _REQUIRED_ATTRS:
|
|
16
|
+
if not hasattr(_mod, _attr):
|
|
17
|
+
raise ImportError(
|
|
18
|
+
f"agent_frameworks/{_mod.__name__} missing required attribute '{_attr}'"
|
|
19
|
+
)
|
|
20
|
+
if FP_CURRENT_VERSION not in _mod.STABLE_IDENTITY_BY_VERSION:
|
|
21
|
+
raise ImportError(
|
|
22
|
+
f"{_mod.__name__} must implement fp version {FP_CURRENT_VERSION}"
|
|
23
|
+
)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Shared types for agent framework modules.
|
|
2
|
+
|
|
3
|
+
Related design: docs/41.tools-designs/design-cybersecured-agent-cli.md
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Any, Dict, NamedTuple
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DetectionResult(NamedTuple):
|
|
10
|
+
matched: bool
|
|
11
|
+
evidence: Dict[str, Any]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ProcessInfo(NamedTuple):
|
|
15
|
+
pid: int
|
|
16
|
+
ppid: int
|
|
17
|
+
args: str
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Hermes framework detection and identity extraction.
|
|
2
|
+
|
|
3
|
+
Related design: docs/41.tools-designs/design-cybersecured-agent-cli.md
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Dict, Mapping
|
|
9
|
+
|
|
10
|
+
from ..infra.config_files import read_json, read_with_desensitize
|
|
11
|
+
from ._types import DetectionResult, ProcessInfo
|
|
12
|
+
|
|
13
|
+
NAME = "hermes"
|
|
14
|
+
|
|
15
|
+
_ENV_VARS = ["HERMES_HOME"]
|
|
16
|
+
_CONFIG_DIRS = [Path.home() / ".hermes"]
|
|
17
|
+
_CONFIG_FILENAMES = ["config.json"]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def detect(env: Mapping[str, str], proc_chain: list[ProcessInfo]) -> DetectionResult:
|
|
21
|
+
evidence: Dict[str, Any] = {}
|
|
22
|
+
|
|
23
|
+
for var in _ENV_VARS:
|
|
24
|
+
val = env.get(var)
|
|
25
|
+
evidence[f"env_var_{var}"] = "present" if val else "absent"
|
|
26
|
+
if val:
|
|
27
|
+
return DetectionResult(matched=True, evidence=evidence)
|
|
28
|
+
|
|
29
|
+
for info in proc_chain:
|
|
30
|
+
if "hermes" in info.args.lower():
|
|
31
|
+
evidence["process_match"] = "hermes-runtime"
|
|
32
|
+
return DetectionResult(matched=True, evidence=evidence)
|
|
33
|
+
evidence["process_match"] = "none"
|
|
34
|
+
|
|
35
|
+
# Config dir existence alone does NOT mean "currently running hermes".
|
|
36
|
+
for cfg_dir in _CONFIG_DIRS:
|
|
37
|
+
if cfg_dir.exists():
|
|
38
|
+
evidence["config_dir"] = str(cfg_dir)
|
|
39
|
+
break
|
|
40
|
+
else:
|
|
41
|
+
evidence["config_dir"] = "not found"
|
|
42
|
+
|
|
43
|
+
return DetectionResult(matched=False, evidence=evidence)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def collect_environment(env: Mapping[str, str],
|
|
47
|
+
proc_chain: list[ProcessInfo],
|
|
48
|
+
workspace: Path) -> Dict[str, Any]:
|
|
49
|
+
data: Dict[str, Any] = {
|
|
50
|
+
"env_vars": {},
|
|
51
|
+
"config_files": {},
|
|
52
|
+
"version": None,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
for var in _ENV_VARS:
|
|
56
|
+
if var in env:
|
|
57
|
+
data["env_vars"][var] = env[var]
|
|
58
|
+
|
|
59
|
+
for cfg_dir in _CONFIG_DIRS:
|
|
60
|
+
if not cfg_dir.exists():
|
|
61
|
+
continue
|
|
62
|
+
for filename in _CONFIG_FILENAMES:
|
|
63
|
+
path = cfg_dir / filename
|
|
64
|
+
if path.exists():
|
|
65
|
+
data["config_files"][filename] = read_with_desensitize(path)
|
|
66
|
+
version_path = cfg_dir / "config.json"
|
|
67
|
+
if version_path.exists():
|
|
68
|
+
cfg = read_json(version_path)
|
|
69
|
+
if isinstance(cfg, dict):
|
|
70
|
+
data["version"] = cfg.get("version")
|
|
71
|
+
break
|
|
72
|
+
|
|
73
|
+
return data
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _resolve_config_path(env: Mapping[str, str], workspace: Path) -> str:
|
|
77
|
+
hermes_home = env.get("HERMES_HOME")
|
|
78
|
+
if hermes_home:
|
|
79
|
+
return str(Path(hermes_home).resolve()).replace("\\", "/")
|
|
80
|
+
|
|
81
|
+
for cfg_dir in _CONFIG_DIRS:
|
|
82
|
+
if cfg_dir.exists():
|
|
83
|
+
for filename in _CONFIG_FILENAMES:
|
|
84
|
+
path = cfg_dir / filename
|
|
85
|
+
if path.exists():
|
|
86
|
+
return str(path.resolve()).replace("\\", "/")
|
|
87
|
+
return str(cfg_dir.resolve()).replace("\\", "/")
|
|
88
|
+
|
|
89
|
+
return str(workspace.resolve()).replace("\\", "/")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _identity_legacy(env: Mapping[str, str],
|
|
93
|
+
proc_chain: list[ProcessInfo],
|
|
94
|
+
workspace: Path) -> Dict[str, str]:
|
|
95
|
+
return {"config_path": _resolve_config_path(env, workspace)}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _identity_v1(env: Mapping[str, str],
|
|
99
|
+
proc_chain: list[ProcessInfo],
|
|
100
|
+
workspace: Path) -> Dict[str, str]:
|
|
101
|
+
return _identity_legacy(env, proc_chain, workspace)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
STABLE_IDENTITY_BY_VERSION = {
|
|
105
|
+
"sha256": _identity_legacy,
|
|
106
|
+
"v0": _identity_legacy,
|
|
107
|
+
"v1": _identity_v1,
|
|
108
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Kimi CLI framework detection and identity extraction.
|
|
2
|
+
|
|
3
|
+
Related design: docs/41.tools-designs/design-cybersecured-agent-cli.md
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Dict, Mapping
|
|
9
|
+
|
|
10
|
+
from ..infra.config_files import read_json, read_with_desensitize
|
|
11
|
+
from ._types import DetectionResult, ProcessInfo
|
|
12
|
+
|
|
13
|
+
NAME = "kimi-cli"
|
|
14
|
+
|
|
15
|
+
_ENV_VARS = ["KIMI_WORK_DIR", "KIMI_SHARE_DIR", "KIMI_CONFIG_PATH"]
|
|
16
|
+
_CONFIG_DIRS = [Path.home() / ".kimi"]
|
|
17
|
+
_CONFIG_FILENAMES = ["config.json", "config.toml"]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def detect(env: Mapping[str, str], proc_chain: list[ProcessInfo]) -> DetectionResult:
|
|
21
|
+
evidence: Dict[str, Any] = {}
|
|
22
|
+
|
|
23
|
+
for var in _ENV_VARS:
|
|
24
|
+
val = env.get(var)
|
|
25
|
+
evidence[f"env_var_{var}"] = "present" if val else "absent"
|
|
26
|
+
if val:
|
|
27
|
+
return DetectionResult(matched=True, evidence=evidence)
|
|
28
|
+
|
|
29
|
+
for info in proc_chain:
|
|
30
|
+
if "kimi" in info.args.lower():
|
|
31
|
+
evidence["process_match"] = "kimi-runtime"
|
|
32
|
+
return DetectionResult(matched=True, evidence=evidence)
|
|
33
|
+
evidence["process_match"] = "none"
|
|
34
|
+
|
|
35
|
+
# Config dir existence alone does NOT mean "currently running kimi-cli".
|
|
36
|
+
for cfg_dir in _CONFIG_DIRS:
|
|
37
|
+
if cfg_dir.exists():
|
|
38
|
+
evidence["config_dir"] = str(cfg_dir)
|
|
39
|
+
break
|
|
40
|
+
else:
|
|
41
|
+
evidence["config_dir"] = "not found"
|
|
42
|
+
|
|
43
|
+
return DetectionResult(matched=False, evidence=evidence)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def collect_environment(env: Mapping[str, str],
|
|
47
|
+
proc_chain: list[ProcessInfo],
|
|
48
|
+
workspace: Path) -> Dict[str, Any]:
|
|
49
|
+
data: Dict[str, Any] = {
|
|
50
|
+
"env_vars": {},
|
|
51
|
+
"config_files": {},
|
|
52
|
+
"version": None,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
for var in _ENV_VARS:
|
|
56
|
+
if var in env:
|
|
57
|
+
data["env_vars"][var] = env[var]
|
|
58
|
+
|
|
59
|
+
for cfg_dir in _CONFIG_DIRS:
|
|
60
|
+
if not cfg_dir.exists():
|
|
61
|
+
continue
|
|
62
|
+
for filename in _CONFIG_FILENAMES:
|
|
63
|
+
path = cfg_dir / filename
|
|
64
|
+
if path.exists():
|
|
65
|
+
data["config_files"][filename] = read_with_desensitize(path)
|
|
66
|
+
version_path = cfg_dir / "config.json"
|
|
67
|
+
if version_path.exists():
|
|
68
|
+
cfg = read_json(version_path)
|
|
69
|
+
if isinstance(cfg, dict):
|
|
70
|
+
data["version"] = cfg.get("version")
|
|
71
|
+
break
|
|
72
|
+
|
|
73
|
+
return data
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _resolve_config_path(env: Mapping[str, str], workspace: Path) -> str:
|
|
77
|
+
config_path = env.get("KIMI_CONFIG_PATH")
|
|
78
|
+
if config_path:
|
|
79
|
+
return str(Path(config_path).resolve()).replace("\\", "/")
|
|
80
|
+
|
|
81
|
+
for cfg_dir in _CONFIG_DIRS:
|
|
82
|
+
if cfg_dir.exists():
|
|
83
|
+
for filename in _CONFIG_FILENAMES:
|
|
84
|
+
path = cfg_dir / filename
|
|
85
|
+
if path.exists():
|
|
86
|
+
return str(path.resolve()).replace("\\", "/")
|
|
87
|
+
return str(cfg_dir.resolve()).replace("\\", "/")
|
|
88
|
+
|
|
89
|
+
return str(workspace.resolve()).replace("\\", "/")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _identity_legacy(env: Mapping[str, str],
|
|
93
|
+
proc_chain: list[ProcessInfo],
|
|
94
|
+
workspace: Path) -> Dict[str, str]:
|
|
95
|
+
return {"config_path": _resolve_config_path(env, workspace)}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _identity_v1(env: Mapping[str, str],
|
|
99
|
+
proc_chain: list[ProcessInfo],
|
|
100
|
+
workspace: Path) -> Dict[str, str]:
|
|
101
|
+
return _identity_legacy(env, proc_chain, workspace)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
STABLE_IDENTITY_BY_VERSION = {
|
|
105
|
+
"sha256": _identity_legacy,
|
|
106
|
+
"v0": _identity_legacy,
|
|
107
|
+
"v1": _identity_v1,
|
|
108
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""OpenClaw framework detection and identity extraction.
|
|
2
|
+
|
|
3
|
+
Related design: docs/41.tools-designs/design-cybersecured-agent-cli.md
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Dict, Mapping
|
|
9
|
+
|
|
10
|
+
from ..infra.config_files import read_json, read_with_desensitize
|
|
11
|
+
from ._types import DetectionResult, ProcessInfo
|
|
12
|
+
|
|
13
|
+
NAME = "openclaw"
|
|
14
|
+
|
|
15
|
+
_ENV_VARS = ["OPENCLAW_WORKSPACE", "OPENCLAW_WORKSPACE_DIR"]
|
|
16
|
+
_CONFIG_DIRS = [Path.home() / ".openclaw"]
|
|
17
|
+
_CONFIG_FILENAMES = ["config.json", "agents.json"]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def detect(env: Mapping[str, str], proc_chain: list[ProcessInfo]) -> DetectionResult:
|
|
21
|
+
evidence: Dict[str, Any] = {}
|
|
22
|
+
|
|
23
|
+
for var in _ENV_VARS:
|
|
24
|
+
val = env.get(var)
|
|
25
|
+
evidence[f"env_var_{var}"] = "present" if val else "absent"
|
|
26
|
+
if val:
|
|
27
|
+
return DetectionResult(matched=True, evidence=evidence)
|
|
28
|
+
|
|
29
|
+
for info in proc_chain:
|
|
30
|
+
if "openclaw" in info.args.lower():
|
|
31
|
+
evidence["process_match"] = "openclaw-runtime"
|
|
32
|
+
return DetectionResult(matched=True, evidence=evidence)
|
|
33
|
+
evidence["process_match"] = "none"
|
|
34
|
+
|
|
35
|
+
# Config dir existence alone does NOT mean "currently running openclaw".
|
|
36
|
+
# It only indicates the user has installed it. Record for diagnostics only.
|
|
37
|
+
for cfg_dir in _CONFIG_DIRS:
|
|
38
|
+
if cfg_dir.exists():
|
|
39
|
+
evidence["config_dir"] = str(cfg_dir)
|
|
40
|
+
break
|
|
41
|
+
else:
|
|
42
|
+
evidence["config_dir"] = "not found"
|
|
43
|
+
|
|
44
|
+
return DetectionResult(matched=False, evidence=evidence)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def collect_environment(env: Mapping[str, str],
|
|
48
|
+
proc_chain: list[ProcessInfo],
|
|
49
|
+
workspace: Path) -> Dict[str, Any]:
|
|
50
|
+
data: Dict[str, Any] = {
|
|
51
|
+
"env_vars": {},
|
|
52
|
+
"config_files": {},
|
|
53
|
+
"version": None,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
for var in _ENV_VARS:
|
|
57
|
+
if var in env:
|
|
58
|
+
data["env_vars"][var] = env[var]
|
|
59
|
+
|
|
60
|
+
for cfg_dir in _CONFIG_DIRS:
|
|
61
|
+
if not cfg_dir.exists():
|
|
62
|
+
continue
|
|
63
|
+
for filename in _CONFIG_FILENAMES:
|
|
64
|
+
path = cfg_dir / filename
|
|
65
|
+
if path.exists():
|
|
66
|
+
data["config_files"][filename] = read_with_desensitize(path)
|
|
67
|
+
# Try to extract version from config.json
|
|
68
|
+
version_path = cfg_dir / "config.json"
|
|
69
|
+
if version_path.exists():
|
|
70
|
+
cfg = read_json(version_path)
|
|
71
|
+
if isinstance(cfg, dict):
|
|
72
|
+
data["version"] = cfg.get("version")
|
|
73
|
+
break
|
|
74
|
+
|
|
75
|
+
return data
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _resolve_config_path(env: Mapping[str, str], workspace: Path) -> str:
|
|
79
|
+
"""Resolve the stable config path for legacy fingerprint generation."""
|
|
80
|
+
config_path = env.get("OPENCLAW_CONFIG")
|
|
81
|
+
if config_path:
|
|
82
|
+
return str(Path(config_path).resolve()).replace("\\", "/")
|
|
83
|
+
|
|
84
|
+
for cfg_dir in _CONFIG_DIRS:
|
|
85
|
+
if cfg_dir.exists():
|
|
86
|
+
for filename in _CONFIG_FILENAMES:
|
|
87
|
+
path = cfg_dir / filename
|
|
88
|
+
if path.exists():
|
|
89
|
+
return str(path.resolve()).replace("\\", "/")
|
|
90
|
+
return str(cfg_dir.resolve()).replace("\\", "/")
|
|
91
|
+
|
|
92
|
+
return str(workspace.resolve()).replace("\\", "/")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _identity_legacy(env: Mapping[str, str],
|
|
96
|
+
proc_chain: list[ProcessInfo],
|
|
97
|
+
workspace: Path) -> Dict[str, str]:
|
|
98
|
+
"""Legacy fingerprint fields (sha256 + v0). Frozen — never change."""
|
|
99
|
+
return {"config_path": _resolve_config_path(env, workspace)}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _identity_v1(env: Mapping[str, str],
|
|
103
|
+
proc_chain: list[ProcessInfo],
|
|
104
|
+
workspace: Path) -> Dict[str, str]:
|
|
105
|
+
"""v1 fingerprint fields. Placeholder — will be refined in Phase B
|
|
106
|
+
after real environment sampling via diagnose.
|
|
107
|
+
"""
|
|
108
|
+
# For Phase A, v1 uses the same stable field as legacy to avoid
|
|
109
|
+
# fp changes before we are ready to bump FP_CURRENT_VERSION.
|
|
110
|
+
return _identity_legacy(env, proc_chain, workspace)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
STABLE_IDENTITY_BY_VERSION = {
|
|
114
|
+
"sha256": _identity_legacy,
|
|
115
|
+
"v0": _identity_legacy,
|
|
116
|
+
"v1": _identity_v1,
|
|
117
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Opencode framework detection and identity extraction.
|
|
2
|
+
|
|
3
|
+
Related design: docs/41.tools-designs/design-cybersecured-agent-cli.md
|
|
4
|
+
|
|
5
|
+
Detection signals (from diagnose sampling on macOS):
|
|
6
|
+
- Process chain contains "opencode" keyword
|
|
7
|
+
- Config dir ~/.config/opencode/ with opencode.json / oh-my-openagent.json
|
|
8
|
+
- No known env vars as of 2026-05-10
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any, Dict, Mapping
|
|
14
|
+
|
|
15
|
+
from ..infra.config_files import read_json, read_with_desensitize
|
|
16
|
+
from ._types import DetectionResult, ProcessInfo
|
|
17
|
+
|
|
18
|
+
NAME = "opencode"
|
|
19
|
+
|
|
20
|
+
_ENV_VARS: list[str] = ['OPENCODE', 'OPENCODE_RUN_ID', 'OPENCODE_PID', 'OPENCODE_PROCESS_ROLE']
|
|
21
|
+
_CONFIG_DIRS = [Path.home() / ".config" / "opencode"]
|
|
22
|
+
_CONFIG_FILENAMES = ["opencode.json"]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def detect(env: Mapping[str, str], proc_chain: list[ProcessInfo]) -> DetectionResult:
|
|
26
|
+
evidence: Dict[str, Any] = {}
|
|
27
|
+
|
|
28
|
+
for var in _ENV_VARS:
|
|
29
|
+
val = env.get(var)
|
|
30
|
+
evidence[f"env_var_{var}"] = "present" if val else "absent"
|
|
31
|
+
if val:
|
|
32
|
+
return DetectionResult(matched=True, evidence=evidence)
|
|
33
|
+
|
|
34
|
+
for info in proc_chain:
|
|
35
|
+
if "opencode" in info.args.lower():
|
|
36
|
+
evidence["process_match"] = "opencode-runtime"
|
|
37
|
+
return DetectionResult(matched=True, evidence=evidence)
|
|
38
|
+
evidence["process_match"] = "none"
|
|
39
|
+
|
|
40
|
+
# Config dir existence alone does NOT mean "currently running opencode".
|
|
41
|
+
for cfg_dir in _CONFIG_DIRS:
|
|
42
|
+
if cfg_dir.exists():
|
|
43
|
+
evidence["config_dir"] = str(cfg_dir)
|
|
44
|
+
break
|
|
45
|
+
else:
|
|
46
|
+
evidence["config_dir"] = "not found"
|
|
47
|
+
|
|
48
|
+
return DetectionResult(matched=False, evidence=evidence)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def collect_environment(env: Mapping[str, str],
|
|
52
|
+
proc_chain: list[ProcessInfo],
|
|
53
|
+
workspace: Path) -> Dict[str, Any]:
|
|
54
|
+
data: Dict[str, Any] = {
|
|
55
|
+
"env_vars": {},
|
|
56
|
+
"config_files": {},
|
|
57
|
+
"version": None,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
for var in _ENV_VARS:
|
|
61
|
+
if var in env:
|
|
62
|
+
data["env_vars"][var] = env[var]
|
|
63
|
+
|
|
64
|
+
for cfg_dir in _CONFIG_DIRS:
|
|
65
|
+
if not cfg_dir.exists():
|
|
66
|
+
continue
|
|
67
|
+
for filename in _CONFIG_FILENAMES:
|
|
68
|
+
path = cfg_dir / filename
|
|
69
|
+
if path.exists():
|
|
70
|
+
data["config_files"][filename] = read_with_desensitize(path)
|
|
71
|
+
version_path = cfg_dir / "opencode.json"
|
|
72
|
+
if version_path.exists():
|
|
73
|
+
cfg = read_json(version_path)
|
|
74
|
+
if isinstance(cfg, dict):
|
|
75
|
+
data["version"] = cfg.get("version")
|
|
76
|
+
break
|
|
77
|
+
|
|
78
|
+
return data
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _resolve_config_path(env: Mapping[str, str], workspace: Path) -> str:
|
|
82
|
+
for cfg_dir in _CONFIG_DIRS:
|
|
83
|
+
if cfg_dir.exists():
|
|
84
|
+
for filename in _CONFIG_FILENAMES:
|
|
85
|
+
path = cfg_dir / filename
|
|
86
|
+
if path.exists():
|
|
87
|
+
return str(path.resolve()).replace("\\", "/")
|
|
88
|
+
return str(cfg_dir.resolve()).replace("\\", "/")
|
|
89
|
+
|
|
90
|
+
return str(workspace.resolve()).replace("\\", "/")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _identity_legacy(env: Mapping[str, str],
|
|
94
|
+
proc_chain: list[ProcessInfo],
|
|
95
|
+
workspace: Path) -> Dict[str, str]:
|
|
96
|
+
"""Legacy fingerprint fields (sha256 + v0). Frozen — never change."""
|
|
97
|
+
return {"config_path": _resolve_config_path(env, workspace)}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _identity_v1(env: Mapping[str, str],
|
|
101
|
+
proc_chain: list[ProcessInfo],
|
|
102
|
+
workspace: Path) -> Dict[str, str]:
|
|
103
|
+
"""v1 fingerprint fields. Placeholder — will be refined in Phase B."""
|
|
104
|
+
return _identity_legacy(env, proc_chain, workspace)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
STABLE_IDENTITY_BY_VERSION = {
|
|
108
|
+
"sha256": _identity_legacy,
|
|
109
|
+
"v0": _identity_legacy,
|
|
110
|
+
"v1": _identity_v1,
|
|
111
|
+
}
|