From 2dee6d413609aa28e41c3d62deae39d6575d0a3f Mon Sep 17 00:00:00 2001 From: Lan_zhijiang Date: Thu, 22 Oct 2026 18:22:07 +0800 Subject: [PATCH 1/4] =?UTF-8?q?docs(contributing):=20=E5=AF=B9=E9=BD=90=20?= =?UTF-8?q?Python=20=E9=A1=B9=E7=9B=AE=E8=B4=A1=E7=8C=AE=E4=B8=8E=E5=8F=91?= =?UTF-8?q?=E5=B8=83=E7=BA=A6=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 区分主要实现 scope 与独立发布项目范围 - 补齐 fork CI、安装依赖和 CLI 验证说明 --- CONTRIBUTING.md | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index feae76bc..d45aadd6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,7 @@ The organization-wide Git and pull-request policy lives in [InKCre Git and GitHub Governance](https://github.com/InKCre/.github/blob/main/GOVERNANCE.md). -This guide adds the `core-py` setup, validation, migration, and delivery details required by that +This guide adds the `impl-py` setup, validation, migration, and delivery details required by that policy; it does not weaken the organization baseline. ## Security Reports @@ -25,7 +25,7 @@ pdm run check `check` is the same hermetic repository gate used by CI. It verifies the lock and requirements export, migration configuration and append-only baseline, Ruff formatting, -repository lint, and static types. Use narrower commands while iterating: +repository lint, static types, and the repository test suite. Use narrower commands while iterating: ```bash pdm run format:check @@ -54,6 +54,13 @@ Core fragments live in `.changes/`; CLI fragments live in `cli/.changes/`; Exten `fixed`, and `security`. Pure release-tooling or contributor-documentation changes do not invent project news. +Use the primary project or implementation area as the `type(scope): summary` scope: `core`, `cli`, +or an Extension ID such as `mail`. Use `extensions` for one coherent change spanning several +Extensions and `release` for release tooling. The title is a navigation aid, not a version plan; list +every affected release project in the pull-request body and add a fragment under each affected +project. Examples: `fix(core): preserve extension state on restart`, `feat(mail): collect newsletter +issues`, and `ref(extensions): share distribution validation`. + After feature fragments merge, the checked-main controller updates the independent `release/next` pull request. Only that pull request runs `pdm run release:prepare`, consumes fragments, applies the maturity-aware SemVer bump, and renders changelogs. It publishes nothing. Merging the prepared pull @@ -98,11 +105,14 @@ python scripts/container.py ready `web` honors `$PORT`; `migrate` only applies checked-in revisions; `ready` performs a read-only connectivity and Alembic-head check. The Core image owns the supported dependency baseline and excludes first-party Extension source. The Extension Host acquires only exact -published wheels and never resolves or mutates host dependencies while enabling them. +published wheels. Applying a selected Python Distribution runs pip dependency resolution in +the shared Host environment and can install or upgrade dependencies before a managed restart. +The [native distribution contract](docs/40-deployment/first-party-extension-distribution.md) +owns admission, environment application, and restart behavior. ## Shared Docs And Skill Discovery -`core-py` is a Spoke repo and consumes shared Hub truth from `docs/_shared/`. +`impl-py` is a Spoke repo and consumes shared Hub truth from `docs/_shared/`. The canonical shared-doc editing skill lives in: @@ -119,6 +129,6 @@ Use the repo-root wrapper only to discover the canonical skill. Do not fork the 1. Capture the local pressure in the active Spoke task packet. 2. Edit shared docs in `InKCre/docs`. 3. Push the shared Hub commit first. -4. Bump `core-py/docs/_shared` to that pushed commit. +4. Bump `impl-py/docs/_shared` to that pushed commit. 5. Keep Spoke-local runtime and implementation docs outside `docs/_shared`. 6. Do not mix shared-doc edits, ref bumps, and local implementation changes in one commit. From 93aae712907b09da4dad5894e3dfbc5321158439 Mon Sep 17 00:00:00 2001 From: Lan_zhijiang Date: Thu, 22 Oct 2026 18:22:07 +0800 Subject: [PATCH 2/4] =?UTF-8?q?fix(cli):=20=E5=AF=B9=E9=BD=90=20Extension?= =?UTF-8?q?=20management=20v2=20=E5=91=BD=E4=BB=A4=E4=B8=8E=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E5=8F=91=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 使用 Peer selection API 并保留取消选择回执 - 从当前 Peer 状态发现配置 schema,增加 status 命令 - 添加 HTTP 边界回归测试和破坏性变更片段 --- cli/.changes/selection-v2.removed.md | 1 + cli/README.md | 10 ++- cli/docs/development.md | 2 +- cli/pyproject.toml | 9 ++- cli/src/inkcre_cli/commands/extension.py | 49 +++++++++--- cli/tests/test_extension.py | 99 ++++++++++++++++++++++++ 6 files changed, 153 insertions(+), 17 deletions(-) create mode 100644 cli/.changes/selection-v2.removed.md create mode 100644 cli/tests/test_extension.py diff --git a/cli/.changes/selection-v2.removed.md b/cli/.changes/selection-v2.removed.md new file mode 100644 index 00000000..1ae04ae1 --- /dev/null +++ b/cli/.changes/selection-v2.removed.md @@ -0,0 +1 @@ +用 `extension select --distribution python|module_federation --version VERSION` 和 `extension unselect` 替换旧的 `install/uninstall` 命令,修复对当前 Core 返回 405 的请求。选择和取消选择均可用 `--peer` 指定目标;取消选择保留共享配置和信息。新增 `extension status` 读取接入 Peer 的状态,配置 schema 同样从该 Peer 读取,不再访问共享身份上已移除的字段。该版本要求 Core 的 Extension management v2 接口,不兼容旧管理接口。 diff --git a/cli/README.md b/cli/README.md index 8cf58635..8c0db59c 100644 --- a/cli/README.md +++ b/cli/README.md @@ -47,6 +47,14 @@ inkcre-cli config schemas --json `connection` 只管理本机连接,`config` 管理远端 deployment config;Agent definition 由 `agent` 管理, 模型目录由 `ai` 发现。Extension 配置和运行目标使用 `extension`,不混入 connection。 +Extension management v2 使用 `extension select COORDINATE --distribution python --version VERSION` +为接入 Peer 选择准确版本,再用 `extension enable COORDINATE` 启用。两者都可用 `--peer PEER_ID` +指定目标;Web Peer 的 Distribution 是 `module_federation`,不能把它当作 Python 包交给 Core 执行。 +先 `extension disable COORDINATE`,再 `extension unselect COORDINATE` 取消该 Peer 的选择;共享配置 +和已收集信息不会随之删除。`extension status [COORDINATE]` 读取接入 Peer 的选择和实际运行状态, +`extension get` 只读共享身份与配置。配置 `--schema` 同样来自接入 Peer,而不是共享 Extension 行。 +上述命令替代旧的 `install/uninstall`,要求当前 Core v2 接口,不自动回退。 + ## Agent Query Agent Query Sink 用 AI 组合已有 info-base 检索、Resolver 读取和图导航,并通过异步 Job 交付答案。先从 @@ -82,4 +90,4 @@ inkcre-cli job wait 1 --for 10s --json 这些是同一次 HTTP 响应的本地文件,不是新下载会话,也不会再次调用 Resolver。 开发使用独立 PDM 项目:`pdm install -p cli`、`pdm run -p cli check`、`pdm build -p cli`。 -0.0.0 是未发布的开发版本;首次发行由仓库的 Release PR 准备。 +版本和 changelog 由仓库 Release PR 准备,功能 PR 只记录 `cli/.changes/` 发布意图。 diff --git a/cli/docs/development.md b/cli/docs/development.md index 03d98acc..4e69ea19 100644 --- a/cli/docs/development.md +++ b/cli/docs/development.md @@ -21,7 +21,7 @@ Feature PR 只放 `cli/.changes/` fragment,使用根 Towncrier 类型。单一 经过正常准备得到 0.1.0;不能因为 base 中尚无 CLI 就直接发布。 `cli-publish.yml` 从精确 main SHA 自行检查、构建 wheel/sdist,并通过 PDM 原生 Trusted Publishing 上传。 -PyPI pending publisher 配置:owner InKCre,repository core-py,workflow cli-publish.yml,environment production。 +PyPI pending publisher 配置:owner InKCre,repository impl-py,workflow cli-publish.yml,environment production。 安装者只需 pip,不需 PDM。新增 REST 依赖时先交付 Core provider,再发布 CLI consumer;没有永久版本相等 gate。 恢复使用 main 上的 workflow_dispatch。PDM `--no-build --skip-existing` 保留当前 run 已检查的构建并续传缺失 diff --git a/cli/pyproject.toml b/cli/pyproject.toml index 24fe1674..d39487bc 100644 --- a/cli/pyproject.toml +++ b/cli/pyproject.toml @@ -29,11 +29,12 @@ dev = ["ruff>=0.15,<0.16", "pyrefly>=0.56,<0.57"] distribution = true [tool.pdm.scripts] -lint = "ruff check src" -format = "ruff format src" -"format:check" = "ruff format --check src" +lint = "ruff check src tests" +format = "ruff format src tests" +"format:check" = "ruff format --check src tests" +test = "python -m unittest discover -s tests" typecheck = "pyrefly check" -check = {composite = ["lint", "format:check", "typecheck"]} +check = {composite = ["lint", "format:check", "typecheck", "test"]} [tool.ruff] line-length = 100 diff --git a/cli/src/inkcre_cli/commands/extension.py b/cli/src/inkcre_cli/commands/extension.py index 2a979a5b..ae3b0669 100644 --- a/cli/src/inkcre_cli/commands/extension.py +++ b/cli/src/inkcre_cli/commands/extension.py @@ -16,7 +16,7 @@ def route(coordinate: str) -> str: @click.group(cls=Group) def extension(): - """安装、配置和启停 Extension;发布仍由 Extension 的交付流程负责。""" + """管理 Peer 的 Extension 选择、配置和启停;发布由 Extension 交付流程负责。""" @extension.command("list") @@ -24,7 +24,7 @@ def extension(): @common @click.pass_obj def list_extensions(inv: Invocation, limit, cursor): - """列出 Extension 的安装、启用意图与运行状态。""" + """列出共享 Extension 身份与配置;Peer 运行状态见 status。""" inv.show(inv.send("GET", "/extensions", params={"limit": limit, "cursor": cursor})) @@ -33,27 +33,50 @@ def list_extensions(inv: Invocation, limit, cursor): @common @click.pass_obj def get(inv: Invocation, coordinate): - """读取一个已安装 Extension。""" + """读取共享 Extension 身份与配置;不代表任一 Peer 已启用。""" inv.show(inv.send("GET", route(coordinate))) +@extension.command() +@click.argument("coordinate", required=False) +@common +@click.pass_obj +def status(inv: Invocation, coordinate): + """读取接入 Peer 的选择、运行版本、配置 schema 与重启状态。""" + inv.show( + inv.send( + "POST", "/extension-management/v2", body={"action": "status", "extension": coordinate} + ) + ) + + @extension.command() @click.argument("coordinate") +@click.option("--distribution", required=True, type=click.Choice(["python", "module_federation"])) @click.option("--version", required=True, help="准确的已发布版本,不解析 latest") +@click.option("--peer", help="选择的目标 Peer;省略时为接入 Peer") @common @click.pass_obj -def install(inv: Invocation, coordinate, version): - """安装准确版本;不隐式启用。""" - inv.show(inv.send("POST", route(coordinate), params={"version": version})) +def select(inv: Invocation, coordinate, distribution, version, peer): + """为目标 Peer 选择准确 Distribution/版本;不隐式启用。""" + inv.show( + inv.send( + "POST", + route(coordinate) + "/selection", + params={"route_to_peer": peer}, + body={"distribution": distribution, "version": version}, + ) + ) @extension.command() @click.argument("coordinate") +@click.option("--peer", help="取消选择的目标 Peer;省略时为接入 Peer") @common @click.pass_obj -def uninstall(inv: Invocation, coordinate): - """卸载 Extension。""" - inv.show(inv.send("DELETE", route(coordinate)), no_content=True) +def unselect(inv: Invocation, coordinate, peer): + """取消已禁用 Peer 的选择;保留共享 Extension 配置和信息。""" + inv.show(inv.send("DELETE", route(coordinate) + "/selection", params={"route_to_peer": peer})) @extension.command() @@ -91,9 +114,13 @@ def get_config(inv: Invocation, coordinate): def config_schema(inv: Invocation, coordinate: str) -> dict: - contract = inv.send("GET", route(coordinate))["config_schema"] + result = inv.send( + "POST", "/extension-management/v2", body={"action": "status", "extension": coordinate} + ) + selected = next((row for row in result["extensions"] if row["name"] == coordinate), None) + contract = selected["config_schema"] if selected else None if contract is None: - raise CommandError("该 Extension 尚未发布配置 schema;可在启用后重新发现") + raise CommandError("接入 Peer 尚未发布该 Extension 的配置 schema;请在选择并启用后重新发现") return contract diff --git a/cli/tests/test_extension.py b/cli/tests/test_extension.py new file mode 100644 index 00000000..04b0cfd3 --- /dev/null +++ b/cli/tests/test_extension.py @@ -0,0 +1,99 @@ +"""CLI requests and receipts at the Extension management v2 HTTP boundary.""" + +import json +import unittest + +import httpx +from click.testing import CliRunner + +from inkcre_cli.command import Invocation +from inkcre_cli.connection import Connection +from inkcre_cli.http import CoreRESTClient +from inkcre_cli.main import cli + + +class ExtensionSelectionTests(unittest.TestCase): + def invoke(self, arguments, handler): + invocation = Invocation() + client = CoreRESTClient( + Connection(base_url="http://core.test", jwt_secret="test-only-secret-at-least-32-bytes") + ) + client.client.close() + client.client = httpx.Client(transport=httpx.MockTransport(handler)) + invocation._client = client + try: + return CliRunner().invoke(cli, ["--json", "extension", *arguments], obj=invocation) + finally: + invocation.close() + + def test_select_uses_v2_body_and_optional_target_peer(self): + for distribution, peer in (("python", None), ("module_federation", "web-peer")): + with self.subTest(distribution=distribution): + requests = [] + + def handle(request, requests=requests, distribution=distribution): + requests.append(request) + return httpx.Response( + 200, json={"distribution": distribution, "enabled": False} + ) + + arguments = [ + "select", + "inkcre/rss", + "--distribution", + distribution, + "--version", + "1.2.3", + ] + if peer: + arguments += ["--peer", peer] + result = self.invoke(arguments, handle) + self.assertEqual(result.exit_code, 0, result.output) + self.assertEqual(len(requests), 1) + request = requests[0] + self.assertEqual(request.method, "POST") + self.assertEqual(request.url.path, "/extensions/inkcre/rss/selection") + self.assertEqual(dict(request.url.params), {"route_to_peer": peer} if peer else {}) + self.assertEqual( + json.loads(request.content), {"distribution": distribution, "version": "1.2.3"} + ) + self.assertEqual(json.loads(result.output)["enabled"], False) + + def test_unselect_preserves_status_receipt(self): + receipt = {"peer_id": "web-peer", "extensions": [{"distribution": None, "enabled": False}]} + + def handle(request): + self.assertEqual(request.method, "DELETE") + self.assertEqual(request.url.path, "/extensions/inkcre/rss/selection") + self.assertEqual(dict(request.url.params), {"route_to_peer": "web-peer"}) + return httpx.Response(200, json=receipt) + + result = self.invoke(["unselect", "inkcre/rss", "--peer", "web-peer"], handle) + self.assertEqual(result.exit_code, 0, result.output) + self.assertEqual(json.loads(result.output), receipt) + + def test_config_schema_comes_from_current_peer_not_shared_identity(self): + schema = {"type": "object", "properties": {"token": {"type": "string"}}} + status = { + "peer_id": "core-peer", + "extensions": [{"name": "inkcre/rss", "config_schema": schema}], + } + + def handle(request): + self.assertEqual(request.method, "POST") + self.assertEqual(request.url.path, "/extension-management/v2") + self.assertEqual( + json.loads(request.content), {"action": "status", "extension": "inkcre/rss"} + ) + return httpx.Response(200, json=status) + + result = self.invoke(["config", "replace", "inkcre/rss", "--schema"], handle) + self.assertEqual(result.exit_code, 0, result.output) + self.assertEqual(json.loads(result.output), schema) + result = self.invoke(["status", "inkcre/rss"], handle) + self.assertEqual(result.exit_code, 0, result.output) + self.assertEqual(json.loads(result.output), status) + + +if __name__ == "__main__": + unittest.main() From 4c556511f25b56d14935b91f845ad1f96ecd9215 Mon Sep 17 00:00:00 2001 From: Lan_zhijiang Date: Mon, 26 Oct 2026 15:09:58 +0800 Subject: [PATCH 3/4] docs(cli): write contribution changes in English - Keep the new v2 selection and release instructions accessible to community contributors. --- cli/README.md | 19 +++++++++++-------- cli/docs/development.md | 3 ++- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/cli/README.md b/cli/README.md index 8c0db59c..1b1a573d 100644 --- a/cli/README.md +++ b/cli/README.md @@ -47,13 +47,15 @@ inkcre-cli config schemas --json `connection` 只管理本机连接,`config` 管理远端 deployment config;Agent definition 由 `agent` 管理, 模型目录由 `ai` 发现。Extension 配置和运行目标使用 `extension`,不混入 connection。 -Extension management v2 使用 `extension select COORDINATE --distribution python --version VERSION` -为接入 Peer 选择准确版本,再用 `extension enable COORDINATE` 启用。两者都可用 `--peer PEER_ID` -指定目标;Web Peer 的 Distribution 是 `module_federation`,不能把它当作 Python 包交给 Core 执行。 -先 `extension disable COORDINATE`,再 `extension unselect COORDINATE` 取消该 Peer 的选择;共享配置 -和已收集信息不会随之删除。`extension status [COORDINATE]` 读取接入 Peer 的选择和实际运行状态, -`extension get` 只读共享身份与配置。配置 `--schema` 同样来自接入 Peer,而不是共享 Extension 行。 -上述命令替代旧的 `install/uninstall`,要求当前 Core v2 接口,不自动回退。 +Extension management v2 uses `extension select COORDINATE --distribution python --version VERSION` +to select an exact version for the connected Peer, then `extension enable COORDINATE` to enable it. +Both commands accept `--peer PEER_ID` to target another Peer. A Web Peer's Distribution is +`module_federation`; it cannot run as a Python package on Core. Disable the Extension before +`extension unselect COORDINATE` to remove that Peer's selection. Shared configuration and collected +information remain. `extension status [COORDINATE]` reads the connected Peer's selection and runtime +state; `extension get` reads shared identity and configuration. Configuration `--schema` also comes +from the connected Peer, rather than the shared Extension row. These commands replace +`install/uninstall` and require the current Core v2 API, with no automatic fallback. ## Agent Query @@ -90,4 +92,5 @@ inkcre-cli job wait 1 --for 10s --json 这些是同一次 HTTP 响应的本地文件,不是新下载会话,也不会再次调用 Resolver。 开发使用独立 PDM 项目:`pdm install -p cli`、`pdm run -p cli check`、`pdm build -p cli`。 -版本和 changelog 由仓库 Release PR 准备,功能 PR 只记录 `cli/.changes/` 发布意图。 +The repository Release PR prepares versions and changelogs; feature PRs record release intent in +`cli/.changes/`. diff --git a/cli/docs/development.md b/cli/docs/development.md index 4e69ea19..a5da0450 100644 --- a/cli/docs/development.md +++ b/cli/docs/development.md @@ -21,7 +21,8 @@ Feature PR 只放 `cli/.changes/` fragment,使用根 Towncrier 类型。单一 经过正常准备得到 0.1.0;不能因为 base 中尚无 CLI 就直接发布。 `cli-publish.yml` 从精确 main SHA 自行检查、构建 wheel/sdist,并通过 PDM 原生 Trusted Publishing 上传。 -PyPI pending publisher 配置:owner InKCre,repository impl-py,workflow cli-publish.yml,environment production。 +Configure the PyPI pending publisher with owner `InKCre`, repository `impl-py`, workflow +`cli-publish.yml`, and environment `production`. 安装者只需 pip,不需 PDM。新增 REST 依赖时先交付 Core provider,再发布 CLI consumer;没有永久版本相等 gate。 恢复使用 main 上的 workflow_dispatch。PDM `--no-build --skip-existing` 保留当前 run 已检查的构建并续传缺失 From f188c37836cada90bd4573d6c8d359f3e8c34dbd Mon Sep 17 00:00:00 2001 From: Lan_zhijiang Date: Mon, 26 Oct 2026 21:59:33 +0800 Subject: [PATCH 4/4] docs(cli): write the v2 release note in English - Preserve the breaking command and API requirements in the public release note. --- cli/.changes/selection-v2.removed.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/.changes/selection-v2.removed.md b/cli/.changes/selection-v2.removed.md index 1ae04ae1..0b464426 100644 --- a/cli/.changes/selection-v2.removed.md +++ b/cli/.changes/selection-v2.removed.md @@ -1 +1 @@ -用 `extension select --distribution python|module_federation --version VERSION` 和 `extension unselect` 替换旧的 `install/uninstall` 命令,修复对当前 Core 返回 405 的请求。选择和取消选择均可用 `--peer` 指定目标;取消选择保留共享配置和信息。新增 `extension status` 读取接入 Peer 的状态,配置 schema 同样从该 Peer 读取,不再访问共享身份上已移除的字段。该版本要求 Core 的 Extension management v2 接口,不兼容旧管理接口。 +Replace `extension install/uninstall` with `extension select --distribution python|module_federation --version VERSION` and `extension unselect`, fixing requests that return 405 against current Core. Selection and unselection accept `--peer`; unselection preserves shared configuration and collected information. Add `extension status` and discover configuration schemas from the connected Peer's status rather than removed shared-identity fields. These commands require Core's Extension management v2 API and do not support the old management interface.