Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions skills/alicloud-ros-agent/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description: Use Alibaba Cloud ROS Agent through its StartChat API for remote in

# Alibaba Cloud ROS Agent

Use the bridge at `scripts/ros_agent.py`. Its default code transport uses the Alibaba Cloud credentials and Core SDKs to sign ROS RPCs, send them directly, and consume StartChat SSE incrementally. Unless local policy pins a CLI Profile, it first uses a complete AK/SK pair from the same environment-variable aliases and precedence as aliyun CLI, including an optional STS token; only when no environment AK/SK is present does it use the selected CLI Profile. A pinned Profile is exclusive and never falls back to environment credentials or another Profile. A direct OAuth Profile reuses its unexpired cached STS credential without starting the CLI; when that credential is missing or expired, native aliyun CLI performs its own expiration check and refresh before the SDK reads the refreshed temporary credential. Credentials exist only inside the request path and are never accepted as bridge arguments, persisted in job state, or returned. An optional compatibility transport lets the native CLI execute the whole RPC without requiring any Python package. Run the bridge with `python3` on macOS/Linux or `py -3` on Windows.
Use the bridge at `scripts/ros_agent.py`. Its default code transport uses Alibaba Cloud credentials and Tea OpenAPI V3 signing to send ROS RPCs directly and consume StartChat SSE incrementally. Unless local policy pins a CLI Profile, it first uses a complete AK/SK pair from the same environment-variable aliases and precedence as aliyun CLI, including an optional STS token; only when no environment AK/SK is present does it use the selected CLI Profile. A pinned Profile is exclusive and never falls back to environment credentials or another Profile. A direct OAuth Profile reuses its unexpired cached STS credential without starting the CLI; when that credential is missing or expired, native aliyun CLI performs its own expiration check and refresh before the SDK reads the refreshed temporary credential. Credentials exist only inside the request path and are never accepted as bridge arguments, persisted in job state, or returned. An optional compatibility transport lets the native CLI execute the whole RPC without requiring any Python package. Run the bridge with `python3` on macOS/Linux or `py -3` on Windows.

## Required interaction contract

