Skip to content

backfill space member names - #1637

Open
liyaqing-l wants to merge 77 commits into
devfrom
space_avernet_qt
Open

backfill space member names#1637
liyaqing-l wants to merge 77 commits into
devfrom
space_avernet_qt

Conversation

@liyaqing-l

Copy link
Copy Markdown
Collaborator
  • 员工工号不足 6 位时,仅在查询员工目录前补零,例如:
    • 1234 → 001234
    • 数据库仍保存原始 1234

FreddieSun and others added 30 commits August 25, 2026 15:34
## Problem

Default SkillSet Skill and MCP removal was routed through the canonical
control plane, where ordinary-membership immutability checks rejected
the shared system Default with `SYSTEM_DEFAULT_IMMUTABLE`. BFF and
OpenAPI Gateway callers therefore could not create the per-owner,
per-Bot exclusions that preserve the published Default Set contract.

## Solution

- Branch Default Skill removal before the ordinary-Set guard and persist
`ac_default_skillset_skill_exclusion` under the resolved Bot owner.
- Retire the Default-governed Skill installation on first exclusion,
while preserving it when an active ordinary Set still provides the
Skill.
- Branch projected Default MCP removal to
`ac_default_skillset_mcp_exclusion` without requiring an ordinary MCP
membership row or deleting explicit MCP installations.
- Include both exclusion collections in desired-state snapshots and
compensating restore so runtime reconcile failures roll back atomically.
- Keep Default Set add/update/delete/deactivate immutability unchanged.

## Validation

- `uv run --project src/backend pytest -q
src/backend/tests/community/repository/skill_center/test_skill_set_control_plane_uow.py
src/backend/tests/community/core/skill_center/test_skill_set_control_plane_service.py
src/backend/tests/community/contracts/gateway/test_rule15_skillsets.py`
— 104 passed.
- `uv run --project src/backend pytest -q
src/backend/tests/community/endpoints/test_endpoint_runner.py -k
"excludes_default_skill or excludes_dynamic_default_mcp"` — 2 passed.
- `uv run --project src/backend pytest -q
src/backend/tests/community/architecture/test_module_boundaries.py
src/backend/tests/community/architecture/test_service_api_conformance.py
src/backend/tests/community/core/skill_center
src/backend/tests/community/repository/skill_center` — 1083 passed, 27
skipped.
- `scripts/ci/python_sast_local.sh src/backend 1 --base origin/dev
--head HEAD` — passed.
- Changed-file Ruff checks — passed; the repository test file retains
its pre-existing unused `sqlalchemy.text` import and was checked with
F401 ignored rather than mixing unrelated cleanup.

## Compatibility and risk

No HTTP schema or database schema changes. The change restores the
existing Default exclusion semantics for both BFF and Gateway through
their shared control plane. Ordinary SkillSet membership behavior is
unchanged. On reconcile failure, exclusion and installation desired
state are restored before runtime compensation.
BotModel.to_dict() does not include template_config — it is dynamically
fetched from the template service (ac_templates.ext) and attached by
BotService.get_bot(). RenderScreenService was using
BotRepository.get_by_id() directly, which bypasses that enrichment, so
resolve_render_screen_scope() could never see
template_config.capabilities.member_management for dynamic template bots.

Fix: inject TemplateService into RenderScreenService and enrich the bot
dict with template_config in _require_bot(), so scope resolution can
detect member-management capability for all bot types.

Tests: update fixtures to provide mock_template_service and configure it
to return template config for dynamic shared bot scenarios.

Validation: uv run pytest on render_screen scope/service/router/handler,
bot_collaborator, and architecture rules suites - 232 passed.
…gine_type check

Replace the complex capability-service-based scope resolution with a
simple rule: active_engine == 'claude_code' AND template_type is not a
plain CC type (empty/None/'normal'/'normalCC').

This removes the need for TemplateService injection and cross-table
template config lookups, making the scope decision purely based on
ac_bots columns that are always available.

- scope.py: replace MemberManagementCapabilityService dependency with
  a simple template_type check
- render_screen_service.py: remove TemplateService injection and
  template_config enrichment
- tests: simplify fixtures, remove mock_template_service, expand scope
  tests to cover all template_type variants

Validation: uv run pytest on render_screen scope/service/router/handler,
architecture rules, and bot_collaborator suites - 237 passed.
## Problem

Caller-aware Bot conversations can target both a shared Caller Service
runtime
for `draft`, `verify`, or `online` and a caller-specific Expert Chat
instance.
The IAM refresh path needed to update every eligible runtime binding
without
mistakenly selecting a different user's dynamic instance or performing a
separate Caller credential exchange for each target.

## Solution

- Add explicit `CALLER_SERVICE` and `CALLER_INSTANCE` runtime-binding
targets
  while preserving Session Files `AUTO` behavior.
- Use the existing collaboration-lock snapshot to select an eligible
Caller
  Service runtime; permit the owner when no lock exists.
- Independently include the authenticated user's active Caller Instance.
- Exchange one opaque Caller Token per IAM request and reuse it for
every
  selected outbound-rule update.
- Return IAM success when at least one target updates; preserve the
existing
  outbound failure when no target can be updated.

## Validation

- `uv run --project src/backend pytest -q
src/backend/tests/community/core/runtime_binding
src/backend/tests/community/core/caller_identity
src/backend/tests/community/adapters/http/openapi_v1/engine_runtime/test_session_files.py
src/backend/tests/community/endpoints/test_openapi_session_files.py
src/backend/tests/community/di/test_community_router_bindings_resolve.py`
— 79 passed.
- `uv run --project src/backend ruff check ...` on the affected Python
files — passed.
- `uv run --project src/backend python -m compileall -q ...` on affected
backend modules — passed.
- `git diff --check` — passed.

## Compatibility and risk

This is a server-side change. It does not accept a client-controlled
binding id
and leaves Agent Run locking, relay/WebSocket behavior, Session Files
`AUTO`
resolution, and BaaS append semantics unchanged.

## Spec

`spec/pipeline/caller-iam-multi-target-refresh/001-spec-output.md`
## Problem

BCN stream conversion shared the broad `core-service` log and only
emitted
warnings for malformed interaction payloads. This made it difficult to
correlate the raw stream chunk received from the engine with the SSE
event
produced for BCN, especially when diagnosing duplicated tool lifecycle
events.

