agentremote-common 1.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.
@@ -0,0 +1,129 @@
1
+ """
2
+ SHA256 文件完整性校验
3
+
4
+ 用于:
5
+ - Skill 包文件完整性验证(安装/加载前校验)
6
+ - Robot 文件防篡改检测(运行时校验)
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import hashlib
12
+ import json
13
+ from pathlib import Path
14
+ from typing import Dict, List, Tuple
15
+
16
+
17
+ def hash_content(content: bytes | str) -> str:
18
+ """计算内容的 SHA256 hex digest"""
19
+ if isinstance(content, str):
20
+ content = content.encode("utf-8")
21
+ return hashlib.sha256(content).hexdigest()
22
+
23
+
24
+ def hash_file(file_path: str | Path) -> str:
25
+ """计算文件的 SHA256 hex digest"""
26
+ return hash_content(Path(file_path).read_bytes())
27
+
28
+
29
+ def hash_skill_directory(skill_dir: str | Path, exclude_self_yaml: bool = True) -> Dict[str, str]:
30
+ """
31
+ 计算 Skill 目录下所有文件的 SHA256。
32
+
33
+ 递归扫描,排除 __pycache__、.git、*.pyc。
34
+
35
+ Args:
36
+ skill_dir: Skill 目录路径
37
+ exclude_self_yaml: 是否排除 skill.yaml(签名场景需要排除自身)
38
+
39
+ Returns:
40
+ {relative_path: sha256_hex}
41
+ """
42
+ skill_dir = Path(skill_dir).resolve()
43
+ hashes: Dict[str, str] = {}
44
+
45
+ for fpath in sorted(skill_dir.rglob("*")):
46
+ if not fpath.is_file():
47
+ continue
48
+ # 跳过无关文件
49
+ parts = set(fpath.parts)
50
+ if "__pycache__" in parts or ".git" in parts:
51
+ continue
52
+ if fpath.suffix == ".pyc":
53
+ continue
54
+ # 跳过签名文件自身
55
+ if fpath.name.startswith("."):
56
+ continue
57
+ # 跳过 skill.yaml 自身(避免鸡生蛋问题:签名写入会改变哈希)
58
+ if exclude_self_yaml and fpath.name == "skill.yaml":
59
+ continue
60
+ # 跳过 Robot Framework 运行产物(每次执行都会变化)
61
+ if fpath.name in ("log.html", "output.xml", "report.html", "@AutomationLog.txt"):
62
+ continue
63
+
64
+ rel = str(fpath.relative_to(skill_dir)).replace("\\", "/")
65
+ hashes[rel] = hash_file(fpath)
66
+
67
+ return hashes
68
+
69
+
70
+ def verify_skill_integrity(
71
+ skill_dir: str | Path,
72
+ expected_hashes: Dict[str, str],
73
+ strict: bool = True,
74
+ ) -> Tuple[bool, List[str]]:
75
+ """
76
+ 验证 Skill 目录完整性。
77
+
78
+ Args:
79
+ skill_dir: Skill 目录路径
80
+ expected_hashes: {文件路径: sha256} 期望的哈希表
81
+ strict: True=拒绝未知文件(启动时校验), False=只检查已知文件(执行时校验)
82
+
83
+ Returns:
84
+ (passed, [错误信息列表])
85
+ """
86
+ errors: List[str] = []
87
+ actual = hash_skill_directory(skill_dir)
88
+
89
+ # 检查期望文件是否存在且哈希一致
90
+ for rel_path, expected_hash in expected_hashes.items():
91
+ fpath = Path(skill_dir) / rel_path
92
+ if not fpath.exists():
93
+ errors.append(f"[MISSING] {rel_path}: 文件不存在或被删除")
94
+ continue
95
+
96
+ actual_hash = actual.get(rel_path, "")
97
+ if actual_hash != expected_hash:
98
+ errors.append(
99
+ f"[TAMPERED] {rel_path}: 哈希不匹配 "
100
+ f"期望 {expected_hash[:16]}... 实际 {actual_hash[:16]}..."
101
+ )
102
+
103
+ # 检查是否有多余文件(仅 strict 模式)
104
+ if strict:
105
+ for rel_path in actual:
106
+ if rel_path not in expected_hashes:
107
+ errors.append(f"[EXTRA] {rel_path}: 未在签名清单中的文件")
108
+
109
+ return len(errors) == 0, errors
110
+
111
+
112
+ def compute_manifest_hash(skill_dir: str | Path, exclude_signature: bool = True) -> str:
113
+ """
114
+ 计算 Skill 目录的内容哈希(用于签名)。
115
+
116
+ 将所有文件按路径排序,拼接 SHA256 后再做一次 SHA256,
117
+ 得到该目录的"内容指纹"。
118
+
119
+ Args:
120
+ skill_dir: Skill 目录
121
+ exclude_signature: 是否排除 _security 相关字段
122
+
123
+ Returns:
124
+ 内容指纹的 hex digest
125
+ """
126
+ hashes = hash_skill_directory(skill_dir)
127
+ # 按路径排序拼接
128
+ combined = "".join(f"{k}:{v}" for k, v in sorted(hashes.items()))
129
+ return hash_content(combined)
@@ -0,0 +1,108 @@
1
+ """
2
+ JWT 认证 — Plugin → Middleware 身份认证
3
+
4
+ Plugin 用设备私钥签 JWT(ES256),Middleware 用设备公钥验证。
5
+
6
+ JWT Payload:
7
+ {
8
+ "sub": "sh_win_01", # 设备 ID
9
+ "tenant": "acme_shanghai", # 租户
10
+ "iat": 1719323500, # 签发时间
11
+ "exp": 1719327100, # 过期时间(1h)
12
+ "jti": "random_nonce" # 防重放
13
+ }
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import time
19
+ import uuid
20
+ from typing import Any, Dict, Optional
21
+
22
+ import jwt # PyJWT
23
+ from cryptography.hazmat.primitives.asymmetric import ec
24
+
25
+
26
+ # Token 有效期(Demo: 1 小时)
27
+ TOKEN_TTL_SEC = 3600
28
+
29
+ # JWT 算法
30
+ ALGORITHM = "ES256"
31
+
32
+
33
+ def create_auth_token(
34
+ device_private_key: ec.EllipticCurvePrivateKey,
35
+ device_id: str,
36
+ tenant_id: str,
37
+ ttl_sec: int = TOKEN_TTL_SEC,
38
+ extra_claims: Optional[Dict[str, Any]] = None,
39
+ ) -> str:
40
+ """
41
+ 创建设备认证 JWT。
42
+
43
+ Args:
44
+ device_private_key: 设备 ECDSA 私钥(P-256)
45
+ device_id: 设备标识 (对应 node_id)
46
+ tenant_id: 租户标识
47
+ ttl_sec: 有效期(秒)
48
+ extra_claims: 额外的 JWT claims
49
+
50
+ Returns:
51
+ JWT 字符串
52
+ """
53
+ now = int(time.time())
54
+ payload = {
55
+ "sub": device_id,
56
+ "tenant": tenant_id,
57
+ "iat": now,
58
+ "exp": now + ttl_sec,
59
+ "jti": uuid.uuid4().hex[:16],
60
+ }
61
+ if extra_claims:
62
+ payload.update(extra_claims)
63
+
64
+ return jwt.encode(payload, device_private_key, algorithm=ALGORITHM)
65
+
66
+
67
+ def verify_auth_token(
68
+ token: str,
69
+ device_public_key: ec.EllipticCurvePublicKey,
70
+ expected_device_id: Optional[str] = None,
71
+ expected_tenant: Optional[str] = None,
72
+ ) -> Dict[str, Any]:
73
+ """
74
+ 验证设备认证 JWT。
75
+
76
+ Args:
77
+ token: JWT 字符串
78
+ device_public_key: 设备 ECDSA 公钥(P-256)
79
+ expected_device_id: 期望的设备 ID(可选,不传则跳过)
80
+ expected_tenant: 期望的租户(可选,不传则跳过)
81
+
82
+ Returns:
83
+ 解码后的 JWT payload
84
+
85
+ Raises:
86
+ jwt.ExpiredSignatureError: Token 已过期
87
+ jwt.InvalidSignatureError: 签名无效
88
+ jwt.InvalidTokenError: Token 无效
89
+ ValueError: sub/tenant 不匹配
90
+ """
91
+ payload = jwt.decode(
92
+ token,
93
+ device_public_key,
94
+ algorithms=[ALGORITHM],
95
+ options={"require": ["sub", "tenant", "iat", "exp", "jti"]},
96
+ )
97
+
98
+ if expected_device_id and payload.get("sub") != expected_device_id:
99
+ raise ValueError(
100
+ f"JWT sub mismatch: expected '{expected_device_id}', got '{payload.get('sub')}'"
101
+ )
102
+
103
+ if expected_tenant and payload.get("tenant") != expected_tenant:
104
+ raise ValueError(
105
+ f"JWT tenant mismatch: expected '{expected_tenant}', got '{payload.get('tenant')}'"
106
+ )
107
+
108
+ return payload
@@ -0,0 +1,128 @@
1
+ """
2
+ ECDSA P-256 密钥管理 — 生成密钥对、签名、验签
3
+
4
+ 用于:
5
+ - 开发者签名 Skill 包(私钥签名,Plugin 用公钥验证)
6
+ - 设备密钥(Plugin 用私钥签 JWT,Middleware 用公钥验证)
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import base64
12
+ from pathlib import Path
13
+ from typing import Tuple
14
+
15
+ from cryptography.hazmat.primitives import hashes, serialization
16
+ from cryptography.hazmat.primitives.asymmetric import ec
17
+ from cryptography.hazmat.primitives.asymmetric.utils import (
18
+ decode_dss_signature,
19
+ encode_dss_signature,
20
+ )
21
+ from cryptography.exceptions import InvalidSignature
22
+
23
+
24
+ # ═══════════════════════════════════════════
25
+ # 密钥生成
26
+ # ═══════════════════════════════════════════
27
+
28
+ def generate_keypair() -> Tuple[ec.EllipticCurvePrivateKey, ec.EllipticCurvePublicKey]:
29
+ """生成 ECDSA P-256 密钥对"""
30
+ private = ec.generate_private_key(ec.SECP256R1())
31
+ return private, private.public_key()
32
+
33
+
34
+ # ═══════════════════════════════════════════
35
+ # PEM 序列化
36
+ # ═══════════════════════════════════════════
37
+
38
+ def private_to_pem(private_key: ec.EllipticCurvePrivateKey, password: bytes | None = None) -> bytes:
39
+ """私钥 → PEM 字节"""
40
+ enc = serialization.BestAvailableEncryption(password) if password else serialization.NoEncryption()
41
+ return private_key.private_bytes(
42
+ encoding=serialization.Encoding.PEM,
43
+ format=serialization.PrivateFormat.PKCS8,
44
+ encryption_algorithm=enc,
45
+ )
46
+
47
+
48
+ def public_to_pem(public_key: ec.EllipticCurvePublicKey) -> bytes:
49
+ """公钥 → PEM 字节"""
50
+ return public_key.public_bytes(
51
+ encoding=serialization.Encoding.PEM,
52
+ format=serialization.PublicFormat.SubjectPublicKeyInfo,
53
+ )
54
+
55
+
56
+ def load_private_key(pem_data: bytes, password: bytes | None = None) -> ec.EllipticCurvePrivateKey:
57
+ """从 PEM 字节加载私钥"""
58
+ key = serialization.load_pem_private_key(pem_data, password=password)
59
+ if not isinstance(key, ec.EllipticCurvePrivateKey):
60
+ raise TypeError(f"Expected EC private key, got {type(key)}")
61
+ return key
62
+
63
+
64
+ def load_public_key(pem_data: bytes) -> ec.EllipticCurvePublicKey:
65
+ """从 PEM 字节加载公钥"""
66
+ key = serialization.load_pem_public_key(pem_data)
67
+ if not isinstance(key, ec.EllipticCurvePublicKey):
68
+ raise TypeError(f"Expected EC public key, got {type(key)}")
69
+ return key
70
+
71
+
72
+ def load_private_key_file(path: str | Path) -> ec.EllipticCurvePrivateKey:
73
+ """从文件加载私钥"""
74
+ return load_private_key(Path(path).read_bytes())
75
+
76
+
77
+ def load_public_key_file(path: str | Path) -> ec.EllipticCurvePublicKey:
78
+ """从文件加载公钥"""
79
+ return load_public_key(Path(path).read_bytes())
80
+
81
+
82
+ # ═══════════════════════════════════════════
83
+ # 签名 / 验签
84
+ # ═══════════════════════════════════════════
85
+
86
+ def sign_data(private_key: ec.EllipticCurvePrivateKey, data: bytes) -> str:
87
+ """
88
+ 用 ECDSA P-256 + SHA256 签名数据。
89
+
90
+ Returns:
91
+ Base64 编码的签名(DER 格式)
92
+ """
93
+ der_sig = private_key.sign(data, ec.ECDSA(hashes.SHA256()))
94
+ return base64.b64encode(der_sig).decode("ascii")
95
+
96
+
97
+ def verify_signature(public_key: ec.EllipticCurvePublicKey, data: bytes, signature_b64: str) -> bool:
98
+ """
99
+ 验证 ECDSA P-256 + SHA256 签名。
100
+
101
+ Args:
102
+ public_key: 签名者的公钥
103
+ data: 原始数据
104
+ signature_b64: Base64 编码的签名(DER 格式)
105
+
106
+ Returns:
107
+ True 如果签名有效
108
+ """
109
+ try:
110
+ der_sig = base64.b64decode(signature_b64)
111
+ public_key.verify(der_sig, data, ec.ECDSA(hashes.SHA256()))
112
+ return True
113
+ except (InvalidSignature, Exception):
114
+ return False
115
+
116
+
117
+ def sign_json(private_key: ec.EllipticCurvePrivateKey, data: dict) -> str:
118
+ """对 JSON 可序列化的 dict 签名(先排序 key 再序列化)"""
119
+ import json
120
+ canonical = json.dumps(data, sort_keys=True, ensure_ascii=False).encode("utf-8")
121
+ return sign_data(private_key, canonical)
122
+
123
+
124
+ def verify_json(public_key: ec.EllipticCurvePublicKey, data: dict, signature_b64: str) -> bool:
125
+ """验证对 JSON dict 的签名"""
126
+ import json
127
+ canonical = json.dumps(data, sort_keys=True, ensure_ascii=False).encode("utf-8")
128
+ return verify_signature(public_key, canonical, signature_b64)
@@ -0,0 +1,157 @@
1
+ """
2
+ AES-256-GCM 许可证加密/解密 — capability-first.
3
+
4
+ 许可证文件包含:
5
+ {
6
+ "tenant_id": "acme_shanghai",
7
+ "capability_id": "desktop.cua",
8
+ "status": "active",
9
+ "max_tasks": 0,
10
+ "task_count": 0,
11
+ "issued_at": "2026-06-25T00:00:00",
12
+ "expires_at": "2026-12-25T00:00:00",
13
+ "node_ids": ["sh_win_01"]
14
+ }
15
+
16
+ 加密: AES-256-GCM, 随机 IV (12 bytes), 认证标签 (16 bytes)
17
+ 格式: base64(iv + ciphertext + tag)
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import base64
23
+ import json
24
+ import os
25
+ from dataclasses import dataclass, field
26
+ from datetime import datetime
27
+ from pathlib import Path
28
+ from typing import Any, Dict, List
29
+
30
+ from cryptography.hazmat.primitives.ciphers.aead import AESGCM
31
+
32
+
33
+ # ═══════════════════════════════════════════
34
+ # 数据模型
35
+ # ═══════════════════════════════════════════
36
+
37
+ @dataclass
38
+ class LicenseData:
39
+ """许可证数据 — capability-first."""
40
+ tenant_id: str
41
+ capability_id: str # ★ 替代 skill_name
42
+ status: str = "active"
43
+ max_tasks: int = 0
44
+ task_count: int = 0
45
+ issued_at: str = ""
46
+ expires_at: str = ""
47
+ node_ids: List[str] = field(default_factory=list)
48
+
49
+ def to_dict(self) -> Dict[str, Any]:
50
+ return {
51
+ "tenant_id": self.tenant_id,
52
+ "capability_id": self.capability_id,
53
+ "status": self.status,
54
+ "max_tasks": self.max_tasks,
55
+ "task_count": self.task_count,
56
+ "issued_at": self.issued_at,
57
+ "expires_at": self.expires_at,
58
+ "node_ids": self.node_ids,
59
+ "version": 2,
60
+ }
61
+
62
+ @classmethod
63
+ def from_dict(cls, d: Dict[str, Any]) -> "LicenseData":
64
+ # Support both old (skill_name) and new (capability_id) format
65
+ cap_id = d.get("capability_id", d.get("skill_name", ""))
66
+ return cls(
67
+ tenant_id=d["tenant_id"],
68
+ capability_id=cap_id,
69
+ status=d.get("status", "active"),
70
+ max_tasks=d.get("max_tasks", 0),
71
+ task_count=d.get("task_count", 0),
72
+ issued_at=d.get("issued_at", ""),
73
+ expires_at=d.get("expires_at", ""),
74
+ node_ids=d.get("node_ids", []),
75
+ )
76
+
77
+ def is_expired(self) -> bool:
78
+ if not self.expires_at:
79
+ return False
80
+ try:
81
+ exp = datetime.fromisoformat(self.expires_at)
82
+ return datetime.utcnow() > exp
83
+ except ValueError:
84
+ return False
85
+
86
+ def is_valid_for_node(self, node_id: str) -> bool:
87
+ if not self.node_ids:
88
+ return True
89
+ return node_id in self.node_ids
90
+
91
+ def can_execute(self) -> bool:
92
+ if self.status == "expired":
93
+ return False
94
+ if self.is_expired():
95
+ return False
96
+ if self.status == "trial" and self.max_tasks > 0 and self.task_count >= self.max_tasks:
97
+ return False
98
+ return True
99
+
100
+
101
+ # ═══════════════════════════════════════════
102
+ # 密钥管理
103
+ # ═══════════════════════════════════════════
104
+
105
+ def generate_license_key() -> bytes:
106
+ return AESGCM.generate_key(bit_length=256)
107
+
108
+
109
+ def load_license_key(path: str | Path) -> bytes:
110
+ return Path(path).read_bytes()
111
+
112
+
113
+ def save_license_key(path: str | Path, key: bytes) -> None:
114
+ Path(path).write_bytes(key)
115
+ try:
116
+ os.chmod(path, 0o600)
117
+ except Exception:
118
+ pass
119
+
120
+
121
+ # ═══════════════════════════════════════════
122
+ # 加密 / 解密
123
+ # ═══════════════════════════════════════════
124
+
125
+ def encrypt_license(license_data: LicenseData, key: bytes) -> str:
126
+ aesgcm = AESGCM(key)
127
+ plaintext = json.dumps(license_data.to_dict(), ensure_ascii=False).encode("utf-8")
128
+ nonce = os.urandom(12)
129
+ ciphertext = aesgcm.encrypt(nonce, plaintext, None)
130
+ return base64.b64encode(nonce + ciphertext).decode("ascii")
131
+
132
+
133
+ def decrypt_license(encrypted: str, key: bytes) -> LicenseData:
134
+ aesgcm = AESGCM(key)
135
+ raw = base64.b64decode(encrypted)
136
+ nonce = raw[:12]
137
+ ciphertext = raw[12:]
138
+ try:
139
+ plaintext = aesgcm.decrypt(nonce, ciphertext, None)
140
+ except Exception as e:
141
+ raise ValueError(f"License decryption failed: {e}") from e
142
+ data = json.loads(plaintext.decode("utf-8"))
143
+ return LicenseData.from_dict(data)
144
+
145
+
146
+ # ═══════════════════════════════════════════
147
+ # 文件操作
148
+ # ═══════════════════════════════════════════
149
+
150
+ def encrypt_license_file(license_data: LicenseData, key: bytes, output_path: str | Path) -> None:
151
+ encrypted = encrypt_license(license_data, key)
152
+ Path(output_path).write_text(encrypted, encoding="ascii")
153
+
154
+
155
+ def decrypt_license_file(file_path: str | Path, key: bytes) -> LicenseData:
156
+ encrypted = Path(file_path).read_text(encoding="ascii").strip()
157
+ return decrypt_license(encrypted, key)
@@ -0,0 +1,34 @@
1
+ """AgentRemote data models — re-exported by domain."""
2
+
3
+ from .capability import CapabilityManifest, FlowManifest
4
+ from .license import GatewayCheckResult, License, LicenseInfo, LicenseStatus
5
+ from .node import BackendStatus, Node, NodeStatus, RuntimeBackend
6
+ from .task import RunnerType, Task, TaskResult, TaskStatus
7
+
8
+ # 向后兼容 — 协议模型原在 models.py,现从 protocol 子包重导出
9
+ from ..protocol.message import MqttConfig, WSMessage, WSMessageType
10
+
11
+ __all__ = [
12
+ # task
13
+ "RunnerType",
14
+ "TaskStatus",
15
+ "Task",
16
+ "TaskResult",
17
+ # capability
18
+ "CapabilityManifest",
19
+ "FlowManifest",
20
+ # node
21
+ "BackendStatus",
22
+ "NodeStatus",
23
+ "RuntimeBackend",
24
+ "Node",
25
+ # license
26
+ "LicenseStatus",
27
+ "LicenseInfo",
28
+ "License",
29
+ "GatewayCheckResult",
30
+ # protocol (backward compat — 原在 models.py)
31
+ "MqttConfig",
32
+ "WSMessage",
33
+ "WSMessageType",
34
+ ]
@@ -0,0 +1,32 @@
1
+ """
2
+ AgentRemote — 能力注册数据模型.
3
+
4
+ CapabilityManifest — 替代旧的 SkillManifest + Capability.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any, Dict, List, Optional
10
+
11
+ from pydantic import BaseModel, Field
12
+
13
+
14
+ class CapabilityManifest(BaseModel):
15
+ """节点上报的能力清单(替代 SkillManifest)"""
16
+ id: str # "desktop.cua" | "crm-transaction-filter"
17
+ runner: str # "cua" | "rpa" — Middleware 用此字段路由
18
+ tools: List[str] = Field(default_factory=list) # ["click","type_text","list_apps",...]
19
+ description: str = ""
20
+ version: str = "1.0.0"
21
+ metadata: Dict[str, Any] = Field(default_factory=dict) # 预留扩展
22
+
23
+
24
+ class FlowManifest(BaseModel):
25
+ """RPA 流程清单(替代旧 skill.yaml 中 runner=rpa 的部分)"""
26
+ id: str # "crm-transaction-filter"
27
+ name: str = ""
28
+ runner: str = "rpa"
29
+ robot_file: str = "" # 相对路径, e.g. "crm_transaction_date_filter.robot"
30
+ tasks: List[Dict[str, str]] = Field(default_factory=list) # [{id:"01", name:"..."}]
31
+ description: str = ""
32
+ version: str = "1.0.0"
@@ -0,0 +1,50 @@
1
+ """
2
+ AgentRemote — 许可证和三道校验数据模型.
3
+
4
+ LicenseStatus, LicenseInfo, License, GatewayCheckResult.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from datetime import datetime
10
+ from enum import Enum
11
+ from typing import List, Optional
12
+
13
+ from pydantic import BaseModel, Field
14
+
15
+
16
+ class LicenseStatus(str, Enum):
17
+ TRIAL = "trial"
18
+ ACTIVE = "active"
19
+ EXPIRED = "expired"
20
+
21
+
22
+ class LicenseInfo(BaseModel):
23
+ """解密后的许可证信息"""
24
+ tenant_id: str
25
+ capability_id: str # 授权的能力 ID
26
+ status: str = "active" # trial | active | expired
27
+ max_tasks: int = 0 # 0 = unlimited
28
+ task_count: int = 0
29
+ issued_at: str = ""
30
+ expires_at: str = ""
31
+ node_ids: List[str] = Field(default_factory=list)
32
+ valid: bool = True # 许可证是否有效
33
+
34
+
35
+ class License(BaseModel):
36
+ """许可证"""
37
+ tenant_id: str
38
+ capability_id: str # 授权的能力 ID
39
+ status: LicenseStatus = LicenseStatus.TRIAL
40
+ max_tasks: int = 10
41
+ task_count: int = 0
42
+ expires_at: Optional[datetime] = None
43
+
44
+
45
+ class GatewayCheckResult(BaseModel):
46
+ """三道校验结果"""
47
+ passed: bool
48
+ check_name: str # "身份" | "能力" | "许可证"
49
+ detail: str = ""
50
+ rejected_reason: str = ""
@@ -0,0 +1,47 @@
1
+ """
2
+ AgentRemote — 节点和运行时后端数据模型.
3
+
4
+ Node, RuntimeBackend, BackendStatus, NodeStatus.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from enum import Enum
10
+ from typing import List, Optional
11
+
12
+ from pydantic import BaseModel, Field
13
+
14
+
15
+ class BackendStatus(str, Enum):
16
+ READY = "ready"
17
+ DEGRADED = "degraded"
18
+ UNAVAILABLE = "unavailable"
19
+ LIMITED = "limited"
20
+
21
+
22
+ class NodeStatus(str, Enum):
23
+ IDLE = "IDLE"
24
+ BUSY = "BUSY"
25
+ SESSION = "SESSION"
26
+ FDE_DEBUG = "FDE_DEBUG"
27
+ OFFLINE = "OFFLINE"
28
+
29
+
30
+ class RuntimeBackend(BaseModel):
31
+ """Plugin 本地的运行时后端(Playwright / Shell / CUA / Native)"""
32
+ backend_name: str # playwright | shell | cua | native
33
+ status: BackendStatus = BackendStatus.READY
34
+ version: Optional[str] = None
35
+ platform: Optional[str] = None
36
+ capabilities: List[str] = Field(default_factory=list)
37
+ limitations: List[str] = Field(default_factory=list)
38
+
39
+
40
+ class Node(BaseModel):
41
+ """节点(一台客户电脑)"""
42
+ node_id: str # "sh_win_01"
43
+ tenant_id: str # "acme_shanghai"
44
+ hostname: str = ""
45
+ os: str = ""
46
+ status: NodeStatus = NodeStatus.IDLE
47
+ plugin_version: str = "v0.3.0"