shinnjyuu-echo-kit 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.
Files changed (40) hide show
  1. shinnjyuu_echo_kit-0.1.0/.github/workflows/publish.yml +55 -0
  2. shinnjyuu_echo_kit-0.1.0/.gitignore +7 -0
  3. shinnjyuu_echo_kit-0.1.0/AGENTS.md +7 -0
  4. shinnjyuu_echo_kit-0.1.0/LICENSE +21 -0
  5. shinnjyuu_echo_kit-0.1.0/PKG-INFO +74 -0
  6. shinnjyuu_echo_kit-0.1.0/README.md +59 -0
  7. shinnjyuu_echo_kit-0.1.0/docs/protocol.md +43 -0
  8. shinnjyuu_echo_kit-0.1.0/docs/publishing.md +25 -0
  9. shinnjyuu_echo_kit-0.1.0/docs/validation-results.json +33 -0
  10. shinnjyuu_echo_kit-0.1.0/docs/validation.md +51 -0
  11. shinnjyuu_echo_kit-0.1.0/examples/create_demo.py +96 -0
  12. shinnjyuu_echo_kit-0.1.0/examples/templates/adapter.py +37 -0
  13. shinnjyuu_echo_kit-0.1.0/examples/templates/auth.py +18 -0
  14. shinnjyuu_echo_kit-0.1.0/examples/templates/frontend.py +33 -0
  15. shinnjyuu_echo_kit-0.1.0/examples/templates/server.py +42 -0
  16. shinnjyuu_echo_kit-0.1.0/examples/validate_failures.py +43 -0
  17. shinnjyuu_echo_kit-0.1.0/pyproject.toml +29 -0
  18. shinnjyuu_echo_kit-0.1.0/src/echo_kit/__init__.py +2 -0
  19. shinnjyuu_echo_kit-0.1.0/src/echo_kit/__main__.py +2 -0
  20. shinnjyuu_echo_kit-0.1.0/src/echo_kit/adapter.py +53 -0
  21. shinnjyuu_echo_kit-0.1.0/src/echo_kit/auth.py +78 -0
  22. shinnjyuu_echo_kit-0.1.0/src/echo_kit/browser.py +102 -0
  23. shinnjyuu_echo_kit-0.1.0/src/echo_kit/cli.py +158 -0
  24. shinnjyuu_echo_kit-0.1.0/src/echo_kit/core.py +151 -0
  25. shinnjyuu_echo_kit-0.1.0/src/echo_kit/processes.py +51 -0
  26. shinnjyuu_echo_kit-0.1.0/src/echo_kit/runner.py +120 -0
  27. shinnjyuu_echo_kit-0.1.0/src/echo_kit/runs.py +45 -0
  28. shinnjyuu_echo_kit-0.1.0/src/echo_kit/services.py +132 -0
  29. shinnjyuu_echo_kit-0.1.0/src/echo_kit/skills/echo-init/SKILL.md +16 -0
  30. shinnjyuu_echo_kit-0.1.0/src/echo_kit/skills/echo-init/agents/openai.yaml +2 -0
  31. shinnjyuu_echo_kit-0.1.0/src/echo_kit/skills/echo-lab/SKILL.md +16 -0
  32. shinnjyuu_echo_kit-0.1.0/src/echo_kit/skills/echo-lab/agents/openai.yaml +2 -0
  33. shinnjyuu_echo_kit-0.1.0/src/echo_kit/skills/echo-workbench/SKILL.md +16 -0
  34. shinnjyuu_echo_kit-0.1.0/src/echo_kit/skills/echo-workbench/agents/openai.yaml +2 -0
  35. shinnjyuu_echo_kit-0.1.0/src/echo_kit/skills/references/protocol.md +43 -0
  36. shinnjyuu_echo_kit-0.1.0/tests/test_auth.py +67 -0
  37. shinnjyuu_echo_kit-0.1.0/tests/test_browser.py +39 -0
  38. shinnjyuu_echo_kit-0.1.0/tests/test_core.py +172 -0
  39. shinnjyuu_echo_kit-0.1.0/tests/test_edges.py +101 -0
  40. shinnjyuu_echo_kit-0.1.0/uv.lock +516 -0