Secret-marked `ask_user` interactions were also dropped during
projection,
which could leave downstream consumers waiting for an interaction that
was
never delivered.

## Solution

- Route existing `DefaultStreamConverter` warnings to the dedicated
  `bcn-converter` logger.
- Wrap the converter only at the BCN `chat.send` streaming boundary and
emit
one structured record per conversion containing the run ID, complete raw
  `StreamChunk`, and complete `SseEvent` result or `null` when filtered.
- Record conversion exceptions with the raw input and structured error
details,
then re-raise them so the existing SSE error path remains authoritative.
- Isolate serialization and logger backend failures from the conversion
path.
- Treat `secret` and `isSecret` as unsupported extension fields: omit
the
markers from the BCN question projection but deliver the interaction as
a
  standard `ask_user` event.
- Leave OpenAPI and Gateway conversion wiring unchanged.
- Register the new canonical logger name and add focused regression
coverage.

## Validation

- `uv run pytest tests/unit/core/service/sse/test_default_converter.py
tests/unit/adapters/web/open_api/test_bcn_router.py
tests/architecture/test_exception_handling.py
tests/architecture/test_logger_usage.py -q`
- 110 passed (the architecture exception audit also emitted its existing
    non-blocking repository-wide warning)
- `uv run ruff format --check
src/secbaas/community/core/service/sse/_default_converter.py
src/secbaas/community/adapters/web/routers/bcn_downlink/bcn_router.py
tests/unit/core/service/sse/test_default_converter.py
tests/unit/adapters/web/open_api/test_bcn_router.py`
  - All files formatted
- `uv run ruff check
src/secbaas/community/core/service/sse/_default_converter.py
src/secbaas/community/adapters/web/routers/bcn_downlink/bcn_router.py
tests/unit/core/service/sse/test_default_converter.py
tests/unit/adapters/web/open_api/test_bcn_router.py`
  - All checks passed

## Compatibility and risk

- The BCN wire schema is unchanged: `secret` and `isSecret` are not
emitted.
- Secret-marked questions are no longer dropped; downstream receives
them as
ordinary plaintext `ask_user` questions so the interaction can complete.
- INFO records intentionally contain complete raw message content and
complete
converted output, including secret-marked interaction content. Access
and
retention must therefore follow the existing SecBaaS sensitive-log
policy.
- Deployment must add an AntLogs collector for
`/home/admin/logs/secbaas/bcn-converter.log`; the application will
create and
  rotate the file through the existing logger plugin.
…1481)

## Problem

Prepub `GET /openapi/v1/bots/all` responds at RT p50=622ms (p90=3.7s,
max 17.4s; access.log, 6h sample). One request runs ~12-14 serial DB
round trips. Two avoidable chunks dominate:

- `DesktopBotService.list_user_bots` queried once per status — 5 serial
trips (PENDING/ACTIVE/OFFLINE/RELEASING/FAILED)
- Every 200-row page pulled by the inventory passed through
`_attach_template_configs_to_bots`, although the inventory response
provably never reads `template_config` (neither `BotInventoryItem` nor
the router `_to_inventory_item` maps it)

## Solution

- `search_bots` gains `bot_status_list` (status IN filter);
`list_user_bots` collapses 5 serial queries into one (500/page with a
fill loop, coverage ≥ the old 100×5; failure keeps the old
swallow-and-log contract)
- `list_bots_by_conditions` gains `attach_templates: bool = True`
(default preserves the established get/list consistency contract); the
inventory cloud pull passes `False` — one fewer batched read per pulled
page, behavior unchanged
- No API shape, response, or error behavior changes

## Validation

- `ci_test.sh --base origin/REL20260826`: 14460 passed, case pass rate
100%, line coverage 87.08%, changed-line coverage 94.44% (≥80% gate)
- `TestListUserBots` rewritten for single-query semantics (single-IN +
pagination fill + swallowed failure); new opt-out (misc) and
pass-through (inventory) coverage
- All `search_bots` call sites verified keyword-only — the new parameter
cannot shift positional mappings
- Post-deploy check: prepub /bots/all RT p50/p90 tracked via antlogs
(current baseline p50 ~600-800ms); P2 (query parallelization + SQL
pagination pushdown) targets <150ms next
#1499)

## Problem
task 模块 `TaskExecutor.form_coop_group` 在创建 state_machine 协作群(BCS
create_group)时,把 `opening_message.params` 以**字符串化
JSON**(`json.dumps({...})`)发出。但 BCS

契约(`ocb-public/src/bcs/docs/custom-collaboration-opening-message-integration-guide.md`
§4)要求 `opening_message.params` 是**JSON object**;字符串化会被真实 BCS 的 untagged
enum `OpeningMessage` 判 `data did not match any variant of untagged enum
OpeningMessage` → **422**,真实预发/生产才能触发。singlebox 的 BCS double 不校验
  `opening_message`,本问题一直被本地测试掩盖。

另外缺少一条直连预发环境、端到端验证 `OpenApiBotAdapter` 单 bot 派发的 e2e(现有
`test_open_api_bot_adapter_live` 仅本地 singlebox 占位)。

  ## Solution
1. **修复 `opening_message.params` 序列化**(`task_executor.py`):把 `"params":
_json.dumps({...})` 改为直接传 dict(JSON object),并删除为此引入的局部 `import json as
_json`;附 BCS 契约注释指向文档 §4。仅影响 state_machine 协作群建群(显式 `api_key_prefix`
不受影响)。
2.
**新增回归断言**(`test_state_machine.py::test_form_coop_group_opening_message_params_is_object`):state_machine
建群带 `task_id` 时,断言 `opening_message.params` 为

`dict`(非字符串)且含关键字段(`taskId`/`apiBaseUrl`/`groupId`/`runId`/`businessScene`),防止该契约被字符串化回退。
3. **新增预发
e2e**(`test_open_api_bot_adapter_pre_e2e.py`):env-gated(`AVERNET_PRE_OPENAPI_API_KEY`
+ `AVERNET_PRE_OPENAPI_BOT_ID` 才启用,`BASE_URL` 默认预发
host,`COOKIE` 支持文件路径或原文并自动去 `Cookie:` 前缀,`PREFIX` 留空走 adapter 默认
`api_key[:8]`),直连预发 BaaS Open API,经 `send_and_wait`(ensure_grant →
send_message
→ 轮询 get_run)到终态断言 `status ∈ {COMPLETED, FAILED}`;空缺即 `skipUnless` 跳过,CI
安全。

  ## Validation
  - `py_compile` 通过;`task_executor.py` 与两个测试文件 SAST(block 规则集)exit 0。
