sandrpod-cli 0.2.2__tar.gz → 0.2.4__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sandrpod-cli
3
- Version: 0.2.2
3
+ Version: 0.2.4
4
4
  Summary: Command-line client for SandrPod — self-hosted, multi-cloud sandbox infrastructure for AI agents
5
5
  License: MIT
6
6
  Project-URL: Homepage, https://github.com/sandrpod/sandrpod
@@ -15,13 +15,21 @@ class CLIClient:
15
15
  api_url: str = "http://localhost:8080",
16
16
  timeout: int = 30,
17
17
  token: Optional[str] = None,
18
+ mcp_token: Optional[str] = None,
18
19
  ):
19
20
  self.api_url = api_url.rstrip("/")
20
21
  self.timeout = timeout
22
+ self.mcp_token = mcp_token
21
23
  self.session = requests.Session()
22
- # Only set Authorization in session headers; Content-Type is set per-request
23
- # so that multipart/form-data uploads can override it automatically.
24
+ # Two-tier auth (see docs MCP_AUTH_HEADER_CONFLICT_FIX): the platform token
25
+ # goes in X-Sandrpod-Token, the server's preferred header. Authorization:
26
+ # Bearer is kept as a legacy fallback for older servers — but crucially it
27
+ # stays free to carry the *personal* mcp_token through to the sandbox's
28
+ # agent bridge on /mcp calls (the server passes Authorization through the
29
+ # tunnel untouched). Content-Type is set per-request so multipart uploads
30
+ # can override it.
24
31
  if token:
32
+ self.session.headers["X-Sandrpod-Token"] = token
25
33
  self.session.headers["Authorization"] = f"Bearer {token}"
26
34
 
27
35
  def _request(self, method: str, path: str, timeout: int = None, **kwargs) -> requests.Response:
@@ -321,7 +329,13 @@ class CLIClient:
321
329
  """沙箱原生 MCP bridge 的实时清单 (聚合 server、工具数、配置文件路径)"""
322
330
  # /mcp (not /toolbox/mcp): the streaming bridge proxy, which flushes SSE —
323
331
  # the path a real MCP client uses. Works for poder + agent sandboxes.
324
- resp = self._request("GET", f"/api/v1/sandboxes/{name}/mcp/manifest")
332
+ # When the agent guards its bridge with a personal mcp_token, carry it in
333
+ # Authorization: Bearer — the server auths via X-Sandrpod-Token and passes
334
+ # this through to the agent's mcpTokenMiddleware untouched.
335
+ headers = {}
336
+ if self.mcp_token:
337
+ headers["Authorization"] = f"Bearer {self.mcp_token}"
338
+ resp = self._request("GET", f"/api/v1/sandboxes/{name}/mcp/manifest", headers=headers)
325
339
  return resp.json()
326
340
 
327
341
  def mcp_url(self, name: str) -> str:
@@ -61,6 +61,20 @@ def get_configured_token():
61
61
  return config.get("api_token")
62
62
 
63
63
 
64
+ def get_configured_mcp_token():
65
+ """获取个人 MCP token(agent bridge 的共享密钥),优先级:环境变量 > 配置文件。
66
+
67
+ 仅用于 /mcp 相关调用(mcp tools/url),当员工机 agent 以 --mcp-token 启动、
68
+ 对 /mcp 面加了共享密钥时需要。它作为 Authorization: Bearer 透传给 agent 校验,
69
+ 与平台 token(X-Sandrpod-Token)互不冲突。
70
+ """
71
+ env_token = os.environ.get("SANDRPOD_MCP_TOKEN")
72
+ if env_token:
73
+ return env_token
74
+ config = load_config()
75
+ return config.get("mcp_token")
76
+
77
+
64
78
  def save_config_api_url(url):
65
79
  """保存 API URL 到配置文件"""
66
80
  config = load_config()
@@ -78,15 +92,18 @@ def save_config_token(token):
78
92
  @click.group()
79
93
  @click.option("--api-url", default=None, help="API URL (overrides config)")
80
94
  @click.option("--timeout", default=30, help="Request timeout")
95
+ @click.option("--mcp-token", default=None, help="个人 MCP token(agent bridge 共享密钥;也可用 SANDRPOD_MCP_TOKEN)")
81
96
  @click.pass_context
