Bundle uds-skill-auth inside the uds-auth plugin for single-package handoff.

Move the public auth helper skill under uds-auth/skills and update docs to point there instead of the repo-root skills tree.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-09-08 23:46:11 +08:00
parent c5c6089f89
commit b60c626d41
8 changed files with 51 additions and 94 deletions

View file

@ -22,7 +22,9 @@ skillCredentialTtlSeconds: 604800
outboundAllowedHosts: icenterapi.zte.com.cn,icentermsg.dt.zte.com.cn
```
Skill auth standard (Chinese): [docs/skill-auth-standard.zh.md](docs/skill-auth-standard.zh.md). Sample skill: `../skills/uds-icenter`.
Skill auth standard (Chinese): [docs/skill-auth-standard.zh.md](docs/skill-auth-standard.zh.md).
Bundled skill: [skills/uds-skill-auth](skills/uds-skill-auth). Handoff notes: [docs/uds-skill-auth.zh.md](docs/uds-skill-auth.zh.md).
Loopback agent APIs: `GET|POST /uds-auth/agent-credentials`, `POST /uds-auth/outbound`.

View file

@ -43,13 +43,15 @@ originSystemCode: ''
- **默认兜底账号**(扫码不可用时):用户名 `administrator`,密码 `Admin@123`(首次启动自动启用;可在设置中改密或关闭)
- **ACL**:租户边界以工作区为准(可见工作区下的会话可访问);`session-owners.json` 仅记录工号供导出/分析,不是主鉴权键
## Skill 认证(给他人改造 skill 时传这些)
## Skill 认证(给他人改造 skill 时)
发整个 **uds-auth 插件** 即可,内含:
1. **标准**:[docs/skill-auth-standard.zh.md](docs/skill-auth-standard.zh.md)
2. **公共 skill 说明(可直接转发)**:[docs/uds-skill-auth.zh.md](docs/uds-skill-auth.zh.md)
3. **公共 skill 目录(可拷贝)**:[../skills/uds-skill-auth](../skills/uds-skill-auth)
2. **公共 skill 说明**:[docs/uds-skill-auth.zh.md](docs/uds-skill-auth.zh.md)
3. **公共 skill 本体**:[skills/uds-skill-auth/](skills/uds-skill-auth/)(随插件提交)
现场顺序:装本插件 → 安装 `uds-skill-auth` → 扫码登录 → 再装业务 skill。
现场:装插件 → 把 `skills/uds-skill-auth` 配进 skills 路径 → 扫码 → 再装业务 skill。
可配置:`retainSkillCredentialsOnLogout`(默认 true)、`skillCredentialTtlSeconds`、`outboundAllowedHosts`。

View file

@ -1,14 +1,14 @@
# Skill 认证标准(uds-auth)
面向 DeepSeekHarness:现场安装 **uds-auth 插件** + **uds-skill-auth skill** 后,第三方 / 自研 skill 按本标准取凭证或出站调用。
面向 DeepSeekHarness:现场安装 **uds-auth 插件**(内含 skill `uds-skill-auth`)后,第三方 / 自研 skill 按本标准取凭证或出站调用。
**转发给改造方时**:连同 [uds-skill-auth.zh.md](./uds-skill-auth.zh.md) 与目录 `skills/uds-skill-auth/` 一起给。
**转发给改造方时**:发整个 `uds-auth` 插件即可;公共 skill 在 [`skills/uds-skill-auth/`](../skills/uds-skill-auth/),说明见 [uds-skill-auth.zh.md](./uds-skill-auth.zh.md)。
## 现场形态
```text
安装 uds-auth 插件
→ 安装 skill「uds-skill-auth」(认证公共库,必须)
安装 uds-auth 插件(内含 skills/uds-skill-auth)
→ 将 uds-skill-auth 加入 skills 路径(或复制到工作区 skills/)
→ 用户扫码登录
→ 安装/加载业务 skill
→ 可用
@ -16,7 +16,7 @@
无需再配全局 `coclaw_token` / `AUTH_VALUE`。
**认证公共库以 skill 分发**:仓库路径 `skills/uds-skill-auth/`。业务 skill 不要假设能访问插件源码目录。
**认证公共库随插件分发**:路径 `uds-auth/skills/uds-skill-auth/`。
## 双轨模型
@ -79,12 +79,12 @@ from uds_skill_auth import resolve
creds = resolve() # empNo + token;默认写入 EMP_NO/AUTH_VALUE
```
Helpers 来源:已安装的 skill `uds-skill-auth` 的 `scripts/`(或环境变量 `UDS_AUTH_HELPERS`)。
Helpers 来源:插件内 skill `uds-auth/skills/uds-skill-auth/scripts/`(装到 skills 路径后,或环境变量 `UDS_AUTH_HELPERS`)。
## 改造现有 Skill 清单
1. 现场确保已安装 `uds-skill-auth`
2. 业务脚本定位 sibling:`skills/uds-skill-auth/scripts` → `import uds_skill_auth`
1. 现场确保已从插件安装 `uds-skill-auth`(`uds-auth/skills/uds-skill-auth`)
2. 业务脚本定位 sibling:`.../skills/uds-skill-auth/scripts` → `import uds_skill_auth`
3. 替换原来的 `coclaw_*` / `EMP_NO`/`AUTH_VALUE` 手工读环境:改用 `resolve()` 或 `request()`
4. SKILL.md 只写「需已 UDS 登录,并已安装 uds-skill-auth」,不写 token 变量名
5. JSON stdout;日志脱敏

View file

@ -1,57 +1,21 @@
# uds-skill-auth(给他人传递用)
# uds-skill-auth(随插件分发)
把**本文 + 同仓库 skill 目录**发给改造方即可。业务 skill 必须依赖本公共 skill,不能只装插件。
## 仓库位置(可直接拷贝整目录)
本公共 skill **在插件仓库内**,与 uds-auth 一起提交、一起给别人:
```text
skills/uds-skill-auth/
uds-auth/skills/uds-skill-auth/
SKILL.md
scripts/uds_skill_auth.py
```
现场:拷到对方 `skills/` 下,与业务 skill **同级**。
## 现场怎么用
## 依赖链
1. 安装 uds-auth 插件
2. 把 `uds-auth/skills/uds-skill-auth` 配进 Harness 的 skills 目录(或复制到工作区 `skills/uds-skill-auth`)
3. 用户扫码登录
4. 业务 skill 通过 sibling / skills 根找到本 skill 的 `scripts/`
```text
uds-auth 插件(Host)
→ skill「uds-skill-auth」(公共库,本页)
→ 用户扫码登录
→ 业务 skill
```
完整契约:[skill-auth-standard.zh.md](./skill-auth-standard.zh.md)
## SKILL.md(安装后目录内同名文件)
```markdown
---
name: uds-skill-auth
description: UDS 技能认证公共库。其它依赖 UDS 登录的 skill 必须安装本 skill,通过其 scripts/uds_skill_auth.py 取凭证或出站调用。需现场已安装 uds-auth 插件且用户已扫码登录。
---
# uds-skill-auth(认证公共 Skill)
本 skill **不是业务能力**,而是给其它 skill 用的公共库。
## 现场依赖
1. 安装并启用 uds-auth 插件
2. 安装本 skill(uds-skill-auth)到 skills 目录
3. 用户扫码登录
4. 再安装/使用业务 skill
## 给其它 Skill 用
业务 skill 的 Python 把本 skill 的 scripts/ 加入 sys.path 后:
from uds_skill_auth import resolve, request, UdsAuthError
creds = resolve()
out = request("POST", url, headers={...}, body={...})
```
## 业务 skill 如何引用(复制这段即可)
## 业务 skill 引用
```python
from pathlib import Path
@ -59,32 +23,18 @@ import sys
def load_uds_skill_auth():
here = Path(__file__).resolve().parent
# .../skills/<业务>/scripts → .../skills/uds-skill-auth/scripts
candidate = here.parents[1] / "uds-skill-auth" / "scripts"
if not (candidate / "uds_skill_auth.py").is_file():
raise RuntimeError("请先安装 uds-skill-auth skill(与业务 skill 同级)")
raise RuntimeError("请先安装插件自带的 uds-skill-auth skill")
sys.path.insert(0, str(candidate))
import uds_skill_auth
return uds_skill_auth
```
或设置环境变量 `UDS_AUTH_HELPERS` = 本 skill 的 `scripts` 绝对路径。
## API 摘要
| 函数 | 用途 |
|------|------|
| `resolve(apply_env_aliases=True)` | 向 Host `POST /uds-auth/agent-credentials` 取 `empNo`+`token`;默认写入进程内 `EMP_NO`/`AUTH_VALUE`/`coclaw_*` |
| `request(method, url, headers=..., body=...)` | 向 Host `POST /uds-auth/outbound` 出站;Host 注入鉴权头 |
| `UdsAuthError` | 未登录 / 连不上 Host / 白名单拒绝等 |
| `resolve()` | `POST /uds-auth/agent-credentials` → empNo+token |
| `request(method, url, ...)` | `POST /uds-auth/outbound` 出站代贴鉴权 |
| `UdsAuthError` | 未登录 / 连不上 Host 等 |
Agent 环境(已注入,勿在 SKILL 里教打印密钥):`DSH_SESSION_ID`、`DSH_WEB_URL`。
## 改造现有 skill 时给 AI 的最短说明
1. 现场已装 uds-auth + 本 skill `uds-skill-auth`
2. 删掉读全局 `coclaw_token`/`AUTH_VALUE` 的逻辑
3. 用上面的 `load_uds_skill_auth()` + `resolve()` 或 `request()`
4. SKILL.md 写明依赖 `uds-skill-auth`,禁止教 printenv token
实现源码见:`skills/uds-skill-auth/scripts/uds_skill_auth.py`。
完整契约:[skill-auth-standard.zh.md](./skill-auth-standard.zh.md)

View file

@ -30,6 +30,8 @@
],
"files": [
"lib",
"skills",
"skill-helpers",
"config.default.yaml",
"cordis.patch.yml",
"docs",

View file

@ -1,7 +1,7 @@
# Deprecated path
认证公共库已改为 **skill 分发**:请安装并使用仓库内的
请使用插件内正式 skill:
`skills/uds-skill-auth/`
`uds-auth/skills/uds-skill-auth/`
本目录仅作兼容备份;新 skill 不要依赖此路径。
本目录仅作兼容备份。

View file

@ -0,0 +1,61 @@
---
name: uds-skill-auth
description: UDS 技能认证公共库。其它依赖 UDS 登录的 skill 必须安装本 skill,通过其 scripts/uds_skill_auth.py 取凭证或出站调用。需现场已安装 uds-auth 插件且用户已扫码登录。本 skill 随 uds-auth 插件一同分发。
---
# uds-skill-auth(认证公共 Skill)
本 skill **不是业务能力**,而是给其它 skill 用的公共库。**随 uds-auth 插件一起提供**,目录在插件内:
```text
uds-auth/skills/uds-skill-auth/
```
## 现场依赖
```text
1. 安装并启用 uds-auth 插件
2. 将本目录(插件内 skills/uds-skill-auth)加入 Harness skills 路径,
或复制到工作区 skills/ 下与业务 skill 同级
3. 用户扫码登录
4. 再安装/使用业务 skill
```
## 给其它 Skill 用
业务 skill 的 Python 脚本里,把本 skill 的 `scripts/` 加入 `sys.path` 后:
```python
from uds_skill_auth import resolve, request, UdsAuthError
creds = resolve()
out = request("POST", "https://icenterapi.zte.com.cn/...", headers={...}, body={...})
```
定位本 skill(与业务 skill 同级,或已加入 skills 根):
```python
from pathlib import Path
import sys
def load_uds_skill_auth():
here = Path(__file__).resolve().parent
for skills_root in [here.parents[1], *here.parents]:
candidate = skills_root / "uds-skill-auth" / "scripts"
if (candidate / "uds_skill_auth.py").is_file():
sys.path.insert(0, str(candidate))
import uds_skill_auth
return uds_skill_auth
raise RuntimeError("未找到 uds-skill-auth,请从 uds-auth 插件 skills/ 安装该 skill")
```
也可设置 `UDS_AUTH_HELPERS` 指向本 skill 的 `scripts` 绝对路径。
## 环境变量(Agent 已注入,勿教模型打印密钥)
- `DSH_SESSION_ID`
- `DSH_WEB_URL`
## 标准文档
见插件内 `docs/skill-auth-standard.zh.md` 与 `docs/uds-skill-auth.zh.md`。

View file

@ -0,0 +1,180 @@
# -*- coding: utf-8 -*-
"""
uds-skill-auth — Python helpers for UDS-backed skills.
Resolve SSO credentials via loopback Host APIs, or call intranet APIs through
the outbound proxy.
Environment:
DSH_SESSION_ID — agent session id (injected by DSH shell-env)
DSH_WEB_URL — optional base URL of the Harness web server
UDS_AUTH_BASE — optional override, e.g. http://127.0.0.1:PORT/uds-auth
"""
from __future__ import annotations
import json
import os
import ssl
import urllib.error
import urllib.request
from typing import Any, Dict, Optional, Tuple
from urllib.parse import urlparse
class UdsAuthError(RuntimeError):
def __init__(self, message: str, *, code: str = "uds_auth_error", status: int = 0):
super().__init__(message)
self.code = code
self.status = status
def _base_url() -> str:
explicit = (os.environ.get("UDS_AUTH_BASE") or "").strip().rstrip("/")
if explicit:
return explicit
web = (os.environ.get("DSH_WEB_URL") or "").strip().rstrip("/")
if web:
return web + "/uds-auth"
return "http://127.0.0.1:8787/uds-auth"
def _session_id() -> str:
return (os.environ.get("DSH_SESSION_ID") or "").strip()
def _opener_no_proxy():
return urllib.request.build_opener(urllib.request.ProxyHandler({}))
def _http_json(
method: str,
url: str,
*,
headers: Optional[Dict[str, str]] = None,
body: Any = None,
timeout: float = 30.0,
) -> Tuple[int, Any, str]:
data = None
hdrs = dict(headers or {})
if body is not None:
if isinstance(body, (dict, list)):
data = json.dumps(body, ensure_ascii=False).encode("utf-8")
hdrs.setdefault("Content-Type", "application/json;charset=UTF-8")
elif isinstance(body, str):
data = body.encode("utf-8")
elif isinstance(body, bytes):
data = body
else:
data = json.dumps(body, ensure_ascii=False).encode("utf-8")
hdrs.setdefault("Content-Type", "application/json;charset=UTF-8")
req = urllib.request.Request(url, data=data, headers=hdrs, method=method.upper())
ctx = ssl._create_unverified_context()
try:
if "127.0.0.1" in url or "localhost" in url.lower():
opener = _opener_no_proxy()
with opener.open(req, timeout=timeout) as resp:
raw = resp.read().decode("utf-8", errors="replace")
status = getattr(resp, "status", 200) or 200
else:
with urllib.request.urlopen(req, timeout=timeout, context=ctx) as resp:
raw = resp.read().decode("utf-8", errors="replace")
status = getattr(resp, "status", 200) or 200
except urllib.error.HTTPError as e:
raw = (e.fp.read().decode("utf-8", errors="replace") if e.fp else "")
status = e.code
except urllib.error.URLError as e:
raise UdsAuthError(f"无法连接 uds-auth: {e.reason}", code="uds_unreachable") from e
parsed: Any = None
if raw.strip():
try:
parsed = json.loads(raw)
except json.JSONDecodeError:
parsed = None
return status, parsed, raw
def resolve(*, apply_env_aliases: bool = True) -> Dict[str, str]:
"""
Fetch {empNo, token} for the current DSH session from Host.
Optionally set process-local EMP_NO / AUTH_VALUE (and coclaw_* aliases).
"""
sid = _session_id()
if not sid:
raise UdsAuthError("缺少 DSH_SESSION_ID,请在 Agent shell 中运行", code="no_session")
url = _base_url() + "/agent-credentials"
status, parsed, raw = _http_json(
"POST",
url,
headers={"X-DSH-Session-Id": sid, "Accept": "application/json"},
body={"sessionId": sid},
timeout=15.0,
)
if status == 401 or (isinstance(parsed, dict) and parsed.get("error") == "no_skill_credentials"):
msg = (parsed or {}).get("message") if isinstance(parsed, dict) else None
raise UdsAuthError(msg or "请先完成 UDS 扫码登录", code="no_credentials", status=status)
if status != 200 or not isinstance(parsed, dict):
raise UdsAuthError(
f"获取凭证失败 (HTTP {status})",
code="credentials_http",
status=status,
)
emp_no = str(parsed.get("empNo") or "").strip()
token = str(parsed.get("token") or "").strip()
if not emp_no or not token:
raise UdsAuthError("凭证响应不完整", code="bad_credentials")
if apply_env_aliases:
os.environ["EMP_NO"] = emp_no
os.environ["AUTH_VALUE"] = token
os.environ["coclaw_empno"] = emp_no
os.environ["coclaw_token"] = token
return {"empNo": emp_no, "token": token, "updatedAt": str(parsed.get("updatedAt") or "")}
def request(
method: str,
url: str,
*,
headers: Optional[Dict[str, str]] = None,
body: Any = None,
timeout: float = 30.0,
) -> Dict[str, Any]:
"""
Call an intranet URL via Host outbound proxy (injects X-Emp-No / X-Auth-Value).
Returns {statusCode, headers, body, json}.
"""
sid = _session_id()
if not sid:
raise UdsAuthError("缺少 DSH_SESSION_ID,请在 Agent shell 中运行", code="no_session")
host = urlparse(url).hostname or ""
payload = {
"sessionId": sid,
"method": method.upper(),
"url": url,
"headers": headers or {},
"body": body,
"timeoutMs": int(timeout * 1000),
}
status, parsed, raw = _http_json(
"POST",
_base_url() + "/outbound",
headers={"X-DSH-Session-Id": sid, "Accept": "application/json"},
body=payload,
timeout=timeout + 5.0,
)
if status == 401 or (isinstance(parsed, dict) and parsed.get("error") == "no_skill_credentials"):
msg = (parsed or {}).get("message") if isinstance(parsed, dict) else None
raise UdsAuthError(msg or "请先完成 UDS 扫码登录", code="no_credentials", status=status)
if status == 403 and isinstance(parsed, dict) and parsed.get("error") == "host_not_allowed":
raise UdsAuthError(f"主机不在 outbound 白名单: {host}", code="host_not_allowed", status=403)
if not isinstance(parsed, dict) or "statusCode" not in parsed:
raise UdsAuthError(
f"outbound 失败 (HTTP {status}): {raw[:200]}",
code="outbound_http",
status=status,
)
return parsed