- `test_state_machine.py` 全绿(8 既有 + 1 新增回归 = 9 passed);新回归按 TDD
验过红→绿(临时把源码回滚到字符串化版本则该断言失败、还原后通过),证明其能守住契约。
  - 预发 e2e 默认 skip(env 未设即跳过,不破 CI);cookie 文件 / 原文 / 空三态解析与 import 已验。
- cherry-pick 到本分支后复验:`py_compile` OK + `test_state_machine` 9 passed +
预发 e2e 1 skipped;pre-push gate(lint-only SAST)passed。

  ## Compatibility and risk
- `opening_message.params` string→object 是修对、与 BCS 契约对齐;为 state_machine
协作群建群必经字段,prod 同样受益,无依赖“字符串 params”的合法消费方(BCS
  本就不收),风险低。
  - singlebox double 不校验 `opening_message`,是本问题漏到生产的根因;新增回归断言补上该盲区。
  - 预发 e2e 仅在本地填入预发凭据时运行,默认 skip,对 CI 无影响、不携带凭据入库。

  ## Spec
-
契约依据:`ocb-public/src/bcs/docs/custom-collaboration-opening-message-integration-guide.md`
§4(`opening_message.params` = 传给业务组件的 JSON object)。
## Problem

BCN conversion logging lived in a router-level wrapper even though
`DefaultStreamConverter` owns the event-type and payload-shape
knowledge. This duplicated conversion-aware payload handling and logged
complete high-frequency chat and thinking bodies, increasing log volume.

## Solution

- Move opt-in input/output logging into `DefaultStreamConverter`, with
logging disabled by default.
- Register a BCN-specific converter configuration at the composition
root and have BCN downlink select it, while OpenAPI and Gateway keep the
silent default.
- Keep one log record for every conversion, including `output=null` and
conversion failures.
- Limit only chat and thinking body fields to a 10-character preview
with an original-length marker.
- Preserve complete tool, interaction, lifecycle, identifier, and
structured metadata fields.
- Remove the router logging wrapper and isolate log serialization or
backend failures from the conversion stream.

## Validation

Local verification was not rerun after the code-only history rewrite, as
requested. Before the rewrite, the unchanged code tree was validated
with:

- Focused BaaS unit and architecture suites: 150 tests passed, with one
existing non-blocking repository-wide architecture warning.
- Ruff formatting and lint checks for all changed Python files: passed.
- `git diff --check`: passed.

GitHub CI will provide final branch validation.

## Compatibility and risk

- No BCN wire contract, SSE sequencing, or delivered payload behavior
changes.
- OpenAPI and Gateway conversion logging remains disabled.
- Only log projections are shortened; delivered `StreamChunk` and
`SseEvent` data remains complete.
- The conversion log format remains `input=<json> output=<json|null>`,
while chat and thinking text over 10 characters now contains a preview
marker.
- Rollback is limited to restoring the router wrapper and default
converter selection.
Rel20260826 task qz bugfix
## Problem

Catalog Search currently replaces the BCS `/bots/search` total with the
number of Backend records that survive the exact `(bot_id, entity_id)`
join. When the BCS page contains records that are temporarily
unavailable in Backend, the public response no longer reflects the
authoritative BCS pagination total.

## Solution

- Preserve the validated non-negative integer `total` from the BCS
catalog page in a transport-neutral page result.
- Keep the existing exact composite-address join, BCS metadata
projection, sensitive-field sanitization, and response item ordering
unchanged.
- Return the BCS total unchanged from Catalog Search; Backend no longer
recomputes it from joined items.
- Fail closed when the BCS total is missing or malformed, and add
regression coverage for totals larger than the joined item count.

## Validation

- `DEPLOY_PROFILE=test uv run pytest
tests/community/adapters/http/openapi_v1/bot_public/test_bot_public_router.py
tests/community/core/bot_public/test_bot_catalog_metadata_service.py
tests/community/core/bot_public/test_bot_public_service.py -q` — 180
passed.
- `DEPLOY_PROFILE=test uv run pytest
tests/community/architecture/test_repository_contracts.py
tests/community/architecture/test_http_adapter_layer_is_http_only.py
tests/community/architecture/test_no_fastapi_in_core.py -q` — 15 passed.
- Targeted `ruff check` — passed.
- Gateway OpenAPI schema JSON parse — passed.
- `git diff --check` against `REL20260826` — passed.

## Compatibility and risk

The public route, query parameters, BCS path, exact join key, item
projection, and error mapping remain unchanged. The only response
behavior change is that `data.total` now follows BCS rather than the
post-join Backend item count.

## Spec