82
- def cli(ctx, api_url, timeout):
97
+ def cli(ctx, api_url, timeout, mcp_token):
83
98
  """SandrPod CLI - Sandbox Management"""
84
99
  ctx.ensure_object(dict)
85
100
  # 如果没有传入 --api-url,使用配置的 URL
86
101
  if api_url is None:
87
102
  api_url = get_configured_api_url()
88
103
  token = get_configured_token()
89
- ctx.obj["client"] = CLIClient(api_url=api_url, timeout=timeout, token=token)
104
+ if mcp_token is None:
105
+ mcp_token = get_configured_mcp_token()
106
+ ctx.obj["client"] = CLIClient(api_url=api_url, timeout=timeout, token=token, mcp_token=mcp_token)
90
107
 
91
108
 
92
109
  # ========== Config Commands ==========
@@ -945,7 +962,13 @@ def _kv(pairs):
945
962
 
946
963
 
947
964
  def _mcp_config_path(client, name, override):
948
- """mcp.json 路径: --config-path > manifest.config_path > 默认 (跨 poder/agent 自适应)"""
965
+ """mcp.json 路径: --config-path > manifest.config_path > 按 substrate 猜默认。
966
+
967
+ 优先用 bridge 在 /mcp/manifest 里自报的绝对路径(精确;需较新的
968
+ agent/toolbox)。旧 bridge 不报 config_path 时按 substrate 回退:
969
+ poder(容器)沙箱用 /workspace/.sandrpod/mcp.json,direct agent(本机)
970
+ 用 ~/.sandrpod/mcp.json(agent 的 DefaultConfigPath)。
971
+ """
949
972
  if override:
950
973
  return override
951
974
  try:
@@ -954,6 +977,12 @@ def _mcp_config_path(client, name, override):
954
977
  return manifest["config_path"]
955
978
  except Exception:
956
979
  pass
980
+ try:
981
+ sb = client.get_sandbox(name)
982
+ if str(sb.get("proxy_url", "")).startswith("direct://"):
983
+ return os.path.expanduser("~/.sandrpod/mcp.json")
984
+ except Exception:
985
+ pass
957
986
  return DEFAULT_MCP_CONFIG
958
987
 
959
988
 
@@ -1082,12 +1111,14 @@ def mcp_rm(ctx, name, server, config_path):
1082
1111
  @click.argument("name")
1083
1112
  @click.pass_context
1084
1113
  def mcp_url(ctx, name):
1085
- """打印沙箱原生 MCP 端点 URL (给 Claude/Cursor 等 MCP 客户端连接)"""
1114
+ """打印沙箱原生 MCP 端点 URL + 连接所需的两层鉴权头"""
1086
1115
  client = ctx.parent.parent.obj["client"]
1087
1116
  click.echo(client.mcp_url(name))
1088
- auth = client.auth_header()
1089
- if auth:
1090
- click.echo(f"# 连接时带鉴权头: Authorization: {auth}", err=True)
1117
+ # 两层鉴权(见 MCP_AUTH_HEADER_CONFLICT_FIX):平台 token 走 X-Sandrpod-Token,
1118
+ # 个人 MCP token 走 Authorization: Bearer(server 原样透传给 agent 校验)。
1119
+ click.echo("# 连接头(两层鉴权):", err=True)
1120
+ click.echo("# X-Sandrpod-Token: <平台 token>", err=True)
1121
+ click.echo("# Authorization: Bearer <个人 mcp_token> (仅当 agent 以 -mcp-token 启动时需要)", err=True)
1091
1122
 
1092
1123
 
1093
1124
  @mcp_group.command(name="tools")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sandrpod-cli
3
- Version: 0.2.2
3
+ Version: 0.2.4
4
4
  Summary: Command-line client for SandrPod — self-hosted, multi-cloud sandbox infrastructure for AI agents
5
5
  License: MIT
6
6
  Project-URL: Homepage, https://github.com/sandrpod/sandrpod
@@ -10,7 +10,7 @@ long_description = _readme.read_text(encoding="utf-8") if _readme.exists() else
10
10
 
11
11
  setup(
12
12
  name="sandrpod-cli",
13
- version="0.2.2",
13
+ version="0.2.4",
14
14
  description=(
15
15
  "Command-line client for SandrPod — self-hosted, multi-cloud sandbox "
16
16
  "infrastructure for AI agents"
File without changes