Expand Down Expand Up @@ -96,7 +96,7 @@ Unknown fields, invalid values, and duplicate modes fail closed. Never edit `con

This invokes the ROS `StopChat` OpenAPI through the job's selected transport; it does not send a StartChat query or a natural-language cancellation message. Present the returned status immediately. `Stopped` means cancellation completed, `Stopping` means it was accepted and the existing job should be observed with `follow` from its current cursor, and `NoActiveStream` means there was no active remote stream to stop. Never call `cancel` merely because `follow` timed out, a local tool call was interrupted, or the outer Agent turn ended.

Without a configured endpoint, the bridge defaults to `ros.aliyuncs.com`. Use `--endpoint <ROS endpoint>` only when the user's ROS region or network requires a different endpoint and `config.json` does not fix one. The code transport sends a generic signed ROS RPC with API version `2019-09-10`, so it does not depend on generated StartChat metadata. The `aliyun_cli` transport retains the CLI's built-in ROS API version and forced-call mechanism because `StartChat` is not in the public CLI metadata. Both transports identify every StartChat and StopChat request with the user-agent segment `AlibabaCloud-Agent-Skills/alibabacloud-ros-agent`.
Without a configured endpoint, the bridge defaults to `ros.aliyuncs.com`. Use `--endpoint <ROS endpoint>` only when the user's ROS region or network requires a different endpoint and `config.json` does not fix one. The code transport sends a generic ROS RPC with API version `2019-09-10` and `ACS3-HMAC-SHA256` signing, so it does not depend on generated StartChat metadata. The `aliyun_cli` transport requires CLI metadata for StartChat and StopChat and does not bypass API validation. Both transports identify every StartChat and StopChat request with the user-agent segment `AlibabaCloud-Agent-Skills/alibabacloud-ros-agent`.

## Architecture before deployment confirmation

Expand Down
3 changes: 2 additions & 1 deletion skills/alicloud-ros-agent/requirements-code.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
alibabacloud-credentials>=1.0.8,<2
aliyun-python-sdk-core>=2.16,<3
alibabacloud-tea-openapi>=0.4.4,<0.5
requests>=2.31,<3
104 changes: 62 additions & 42 deletions skills/alicloud-ros-agent/scripts/ros_agent.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Bounded Alibaba Cloud ROS Agent bridge using Alibaba Cloud CLI."""
"""Bounded Alibaba Cloud ROS Agent bridge using signed StartChat RPCs."""

import argparse
import contextlib
Expand All @@ -18,6 +18,7 @@
import tempfile
import time
import urllib.error
import urllib.parse
import urllib.request
import uuid
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
Expand Down Expand Up @@ -653,7 +654,6 @@ def build_command(
resolve_aliyun(args.aliyun_path),
"ros",
"StartChat",
"--force",
"--method",
"POST",
"--endpoint",
Expand Down Expand Up @@ -686,7 +686,6 @@ def build_stop_command(job: Dict[str, Any], session_id: str) -> List[str]:
resolve_aliyun(str(job.get("aliyunPath") or "aliyun")),
"ros",
"StopChat",
"--force",
"--method",
"POST",
"--endpoint",
Expand Down Expand Up @@ -718,17 +717,9 @@ def _load_code_sdk() -> Dict[str, Any]:
importlib.import_module("alibabacloud_credentials.provider.cli_profile"),
"CLIProfileCredentialsProvider",
),
"AccessKeyCredential": getattr(
importlib.import_module("aliyunsdkcore.auth.credentials"), "AccessKeyCredential"
),
"StsTokenCredential": getattr(
importlib.import_module("aliyunsdkcore.auth.credentials"), "StsTokenCredential"
),
"AcsClient": getattr(importlib.import_module("aliyunsdkcore.client"), "AcsClient"),
"CommonRequest": getattr(importlib.import_module("aliyunsdkcore.request"), "CommonRequest"),
"protocolType": importlib.import_module("aliyunsdkcore.http.protocol_type"),
"methodType": importlib.import_module("aliyunsdkcore.http.method_type"),
"requests": importlib.import_module("aliyunsdkcore.vendored.requests"),
"DaraRequest": getattr(importlib.import_module("darabonba.request"), "DaraRequest"),
"OpenApiUtils": getattr(importlib.import_module("alibabacloud_tea_openapi.utils"), "Utils"),
"requests": importlib.import_module("requests"),
}
except (ImportError, AttributeError) as exc:
raise BridgeError(
Expand Down Expand Up @@ -917,7 +908,7 @@ def _code_credentials(
profile: Optional[str],
region_id: Optional[str],
credential_source: Optional[str] = None,
) -> Any:
) -> Tuple[str, str, Optional[str]]:
if credential_source not in {None, "environment", "profile"}:
raise BridgeError("credential_failed", "The managed Alibaba Cloud credential source is invalid.")
environment = None if credential_source == "profile" else _environment_credentials()
Expand All @@ -944,9 +935,57 @@ def _code_credentials(
security_token = credentials.get_security_token()
if not access_key_id or not access_key_secret:
raise ValueError("empty credentials")
return access_key_id, access_key_secret, security_token or None


def _canonical_query_string(parameters: Dict[str, str]) -> str:
return "&".join(
"{}={}".format(name, urllib.parse.quote(value, safe="~", encoding="utf-8"))
for name, value in sorted(parameters.items())
)


def _build_v3_request(
sdk: Dict[str, Any],
operation: str,
parameters: Dict[str, str],
endpoint: str,
credentials: Tuple[str, str, Optional[str]],
) -> Tuple[str, Dict[str, str]]:
access_key_id, access_key_secret, security_token = credentials
signature_algorithm = "ACS3-HMAC-SHA256"
utils = sdk["OpenApiUtils"]
payload_hash = utils.hash(b"", signature_algorithm).hex()
headers = {
"accept": "text/event-stream" if operation == "StartChat" else "application/json",
"accept-encoding": "identity",
"host": endpoint,
"user-agent": USER_AGENT,
"x-acs-action": operation,
"x-acs-content-sha256": payload_hash,
"x-acs-date": utils.get_timestamp(),
"x-acs-signature-nonce": utils.get_nonce(),
"x-acs-version": "2019-09-10",
}
if security_token:
return sdk["StsTokenCredential"](access_key_id, access_key_secret, security_token)
return sdk["AccessKeyCredential"](access_key_id, access_key_secret)
headers["x-acs-accesskey-id"] = access_key_id
headers["x-acs-security-token"] = security_token

request = sdk["DaraRequest"]()
request.protocol = "https"
request.method = "POST"
request.pathname = "/"
request.query = dict(parameters)
request.headers = headers
headers["Authorization"] = utils.get_authorization(
request,
signature_algorithm,
payload_hash,
access_key_id,
access_key_secret,
)
query = _canonical_query_string(parameters)
return "https://{}/{}".format(endpoint, "?{}".format(query) if query else ""), headers


class _CodeHttpResponse:
Expand Down Expand Up @@ -983,27 +1022,8 @@ def _open_code_request(
) -> Any:
sdk = _load_code_sdk()
try:
core_credentials = _code_credentials(sdk, aliyun_path, profile, region_id, credential_source)
client = sdk["AcsClient"](
region_id=region_id or "cn-hangzhou",
credential=core_credentials,
auto_retry=False,
verify=False if _endpoint_kind(endpoint) == "loopback" else None,
)
client.append_user_agent("AlibabaCloud-Agent-Skills", "alibabacloud-ros-agent")
request = sdk["CommonRequest"](
domain=endpoint,
version="2019-09-10",
action_name=operation,
product="ROS",
)
request.set_protocol_type(sdk["protocolType"].HTTPS)
request.set_method(sdk["methodType"].POST)
request.add_header("Accept-Encoding", "identity")
request.add_header("Accept", "text/event-stream" if operation == "StartChat" else "application/json")
for name, value in parameters.items():
request.add_query_param(name, value)
signed = client._make_http_response(endpoint, request, read_timeout, connect_timeout)
credentials = _code_credentials(sdk, aliyun_path, profile, region_id, credential_source)
url, headers = _build_v3_request(sdk, operation, parameters, endpoint, credentials)
except BridgeError:
raise
except Exception as exc:
Expand All @@ -1016,10 +1036,10 @@ def _open_code_request(
session = sdk["requests"].Session()
try:
response = session.request(
method=signed.get_method(),
url="https://{}{}".format(endpoint, signed.get_url()),
data=signed.get_body(),
headers=signed.get_headers(),
method="POST",
url=url,
data=None,
headers=headers,
timeout=(connect_timeout, read_timeout),
allow_redirects=False,
verify=_endpoint_kind(endpoint) != "loopback",
Expand Down
Loading
Loading