@@ -0,0 +1,55 @@
1
+ name: Publish
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ tags: ['v*']
7
+ pull_request:
8
+ workflow_dispatch:
9
+
10
+ permissions:
11
+ contents: read
12
+
13
+ jobs:
14
+ test:
15
+ strategy:
16
+ matrix:
17
+ os: [ubuntu-latest, windows-latest]
18
+ runs-on: ${{ matrix.os }}
19
+ steps:
20
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
21
+ - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9
22
+ with:
23
+ version: '0.11.27'
24
+ python-version: '3.12'
25
+ - run: uv sync --locked
26
+ - run: uv run --no-sync pytest -q
27
+ - run: uv run --no-sync echo-kit --version
28
+
29
+ publish:
30
+ if: startsWith(github.ref, 'refs/tags/v') && github.event_name != 'pull_request'
31
+ needs: test
32
+ runs-on: ubuntu-latest
33
+ environment: pypi
34
+ permissions:
35
+ contents: read
36
+ id-token: write
37
+ steps:
38
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
39
+ - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9
40
+ with:
41
+ version: '0.11.27'
42
+ python-version: '3.12'
43
+ - name: Check release version
44
+ run: |
45
+ uv run --no-project python - <<'PY'
46
+ import os, tomllib, runpy
47
+ from pathlib import Path
48
+ version = tomllib.loads(Path('pyproject.toml').read_text())['project']['version']
49
+ assert os.environ['GITHUB_REF_NAME'] == 'v' + version, 'Tag/package version mismatch'
50
+ assert runpy.run_path('src/echo_kit/__init__.py')['__version__'] == version
51
+ PY
52
+ - run: uv build --no-sources
53
+ - name: Check package metadata
54
+ run: uvx --from twine twine check dist/*
55
+ - run: uv publish --trusted-publishing always
@@ -0,0 +1,7 @@
1
+ .venv/
2
+ .echo-kit/
3
+ __pycache__/
4
+ *.pyc
5
+ .pytest_cache/
6
+ dist/
7
+ *.egg-info/
@@ -0,0 +1,7 @@
1
+ # Echo Kit
2
+
3
+ Single install, independent capabilities. Preserve project-owned adapters and external instances. Core must not import Magic Cube business modules or embed environment addresses or credentials.
4
+
5
+ Run `uv run pytest` for targeted core checks. Real Docker and sample integration runs are separate evidence. Document platform and authentication limitations honestly. Do not publish, push, deploy or operate shared business environments without explicit task authorization.
6
+
7
+ Skills are bundled in `src/echo_kit/skills`; public protocol lives in `docs/protocol.md` and is included with exported skills. Keep these copies synchronized.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 黄悦峰
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,74 @@
1
+ Metadata-Version: 2.5
2
+ Name: shinnjyuu-echo-kit
3
+ Version: 0.1.0
4
+ Summary: Reusable experiments and verification workflows for AI coding assistants
5
+ Project-URL: Repository, https://github.com/shinnjyuu/echo-kit
6
+ Project-URL: Issues, https://github.com/shinnjyuu/echo-kit/issues
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Requires-Python: >=3.11
10
+ Requires-Dist: filelock<4,>=3.16
11
+ Requires-Dist: keyring<26,>=25
12
+ Requires-Dist: playwright==1.63.0
13
+ Requires-Dist: psutil<8,>=6
14
+ Description-Content-Type: text/markdown
15
+
16
+ # Echo Kit
17
+
18
+ 让 AI 的实验、调试与验收成为团队可复用的工程能力。
19
+
20
+ 发布维护见 [PyPI 发布说明](docs/publishing.md)。首次正式发布完成后可使用 `uvx --from shinnjyuu-echo-kit@latest echo-kit --version` 获取当前版本,再通过 `uvx --from shinnjyuu-echo-kit@具体版本 echo-kit …` 固定一轮任务的执行版本。
21
+
22
+ Python 3.11+。一个包提供 workspace、doctor、services、auth、browser、lab、verify、runs 和 Skill 导出。各能力按需使用,Lab 不要求 Docker 或服务启动。
23
+
24
+ ```powershell
25
+ uv tool install .
26
+ echo-kit --help
27
+ echo-kit --workspace D:/my-project workspace init
28
+ echo-kit skills export D:/my-project/.agents/skills
29
+ ```
30
+
31
+ 也可以 `uv tool install git+<仓库地址>` 或安装 `dist` 中的 wheel。不需要发布公共包。Skills 的安装目录遵循所用 AI 助手规则;导出不会改助手配置。
32
+
33
+ ## 独立演示
34
+
35
+ ```powershell
36
+ uv sync --group dev
37
+ uv run python examples/create_demo.py --output D:/echo-demo
38
+ uv run echo-kit --workspace D:/echo-demo/qa --json workspace check
39
+ uv run echo-kit --workspace D:/echo-demo/qa lab compare arithmetic --variants baseline double --repeat 3
40
+ uv run echo-kit --workspace D:/echo-demo/qa verify run api
41
+ uv run echo-kit --workspace D:/echo-demo/qa verify run page
42
+ uv run echo-kit --workspace D:/echo-demo/qa services status
43
+ uv run echo-kit --workspace D:/echo-demo/qa runs list
44
+ ```
45
+
46
+ 示例生成互不嵌套的 frontend、backend、qa 三个目录,均不依赖魔方。`page` 需要正在运行的 Docker,首次启动容器需下载固定 Playwright 镜像及 npm 包。
47
+
48
+ ## 配置与执行
49
+
50
+ 共享 `echo-kit.toml` 与 `echo/` 适配、用例入 Git;`.echo-kit/local.toml` 深度覆盖共享配置,environment 覆盖随后生效。所有 `.echo-kit/` 状态不提交。绝对路径放本机覆盖,命令数组中的 `{python}` 指 Kit 解释器;业务 Python 应改为项目实际解释器。
51
+
52
+ 命令必须使用数组,不隐式经过 Shell。`env_refs` 把指定环境变量传给适配器,敏感信息禁止写在 `command` 或 `env` 字面量。Kit 不记录全环境变量;项目脚本不得把秘密打印到 stdout 或写入证据。
53
+
54
+ 全局选项置于模块前:`echo-kit --workspace PATH --environment NAME --json lab run CASE`。退出码 0 为已执行范围成功,1 为检查失败,2 为受阻或未验证,130 为中断。`services status` 的成功表示查询成功,请检查各实例的 ready 字段。
55
+
56
+ 适配协议、认证和配置详见 [协议文档](docs/protocol.md)。业务依赖由项目管理,CLI 不导入业务模块。简单 `protocol="command"` 只核对退出码,不能证明语义正确。
57
+
58
+ ## 运行与安全边界
59
+
60
+ 托管服务持续运行;外部服务显式 `mode="external"`,不会停止。进程身份不匹配时不发送终止信号。运行存在 active.json 时阻止新用例与停服务,使用 `runs cleanup ID` 调用项目清理并核验终态,不能删除标记伪造收尾。
61
+
62
+ 认证协议通过内存管道传秘密。只向受支持的系统凭据后端持久化;不可用时只保留当前调用内状态。独立 auth login 在这种情况下不会为下一条命令保留状态;verify 同次调用仍可使用。
63
+
64
+ 报告只自动链接显式声明的产物。请求文件、适配输出、截图及 trace 可能包含业务数据,分享前审查。Kit 无法阻止恶意或错误适配器自行泄露数据;适配器是项目受信代码。
65
+
66
+ ## 验证与限制
67
+
68
+ `uv run pytest` 运行核心测试;真实演示与 Docker 验收单独运行。具体证据见 `docs/validation.md`。macOS 未验证;不把单元模拟当成真实业务或平台验收。
69
+
70
+ 第一版不提供 IDE 插件、Agent 引擎、通用验证码绕过、后台自动恢复。回放为已有记录与 Playwright trace 查看,不承诺重跑结果一致。
71
+
72
+ ## 来源
73
+
74
+ 服务归属、就绪等待与持续复用的设计,及 Lab 的案例/候选/独立重复执行概念,源自既有 Magic Cube Workbench 和 Agent Lab。为跨项目子进程协议重新实现,不引入原业务依赖、服务名、模型配置或私有凭据。
@@ -0,0 +1,59 @@
1
+ # Echo Kit
2
+
3
+ 让 AI 的实验、调试与验收成为团队可复用的工程能力。
4
+
5
+ 发布维护见 [PyPI 发布说明](docs/publishing.md)。首次正式发布完成后可使用 `uvx --from shinnjyuu-echo-kit@latest echo-kit --version` 获取当前版本,再通过 `uvx --from shinnjyuu-echo-kit@具体版本 echo-kit …` 固定一轮任务的执行版本。
6
+
7
+ Python 3.11+。一个包提供 workspace、doctor、services、auth、browser、lab、verify、runs 和 Skill 导出。各能力按需使用,Lab 不要求 Docker 或服务启动。
8
+
9
+ ```powershell
10
+ uv tool install .
11
+ echo-kit --help
12
+ echo-kit --workspace D:/my-project workspace init
13
+ echo-kit skills export D:/my-project/.agents/skills
14
+ ```
15
+
16
+ 也可以 `uv tool install git+<仓库地址>` 或安装 `dist` 中的 wheel。不需要发布公共包。Skills 的安装目录遵循所用 AI 助手规则;导出不会改助手配置。
17
+
18
+ ## 独立演示
19
+
20
+ ```powershell
21
+ uv sync --group dev
22
+ uv run python examples/create_demo.py --output D:/echo-demo
23
+ uv run echo-kit --workspace D:/echo-demo/qa --json workspace check
24
+ uv run echo-kit --workspace D:/echo-demo/qa lab compare arithmetic --variants baseline double --repeat 3
25
+ uv run echo-kit --workspace D:/echo-demo/qa verify run api
26
+ uv run echo-kit --workspace D:/echo-demo/qa verify run page
27
+ uv run echo-kit --workspace D:/echo-demo/qa services status
28
+ uv run echo-kit --workspace D:/echo-demo/qa runs list
29
+ ```
30
+
31
+ 示例生成互不嵌套的 frontend、backend、qa 三个目录,均不依赖魔方。`page` 需要正在运行的 Docker,首次启动容器需下载固定 Playwright 镜像及 npm 包。
32
+
33
+ ## 配置与执行
34
+
35
+ 共享 `echo-kit.toml` 与 `echo/` 适配、用例入 Git;`.echo-kit/local.toml` 深度覆盖共享配置,environment 覆盖随后生效。所有 `.echo-kit/` 状态不提交。绝对路径放本机覆盖,命令数组中的 `{python}` 指 Kit 解释器;业务 Python 应改为项目实际解释器。
36
+
37
+ 命令必须使用数组,不隐式经过 Shell。`env_refs` 把指定环境变量传给适配器,敏感信息禁止写在 `command` 或 `env` 字面量。Kit 不记录全环境变量;项目脚本不得把秘密打印到 stdout 或写入证据。
38
+
39
+ 全局选项置于模块前:`echo-kit --workspace PATH --environment NAME --json lab run CASE`。退出码 0 为已执行范围成功,1 为检查失败,2 为受阻或未验证,130 为中断。`services status` 的成功表示查询成功,请检查各实例的 ready 字段。
40
+
41
+ 适配协议、认证和配置详见 [协议文档](docs/protocol.md)。业务依赖由项目管理,CLI 不导入业务模块。简单 `protocol="command"` 只核对退出码,不能证明语义正确。
42
+
43
+ ## 运行与安全边界
44
+
45
+ 托管服务持续运行;外部服务显式 `mode="external"`,不会停止。进程身份不匹配时不发送终止信号。运行存在 active.json 时阻止新用例与停服务,使用 `runs cleanup ID` 调用项目清理并核验终态,不能删除标记伪造收尾。
46
+
47
+ 认证协议通过内存管道传秘密。只向受支持的系统凭据后端持久化;不可用时只保留当前调用内状态。独立 auth login 在这种情况下不会为下一条命令保留状态;verify 同次调用仍可使用。
48
+
49
+ 报告只自动链接显式声明的产物。请求文件、适配输出、截图及 trace 可能包含业务数据,分享前审查。Kit 无法阻止恶意或错误适配器自行泄露数据;适配器是项目受信代码。
50
+
51
+ ## 验证与限制
52
+
53
+ `uv run pytest` 运行核心测试;真实演示与 Docker 验收单独运行。具体证据见 `docs/validation.md`。macOS 未验证;不把单元模拟当成真实业务或平台验收。
54
+
55
+ 第一版不提供 IDE 插件、Agent 引擎、通用验证码绕过、后台自动恢复。回放为已有记录与 Playwright trace 查看,不承诺重跑结果一致。
56
+
57
+ ## 来源
58
+
59
+ 服务归属、就绪等待与持续复用的设计,及 Lab 的案例/候选/独立重复执行概念,源自既有 Magic Cube Workbench 和 Agent Lab。为跨项目子进程协议重新实现,不引入原业务依赖、服务名、模型配置或私有凭据。
@@ -0,0 +1,43 @@
1
+ # Echo Kit v1 adapter protocol
2
+
3
+ ## Configuration
4
+
5
+ `schema_version=1`. Projects are `[projects.NAME] path="relative-or-absolute"`. `.echo-kit/local.toml` overrides shared values; `[environments.NAME]` overlays for explicit `--environment NAME`. Arrays replace, dictionaries merge. `output` overrides the default `.echo-kit/runs`.
6
+
7
+ Services define `command=[...]`, optional `project`, `depends`, `port`, `timeout` and a `ready` table containing `url` plus expected `status`, or `host/port`, or `command/timeout`. `mode="external"` is check-only. `prepare` is an optional explicit build command table. No implicit framework detection at runtime. Startup versions describe owned launch snapshots, not unowned remote software.
8
+
9
+ Commands execute without shell, with project cwd. `{python}` is the Kit interpreter for helpers; specify a project's interpreter for its business dependencies. `${ENV_NAME}` expansion is for non-secret command arguments. `env_refs` maps child variable names to parent variables. Secret arguments are forbidden because OS command lines and process state are observable.
10
+
11
+ Cases use `[cases.NAME]`, `command`, `project`, `inputs`, `variants.NAME` input overrides, `timeout`, `required_checks`, and `mode` (unit/probe/integration or project-defined scope). `protocol="command"` records only exit-code success. Verify additionally accepts `services=[...]`, `auth="NAME"`, `browser=true`. Lab deliberately does not prepare these capabilities.
12
+
13
+ ## Experiment and verification subprocess
14
+
15
+ Environment variables: ECHO_REQUEST (JSON input path), ECHO_RESULT (JSON output path), ECHO_RUN_DIR (artifact directory), ECHO_ACTIVE (parent run task state).
16
+
17
+ Request: `protocol`, `run_id`, `environment`, `inputs`, `variant`, `iteration`, optional `browser.endpoint`, `active_file`. Credentials never belong in inputs. JSON stdin contains ephemeral private `auth` state. Read stdin once; never echo it.
18
+
19
+ Result example:
20
+
21
+ ```json
22
+ {"status":"passed","checks":[{"name":"download","status":"passed","detail":"bytes matched"}],"metrics":{"elapsed_ms":120},"artifacts":["download.txt"]}
23
+ ```
24
+
25
+ Result statuses: passed, failed, blocked, unverified, interrupted. Checks additionally allow not_applicable; a required not_applicable check does not pass. Checks must have unique names. Artifacts must exist beneath ECHO_RUN_DIR. stdout is saved as a log: adapters are trusted project code and must redact private material.
26
+
27
+ If the adapter starts remote/asynchronous work, set `creates_tasks=true`, immediately save IDs to ECHO_ACTIVE, retaining run_id. Successful result must include `terminal_confirmed=true`. Optional `[cases.NAME.cleanup]` defines a separate command that receives `{run_id, active}`; it must target only those IDs and return `status="passed", terminal_confirmed=true`. Cleanup runs after the case if configured, including timeout. Missing terminal evidence retains active.json. Later use `runs cleanup ID`; it preserves the original failure verdict.
28
+
29
+ ## Authentication subprocess
30
+
31
+ Auth differs intentionally: no secret request/result files. `[auth.NAME]` has command, project, account, target, timeout and optional credentials mapping fields to environment variable names.
32
+
33
+ stdin JSON: `{action, account, target, state, credentials}`. Actions: login, check, refresh, logout. stdout JSON: `{valid, account, target, state}`. No diagnostic text on stdout; stderr is private and not persisted by Kit. Returned identity and target must exactly match configuration. State is opaque JSON, passed to case stdin. Browser adapters can translate it to normal application storage or Playwright storage_state.
34
+
35
+ Kit checks stored state, tries refresh, then login. Only system credential backends are used for persistence. Without one, the same verify invocation can use the state; another CLI invocation logs in again. This does not bypass user, organization or password checks. Demonstration credentials are fixtures only.
36
+
37
+ ## Browser and records
38
+
39
+ Managed Docker browser is pinned to Playwright 1.63.0 and loopback host port 19323 (override `browser.port`). `browser.image` can select a registry mirror with the same version tag. External `browser.endpoint` is connect-only; Playwright handshake verifies compatible protocol. Browser project adapters can import `echo_kit.browser.session` when Echo is installed in their interpreter; otherwise use their own compatible Playwright client. Context traces are saved and closed in finally.
40
+
41
+ Every run writes run.json, report.html, adapter request/result/stdout and declared artifacts. Active records block new business runs in that workspace. HTML is an escaped static view; trace.zip opens in Playwright Trace Viewer. Sharing raw traces and files requires business-data review.
42
+
43
+ CLI code 0: executed scope passed; 1: check failure; 2: blocked/unverified/config/dependency error; 130: interrupted. Cancellation/cleanup never automatically resubmits the case. Initialization never overwrites existing configuration. macOS remains unverified until real validation.
@@ -0,0 +1,25 @@
1
+ # PyPI 发布
2
+
3
+ 仓库:`shinnjyuu/echo-kit`。包名:`shinnjyuu-echo-kit`。
4
+
5
+ 首次在 https://pypi.org/manage/account/publishing/ 添加 pending publisher:
6
+
7
+ | 字段 | 值 |
8
+ | --- | --- |
9
+ | PyPI Project Name | shinnjyuu-echo-kit |
10
+ | Owner | shinnjyuu |
11
+ | Repository name | echo-kit |
12
+ | Workflow name | publish.yml |
13
+ | Environment name | pypi |
14
+
15
+ 启用账号双因素认证。首次上传成功才实际占用包名。无需保存长期 API Token。
16
+
17
+ 普通 main 推送仅运行 Windows/Linux 核心测试。推送 `v*` 标签时,通过测试后构建并使用 OIDC 发布;也可对已有标签手动重跑工作流。
18
+
19
+ 发布前同步修改 pyproject.toml 和 src/echo_kit/__init__.py 的版本,运行 `uv lock`、`uv run pytest`、`uv build --no-sources`。
20
+ 提交并推送后,创建与包版本一致的标签,例如 `v0.1.0`,并推送该标签。GitHub Release 可随后附加版本说明,不是此工作流的触发条件。
21
+
22
+ 发布后从仓库外验证 `uvx --from shinnjyuu-echo-kit@latest echo-kit --version` 和 `uvx --from shinnjyuu-echo-kit@0.1.0 echo-kit --help`。
23
+ 一轮验收开始时获取最新版,随后固定本轮版本。包更新不覆盖项目适配脚本、配置或已导出的 Skill 副本。
24
+
25
+ 已发布文件不能覆盖;修改内容需提升版本。未完成 PyPI 授权时不要推送发布标签。
@@ -0,0 +1,33 @@
1
+ {
2
+ "date": "2026-09-30",
3
+ "package": "echo-kit",
4
+ "version": "0.1.0",
5
+ "windows": {"python": "3.12.6", "passed": 28, "skipped": 1, "skip_reason": "POSIX SIGINT transport test"},
6
+ "linux_container": {"distribution": "Ubuntu Noble", "python": "3.12.3", "passed": 29, "skipped": 0},
7
+ "macos": "unverified",
8
+ "wheel_clean_install": "passed",
9
+ "skill_export": "passed",
10
+ "real_examples": {
11
+ "lab_variants": "passed, baseline and double each repeated three times",
12
+ "api_login_download": "passed",
13
+ "windows_auth_reuse": "passed with system credential backend",
14
+ "linux_auth_ephemeral": "passed without persistence",
15
+ "docker_page_download": "passed",
16
+ "external_services": "passed using prestarted local demo instances",
17
+ "frontend_only": "passed after correctly refusing occupied port",
18
+ "clean_install_lab_run": "20260930-144131-187ca8c753",
19
+ "clean_install_page_run": "20260930-144132-152678fbf8"
20
+ },
21
+ "evidence_roots": [
22
+ "D:/Codex/echo-kit-v1-demo/qa/.echo-kit/runs",
23
+ "D:/Codex/echo-kit-installed-demo/qa/.echo-kit/runs",
24
+ "D:/Codex/echo-kit-failures/.echo-kit/runs"
25
+ ],
26
+ "limitations": [
27
+ "No production or Magic Cube integration claimed",
28
+ "Linux desktop keyring not exercised",
29
+ "No macOS runtime validation",
30
+ "Windows real console interrupt transport not exercised",
31
+ "External service demonstration is local network topology, not Internet deployment"
32
+ ]
33
+ }
@@ -0,0 +1,51 @@
1
+ # 第一版验证记录
2
+
3
+ 日期:2026-09-30。范围是 Echo Kit 自身与独立示例,没有访问或修改魔方业务环境。
4
+
5
+ ## 环境
6
+
7
+ - Windows 本机,Python 3.12.6,Docker Desktop Linux engine。
8
+ - Linux:Ubuntu Noble Playwright 容器,Python 3.12.3;这是 Linux 容器证据,不代表所有 Linux 桌面发行版。
9
+ - Playwright Python 与浏览器服务器均固定 1.63.0。验证使用已有同版本镜像镜像源 `mcr.m.daocloud.io/playwright:v1.63.0-noble`;产品默认官方仓库。
10
+ - macOS 未验证。Linux 容器没有系统凭据会话,验证了不明文落盘、同次调用使用登录态的退化路径;Linux 桌面安全存储未实测。
11
+
12
+ ## 真实用例
13
+
14
+ 独立前端、后端、QA 目录位于 `D:/Codex/echo-kit-v1-demo`。无 Git 历史的示例在报告中 commit 为 null,没有冒充真实版本。
15
+
16
+ | 场景 | 结果与记录 |
17
+ |---|---|
18
+ | Lab baseline / double 各三次 | 6 次通过,指标分别 5 / 10;不需要 Docker 或业务服务 |
19
+ | 正常登录与 API 文件下载 | 通过,`20260930-143555-617ee969fc` |
20
+ | 第二次复用服务与登录态 | 通过,`20260930-143611-ceec4a7307`,reused=true,Windows 系统凭据库持久化 |
21
+ | 托管前后端与 Docker 页面下载 | 通过,`20260930-143800-4c558f5e7a`,含截图、文件和 trace |
22
+ | 全部业务服务外部连接 | 通过,`20260930-143831-892802b3e0`,没有接管进程 |
23
+ | 前端占用冲突 | 按预期受阻,`20260930-143833-c2a8fa8893`;失败记录保留 |
24
+ | 仅托管前端,连接已有 API | 停止本工具原前端后复验通过,`20260930-143843-feb6eb94f9` |
25
+ | Linux Lab 与真实 API 验收 | 通过;临时登录态 current invocation only;停止服务成功 |
26
+ | 干净环境 wheel 安装 | 通过,`D:/Codex/echo-kit-clean-install`,独立实验与三个 Skill 导出成功 |
27
+
28
+ 上述“远端连接”使用本机预先运行的示例实例模拟已部署目标,证明连接和归属行为;没有声称验证跨公网、反向代理或真实测试环境。
29
+
30
+ ## 测试覆盖
31
+
32
+ pytest 覆盖初始化不覆盖、多目录覆盖、缺失环境变量、独立重复、必需证据缺失、HTML 转义、非法产物、超时、清理确认与失败、进程复用及身份不匹配、端口冲突、外部实例不停止、互斥、依赖环、部分启动失败、重启与状态重读、认证刷新与复用、认证身份拒绝、不可用安全存储、浏览器外部连接与所有权及版本校验。
33
+
34
+ Linux 额外通过真实 SIGINT 中断测试;Windows 跳过 POSIX SIGINT 测试,使用受控 runner 中断检查,不将两者等同。
35
+
36
+ 受控失败脚本 `examples/validate_failures.py` 产生真实失败/超时/缺失证据/清理失败报告。它没有真实远端任务,后续通过清理协议确认本地 fixture 范围,不伪称验证了任意业务停止接口。
37
+
38
+ ## 明确限制
39
+
40
+ - 示例登录使用公开测试凭据,证明正常 HTTP 认证适配;不证明企业 SSO、组织初始化、验证码或所有业务权限。
41
+ - 当前元数据在适配器输出异常时保存失败,但未设计断电后自动恢复业务,也不会自动重发。
42
+ - 系统凭据不可用时,独立 auth login 不能给下一条命令提供跨进程临时状态;verify 内可用。
43
+ - 外部服务版本由项目检查提供;本机源码快照不能证明远端部署版本。
44
+ - 适配器及页面 trace 可能含业务数据,分享前必须审查;HTML 报告不自动展示认证态与完整环境变量。
45
+ - 原型没有图形管理后台、完整确定性回放或公共 PyPI 发布。
46
+
47
+ ## 复现
48
+
49
+ 运行 README 中的示例流程。`uv run pytest -q` 为自动化测试入口;`uv build` 后在独立虚拟环境安装 wheel 验证分发。
50
+
51
+ 最终测试数量和构建结果以同目录 `validation-results.json` 记录为准。
@@ -0,0 +1,96 @@
1
+ """Create three independent directories. Does not launch anything or overwrite."""
2
+ import argparse
3
+ import json
4
+ import shutil
5
+ import sys
6
+ from pathlib import Path
7
+
8
+
9
+ def create(root, api_port=18761, web_port=18762):
10
+ root = Path(root).resolve()
11
+ if root.exists() and any(root.iterdir()):
12
+ raise SystemExit('Output must be absent or empty')
13
+ for folder in ('backend', 'frontend', 'qa/echo'):
14
+ (root / folder).mkdir(parents=True, exist_ok=True)
15
+ source = Path(__file__).parent / 'templates'
16
+ for filename, dest in [('server.py', 'backend/server.py'), ('frontend.py', 'frontend/server.py'),
17
+ ('adapter.py', 'qa/echo/adapter.py'), ('auth.py', 'qa/echo/auth.py')]:
18
+ shutil.copyfile(source / filename, root / dest)
19
+ python = json.dumps(sys.executable)
20
+ config = f'''schema_version = 1
21
+ name = "echo-demo"
22
+ default_environment = "local"
23
+ [projects.api]
24
+ path = "../backend"
25
+ [projects.web]
26
+ path = "../frontend"
27
+ [projects.qa]
28
+ path = "."
29
+ [services.api]
30
+ project = "api"
31
+ command = [{python}, "server.py", "{api_port}"]
32
+ port = {api_port}
33
+ timeout = 10
34
+ [services.api.ready]
35
+ url = "http://127.0.0.1:{api_port}/health"
36
+ [services.web]
37
+ project = "web"
38
+ command = [{python}, "server.py", "{web_port}", "{api_port}"]
39
+ port = {web_port}
40
+ timeout = 10
41
+ [services.web.ready]
42
+ url = "http://127.0.0.1:{web_port}/health"
43
+ [auth.demo]
44
+ project = "qa"
45
+ command = [{python}, "echo/auth.py"]
46
+ account = "demo"
47
+ target = "http://127.0.0.1:{api_port}"
48
+ [cases.arithmetic]
49
+ project = "qa"
50
+ command = [{python}, "echo/adapter.py", "math"]
51
+ required_checks = ["sum"]
52
+ mode = "unit"
53
+ [cases.arithmetic.inputs]
54
+ numbers = [2, 3]
55
+ factor = 1
56
+ [cases.arithmetic.variants.double]
57
+ factor = 2
58
+ [cases.api]
59
+ project = "qa"
60
+ command = [{python}, "echo/adapter.py", "api"]
61
+ services = ["api"]
62
+ auth = "demo"
63
+ required_checks = ["download"]
64
+ mode = "integration"
65
+ [cases.api.inputs]
66
+ url = "http://127.0.0.1:{api_port}"
67
+ [cases.page]
68
+ project = "qa"
69
+ command = [{python}, "echo/adapter.py", "page"]
70
+ services = ["api", "web"]
71
+ auth = "demo"
72
+ browser = true
73
+ required_checks = ["download", "page"]
74
+ mode = "integration"
75
+ timeout = 90
76
+ [cases.page.inputs]
77
+ url = "http://host.docker.internal:{web_port}"
78
+ [environments.external.services.api]
79
+ mode = "external"
80
+ [environments.external.services.web]
81
+ mode = "external"
82
+ [environments.frontend-only.services.api]
83
+ mode = "external"
84
+ '''
85
+ (root / 'qa/echo-kit.toml').write_text(config, encoding='utf-8')
86
+ (root / 'qa/.gitignore').write_text('.echo-kit/\n', encoding='utf-8')
87
+ return root / 'qa'
88
+
89
+
90
+ if __name__ == '__main__':
91
+ p = argparse.ArgumentParser()
92
+ p.add_argument('--output', required=True)
93
+ p.add_argument('--api-port', type=int, default=18761)
94
+ p.add_argument('--web-port', type=int, default=18762)
95
+ a = p.parse_args()
96
+ print(create(a.output, a.api_port, a.web_port))
@@ -0,0 +1,37 @@
1
+ import json
2
+ import os
3
+ import sys
4
+ import urllib.request
5
+ from pathlib import Path
6
+
7
+ q = json.loads(Path(os.environ['ECHO_REQUEST']).read_text())
8
+ secret = json.load(sys.stdin)
9
+ out = Path(os.environ['ECHO_RUN_DIR'])
10
+ checks, artifacts, metrics = [], [], {}
11
+ mode = sys.argv[1]
12
+ if mode == 'math':
13
+ answer = sum(q['inputs']['numbers']) * q['inputs'].get('factor', 1)
14
+ metrics['answer'] = answer
15
+ checks.append({'name': 'sum', 'status': 'passed' if answer == 5 * q['inputs'].get('factor', 1) else 'failed'})
16
+ elif mode == 'api':
17
+ req = urllib.request.Request(q['inputs']['url'] + '/download', headers={'Authorization': 'Bearer ' + secret['auth']['token']})
18
+ with urllib.request.urlopen(req, timeout=5) as r:
19
+ data = r.read()
20
+ (out / 'echo.txt').write_bytes(data)
21
+ checks.append({'name': 'download', 'status': 'passed' if data == b'Echo demo verified\n' else 'failed'})
22
+ artifacts.append('echo.txt')
23
+ else:
24
+ from echo_kit.browser import session
25
+ # The demo's normal auth endpoint yielded the token. Initialize the demo page's normal token slot.
26
+ with session(q['browser']['endpoint'], out) as context:
27
+ context.add_init_script('localStorage.token=' + json.dumps(secret['auth']['token']))
28
+ page = context.new_page()
29
+ page.goto(q['inputs']['url'])
30
+ checks.append({'name': 'page', 'status': 'passed' if page.title() == 'Echo demo' else 'failed'})
31
+ with page.expect_download() as download:
32
+ page.locator('#download').click()
33
+ download.value.save_as(out / 'echo.txt')
34
+ page.screenshot(path=str(out / 'page.png'))
35
+ checks.append({'name': 'download', 'status': 'passed' if (out / 'echo.txt').read_bytes() == b'Echo demo verified\n' else 'failed'})
36
+ artifacts.extend(['echo.txt', 'page.png', 'trace.zip'])
37
+ Path(os.environ['ECHO_RESULT']).write_text(json.dumps({'status': 'passed', 'checks': checks, 'artifacts': artifacts, 'metrics': metrics}), encoding='utf-8')
@@ -0,0 +1,18 @@
1
+ """Demo credentials are public fixture values, not a production login shortcut."""
2
+ import json
3
+ import sys
4
+ import urllib.request
5
+ import urllib.error
6
+
7
+ q = json.load(sys.stdin)
8
+ token = (q.get('state') or {}).get('token', '')
9
+ action = q['action']
10
+ url = q['target'] + ('/login' if action in ('login', 'refresh') else '/logout' if action == 'logout' else '/me')
11
+ data = json.dumps({'account': q['account'], 'password': q.get('credentials', {}).get('password', 'demo-only')}).encode() if action in ('login', 'refresh', 'logout') else None
12
+ req = urllib.request.Request(url, data=data, headers={'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json'})
13
+ try:
14
+ with urllib.request.urlopen(req, timeout=5) as r:
15
+ value = json.load(r)
16
+ print(json.dumps({'valid': True, 'account': q['account'], 'target': q['target'], 'state': {'token': value.get('token', token)}}))
17
+ except urllib.error.HTTPError:
18
+ print(json.dumps({'valid': False, 'account': q['account'], 'target': q['target']}))
@@ -0,0 +1,33 @@
1
+ import sys
2
+ import urllib.request
3
+ import urllib.error
4
+ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
5
+
6
+ HTML = b'''<!doctype html><html><title>Echo demo</title><body><h1>Echo demo</h1>
7
+ <button id="download">Download verified file</button><p id="status">Ready</p>
8
+ <script>document.querySelector('button').onclick=async()=>{const r=await fetch('/api/download',{headers:{Authorization:'Bearer '+localStorage.token}});if(!r.ok){document.querySelector('#status').textContent='Failed';return;}const b=await r.blob();const a=document.createElement('a');a.href=URL.createObjectURL(b);a.download='echo.txt';a.click();document.querySelector('#status').textContent='Downloaded';};</script></body></html>'''
9
+
10
+
11
+ class Handler(BaseHTTPRequestHandler):
12
+ def log_message(self, *_):
13
+ pass
14
+
15
+ def do_GET(self):
16
+ if self.path.startswith('/api/'):
17
+ req = urllib.request.Request('http://127.0.0.1:' + sys.argv[2] + self.path[4:], headers={'Authorization': self.headers.get('Authorization', '')})
18
+ try:
19
+ with urllib.request.urlopen(req) as r:
20
+ status, data = r.status, r.read()
21
+ except urllib.error.HTTPError as e:
22
+ status, data = e.code, e.read()
23
+ self.send_response(status)
24
+ self.end_headers()
25
+ self.wfile.write(data)
26
+ else:
27
+ self.send_response(200)
28
+ self.send_header('Content-Type', 'text/html')
29
+ self.end_headers()
30
+ self.wfile.write(HTML)
31
+
32
+
33
+ ThreadingHTTPServer(('0.0.0.0', int(sys.argv[1])), Handler).serve_forever()
@@ -0,0 +1,42 @@
1
+ import json
2
+ import secrets
3
+ import sys
4
+ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
5
+
6
+ tokens = set()
7
+
8
+
9
+ class Handler(BaseHTTPRequestHandler):
10
+ def log_message(self, *_):
11
+ pass
12
+
13
+ def reply(self, status, data, content='application/json'):
14
+ self.send_response(status)
15
+ self.send_header('Content-Type', content)
16
+ self.end_headers()
17
+ self.wfile.write(data if isinstance(data, bytes) else json.dumps(data).encode())
18
+
19
+ def do_GET(self):
20
+ if self.path == '/health':
21
+ return self.reply(200, {'ready': True})
22
+ if self.headers.get('Authorization', '').removeprefix('Bearer ') not in tokens:
23
+ return self.reply(401, {'error': 'unauthenticated'})
24
+ if self.path == '/me':
25
+ return self.reply(200, {'account': 'demo'})
26
+ if self.path == '/download':
27
+ return self.reply(200, b'Echo demo verified\n', 'text/plain')
28
+ return self.reply(404, {})
29
+
30
+ def do_POST(self):
31
+ body = json.loads(self.rfile.read(int(self.headers.get('Content-Length', '0'))) or '{}')
32
+ if self.path == '/login' and body == {'account': 'demo', 'password': 'demo-only'}:
33
+ token = secrets.token_hex(24)
34
+ tokens.add(token)
35
+ return self.reply(200, {'token': token})
36
+ if self.path == '/logout':
37
+ tokens.discard(self.headers.get('Authorization', '').removeprefix('Bearer '))
38
+ return self.reply(200, {})
39
+ return self.reply(401, {})
40
+
41
+
42
+ ThreadingHTTPServer(('0.0.0.0', int(sys.argv[1])), Handler).serve_forever()