docs(architecture): define product boundary contract

This commit is contained in:
rookiestar28
2026-04-24 00:04:44 +08:00
parent e84aacf295
commit ff0ddfd65b
8 changed files with 325 additions and 5 deletions
+8
View File
@@ -13,6 +13,13 @@ ComfyUI-OpenClaw is a **security-first orchestration layer** for ComfyUI that co
- **Now supports 8 major messaging platforms, including Discord, Telegram, WhatsApp, LINE, WeChat, KakaoTalk, Slack, and Feishu/Lark.**
- **And more exciting features being added continuously**
Supported product boundary:
- **Primary artifact**: ComfyUI custom node pack
- **First-class runtime identity**: embedded operator platform
- **Optional attached subsystem**: connector-capable control surface via the sidecar runtime
- **Decision record**: [ADR-0002 Product Boundary And Packaging Contract](docs/adr/ADR-0002-product-boundary-and-packaging-contract.md)
---
<br>
@@ -1076,6 +1083,7 @@ Primary references:
- [Security deployment guide](docs/security_deployment_guide.md)
- [Security checklist](docs/security_checklist.md)
- [Config surface ADR](docs/adr/ADR-0001-config-surface-unification.md)
- [Product boundary ADR](docs/adr/ADR-0002-product-boundary-and-packaging-contract.md)
- [Config and secrets contract](docs/release/config_secrets_contract.md)
- [Advanced registry and transforms](docs/advanced_registry_and_transforms.md)
- [Connector guide](docs/connector.md)
@@ -0,0 +1,96 @@
# ADR-0002: Product Boundary And Packaging Contract
- Status: Accepted
- Date: 2026-04-23
- Owners: OpenClaw maintainers
- Related roadmap items: `R160` with follow-up execution in `R161` and `R162`
## Context
OpenClaw started as a ComfyUI-focused node pack, but the repository now also contains:
- embedded HTTP APIs and operator UI surfaces,
- a standalone Remote Admin Console route,
- connector runtime code for multiple chat platforms,
- split control-plane governance for higher-risk deployments.
That evolution made one question increasingly ambiguous: what is the supported identity of this repo/package today, and which parts are first-class versus optional attached subsystems?
Without a boundary decision, future work such as config decomposition, connector extraction, or packaging hygiene is forced to rely on repo intuition instead of an explicit contract.
## Decision
OpenClaw is defined as a **ComfyUI custom node pack** first, with two explicit first-class identities layered on top of that package:
1. **ComfyUI custom node pack**
- This is the primary distribution artifact and runtime anchor.
- `__init__.py` remains the package entrypoint loaded from `custom_nodes/`.
2. **embedded operator platform**
- In-process OpenClaw APIs, runtime/security governance, sidebar UX, and remote admin surfaces are treated as part of the shipped package, not as separate products.
3. **connector-capable control surface**
- Remote chat control is supported through the in-repo connector sidecar, but the connector remains an **optional attached subsystem**, not the primary package artifact.
## Core vs Attached Subsystems
Core to the package:
- custom node pack entrypoint and exported nodes
- embedded API/runtime governance (`/openclaw/*`, route bootstrap, control-plane policy)
- embedded operator UI surfaces (sidebar plus `/openclaw/admin`)
Optional attached subsystem:
- connector sidecar (`python -m connector`) and platform-specific adapters
This means:
- the connector is supported and intentionally in-repo,
- but the repo is **not** currently defined as a connector-first distribution,
- and the repo is **not** currently defined as a standalone generic backend independent of ComfyUI.
## Supported Topologies
Supported:
1. **embedded local/lan**
- OpenClaw runs inside the ComfyUI process as the primary package artifact.
2. **embedded package with split high-risk control plane**
- The same package stays primary, while higher-risk control surfaces are externalized according to the split-mode contract.
3. **embedded package plus optional connector sidecar**
- The connector runs as a companion process that calls the local OpenClaw APIs.
Unsupported as first-class package identities today:
1. **connector-only distribution**
2. **standalone non-ComfyUI backend package**
Those possibilities are future design questions, not current promises. Connector extraction feasibility remains explicitly deferred to `R162`.
## Consequences
Positive:
- future extraction/pruning decisions now have one explicit contract to evaluate against
- docs and contributor discussions can use the same terms instead of mixing "node pack", "server", and "sidecar" loosely
- `R161` and `R162` can narrow their scope around a known boundary instead of re-litigating product identity
Trade-offs:
- the repo still carries multiple execution surfaces inside one codebase
- connector remains intentionally attached even though it is operationally separable
- some public docs must stay careful not to imply standalone server packaging that does not exist yet
## Rejected Alternatives
1. Treat the connector as an equal primary package artifact today
- Rejected because there is no separate connector package/distribution contract yet.
2. Define OpenClaw as a generic standalone backend package
- Rejected because current runtime ownership still assumes a ComfyUI host process.
3. Keep the boundary implicit and rely on contributor convention
- Rejected because packaging and extraction follow-ups now depend on an explicit contract.
+2
View File
@@ -2,6 +2,8 @@
The **OpenClaw Connector** (`connector`) is a standalone process that allows you to control your local ComfyUI instance remotely via chat platforms like **Telegram**, **Discord**, **LINE**, **WhatsApp**, **WeChat Official Account**, **KakaoTalk (Kakao i Open Builder)**, **Slack**, and **Feishu/Lark**.
Per the product boundary contract, the connector is an **optional attached subsystem**. The primary published artifact of this repo remains the **ComfyUI custom node pack**, and the connector augments that package rather than replacing it.
## How It Works
The connector runs alongside ComfyUI on your machine.
+1
View File
@@ -15,4 +15,5 @@
## Status Note
- Bridge APIs and connector runtime are available.
- Connector/sidecar runtime remains an optional attached subsystem; the primary package artifact is the ComfyUI custom node pack.
- Standalone sidecar/gateway evolution is tracked in `.planning/roadmap.md`.
-1
View File
@@ -95,4 +95,3 @@ def get_node_portability_mappings() -> Dict[str, Dict[str, Any]]:
NODE_PORTABILITY_MAPPINGS = get_node_portability_mappings()
+137
View File
@@ -0,0 +1,137 @@
"""
Machine-readable product/package boundary contract for OpenClaw.
Keep this module dependency-light so contract tests and future packaging work can
read one stable source of truth without importing heavy runtime surfaces.
"""
from __future__ import annotations
import copy
from typing import Any, Dict
PRODUCT_BOUNDARY_CONTRACT_VERSION = 1
_PRODUCT_BOUNDARY_CONTRACT: Dict[str, Any] = {
"version": PRODUCT_BOUNDARY_CONTRACT_VERSION,
"package_name": "comfyui-openclaw",
"primary_distribution": {
"id": "comfyui_custom_node_pack",
"label": "ComfyUI custom node pack",
"summary": (
"The published package artifact is a ComfyUI custom node pack loaded "
"from custom_nodes/ and anchored by __init__.py."
),
},
"supported_identities": [
{
"id": "comfyui_node_pack",
"label": "ComfyUI custom node pack",
"summary": "Primary distribution and entrypoint ownership surface.",
},
{
"id": "embedded_operator_platform",
"label": "embedded operator platform",
"summary": (
"In-process API, security/runtime governance, embedded sidebar, "
"and remote admin surfaces that run alongside ComfyUI."
),
},
{
"id": "connector_capable_control_surface",
"label": "connector-capable control surface",
"summary": (
"The repo supports remote chat control through an optional "
"connector sidecar, but the connector is not the primary package "
"artifact."
),
},
],
"core_subsystems": [
{
"id": "node_pack_entrypoint",
"label": "custom node pack entrypoint",
"entrypoints": ["__init__.py", "nodes", "web"],
},
{
"id": "embedded_api_and_runtime",
"label": "embedded API and runtime governance",
"entrypoints": [
"api/routes.py",
"services/route_bootstrap.py",
"services/control_plane.py",
],
},
{
"id": "embedded_operator_ui",
"label": "embedded operator UI surfaces",
"entrypoints": [
"web/openclaw.js",
"web/openclaw_ui.js",
"api/remote_admin.py",
],
},
],
"attached_subsystems": [
{
"id": "connector_sidecar",
"label": "connector sidecar",
"classification": "optional_attached_subsystem",
"entrypoints": ["connector/__main__.py", "docs/connector.md"],
"summary": (
"Standalone chat-platform bridge that remains in-repo but is not "
"the primary published package boundary."
),
}
],
"supported_topologies": [
{
"id": "embedded_local",
"label": "embedded local/lan",
"summary": (
"OpenClaw runs in the ComfyUI process as the primary node-pack "
"artifact with in-process operator surfaces."
),
},
{
"id": "embedded_split_control_plane",
"label": "embedded package with split high-risk control plane",
"summary": (
"The same node-pack artifact remains primary, while high-risk "
"control surfaces are externalized according to the control-plane "
"contract."
),
},
{
"id": "embedded_with_connector_sidecar",
"label": "embedded package plus optional connector sidecar",
"summary": (
"The connector runs as a companion process that calls the local "
"OpenClaw APIs; it augments the package but does not replace it."
),
},
],
"unsupported_topologies": [
{
"id": "connector_only_distribution",
"label": "connector-only distribution",
"summary": (
"This repo does not currently define the connector as a standalone "
"published package artifact."
),
},
{
"id": "standalone_non_comfyui_backend",
"label": "standalone non-ComfyUI backend",
"summary": (
"This repo does not currently publish OpenClaw as a generic "
"server package that runs without the ComfyUI host."
),
},
],
}
def get_product_boundary_contract() -> Dict[str, Any]:
return copy.deepcopy(_PRODUCT_BOUNDARY_CONTRACT)
+2 -4
View File
@@ -73,9 +73,8 @@ def analyze_workflow_portability(workflow: Dict[str, Any]) -> Dict[str, Any]:
total_nodes = len(entries)
portable_mode_required = total_nodes > 0
portable_mode_supported = (
portable_mode_required
and all(entry["portable_mode"] != "unsupported" for entry in entries)
portable_mode_supported = portable_mode_required and all(
entry["portable_mode"] != "unsupported" for entry in entries
)
return {
@@ -116,4 +115,3 @@ def _dedupe_preserve_order(items: Iterable[str]) -> list[str]:
seen.add(item)
result.append(item)
return result
@@ -0,0 +1,79 @@
import tomllib
import unittest
from pathlib import Path
from services.product_boundary import get_product_boundary_contract
ROOT = Path(__file__).resolve().parents[1]
ADR_PATH = ROOT / "docs" / "adr" / "ADR-0002-product-boundary-and-packaging-contract.md"
README_PATH = ROOT / "README.md"
CONNECTOR_DOC_PATH = ROOT / "docs" / "connector.md"
class TestR160ProductBoundaryContract(unittest.TestCase):
def test_contract_terms_and_topologies_are_stable(self):
contract = get_product_boundary_contract()
self.assertEqual(contract["package_name"], "comfyui-openclaw")
self.assertEqual(
contract["primary_distribution"]["id"],
"comfyui_custom_node_pack",
)
self.assertEqual(
[item["label"] for item in contract["supported_identities"]],
[
"ComfyUI custom node pack",
"embedded operator platform",
"connector-capable control surface",
],
)
self.assertEqual(
[item["id"] for item in contract["supported_topologies"]],
[
"embedded_local",
"embedded_split_control_plane",
"embedded_with_connector_sidecar",
],
)
self.assertEqual(
[item["id"] for item in contract["attached_subsystems"]],
["connector_sidecar"],
)
def test_contract_entrypoints_exist_in_repo(self):
contract = get_product_boundary_contract()
for section in ("core_subsystems", "attached_subsystems"):
for subsystem in contract[section]:
for rel_path in subsystem["entrypoints"]:
self.assertTrue(
(ROOT / rel_path).exists(),
f"Missing contract entrypoint: {rel_path}",
)
def test_contract_matches_pyproject_package_name(self):
with (ROOT / "pyproject.toml").open("rb") as fh:
pyproject = tomllib.load(fh)
contract = get_product_boundary_contract()
self.assertEqual(contract["package_name"], pyproject["project"]["name"])
def test_docs_reference_boundary_terms(self):
adr_text = ADR_PATH.read_text(encoding="utf-8")
readme_text = README_PATH.read_text(encoding="utf-8")
connector_text = CONNECTOR_DOC_PATH.read_text(encoding="utf-8")
for phrase in (
"ComfyUI custom node pack",
"embedded operator platform",
"connector-capable control surface",
):
self.assertIn(phrase, adr_text)
self.assertIn(phrase, readme_text)
self.assertIn("optional attached subsystem", adr_text)
self.assertIn("optional attached subsystem", connector_text)
if __name__ == "__main__":
unittest.main()