`spec/pipeline/catalog-search-bcs-total-rel20260826/001-spec-output.md`
(local task artifact; intentionally not committed).
…piBotAdapter (#1504)

PR desc

  ## Problem
预发/线上 `agentclaw-*` / `secbaas-*` 这类 ACE 网关后的真实 host
上,`OpenApiBotAdapter` 的 `send_message` / `get_run` / `ensure_grant` GET
**只发 `Authorization:
  Bearer`**,既不随请求带登录 Cookie/Referer,也不校验响应业务 `code`。后果:

- Bearer-only 被 ACE 当未登录 → 回 **HTTP 200 的 `USER_NOT_LOGIN` 登录门**(body 无
`data.message_id`);
  - adapter 只看 HTTP 状态、不校验 `code`,把“无 `message_id`”当成功 → `run_id=None`;
- `send_and_wait` 接着 `get_run(None)` → 误导性 `404 "Message not found:
None"`,真实根因(ACE 登录门)被掩盖。

对照工具 `send_bot_message.py`(带 Cookie + Referer)同入参可成功拿到
`message_id`,印证问题在 adapter 这一侧。

  ## Solution
  `open_api_bot_adapter.py`:
  1. 新增 `_headers()`:返回 `Bearer + 可选 Cookie/Referer`(key 有才加)。
2. `ensure_grant` GET / `send_message` / `get_run` 三处请求头由 Bearer-only 改为
`self._headers()`(`grant` POST 本就带 Cookie/Referer,未动)。
3. `send_message` 业务信封校验:HTTP 200 但 `code!=0` 或无 `message_id` → 直接
`raise OpenApiError(带 code/payload)`,不再吞成 `run_id=None`。
  4. `get_run`:`code!=0` → `raise OpenApiError`。
5. 给 `ensure_grant` / `send_and_wait` / `send_and_wait_async` 入口加 3 行
debug `logger.info`,便于联调定位。

行为:`CorpApiKeyProvider` / singlebox 的 cookie/referer 为空 → 不加头、行为不变(prod
走 service-to-service、不经 ACE);带 cookie 的调用方(预发 e2e/联调)→ 过 ACE → 拿到真
  `message_id`。

  ## Validation
- 单测 `test_open_api_bot_adapter.py`:新增 5 条(cookie/referer 透传 3 + 业务信封校验
2),TDD 红→绿;整文件 **17 passed**。
- 集成目录 `task_runner/integration`:**113 passed, 5 skipped**(预发 e2e 等
skip-by-default),无回归。
  - SAST(block 规则集,源码 + 测试)exit 0。
- A/B 印证:同 key/url/bot/cookie 下 `send_bot_message.py` 成功拿到
`message_id`(改前 adapter 失败;改后 adapter 透传 cookie → 对齐工具)。
  - pre-push gate(lint-only SAST)passed。

  ## Compatibility and risk
  - 改动为**加法**:cookie/referer 非空才加头,空则行为不变,不影响 prod/singlebox。
- 业务 `code` 校验是新行为:此前 HTTP 200 + `code!=0` 会被当成功(返 `run_id=None`),现会抛
`OpenApiError`;旧行为本身即 bug,无合理依赖方。
- ⚠ `ensure_grant` 入口的 `logger.info("[task][openapi_bot] ensure_grant
bot_id=%s")` 缺格式化参数(`%s` 无对应 arg);若该 logger 在 INFO 级启用,`"...%s" % ()` 会抛
`TypeError` 使 `ensure_grant` 入口崩溃。建议补成 `..., bot_id)` 或删除该行(另两行
`send_and_wait`/`send_and_wait_async` 的 log 已带参数,不受影响)。

  ## Related issues
- 预发 e2e `test_open_api_bot_adapter_pre_e2e.py` 此前报 `404 "Message not
found: None"`,根因即本题;改后 adapter 透传 cookie/Referer 过 ACE,可跑通。

---------

Co-authored-by: Claude <noreply@anthropic.com>
The shared-scope list path enforced collaborator checks on reads, which
broke group-chat rendering: members who added a coding bot as a friend
are neither the owner nor collaborators, so GET /api/bot-render-screens
answered 403 and panels failed with component-library-missing errors.

CDN mappings (library name -> CDN URL) are non-sensitive render
resources. Restore the original read-open semantics: list returns all
records of a shared bot without identity checks, while create, update
and delete keep the strict owner-or-collaborator authorization.

- service: drop the collaborator gate from the shared-scope list branch
- router: stop calling authorize_render_screen_bot on list
- tests: flip the non-collaborator list case to assert read access,
  and assert the router no longer authorizes reads

Validation: uv run pytest on render_screen scope/service/router/handler
suites - 56 passed.
1. 完善 MCP、MCP 市场、本地注册相关接口和 Schema;
  2. 更新 OpenAPI 文档及相关测试;
  3. 创建团队空间时校验:
      - 当前环境;
      - 创建人;
      - 归一化后的空间名称;

  4. 同一用户不能重复创建同名团队空间,返回 409;
  5. 不同用户仍允许创建同名空间;
  6. 补充空间和 MCP 测试。
…rant model) (#1514)

## Problem
`OpenApiBotAdapter.ensure_grant` 之前**默认总会先 GET
`/api/v1/api-keys/{prefix}/allowed-bots`**(判断 bot 是否已授权 → 是否跳过 grant)。但该
admin 端点**只认
Human(登录)Cookie**(`create_api_key.py` 的 create/grant/list 都走
Cookie+Referer、不带 Bearer),而 corp/pre/prod 部署里 `OpenApiBotAdapter` 的
cookie/referer 为空(`CorpApiKeyProvider`
  默认留空,prod 假定 service-to-service + OOB 预授权)。于是:

- `ensure_grant` 的 GET 用 **Bearer-only** 打一个只认 Human Cookie 的 admin 端点 →
BaaS 拿不到登录用户上下文 → **500 `INTERNAL_ERROR`**(线上实测:`<<< ensure_grant GET
allowed-bots status=500
  INTERNAL_ERROR`,一路上抛到 `on_execute` 500)。
- 即便 bot 已被外部 OOB grant 过也没用:`ensure_grant` 总在“判断是否已授权→跳过 grant”**之前**就
GET,GET 自己崩了,根本到不了 skip 分支。

这与 corp 一直声称的“api_key 已 OOB 预授权 bot,ensure_grant 是 no-op”假设矛盾——该假设依赖 GET
能跑通,而 GET 在无 cookie 的 prod/corp 上跑不通 → 真实 BaaS 上单 bot 派发直接断(prod 之前可能没真跑
  single_bot 才没暴露)。

  ## Solution
  `open_api_bot_adapter.py`:
  1. `__init__` 新增 `ensure_grant: bool = False`(默认**跳过**)。
2. `ensure_grant` 顶部:`if not self._ensure_grant: 日志 + return` —— 默认不发
allowed-bots GET、不 grant,直进 `send_message`(dispatch 端点
`/openapi/v1/messages` 认 Bearer,正常)。
3. 顺带修 `ensure_grant` 入口 `logger.info("...bot_id=%s")` **缺 `bot_id`
参数**的 bug(opt-in 模式否则 `TypeError`)。

  测试 `test_open_api_bot_adapter.py`:
- 新增 `test_ensure_grant_skipped_by_default`:默认构造 → ensure_grant → 断言不发
allowed-bots GET。
- `_adapter` helper + prefix 回落用例显式 `ensure_grant=True`(保留对
GET/grant/prefix/cookie 透传的覆盖)。

行为:corp/prod/pre(`CorpTaskIntegrationModule` 构造
`OpenApiBotAdapter(keys)` 不传 flag)→ ensure_grant 跳过、直接派发;需要自查/grant
的(单测/联调)显式 `ensure_grant=True`。bot 由外部 OOB
  预授权(prod 既定模型)。

  ## Validation
- TDD 红→绿:`test_ensure_grant_skipped_by_default` 先 RED(默认原先会 GET)、改源码后
GREEN。
- `test_open_api_bot_adapter.py`:**18 passed**(17 既有 + 1 新增),无回归(opt-in
用例继续覆盖 GET/grant/cookie 透传/业务 code 校验)。
  - 集成目录 `task_runner/integration`:**114 passed, 5 skipped**,无回归。
  - SAST(block 规则集,源码 + 测试)exit 0。
- 线上实证:改前 `ensure_grant GET allowed-bots status=500 INTERNAL_ERROR` →
`on_execute` 500;改后默认跳过该 GET、直进 send_message,500 消失。

  ## Compatibility and risk
- **默认行为变更**:`ensure_grant` 由“默认执行 GET/grant”改为“默认跳过”。prod 假定 bot 已 OOB
预授权 → 跳过是正确姿势;**前提是部署侧确实已对目标 bot 做 OOB grant**(`create_api_key.py
--action grant`
或 BaaS 控制台)。若某部署未预授权、仍想让 adapter 自查/grant,需显式 `ensure_grant=True` 且配
Human Cookie(admin 端点要 Cookie)。
  - 不影响 singlebox(用 `SingleboxEngineAdapter`,非本 adapter)。
- 不影响 dispatch 端点(`/openapi/v1/messages`、`get_run`)——它们认 Bearer,与
ensure_grant 无关。

  ## Related issues
- 直接起因:线上 `OpenApiBotAdapter.ensure_grant` GET
`/api/v1/api-keys/{prefix}/allowed-bots` 返回 500
`INTERNAL_ERROR`(Bearer-only 打只认 Human Cookie 的 admin 端点),即便 bot 已 OOB
grant
  也复现(卡在 GET,到不了 skip 分支)。
- 关联已合入的 `fix(task): forward cookie/referer and validate business code
in OpenApiBotAdapter`:那条解决 dispatch 端点的 ACE/信封问题;本条解决 admin
`ensure_grant` 在无 cookie 部署下的
  500(改为默认不调)。
…1536)

