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.
@@ -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,5 @@
1
+ .venv/
2
+ .pytest_cache/
3
+ dist/
4
+ __pycache__/
5
+ *.py[cod]
@@ -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 ""))