debug-control-plane 0.4.0__tar.gz → 0.5.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.
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.0}/PKG-INFO +4 -4
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.0}/README.md +3 -3
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.0}/debug_control_plane/__init__.py +1 -1
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.0}/debug_control_plane/mcp_plane/server.py +5 -2
- debug_control_plane-0.5.0/debug_control_plane/mcp_plane/token_provider.py +138 -0
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.0}/debug_control_plane.egg-info/PKG-INFO +4 -4
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.0}/debug_control_plane.egg-info/SOURCES.txt +3 -1
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.0}/pyproject.toml +1 -1
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.0}/tests/test_server.py +1 -1
- debug_control_plane-0.5.0/tests/test_token_provider.py +168 -0
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.0}/LICENSE +0 -0
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.0}/debug_control_plane/device_discovery/__init__.py +0 -0
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.0}/debug_control_plane/device_discovery/device_candidates.py +0 -0
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.0}/debug_control_plane/device_discovery/device_pool.py +0 -0
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.0}/debug_control_plane/device_discovery/discovery/__init__.py +0 -0
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.0}/debug_control_plane/device_discovery/discovery/cross_identify.py +0 -0
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.0}/debug_control_plane/device_discovery/discovery/lan_scan.py +0 -0
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.0}/debug_control_plane/device_discovery/discovery/manual_registry.py +0 -0
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.0}/debug_control_plane/device_discovery/discovery/usb_identity.py +0 -0
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.0}/debug_control_plane/device_discovery/discovery/vpn_immune.py +0 -0
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.0}/debug_control_plane/device_discovery/endpoint.py +0 -0
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.0}/debug_control_plane/device_discovery/protocol.py +0 -0
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.0}/debug_control_plane/mcp_plane/__init__.py +0 -0
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.0}/debug_control_plane/mcp_plane/bridge_client.py +0 -0
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.0}/debug_control_plane/mcp_plane/capability_mirror.py +0 -0
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.0}/debug_control_plane/mcp_plane/semantic_provider.py +0 -0
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.0}/debug_control_plane.egg-info/dependency_links.txt +0 -0
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.0}/debug_control_plane.egg-info/entry_points.txt +0 -0
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.0}/debug_control_plane.egg-info/requires.txt +0 -0
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.0}/debug_control_plane.egg-info/top_level.txt +0 -0
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.0}/setup.cfg +0 -0
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.0}/tests/test_acceptance_flutter_app_auth.py +0 -0
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.0}/tests/test_bridge_client.py +0 -0
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.0}/tests/test_capability_mirror.py +0 -0
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.0}/tests/test_cross_lang_kotlin_plane.py +0 -0
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.0}/tests/test_device_candidates.py +0 -0
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.0}/tests/test_device_pool.py +0 -0
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.0}/tests/test_discovery_cross_identify.py +0 -0
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.0}/tests/test_discovery_lan_scan.py +0 -0
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.0}/tests/test_discovery_manual_registry.py +0 -0
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.0}/tests/test_discovery_usb_identity.py +0 -0
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.0}/tests/test_discovery_vpn_immune.py +0 -0
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.0}/tests/test_e2e_mock.py +0 -0
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.0}/tests/test_endpoint.py +0 -0
- {debug_control_plane-0.4.0 → debug_control_plane-0.5.0}/tests/test_import_sanity.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: debug-control-plane
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Multi-product reusable debug control plane (device discovery + MCP adapter).
|
|
5
5
|
Author: tangxiaolu
|
|
6
6
|
License: MIT
|
|
@@ -62,7 +62,7 @@ python/
|
|
|
62
62
|
├── README.md # this file
|
|
63
63
|
├── LICENSE # MIT
|
|
64
64
|
└── debug_control_plane/
|
|
65
|
-
├── __init__.py # __version__ = "0.
|
|
65
|
+
├── __init__.py # __version__ = "0.5.0"
|
|
66
66
|
├── device_discovery/ # USB/LAN device discovery + device pool
|
|
67
67
|
│ ├── device_candidates.py
|
|
68
68
|
│ ├── device_pool.py # identity-keyed pool, TTL expiry / 身份键池,TTL 过期
|
|
@@ -92,8 +92,8 @@ Points at `debug_control_plane.mcp_plane.server:main` — a bare server (no busi
|
|
|
92
92
|
|
|
93
93
|
## Version / 版本
|
|
94
94
|
|
|
95
|
-
`0.
|
|
96
|
-
`0.
|
|
95
|
+
`0.5.0` — aligned with Kotlin/Dart/Flutter `0.5.0`, API unstable.
|
|
96
|
+
`0.5.0` —— 与 Kotlin/Dart/Flutter `0.5.0` 对齐,API 不稳定。
|
|
97
97
|
|
|
98
98
|
## License / 许可证
|
|
99
99
|
|
|
@@ -43,7 +43,7 @@ python/
|
|
|
43
43
|
├── README.md # this file
|
|
44
44
|
├── LICENSE # MIT
|
|
45
45
|
└── debug_control_plane/
|
|
46
|
-
├── __init__.py # __version__ = "0.
|
|
46
|
+
├── __init__.py # __version__ = "0.5.0"
|
|
47
47
|
├── device_discovery/ # USB/LAN device discovery + device pool
|
|
48
48
|
│ ├── device_candidates.py
|
|
49
49
|
│ ├── device_pool.py # identity-keyed pool, TTL expiry / 身份键池,TTL 过期
|
|
@@ -73,8 +73,8 @@ Points at `debug_control_plane.mcp_plane.server:main` — a bare server (no busi
|
|
|
73
73
|
|
|
74
74
|
## Version / 版本
|
|
75
75
|
|
|
76
|
-
`0.
|
|
77
|
-
`0.
|
|
76
|
+
`0.5.0` — aligned with Kotlin/Dart/Flutter `0.5.0`, API unstable.
|
|
77
|
+
`0.5.0` —— 与 Kotlin/Dart/Flutter `0.5.0` 对齐,API 不稳定。
|
|
78
78
|
|
|
79
79
|
## License / 许可证
|
|
80
80
|
|
{debug_control_plane-0.4.0 → debug_control_plane-0.5.0}/debug_control_plane/mcp_plane/server.py
RENAMED
|
@@ -87,6 +87,7 @@ from .bridge_client import (
|
|
|
87
87
|
)
|
|
88
88
|
from .capability_mirror import CapabilityMirror, CapabilitySchema, ToolSpec
|
|
89
89
|
from .semantic_provider import SemanticProvider
|
|
90
|
+
from .token_provider import FileTokenProvider
|
|
90
91
|
|
|
91
92
|
# ★ BF008-010 (Contract §0.1 边界 1 收尾 + 方案 X): capability-specific semantic
|
|
92
93
|
# sugar 已迁业务侧(强耦合产品 protocol,属业务知识)。平面 server 零业务依赖
|
|
@@ -256,7 +257,7 @@ class McpServer:
|
|
|
256
257
|
pool: DevicePool,
|
|
257
258
|
*,
|
|
258
259
|
server_name: str = "mcp-debug-bridge",
|
|
259
|
-
server_version: str = "0.
|
|
260
|
+
server_version: str = "0.5.0",
|
|
260
261
|
providers: list[SemanticProvider] | None = None,
|
|
261
262
|
tool_handlers: dict[str, Any] | None = None,
|
|
262
263
|
) -> None:
|
|
@@ -1052,7 +1053,9 @@ def main() -> None:
|
|
|
1052
1053
|
format="%(asctime)s %(name)s %(levelname)s %(message)s",
|
|
1053
1054
|
)
|
|
1054
1055
|
pool = DevicePool(persist_path=Path.home() / ".debug-control-plane" / "devices.json")
|
|
1055
|
-
client = BridgeClient(
|
|
1056
|
+
client = BridgeClient(
|
|
1057
|
+
pool=pool, token_provider=FileTokenProvider()
|
|
1058
|
+
)
|
|
1056
1059
|
mirror = CapabilityMirror(client=client)
|
|
1057
1060
|
server = McpServer(mirror=mirror, client=client, pool=pool)
|
|
1058
1061
|
server.run_stdio() # blocks; exits when the AI client closes stdin
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""File-backed debug auth token provider (R004-BF001).
|
|
2
|
+
|
|
3
|
+
FileTokenProvider is the production implementation of
|
|
4
|
+
:class:`debug_control_plane.mcp_plane.bridge_client.DebugAuthTokenProvider`.
|
|
5
|
+
It persists per-device bearer tokens to ``~/.debug-control-plane/tokens.json``
|
|
6
|
+
so tokens survive python process restarts (design R004 §3.2).
|
|
7
|
+
|
|
8
|
+
Schema (independent from devices.json — no shared structure or imports)::
|
|
9
|
+
|
|
10
|
+
{"version": 1, "tokens": {device_id: {"token": ..., "tokenId": ...,
|
|
11
|
+
"expiresAt": ...}}}
|
|
12
|
+
|
|
13
|
+
Concurrency model: lazy-loaded, single-threaded. This provider assumes it is
|
|
14
|
+
only used from the asyncio event-loop thread (the MCP server's threading
|
|
15
|
+
model). No locking is performed — the design explicitly accepted this
|
|
16
|
+
asyncio-single-thread assumption.
|
|
17
|
+
|
|
18
|
+
Security: plaintext tokens on the developer machine with 0600 permissions
|
|
19
|
+
(user-approved decision). The 0600 mode is guaranteed by creating the tmp
|
|
20
|
+
file via ``os.open(..., 0o600)`` (bypassing umask, so no 0644 window exists)
|
|
21
|
+
followed by an atomic ``os.replace``.
|
|
22
|
+
|
|
23
|
+
Corruption policy: a corrupt file or an unknown ``version`` silently falls
|
|
24
|
+
back to an empty store — tokens are regenerable and the server must not
|
|
25
|
+
crash on load.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import json
|
|
31
|
+
import os
|
|
32
|
+
from datetime import datetime, timezone
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
from typing import Any, Mapping
|
|
35
|
+
|
|
36
|
+
_SCHEMA_VERSION = 1
|
|
37
|
+
_DEFAULT_PATH = Path.home() / ".debug-control-plane" / "tokens.json"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _is_expired(value: str) -> bool:
|
|
41
|
+
"""Return True if ``value`` is a tz-aware ISO timestamp in the past.
|
|
42
|
+
|
|
43
|
+
Parse failures and naive timestamps are treated as *not* expired: the
|
|
44
|
+
phone's 401 is the final arbiter, and a parsing bug must not break the
|
|
45
|
+
auth chain (design §5). Evaluated at read time; never written back.
|
|
46
|
+
"""
|
|
47
|
+
try:
|
|
48
|
+
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
49
|
+
except (ValueError, TypeError):
|
|
50
|
+
return False
|
|
51
|
+
if parsed.tzinfo is None:
|
|
52
|
+
return False
|
|
53
|
+
return parsed <= datetime.now(timezone.utc)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class FileTokenProvider:
|
|
57
|
+
"""File-backed token store implementing the DebugAuthTokenProvider protocol."""
|
|
58
|
+
|
|
59
|
+
def __init__(self, path: Path | None = None) -> None:
|
|
60
|
+
self._path = path or _DEFAULT_PATH
|
|
61
|
+
self._loaded = False
|
|
62
|
+
self._tokens: dict[str, dict[str, str]] = {}
|
|
63
|
+
|
|
64
|
+
# ------------------------------------------------------------------
|
|
65
|
+
# Protocol surface
|
|
66
|
+
# ------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
def get_token(self, device_id: str) -> str | None:
|
|
69
|
+
"""Return the stored token for ``device_id`` if present and unexpired."""
|
|
70
|
+
self._ensure_loaded()
|
|
71
|
+
row = self._tokens.get(device_id)
|
|
72
|
+
if row is None:
|
|
73
|
+
return None
|
|
74
|
+
expires_at = row.get("expiresAt", "")
|
|
75
|
+
if expires_at and _is_expired(expires_at):
|
|
76
|
+
return None
|
|
77
|
+
return row.get("token")
|
|
78
|
+
|
|
79
|
+
def save_token(
|
|
80
|
+
self, device_id: str, token: str, metadata: Mapping[str, Any]
|
|
81
|
+
) -> None:
|
|
82
|
+
"""Store ``token`` for ``device_id`` and persist (full file rewrite)."""
|
|
83
|
+
self._ensure_loaded()
|
|
84
|
+
row: dict[str, str] = {"token": token}
|
|
85
|
+
for key, value in metadata.items():
|
|
86
|
+
row[key] = value if isinstance(value, str) else str(value)
|
|
87
|
+
self._tokens[device_id] = row
|
|
88
|
+
self._flush()
|
|
89
|
+
|
|
90
|
+
def clear_token(self, device_id: str, reason: str) -> None:
|
|
91
|
+
"""Drop the row for ``device_id`` and persist (rewrite without it)."""
|
|
92
|
+
self._ensure_loaded()
|
|
93
|
+
if device_id in self._tokens:
|
|
94
|
+
del self._tokens[device_id]
|
|
95
|
+
self._flush()
|
|
96
|
+
|
|
97
|
+
# ------------------------------------------------------------------
|
|
98
|
+
# Internals
|
|
99
|
+
# ------------------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
def _ensure_loaded(self) -> None:
|
|
102
|
+
if self._loaded:
|
|
103
|
+
return
|
|
104
|
+
self._loaded = True
|
|
105
|
+
try:
|
|
106
|
+
data = json.loads(self._path.read_text(encoding="utf-8"))
|
|
107
|
+
except (OSError, ValueError):
|
|
108
|
+
return # missing or corrupt → silent empty fallback
|
|
109
|
+
if not isinstance(data, dict) or data.get("version") != _SCHEMA_VERSION:
|
|
110
|
+
return # unknown schema → silent empty fallback
|
|
111
|
+
tokens = data.get("tokens")
|
|
112
|
+
if not isinstance(tokens, dict):
|
|
113
|
+
return
|
|
114
|
+
self._tokens = {
|
|
115
|
+
device_id: row
|
|
116
|
+
for device_id, row in tokens.items()
|
|
117
|
+
if isinstance(row, dict)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
def _flush(self) -> None:
|
|
121
|
+
"""Atomically write the store with 0600 permissions."""
|
|
122
|
+
payload = json.dumps(
|
|
123
|
+
{"version": _SCHEMA_VERSION, "tokens": self._tokens},
|
|
124
|
+
indent=2,
|
|
125
|
+
ensure_ascii=False,
|
|
126
|
+
)
|
|
127
|
+
self._path.parent.mkdir(parents=True, exist_ok=True)
|
|
128
|
+
tmp_path = self._path.with_name(self._path.name + ".tmp")
|
|
129
|
+
fd = os.open(tmp_path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
|
130
|
+
try:
|
|
131
|
+
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
|
132
|
+
f.write(payload)
|
|
133
|
+
f.flush()
|
|
134
|
+
os.fsync(f.fileno())
|
|
135
|
+
except BaseException:
|
|
136
|
+
tmp_path.unlink(missing_ok=True)
|
|
137
|
+
raise
|
|
138
|
+
os.replace(tmp_path, self._path)
|
{debug_control_plane-0.4.0 → debug_control_plane-0.5.0}/debug_control_plane.egg-info/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: debug-control-plane
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Multi-product reusable debug control plane (device discovery + MCP adapter).
|
|
5
5
|
Author: tangxiaolu
|
|
6
6
|
License: MIT
|
|
@@ -62,7 +62,7 @@ python/
|
|
|
62
62
|
├── README.md # this file
|
|
63
63
|
├── LICENSE # MIT
|
|
64
64
|
└── debug_control_plane/
|
|
65
|
-
├── __init__.py # __version__ = "0.
|
|
65
|
+
├── __init__.py # __version__ = "0.5.0"
|
|
66
66
|
├── device_discovery/ # USB/LAN device discovery + device pool
|
|
67
67
|
│ ├── device_candidates.py
|
|
68
68
|
│ ├── device_pool.py # identity-keyed pool, TTL expiry / 身份键池,TTL 过期
|
|
@@ -92,8 +92,8 @@ Points at `debug_control_plane.mcp_plane.server:main` — a bare server (no busi
|
|
|
92
92
|
|
|
93
93
|
## Version / 版本
|
|
94
94
|
|
|
95
|
-
`0.
|
|
96
|
-
`0.
|
|
95
|
+
`0.5.0` — aligned with Kotlin/Dart/Flutter `0.5.0`, API unstable.
|
|
96
|
+
`0.5.0` —— 与 Kotlin/Dart/Flutter `0.5.0` 对齐,API 不稳定。
|
|
97
97
|
|
|
98
98
|
## License / 许可证
|
|
99
99
|
|
{debug_control_plane-0.4.0 → debug_control_plane-0.5.0}/debug_control_plane.egg-info/SOURCES.txt
RENAMED
|
@@ -24,6 +24,7 @@ debug_control_plane/mcp_plane/bridge_client.py
|
|
|
24
24
|
debug_control_plane/mcp_plane/capability_mirror.py
|
|
25
25
|
debug_control_plane/mcp_plane/semantic_provider.py
|
|
26
26
|
debug_control_plane/mcp_plane/server.py
|
|
27
|
+
debug_control_plane/mcp_plane/token_provider.py
|
|
27
28
|
tests/test_acceptance_flutter_app_auth.py
|
|
28
29
|
tests/test_bridge_client.py
|
|
29
30
|
tests/test_capability_mirror.py
|
|
@@ -38,4 +39,5 @@ tests/test_discovery_vpn_immune.py
|
|
|
38
39
|
tests/test_e2e_mock.py
|
|
39
40
|
tests/test_endpoint.py
|
|
40
41
|
tests/test_import_sanity.py
|
|
41
|
-
tests/test_server.py
|
|
42
|
+
tests/test_server.py
|
|
43
|
+
tests/test_token_provider.py
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "debug-control-plane"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.5.0"
|
|
8
8
|
description = "Multi-product reusable debug control plane (device discovery + MCP adapter)."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -1313,7 +1313,7 @@ class TestMakeApp:
|
|
|
1313
1313
|
srv = assembled[0]
|
|
1314
1314
|
app = srv._make_app() # noqa: SLF001
|
|
1315
1315
|
assert app.name == "mcp-debug-bridge"
|
|
1316
|
-
assert app.version == "0.
|
|
1316
|
+
assert app.version == "0.5.0"
|
|
1317
1317
|
|
|
1318
1318
|
def test_make_app_registers_stub_provider(self, assembled):
|
|
1319
1319
|
"""★ BF008-010 (A 类): assembled fixture 注入的 _StubProvider 已注册到
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""Unit tests for FileTokenProvider (R004-BF001).
|
|
2
|
+
|
|
3
|
+
Covers the 7 BF001-T scenarios from test §2:
|
|
4
|
+
1. roundtrip-cross-instance: save → new instance get hits disk.
|
|
5
|
+
2. 0600-permission: file mode == 0o600 after save.
|
|
6
|
+
3. expired-past-future-Z-invalid: expiresAt four-state judgement.
|
|
7
|
+
4. clear-deletes-row: clear_token rewrites file without the row.
|
|
8
|
+
5. corrupt-fallback: invalid JSON → silent empty fallback.
|
|
9
|
+
6. metadata-merge: metadata str values merged into the row.
|
|
10
|
+
7. version-mismatch: version != 1 → silent empty fallback.
|
|
11
|
+
|
|
12
|
+
All tests use tmp_path — the real ~/.debug-control-plane/ is never touched.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import stat
|
|
19
|
+
from datetime import datetime, timedelta, timezone
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
from debug_control_plane.mcp_plane.token_provider import FileTokenProvider
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _provider(tmp_path: Path) -> FileTokenProvider:
|
|
26
|
+
return FileTokenProvider(path=tmp_path / "tokens.json")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _write_raw(tmp_path: Path, payload: dict) -> Path:
|
|
30
|
+
p = tmp_path / "tokens.json"
|
|
31
|
+
p.write_text(json.dumps(payload), encoding="utf-8")
|
|
32
|
+
return p
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
# 1. roundtrip-cross-instance
|
|
37
|
+
# ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
def test_roundtrip_cross_instance(tmp_path: Path) -> None:
|
|
40
|
+
p1 = _provider(tmp_path)
|
|
41
|
+
p1.save_token(
|
|
42
|
+
"device-1",
|
|
43
|
+
"dcp_tok_1",
|
|
44
|
+
{"tokenId": "tid-1", "expiresAt": "2099-01-01T00:00:00+00:00"},
|
|
45
|
+
)
|
|
46
|
+
# A new instance must see the token (persistence, not memory).
|
|
47
|
+
p2 = _provider(tmp_path)
|
|
48
|
+
assert p2.get_token("device-1") == "dcp_tok_1"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# ---------------------------------------------------------------------------
|
|
52
|
+
# 2. 0600-permission
|
|
53
|
+
# ---------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
def test_file_mode_0600(tmp_path: Path) -> None:
|
|
56
|
+
provider = _provider(tmp_path)
|
|
57
|
+
provider.save_token("device-1", "dcp_tok_1", {"tokenId": "tid-1"})
|
|
58
|
+
mode = stat.S_IMODE(provider._path.stat().st_mode) # noqa: SLF001
|
|
59
|
+
assert mode == 0o600
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# ---------------------------------------------------------------------------
|
|
63
|
+
# 3. expired four states
|
|
64
|
+
# ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
def test_expired_past_returns_none(tmp_path: Path) -> None:
|
|
67
|
+
past = (datetime.now(timezone.utc) - timedelta(hours=1)).isoformat()
|
|
68
|
+
provider = _provider(tmp_path)
|
|
69
|
+
provider.save_token("d", "t", {"expiresAt": past})
|
|
70
|
+
assert provider.get_token("d") is None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_not_expired_future(tmp_path: Path) -> None:
|
|
74
|
+
future = (datetime.now(timezone.utc) + timedelta(days=6)).isoformat()
|
|
75
|
+
provider = _provider(tmp_path)
|
|
76
|
+
provider.save_token("d", "t", {"expiresAt": future})
|
|
77
|
+
assert provider.get_token("d") == "t"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_not_expired_z_suffix(tmp_path: Path) -> None:
|
|
81
|
+
future = (
|
|
82
|
+
datetime.now(timezone.utc) + timedelta(days=6)
|
|
83
|
+
).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
84
|
+
provider = _provider(tmp_path)
|
|
85
|
+
provider.save_token("d", "t", {"expiresAt": future})
|
|
86
|
+
assert provider.get_token("d") == "t"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_invalid_expiresat_treated_as_not_expired(tmp_path: Path) -> None:
|
|
90
|
+
provider = _provider(tmp_path)
|
|
91
|
+
provider.save_token("d", "t", {"expiresAt": "not-a-date"})
|
|
92
|
+
assert provider.get_token("d") == "t"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_naive_expiresat_treated_as_not_expired(tmp_path: Path) -> None:
|
|
96
|
+
provider = _provider(tmp_path)
|
|
97
|
+
provider.save_token("d", "t", {"expiresAt": "2099-01-01T00:00:00"})
|
|
98
|
+
assert provider.get_token("d") == "t"
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# ---------------------------------------------------------------------------
|
|
102
|
+
# 4. clear-deletes-row
|
|
103
|
+
# ---------------------------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
def test_clear_deletes_row_on_disk(tmp_path: Path) -> None:
|
|
106
|
+
provider = _provider(tmp_path)
|
|
107
|
+
provider.save_token("d1", "t1", {"tokenId": "tid-1"})
|
|
108
|
+
provider.save_token("d2", "t2", {"tokenId": "tid-2"})
|
|
109
|
+
provider.clear_token("d1", reason="token_expired")
|
|
110
|
+
assert provider.get_token("d1") is None
|
|
111
|
+
assert provider.get_token("d2") == "t2"
|
|
112
|
+
# A fresh instance sees the same on-disk state.
|
|
113
|
+
assert _provider(tmp_path).get_token("d1") is None
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def test_clear_missing_device_is_noop(tmp_path: Path) -> None:
|
|
117
|
+
provider = _provider(tmp_path)
|
|
118
|
+
provider.clear_token("ghost", reason="token_expired") # must not raise
|
|
119
|
+
assert provider.get_token("ghost") is None
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# ---------------------------------------------------------------------------
|
|
123
|
+
# 5. corrupt-fallback
|
|
124
|
+
# ---------------------------------------------------------------------------
|
|
125
|
+
|
|
126
|
+
def test_corrupt_file_silent_fallback(tmp_path: Path) -> None:
|
|
127
|
+
p = tmp_path / "tokens.json"
|
|
128
|
+
p.write_text("{not valid json", encoding="utf-8")
|
|
129
|
+
provider = FileTokenProvider(path=p)
|
|
130
|
+
assert provider.get_token("d") is None # no raise, empty
|
|
131
|
+
# After a save the file becomes valid again (self-heal).
|
|
132
|
+
provider.save_token("d", "t", {"tokenId": "tid"})
|
|
133
|
+
assert json.loads(p.read_text(encoding="utf-8"))["version"] == 1
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# ---------------------------------------------------------------------------
|
|
137
|
+
# 6. metadata-merge
|
|
138
|
+
# ---------------------------------------------------------------------------
|
|
139
|
+
|
|
140
|
+
def test_metadata_merged_into_row(tmp_path: Path) -> None:
|
|
141
|
+
provider = _provider(tmp_path)
|
|
142
|
+
provider.save_token("d", "t", {"tokenId": "tid-1", "expiresAt": "x", "count": 3})
|
|
143
|
+
row = json.loads((tmp_path / "tokens.json").read_text(encoding="utf-8"))[
|
|
144
|
+
"tokens"
|
|
145
|
+
]["d"]
|
|
146
|
+
assert row["token"] == "t"
|
|
147
|
+
assert row["tokenId"] == "tid-1"
|
|
148
|
+
assert row["expiresAt"] == "x"
|
|
149
|
+
# Non-str metadata values are stringified (schema keeps str values only).
|
|
150
|
+
assert isinstance(row["count"], str)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
# ---------------------------------------------------------------------------
|
|
154
|
+
# 7. version-mismatch
|
|
155
|
+
# ---------------------------------------------------------------------------
|
|
156
|
+
|
|
157
|
+
def test_version_mismatch_fallback(tmp_path: Path) -> None:
|
|
158
|
+
_write_raw(
|
|
159
|
+
tmp_path,
|
|
160
|
+
{"version": 2, "tokens": {"d": {"token": "t", "tokenId": "tid"}}},
|
|
161
|
+
)
|
|
162
|
+
provider = _provider(tmp_path)
|
|
163
|
+
assert provider.get_token("d") is None
|
|
164
|
+
# Save under v1 schema after the fallback.
|
|
165
|
+
provider.save_token("d", "t2", {})
|
|
166
|
+
data = json.loads((tmp_path / "tokens.json").read_text(encoding="utf-8"))
|
|
167
|
+
assert data["version"] == 1
|
|
168
|
+
assert data["tokens"]["d"]["token"] == "t2"
|
|
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
|
{debug_control_plane-0.4.0 → debug_control_plane-0.5.0}/debug_control_plane/mcp_plane/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{debug_control_plane-0.4.0 → debug_control_plane-0.5.0}/debug_control_plane.egg-info/requires.txt
RENAMED
|
File without changes
|
{debug_control_plane-0.4.0 → debug_control_plane-0.5.0}/debug_control_plane.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
{debug_control_plane-0.4.0 → debug_control_plane-0.5.0}/tests/test_acceptance_flutter_app_auth.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{debug_control_plane-0.4.0 → debug_control_plane-0.5.0}/tests/test_cross_lang_kotlin_plane.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{debug_control_plane-0.4.0 → debug_control_plane-0.5.0}/tests/test_discovery_cross_identify.py
RENAMED
|
File without changes
|
|
File without changes
|
{debug_control_plane-0.4.0 → debug_control_plane-0.5.0}/tests/test_discovery_manual_registry.py
RENAMED
|
File without changes
|
{debug_control_plane-0.4.0 → debug_control_plane-0.5.0}/tests/test_discovery_usb_identity.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|