## Problem

REL20260826 基线上的 task_discovery 模块缺少 dev 分支已落地的多项能力,会阻碍后续基于 REL20260826
的发布线引入这些改进:

- 多机 cron 并发时缺少 per-bot 分布式锁,导致同一 bot 可能被多台机器重复发现
- 缺少工单 NOTICE 通道,发现通知仅依赖外发卡片,单通道有漏达风险
- `DiscoveredTask` 字段未对齐执行层 `TaskSpec`
语义(`project_name`/`description`/`business_scenario`/`work_item_url` vs
`title`/`instruction`/`background`/`objective`/`acceptances`),下游确认链路需要二次映射
- 缺少运行时 cron reschedule 与前端 URL 动态注入能力,调整触发时间或前端地址需重启 backend
- 缺少 `/discovery/reschedule`、`/discovery/dingtalk-config` 两个 HTTP 端点

需要在 REL20260826 之上新建 `REL20260826_lz` 分支,将上述 task_discovery
相关变更以最小代价同步过来。

## Solution

基于 `origin/REL20260826` 创建本地分支 `REL20260826_lz`,使用 `git checkout
origin/dev -- <file>` 将 dev 上与 task_discovery
相关的源码/测试/文档一次性搬到该分支,整合为一个干净的 commit。

**同步范围(与 dev 完全一致)**:
1. task_discovery
核心:`discovery_service.py`、`lock_models.py`(新)、`models.py`、`protocols.py`、`scheduler.py`、`session_creator.py`、`session_initiator.py`、`task_reader.py`
2. DI 模块:`task_discovery_module.py`、`infrastructure/community/notify.py`
3. Repository
协议/实现:`protocols/task.py`、`implementations/task/discovery_lock.py`(新)
4. HTTP Router:`adapters/http/task/router.py`(新增 `/discovery/reschedule`
与 `/discovery/dingtalk-config`)
5. 通知插件:`plugins/community/notify_sender.py`(新增
`DingTalkCredentialHolder` + `DingTalkNotifySender`)
6. 测试:5 个测试文件,含新增的 `test_task_discovery_coverage.py` 与
`test_task_discovery_lock_repository.py`
7. Spec
文档:`specs/2026-08-25-task-discovery-taskspec-align-and-notify-channels/{design,spec,tasks}.md`

**有意不同步的内容**(避免引入无关重构):
- `core/work_orders/models.py`:仅手工添加 `TASK_DISCOVERED` 枚举值 +
`EVENT_CATEGORIES` 条目;dev 中把 `SPACE_MEMBER_REMOVED` 移到末尾的纯位置重排未同步(无功能差异)
- `core/repository/README.md`:仅添加 `TaskDiscoveryLockRepositoryProtocol`
/ `TaskDiscoveryLockRepository` 两行清单;dev 中 skill_center 重构相关条目未同步(与
task_discovery 无关)

**拒绝的备选方案**:
- 直接 merge dev:会带入 dev 比 REL20260826 多出的全部其他模块改动,超出"只同步
task_discovery"的范围
- cherry-pick dev 上的 commit:task_discovery 改动散落在多次 commit,与 skill_center
等其他重构交织,无法干净隔离

## Validation

`src/backend/` 下用 `uv run pytest` 跑相关测试:

| 测试文件 | 结果 |
| --- | --- |
| `tests/community/core/task/test_task_discovery_unit.py` | 23 passed |
| `tests/community/core/task/test_task_discovery_coverage.py` | 27
passed |
|
`tests/community/repository/task/test_task_discovery_lock_repository.py`
| 11 passed |
| `tests/community/endpoints/test_task_discovery_router.py` | 3 passed |
| `tests/community/core/task/singlebox_e2e/test_task_discovery_e2e.py` |
2 skipped(需要 singlebox 全栈基础设施) |
| **合计** | **64 passed, 2 skipped** |

**Diff 一致性验证**(`git diff origin/dev -- <path>`):
- task_discovery 核心、DI、Repository、Router、插件、Spec、测试 — 与 dev 完全一致(diff
为空)
- `work_orders/models.py` 与 dev 仅有 `SPACE_MEMBER_REMOVED`
位置差异,task_discovery 相关条目已对齐
- `repository/README.md` 与 dev 仅有 skill_center 相关条目差异,task_discovery
条目已对齐

