uv-agent-auth-code 0.1.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.
- uv_agent_auth_code-0.1.0/.gitattributes +1 -0
- uv_agent_auth_code-0.1.0/.github/workflows/ci.yml +42 -0
- uv_agent_auth_code-0.1.0/.github/workflows/release.yml +227 -0
- uv_agent_auth_code-0.1.0/.gitignore +5 -0
- uv_agent_auth_code-0.1.0/PKG-INFO +59 -0
- uv_agent_auth_code-0.1.0/README.md +51 -0
- uv_agent_auth_code-0.1.0/pyproject.toml +24 -0
- uv_agent_auth_code-0.1.0/src/uv_agent_auth_code/__init__.py +73 -0
- uv_agent_auth_code-0.1.0/src/uv_agent_auth_code/service.py +465 -0
- uv_agent_auth_code-0.1.0/tests/test_plugin.py +142 -0
- uv_agent_auth_code-0.1.0/uv.lock +1128 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
* text=auto eol=lf
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
push:
|
|
6
|
+
branches:
|
|
7
|
+
- main
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
test:
|
|
14
|
+
name: Tests (${{ matrix.os }})
|
|
15
|
+
runs-on: ${{ matrix.os }}
|
|
16
|
+
strategy:
|
|
17
|
+
fail-fast: false
|
|
18
|
+
matrix:
|
|
19
|
+
os:
|
|
20
|
+
- ubuntu-latest
|
|
21
|
+
- macos-latest
|
|
22
|
+
- windows-latest
|
|
23
|
+
|
|
24
|
+
steps:
|
|
25
|
+
- name: Check out repository
|
|
26
|
+
uses: actions/checkout@v6
|
|
27
|
+
|
|
28
|
+
- name: Set up Python
|
|
29
|
+
uses: actions/setup-python@v6
|
|
30
|
+
with:
|
|
31
|
+
python-version: "3.13"
|
|
32
|
+
|
|
33
|
+
- name: Set up uv
|
|
34
|
+
uses: astral-sh/setup-uv@v8.1.0
|
|
35
|
+
with:
|
|
36
|
+
enable-cache: true
|
|
37
|
+
|
|
38
|
+
- name: Install dependencies
|
|
39
|
+
run: uv sync --locked --dev
|
|
40
|
+
|
|
41
|
+
- name: Run tests
|
|
42
|
+
run: uv run pytest
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
inputs:
|
|
6
|
+
version:
|
|
7
|
+
description: "Version to release, for example 0.1.0. Leave empty to use pyproject.toml."
|
|
8
|
+
required: false
|
|
9
|
+
type: string
|
|
10
|
+
tag:
|
|
11
|
+
description: "Release tag. Leave empty to use v<version>."
|
|
12
|
+
required: false
|
|
13
|
+
type: string
|
|
14
|
+
prerelease:
|
|
15
|
+
description: "Mark the GitHub release as a prerelease."
|
|
16
|
+
required: true
|
|
17
|
+
default: false
|
|
18
|
+
type: boolean
|
|
19
|
+
publish_pypi:
|
|
20
|
+
description: "Publish the built distributions to PyPI after creating the GitHub release."
|
|
21
|
+
required: true
|
|
22
|
+
default: false
|
|
23
|
+
type: boolean
|
|
24
|
+
|
|
25
|
+
jobs:
|
|
26
|
+
build:
|
|
27
|
+
name: Build and verify distributions
|
|
28
|
+
runs-on: ubuntu-latest
|
|
29
|
+
permissions:
|
|
30
|
+
contents: write
|
|
31
|
+
outputs:
|
|
32
|
+
tag: ${{ steps.meta.outputs.tag }}
|
|
33
|
+
version: ${{ steps.meta.outputs.version }}
|
|
34
|
+
commit_sha: ${{ steps.release-ref.outputs.commit_sha }}
|
|
35
|
+
|
|
36
|
+
steps:
|
|
37
|
+
- name: Check out repository
|
|
38
|
+
uses: actions/checkout@v6
|
|
39
|
+
with:
|
|
40
|
+
fetch-depth: 0
|
|
41
|
+
ref: ${{ github.ref_name }}
|
|
42
|
+
|
|
43
|
+
- name: Set up Python
|
|
44
|
+
uses: actions/setup-python@v6
|
|
45
|
+
with:
|
|
46
|
+
python-version: "3.13"
|
|
47
|
+
|
|
48
|
+
- name: Set up uv
|
|
49
|
+
uses: astral-sh/setup-uv@v8.1.0
|
|
50
|
+
with:
|
|
51
|
+
enable-cache: true
|
|
52
|
+
|
|
53
|
+
- name: Resolve release metadata
|
|
54
|
+
id: meta
|
|
55
|
+
shell: bash
|
|
56
|
+
env:
|
|
57
|
+
INPUT_VERSION: ${{ inputs.version }}
|
|
58
|
+
INPUT_TAG: ${{ inputs.tag }}
|
|
59
|
+
run: |
|
|
60
|
+
python3 - <<'PY' >> "$GITHUB_OUTPUT"
|
|
61
|
+
import os
|
|
62
|
+
import re
|
|
63
|
+
import tomllib
|
|
64
|
+
|
|
65
|
+
input_version = os.environ.get("INPUT_VERSION", "").strip()
|
|
66
|
+
with open("pyproject.toml", "rb") as file:
|
|
67
|
+
current_version = tomllib.load(file)["project"]["version"]
|
|
68
|
+
|
|
69
|
+
version = input_version or current_version
|
|
70
|
+
if not re.fullmatch(r"\d+\.\d+\.\d+(?:(?:a|b|rc)\d+)?(?:\.post\d+)?(?:\.dev\d+)?", version):
|
|
71
|
+
raise SystemExit(f"Invalid release version: {version!r}")
|
|
72
|
+
|
|
73
|
+
tag = os.environ.get("INPUT_TAG", "").strip() or f"v{version}"
|
|
74
|
+
if not re.fullmatch(r"v?[A-Za-z0-9][A-Za-z0-9._+-]*", tag):
|
|
75
|
+
raise SystemExit(f"Invalid release tag: {tag!r}")
|
|
76
|
+
|
|
77
|
+
tag_version = tag[1:] if tag.startswith("v") else tag
|
|
78
|
+
if tag_version != version:
|
|
79
|
+
raise SystemExit(
|
|
80
|
+
f"Release tag {tag!r} does not match release version {version!r}. "
|
|
81
|
+
"Use the matching tag or leave tag empty."
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
print(f"version={version}")
|
|
85
|
+
print(f"tag={tag}")
|
|
86
|
+
PY
|
|
87
|
+
|
|
88
|
+
- name: Update version files
|
|
89
|
+
shell: bash
|
|
90
|
+
env:
|
|
91
|
+
RELEASE_VERSION: ${{ steps.meta.outputs.version }}
|
|
92
|
+
run: |
|
|
93
|
+
python3 - <<'PY'
|
|
94
|
+
import os
|
|
95
|
+
import re
|
|
96
|
+
from pathlib import Path
|
|
97
|
+
|
|
98
|
+
version = os.environ["RELEASE_VERSION"]
|
|
99
|
+
path = Path("pyproject.toml")
|
|
100
|
+
text = path.read_text(encoding="utf-8")
|
|
101
|
+
pattern = r'(?m)^version = "[^"]+"$'
|
|
102
|
+
updated, count = re.subn(pattern, f'version = "{version}"', text, count=1)
|
|
103
|
+
if count != 1:
|
|
104
|
+
raise SystemExit("Could not find project version in pyproject.toml")
|
|
105
|
+
path.write_text(updated, encoding="utf-8")
|
|
106
|
+
PY
|
|
107
|
+
uv lock
|
|
108
|
+
|
|
109
|
+
- name: Verify version files
|
|
110
|
+
shell: bash
|
|
111
|
+
env:
|
|
112
|
+
RELEASE_VERSION: ${{ steps.meta.outputs.version }}
|
|
113
|
+
run: |
|
|
114
|
+
python3 - <<'PY'
|
|
115
|
+
import os
|
|
116
|
+
import tomllib
|
|
117
|
+
|
|
118
|
+
with open("pyproject.toml", "rb") as file:
|
|
119
|
+
version = tomllib.load(file)["project"]["version"]
|
|
120
|
+
expected = os.environ["RELEASE_VERSION"]
|
|
121
|
+
if version != expected:
|
|
122
|
+
raise SystemExit(f"pyproject.toml version {version!r} does not match {expected!r}")
|
|
123
|
+
PY
|
|
124
|
+
uv lock --check
|
|
125
|
+
|
|
126
|
+
- name: Commit version bump
|
|
127
|
+
id: version-commit
|
|
128
|
+
shell: bash
|
|
129
|
+
env:
|
|
130
|
+
RELEASE_TAG: ${{ steps.meta.outputs.tag }}
|
|
131
|
+
run: |
|
|
132
|
+
git config user.name "github-actions[bot]"
|
|
133
|
+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
134
|
+
git add pyproject.toml uv.lock
|
|
135
|
+
if git diff --cached --quiet; then
|
|
136
|
+
echo "No version changes to commit."
|
|
137
|
+
echo "committed=false" >> "$GITHUB_OUTPUT"
|
|
138
|
+
else
|
|
139
|
+
git commit -m "Release uv-agent-auth-code ${RELEASE_TAG}"
|
|
140
|
+
echo "committed=true" >> "$GITHUB_OUTPUT"
|
|
141
|
+
fi
|
|
142
|
+
|
|
143
|
+
- name: Install dependencies
|
|
144
|
+
run: uv sync --locked --dev
|
|
145
|
+
|
|
146
|
+
- name: Run tests
|
|
147
|
+
run: uv run pytest
|
|
148
|
+
|
|
149
|
+
- name: Build distributions
|
|
150
|
+
run: uv build
|
|
151
|
+
|
|
152
|
+
- name: Check distributions
|
|
153
|
+
run: uvx twine check --strict dist/*
|
|
154
|
+
|
|
155
|
+
- name: Push version bump
|
|
156
|
+
if: ${{ steps.version-commit.outputs.committed == 'true' }}
|
|
157
|
+
shell: bash
|
|
158
|
+
run: |
|
|
159
|
+
if [ "${GITHUB_REF_TYPE}" != "branch" ]; then
|
|
160
|
+
echo "Version bump commits require running the workflow from a branch." >&2
|
|
161
|
+
exit 1
|
|
162
|
+
fi
|
|
163
|
+
git push origin "HEAD:${GITHUB_REF_NAME}"
|
|
164
|
+
|
|
165
|
+
- name: Resolve release revision
|
|
166
|
+
id: release-ref
|
|
167
|
+
shell: bash
|
|
168
|
+
run: echo "commit_sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
|
|
169
|
+
|
|
170
|
+
- name: Upload distribution artifacts
|
|
171
|
+
uses: actions/upload-artifact@v7
|
|
172
|
+
with:
|
|
173
|
+
name: python-distributions
|
|
174
|
+
path: dist/*
|
|
175
|
+
if-no-files-found: error
|
|
176
|
+
|
|
177
|
+
github-release:
|
|
178
|
+
name: Create GitHub release
|
|
179
|
+
needs: build
|
|
180
|
+
runs-on: ubuntu-latest
|
|
181
|
+
permissions:
|
|
182
|
+
contents: write
|
|
183
|
+
|
|
184
|
+
steps:
|
|
185
|
+
- name: Download distribution artifacts
|
|
186
|
+
uses: actions/download-artifact@v8
|
|
187
|
+
with:
|
|
188
|
+
name: python-distributions
|
|
189
|
+
path: dist
|
|
190
|
+
|
|
191
|
+
- name: Create GitHub release
|
|
192
|
+
uses: softprops/action-gh-release@v3
|
|
193
|
+
with:
|
|
194
|
+
tag_name: ${{ needs.build.outputs.tag }}
|
|
195
|
+
target_commitish: ${{ needs.build.outputs.commit_sha }}
|
|
196
|
+
name: uv-agent-auth-code ${{ needs.build.outputs.tag }}
|
|
197
|
+
generate_release_notes: true
|
|
198
|
+
prerelease: ${{ inputs.prerelease }}
|
|
199
|
+
fail_on_unmatched_files: true
|
|
200
|
+
files: dist/*
|
|
201
|
+
|
|
202
|
+
pypi:
|
|
203
|
+
name: Publish to PyPI
|
|
204
|
+
needs:
|
|
205
|
+
- build
|
|
206
|
+
- github-release
|
|
207
|
+
if: ${{ inputs.publish_pypi }}
|
|
208
|
+
runs-on: ubuntu-latest
|
|
209
|
+
environment:
|
|
210
|
+
name: pypi
|
|
211
|
+
url: https://pypi.org/p/uv-agent-auth-code
|
|
212
|
+
permissions:
|
|
213
|
+
contents: read
|
|
214
|
+
id-token: write
|
|
215
|
+
|
|
216
|
+
steps:
|
|
217
|
+
- name: Download distribution artifacts
|
|
218
|
+
uses: actions/download-artifact@v8
|
|
219
|
+
with:
|
|
220
|
+
name: python-distributions
|
|
221
|
+
path: dist
|
|
222
|
+
|
|
223
|
+
- name: Publish distributions to PyPI
|
|
224
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
225
|
+
with:
|
|
226
|
+
packages-dir: dist/
|
|
227
|
+
print-hash: true
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: uv-agent-auth-code
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Local auth-code challenge plugin for uv-agent.
|
|
5
|
+
Requires-Python: >=3.12
|
|
6
|
+
Requires-Dist: uv-agent>=0.21.0
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
|
|
9
|
+
# uv-agent auth code plugin
|
|
10
|
+
|
|
11
|
+
This plugin starts a small token-protected HTTP page that shows a single
|
|
12
|
+
six-character challenge code. Other uv-agent plugins can call the
|
|
13
|
+
`auth_code.verify` action with a user-provided code.
|
|
14
|
+
|
|
15
|
+
The code is uppercase alphanumeric, case-insensitive when verified, short lived,
|
|
16
|
+
and consumed after one successful verification.
|
|
17
|
+
|
|
18
|
+
## Configuration
|
|
19
|
+
|
|
20
|
+
```json
|
|
21
|
+
{
|
|
22
|
+
"plugins": {
|
|
23
|
+
"auth-code": {
|
|
24
|
+
"enabled": true,
|
|
25
|
+
"config": {
|
|
26
|
+
"token": "replace-with-a-long-random-token",
|
|
27
|
+
"host": "0.0.0.0",
|
|
28
|
+
"port": 8765,
|
|
29
|
+
"ttl_s": 120
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`token` is required. The page can be opened as:
|
|
37
|
+
|
|
38
|
+
```text
|
|
39
|
+
http://127.0.0.1:8765/?token=replace-with-a-long-random-token
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
After token login, the plugin stores an in-memory HttpOnly session cookie.
|
|
43
|
+
|
|
44
|
+
## Action
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
result = await context.actions.call("auth_code.verify", {"code": "A7K2Q9"})
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The plugin id is `auth-code`. The action id remains `auth_code.verify` because
|
|
51
|
+
uv-agent action ids use dotted Python-style names.
|
|
52
|
+
|
|
53
|
+
Successful verification returns:
|
|
54
|
+
|
|
55
|
+
```json
|
|
56
|
+
{"ok": true, "verified": true}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Failed verification returns `ok: false`, `verified: false`, and a `reason`.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# uv-agent auth code plugin
|
|
2
|
+
|
|
3
|
+
This plugin starts a small token-protected HTTP page that shows a single
|
|
4
|
+
six-character challenge code. Other uv-agent plugins can call the
|
|
5
|
+
`auth_code.verify` action with a user-provided code.
|
|
6
|
+
|
|
7
|
+
The code is uppercase alphanumeric, case-insensitive when verified, short lived,
|
|
8
|
+
and consumed after one successful verification.
|
|
9
|
+
|
|
10
|
+
## Configuration
|
|
11
|
+
|
|
12
|
+
```json
|
|
13
|
+
{
|
|
14
|
+
"plugins": {
|
|
15
|
+
"auth-code": {
|
|
16
|
+
"enabled": true,
|
|
17
|
+
"config": {
|
|
18
|
+
"token": "replace-with-a-long-random-token",
|
|
19
|
+
"host": "0.0.0.0",
|
|
20
|
+
"port": 8765,
|
|
21
|
+
"ttl_s": 120
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`token` is required. The page can be opened as:
|
|
29
|
+
|
|
30
|
+
```text
|
|
31
|
+
http://127.0.0.1:8765/?token=replace-with-a-long-random-token
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
After token login, the plugin stores an in-memory HttpOnly session cookie.
|
|
35
|
+
|
|
36
|
+
## Action
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
result = await context.actions.call("auth_code.verify", {"code": "A7K2Q9"})
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The plugin id is `auth-code`. The action id remains `auth_code.verify` because
|
|
43
|
+
uv-agent action ids use dotted Python-style names.
|
|
44
|
+
|
|
45
|
+
Successful verification returns:
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{"ok": true, "verified": true}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Failed verification returns `ok: false`, `verified: false`, and a `reason`.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "uv-agent-auth-code"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Local auth-code challenge plugin for uv-agent."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"uv-agent>=0.21.0",
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
[project.entry-points."uv_agent.plugins"]
|
|
12
|
+
auth-code = "uv_agent_auth_code:plugin"
|
|
13
|
+
|
|
14
|
+
[dependency-groups]
|
|
15
|
+
dev = [
|
|
16
|
+
"pytest>=9.0.0",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[build-system]
|
|
20
|
+
requires = ["hatchling"]
|
|
21
|
+
build-backend = "hatchling.build"
|
|
22
|
+
|
|
23
|
+
[tool.hatch.build.targets.wheel]
|
|
24
|
+
packages = ["src/uv_agent_auth_code"]
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from uv_agent.plugins import PluginManifest, SetupPlugin
|
|
6
|
+
|
|
7
|
+
from .service import AuthCodeConfig, AuthCodeService
|
|
8
|
+
|
|
9
|
+
MANIFEST = PluginManifest(
|
|
10
|
+
id="auth-code",
|
|
11
|
+
version="0.1.0",
|
|
12
|
+
display_name={"en": "Auth Code", "zh": "验证码鉴权"},
|
|
13
|
+
description={
|
|
14
|
+
"en": "Starts a token-protected web page with a short-lived challenge code and exposes auth_code.verify.",
|
|
15
|
+
"zh": "启动受 token 保护的验证码页面,并提供 auth_code.verify action。",
|
|
16
|
+
},
|
|
17
|
+
capabilities=("action", "http_server"),
|
|
18
|
+
config_schema={
|
|
19
|
+
"type": "object",
|
|
20
|
+
"properties": {
|
|
21
|
+
"token": {"type": "string", "minLength": 1},
|
|
22
|
+
"host": {"type": "string", "default": "0.0.0.0"},
|
|
23
|
+
"port": {"type": "integer", "minimum": 0, "maximum": 65535, "default": 8765},
|
|
24
|
+
"ttl_s": {"type": "integer", "minimum": 1, "default": 120},
|
|
25
|
+
"session_ttl_s": {"type": "integer", "minimum": 60, "default": 43200},
|
|
26
|
+
},
|
|
27
|
+
"required": ["token"],
|
|
28
|
+
},
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
_SERVICES: dict[int, AuthCodeService] = {}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def plugin() -> SetupPlugin:
|
|
35
|
+
return SetupPlugin(manifest=MANIFEST, setup=setup, stop=stop)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def setup(context) -> None:
|
|
39
|
+
config = AuthCodeConfig.from_mapping(context.config)
|
|
40
|
+
service = AuthCodeService(config, logger=context.logger)
|
|
41
|
+
service.start()
|
|
42
|
+
_SERVICES[id(context)] = service
|
|
43
|
+
try:
|
|
44
|
+
context.actions.register(
|
|
45
|
+
"auth_code.verify",
|
|
46
|
+
_verify_action,
|
|
47
|
+
doc="Verify the current auth-code challenge. Payload: {'code': 'A7K2Q9'}.",
|
|
48
|
+
schema={
|
|
49
|
+
"type": "object",
|
|
50
|
+
"properties": {"code": {"type": "string"}},
|
|
51
|
+
"required": ["code"],
|
|
52
|
+
},
|
|
53
|
+
)
|
|
54
|
+
except Exception:
|
|
55
|
+
_SERVICES.pop(id(context), None)
|
|
56
|
+
service.stop()
|
|
57
|
+
raise
|
|
58
|
+
context.logger.info("Auth code server started url=%s", service.url)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def stop(context) -> None:
|
|
62
|
+
service = _SERVICES.pop(id(context), None)
|
|
63
|
+
if service is not None:
|
|
64
|
+
service.stop()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _verify_action(payload: dict[str, Any], context=None) -> dict[str, Any]:
|
|
68
|
+
if context is None:
|
|
69
|
+
return {"ok": False, "verified": False, "reason": "missing_context"}
|
|
70
|
+
service = _SERVICES.get(id(context))
|
|
71
|
+
if service is None:
|
|
72
|
+
return {"ok": False, "verified": False, "reason": "service_unavailable"}
|
|
73
|
+
return service.verify(str(payload.get("code") or ""))
|