**未能运行的检查**:
- e2e 测试依赖 baas / mosn / sofa-registry,本地环境不具备(按 `pytest.ini` 注释需
`RUN_ACCEPTANCE=1` + CI 容器)
- `docs/arch/ci.enforce.md` 描述的完整 CI 门禁(依赖边界检查 / 配置 schema 校验 / 协议契约测试 /
红旗检测)依赖 CI 执行,本地未跑全

## Compatibility and risk

- **`DiscoveredTask`
字段重命名**:`project_name`/`description`/`business_scenario`/`work_item_url`
→
`title`/`instruction`/`background`(+`objective`+`acceptances`)。`task_reader.py`
通过 `DROP+CREATE` 重建 `discovered_tasks` 表,**旧库数据会被清空**;该表为 task_discovery
独立 SQLite,不影响其他持久化表
- **新增数据库表 `ac_task_discovery_lock`**:UNIQUE 约束 `(env, bot_id,
discovery_date)` 即锁本体。需确认 REL20260826 部署环境上 `Base.metadata.create_all`
能正确建表,或已有对应 DDL 迁移
- **新增两个 HTTP 端点**:`POST
/api/v1/collaboration/tasks/discovery/reschedule`、`POST
/api/v1/collaboration/tasks/discovery/dingtalk-config`。无破坏性,鉴权沿用现有 task
router
- **回滚路径**:直接 revert 该 commit;无外部不可逆状态需要清理(除已落地的 lock 表行外,按日 TTL 自动失效)

## Spec

-
`src/backend/specs/2026-08-25-task-discovery-taskspec-align-and-notify-channels/design.md`
— 技术设计:DiscoveredTask 对齐 TaskSpec + 发现流程双通知通道
-
`src/backend/specs/2026-08-25-task-discovery-taskspec-align-and-notify-channels/spec.md`
— 规范定义
-
`src/backend/specs/2026-08-25-task-discovery-taskspec-align-and-notify-channels/tasks.md`
— 任务跟踪

## Related issues (optional)

无显式 issue 关联。本 PR 即把 `origin/dev` 上已落地的任务发现能力回填到 REL20260826 基线,便于发布线集成。
… in ORM truth and test anchors

- TtlRenewalScheduleModel.__tablename__ + module docstring aligned to production DDL name (D-01)
- contract TABLE constant + docstring renamed; has_table(TABLE) anchor untouched
- 25 compiled-SQL assertions in repository unit tests renamed (D-02: strings only)
… retarget discovery-scan ttl projections

- ArcaCreationResult.ttl_expiration_time restored to str | None (fixed +08:00
  '%Y-%m-%d %H:%M:%S'), new ttl_expiration_timestamp: int | None (ms epoch)
- time_utils gains CST=ZoneInfo('Asia/Shanghai') and format_ttl_expiration_time
- arca_paas_service emits both fields (numeric guard unchanged); facade passes
  the pair through verbatim
- wrapper reads ttl_expiration_timestamp (ms) with never-raise posture intact
- find_unregistered both ttl projections retargeted to $.ttl_expiration_timestamp
  (CR-GAP-01), label 'ttl' unchanged; compiled-SQL assertions pinned (mysql +
  sqlite bare json_extract)
…anghai

- time_utils: rename naive_utc_now/naive_utc_fromtimestamp to
  naive_cst_now/naive_cst_fromtimestamp with fixed Asia/Shanghai
  (+08:00, no DST) semantics; format_ttl_expiration_time delegates to
  naive_cst_fromtimestamp; module/function docstrings rewritten to the
  single-clock-domain CR-01 invariant
- _deadline_renewal_task.py: 8 call sites + import block + CR-01 comment
  switched to naive_cst_* (arithmetic untouched)
- _device_service_arca_ttl.py: register-path expiration conversion
  switched to naive_cst_fromtimestamp
- tests: UTC expectation anchors in the scheduler and wrapper suites
  moved to fixed Asia/Shanghai; new tests/unit/core/utils/
  test_time_utils.py pins all time_utils helpers (host-tz independent)
- time_utils.py line-wrap fix
- test_device_service_arca_ttl.py spacing fixes
- test_time_utils.py line-wrap fix

Post-merge gate fix: the pre-merge tree passed targeted unit tests but
failed tests/architecture/test_ruff_lint_rules.py::test_ruff_formatting_passes.
…gistered reader

Regression gate found two TestFindUnregistered cases pinning the OLD reader
contract (fixtures seeded only the ttl_expiration_time string key). Under the
approved CR-GAP-01 retarget both projections now read $.ttl_expiration_timestamp:
- _seed_hot_device gains a provider_device_props override parameter
  (mirroring _seed_hot_binding's device_props style)
- found-row fixtures seed the dual-key scanner-format props
- expected labels become the millisecond values (sqlite json_extract
  returns JSON integers as int)

User-approved scope expansion (2026-08-25); file was outside the plan's
declared file set.
…R-01/IN-01)

- list_due_for_renewal docstring now states the naive_cst_now (+08:00)
  caller contract instead of the pre-refactor naive-UTC wording
- naive_cst_fromtimestamp docstring drops the misleading '(or millis)'
  and states the divide-by-1000 requirement for millisecond epochs
…L pair chain

WR-02 (code review): the creation extraction guard accepted 0 and the
wrapper 'is None' check passed it through, so ttl_expiration_timestamp=0
became an epoch-0 (1970) renewal anchor while _renew_one treats 0 as
missing - a due -> failure -> STOPPED oscillation loop. The creation
chain now accepts only positive numbers (0 / negatives are not valid
deadlines) and the wrapper treats falsy values as missing (skip
register, warn, defer to discovery scan), matching _renew_one.

WR-03 (code review): the creation chain silently dropped numeric-string
ttl_timestamp values while the same pipeline's consumers (_renew_one,
_get_info_sync) coerce them via int(float()). The creation chain now
mirrors that coercion with an explicit TypeError/ValueError/OverflowError
guard; non-numeric strings fall back to both fields None.

Both are within the plan's declared file set; tests added for
zero / numeric-string / non-numeric-string extraction cases and the
wrapper zero-skip case.
panqiwp and others added 17 commits August 27, 2026 18:52
Add rsync excludes for workspace/.repos and
workspace/.prewarm/ready.json.
Problem
OCB corp-side DI modules reference baas_config.eval_template_uuid, but
the community BaasConfig dataclass lacks this field, causing
AttributeError: 'BaasConfig' object has no attribute
'eval_template_uuid' at runtime.
This field is required for the eval environment scenarios: when creating
persistent eval sandboxes (Scene 2) and ephemeral eval containers (Scene
3), the eval-specific ARCA template UUID must be passed to the BaaS
publish API. BaasConfig already has template_uuid (production) and
teclaw_template_uuid (TeClaw), but is missing the eval template field,
preventing corp-side code from routing to the correct eval template.
## Solution
Add eval_template_uuid: str = "" to the BaasConfig dataclass, positioned
between teclaw_template_uuid and personal_bot_template_uuid. Sync
ConfigModule.baas() provider to read eval_template_uuid from the YAML
config
block.
- Default value is an empty string (""), consistent with existing
teclaw_template_uuid / personal_bot_template_uuid.
- Non-breaking: pure field addition with a safe default. Community
deployments that don't configure this field get an empty string; corp
overlays inject the actual value.
- Rejected alternative: subclassing BaasConfig on the corp side —
violates Microkernel Architecture Rule 14 (all DI wiring must be
configuration-driven, no if is_local_mode() branches). Corp modules
(SingleboxDefaultEnvBotModule, CorpDefaultEnvBotModule) already
reference baas_config.eval_template_uuid directly, so the community
class must expose the attribute.
## Validation
- OCB singlebox end-to-end: after updating ocb-public pointer, POST
/api/service-bot/default-env/create no longer throws AttributeError;
switch validation and business logic return correctly.
- OCB corp DI unit tests: test_default_env_bot_di.py — 16/16 passed.
- Community CI: unaffected — pure field addition with default value.
## Compatibility and risk
- Backward-compatible: new field has a default value; existing configs
and deployments are unaffected.
- Config migration: corp deployments that need the eval environment must
add eval_template_uuid under user_config.baas (no change if already
present).
- Rollback: remove the config entry; the field defaults to empty,
causing eval template routing to fail gracefully rather than crash.
## Related issues (optional)
Required by OCB eval environment infrastructure (Scene 2: persistent
eval sandbox; Scene 3: ephemeral eval container).

---------

Co-authored-by: jiumu.zd <jiumu.zd@alipay.com>
Co-authored-by: Claude (GLM-5.1) <noreply@anthropic.com>
为不经过 Gateway 的普通 HTTP 调用链增加 JWT 身份认证能力,同时复用系统已有的 Principal 验签逻辑,不重新实现
JWT 安全机制。

  调用方
    ↓ 携带 X-Avernet-Principal JWT
  Backend 普通 HTTP 接口
    ↓
  复用 resolve_caller()
    ↓
  复用 verify_principal_token()
    ↓
  校验签名、有效期、issuer、audience 和 Principal 结构
    ↓
  获取 VerifiedCaller.user

---------

Co-authored-by: Weijia Sun <freddiesun95@gmail.com>
Co-authored-by: tianxun <tianxun.wr@antgroup.com>
Co-authored-by: Dylan <48973507+yhzhang35@users.noreply.github.com>
Co-authored-by: VINCE <12515529+vzvince@users.noreply.github.com>
Co-authored-by: lucas <rongzhi.xzp@antgroup.com>
Co-authored-by: jiangj0627 <jian.jiangj@antgroup.com>
Co-authored-by: sj_mei <shangjian.msj@antfin.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: WEN <luzhi.zw@antgroup.com>
Co-authored-by: pf_miles <yue.weny@antgroup.com>
Co-authored-by: pf_miles <miles.wy.1@gmail.com>
## Summary
- refresh AICoding MCP scope/details on restart when
`confirmed_template_update` is set
- actively resync runtime skill symlinks after confirmed template
restart
- keep restart refresh logic inside AICoding strategy and avoid CLI
grant changes
- inject MCP sync into `BotService` from the DI composition root instead
of exposing it through `DeviceService`

## Compatibility and risk
- Scope is gated by `extra_configs["confirmed_template_update"]`; plain
restart keeps existing behavior and does not resync MCP/skills.
- Affected call sites are the restart paths that can reach a ready
device:
- replacement restart: `_allocate_device_async` calls
`refresh_restart_authorization` after `apply_device` succeeds, so the
refresh targets the new runtime instead of the old binding.
- BaaS in-place restart: `restart_bot` calls
`refresh_restart_authorization` after `_restart_bot_baas` succeeds.
- pre-allocation restart extension handling only applies config; runtime
refresh is intentionally deferred to the ready-device points above.
- `refresh_restart_authorization` return value means "engine opted into
async best-effort refresh"; it does not mean MCP/skill sync succeeded.
- The previous restart Passport scope rewrite is intentionally not
reused for this AICoding path: this change syncs latest MCP runtime
configuration and refreshes `~/.claude/skills` symlinks, without
modeling `mcporter` or `la ~/.claude/skills/` as CLI grants.
- `caller_identity_repo` remains in `BotService` construction for
release-branch DI/API compatibility, but is no longer used by this
restart-runtime refresh path.

## Tests
- `.venv/bin/python -m py_compile
src/agentclaw/community/core/bot_management/engines/aicoding/strategy.py
src/agentclaw/community/core/bot_management/engines/default.py
src/agentclaw/community/core/bot_management/engines/provisioning.py
src/agentclaw/community/core/bot_management/services/bot_service.py
src/agentclaw/community/core/devices/services/device_service.py
src/agentclaw/community/di/modules/bot_management_module.py
tests/community/core/bot_management/services/test_restart_authorization_refresh.py
tests/community/core/bot_management/services/test_bot_service_restart_idempotency.py`
- `.venv/bin/python -m pytest
tests/community/core/bot_management/services/test_restart_authorization_refresh.py
tests/community/core/bot_management/services/test_bot_service_restart_idempotency.py::TestRestartAuthorizationResyncWiring
tests/community/core/mcp/services/test_sync_service.py
tests/community/core/mcp/test_defaults_per_engine.py -q` (`97 passed, 17
warnings`)
## Problem

任务派发前缺少基于 claim_on 关系的候选过滤,可能将任务派发给无权限的 Bot。协作群任务也缺少稳定的 driver Bot
身份和回调鉴权信息,导致任务执行结果
  无法可靠回投和收敛。

  ## Solution

  - 新增 claim_on JOIN 灰度开关和无状态授权转发能力。
  - 搜推候选增加 claim 关系过滤,并保留未授权候选的诊断信息。
  - 协作群创建时补充 driver Bot token,明确 Bot 调用身份。
  - 支持按 group_kind 区分 chat、manager_worker 和 state_machine。
  - 完善协作群事件订阅和任务回调链路,支持任务执行状态与结果回投。
  - 增加 OpenAPI Bot 跨事件循环客户端处理,避免异步任务跨 loop 使用连接池。
  - 增加 task-discovery 的 OpenAPI 会话发起能力及相关 DI 接线。
  - 保留 stateless grant/revoke 能力,移除本地授权状态依赖。
  - 补充 claim JOIN、Bot token、OpenAPI 会话、状态机和任务发现相关测试。
  - 同步相关 Backend、BaaS 配置及 ACK Sandbox TTL 处理逻辑。

  ## Validation

  - Pre-push SAST/lint gate:通过。
  - 任务相关测试、OpenAPI Bot 适配器测试及 task-discovery 测试已补充。
  - 完整 Backend CI 已执行,当前分支后续需以最新 CI 结果为准。

  ## Compatibility and risk

  - 任务执行接口保持兼容,新增字段均通过可选配置或 DI 注入。
  - 未配置 claim JOIN 或 Bot token provider 时,保留降级路径,不阻断基础任务派发。
  - OpenAPI Bot 默认保持 OOB 预授权模式,不强制调用 grant 接口。
  - 协作群回调失败时保留 poller 兜底收敛机制。

---------

Co-authored-by: WEN6Lev57q4 <luzhi.zw@antgroup.com>
Co-authored-by: jian.jiangj <jian.jiangj@antgroup.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Cassius <langlang.cai@antgroup.com>
## Summary
- refresh AICoding restart MCP scope only after confirmed template
updates
- retry runtime MCP detail sync while BaaS devices are not active yet
- retry Claude skills symlink runtime sync in the same best-effort
background flow

## Test
- cd src/backend && .venv/bin/python -m pytest
tests/community/core/bot_management/services/test_restart_authorization_refresh.py
tests/community/core/bot_management/services/test_bot_service_restart_idempotency.py::TestRestartAuthorizationResyncWiring
-q
仅使用 skill_id 定位 Skill;
  - 用户身份从已验证的 Principal 中解析,不再接收 user_id Query 参数;
  - Git Skill 直接从公共 Skill 仓库读取;
  - Local Skill 根据 Skill 记录反查 Bot,并校验访问权限;
  - 返回统一 OpenAPI Envelope,data 内保留普通接口的 content 字段;

---------

Co-authored-by: Weijia Sun <freddiesun95@gmail.com>
Co-authored-by: tianxun <tianxun.wr@antgroup.com>
Co-authored-by: Dylan <48973507+yhzhang35@users.noreply.github.com>
Co-authored-by: VINCE <12515529+vzvince@users.noreply.github.com>
Co-authored-by: lucas <rongzhi.xzp@antgroup.com>
Co-authored-by: jiangj0627 <jian.jiangj@antgroup.com>
Co-authored-by: sj_mei <shangjian.msj@antfin.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: WEN <luzhi.zw@antgroup.com>
Co-authored-by: pf_miles <yue.weny@antgroup.com>
Co-authored-by: pf_miles <miles.wy.1@gmail.com>
## Problem

`McpSkillSetControlPlaneCommands.remove_mcp()` duplicated a partial
Default-SkillSet exclusion path even though
`SkillSetManagementService.remove_mcp()` already routes Default Sets to
`exclude_default_mcp()`. The duplicate branch did not validate
platform-default policy, retire stale Installation rows, or return the
changed MCP code for projection.

This is the follow-up cleanup requested in the review of #1608.

## Solution

- Keep Default/ordinary routing at the `SkillSetManagementService` seam,
where engine/template default policy is available.
- Make repository `remove_mcp()` ordinary-membership-only, matching
`add_mcp()`, by enforcing `_ordinary(row)` under the Set row lock.
- Remove the unreachable duplicate Default exclusion implementation and
its unused ORM import.
- Add a repository contract regression proving ordinary membership
removal rejects a Default Set address without creating an exclusion.

## Validation

- TDD red: focused regression failed with `DID NOT RAISE
SkillSetControlPlaneConflictError`.
- TDD green: focused regression passed (`1 passed`).
- Repository UoW + `SkillSetManagementService`: `150 passed`.
- Affected Skill Center core/repository suites: `1075 passed, 27
skipped`.
- Backend architecture suite: `201 passed`.
- Ruff lint on changed files: passed.
- Backend local SAST block scan against `github/REL20260828...HEAD`:
passed.
- Full Backend suite was not run locally because this is an isolated
repository refactor; GitHub CI remains the full-suite gate.
- Whole-file `ruff format --check` reports existing formatting drift in
both touched legacy files; they were not bulk-formatted to avoid an
unrelated large diff.

## Compatibility and risk

No HTTP, Service API, Plugin API, database schema, or runtime projection
behavior changes for supported callers. Default MCP opt-out continues
through `exclude_default_mcp()`; ordinary membership removal continues
through `remove_mcp()`. Direct repository misuse of `remove_mcp()` with
a Default Set now fails closed with `SYSTEM_DEFAULT_IMMUTABLE` instead
of executing the incomplete duplicate path.

## Related issues

Follow-up to #1608 and its Default MCP exclusion review comments.
## Summary
- Persist AICoding confirmed restart resync intent with template config
- Defer BaaS in-place restart MCP/CLI/skill runtime refresh until
restart publish completes
- Add AICoding BaaS publish listener and targeted tests

## Tests
- uv run python -m py_compile
src/agentclaw/community/core/bot_management/engines/aicoding/strategy.py
src/agentclaw/community/core/bot_management/engines/aicoding/restart_authorization_listener.py
src/agentclaw/community/core/bot_management/engines/provisioning.py
src/agentclaw/community/core/bot_management/engines/default.py
src/agentclaw/community/core/bot_management/services/bot_service.py
src/agentclaw/community/di/modules/bot_management_module.py
- uv run pytest
tests/community/core/bot_management/services/test_restart_authorization_refresh.py
tests/community/core/bot_management/services/test_restart_template_refresh.py
tests/community/core/bot_management/services/test_bot_service_restart_idempotency.py::TestRestartAuthorizationResyncWiring
-q
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.