diff --git a/README.md b/README.md index ea0b533..219baa6 100644 --- a/README.md +++ b/README.md @@ -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) + ---
@@ -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) diff --git a/docs/adr/ADR-0002-product-boundary-and-packaging-contract.md b/docs/adr/ADR-0002-product-boundary-and-packaging-contract.md new file mode 100644 index 0000000..4d798e2 --- /dev/null +++ b/docs/adr/ADR-0002-product-boundary-and-packaging-contract.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. diff --git a/docs/connector.md b/docs/connector.md index 229c96b..a45db29 100644 --- a/docs/connector.md +++ b/docs/connector.md @@ -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. diff --git a/docs/sidecar.md b/docs/sidecar.md index 2bbbfb8..d79dc37 100644 --- a/docs/sidecar.md +++ b/docs/sidecar.md @@ -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`. diff --git a/nodes/portability_contract.py b/nodes/portability_contract.py index d92d6dd..9bcdb96 100644 --- a/nodes/portability_contract.py +++ b/nodes/portability_contract.py @@ -95,4 +95,3 @@ def get_node_portability_mappings() -> Dict[str, Dict[str, Any]]: NODE_PORTABILITY_MAPPINGS = get_node_portability_mappings() - diff --git a/services/product_boundary.py b/services/product_boundary.py new file mode 100644 index 0000000..ee87ad9 --- /dev/null +++ b/services/product_boundary.py @@ -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) + diff --git a/services/workflow_portability.py b/services/workflow_portability.py index a4caa22..75ca780 100644 --- a/services/workflow_portability.py +++ b/services/workflow_portability.py @@ -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 - diff --git a/tests/test_r160_product_boundary_contract.py b/tests/test_r160_product_boundary_contract.py new file mode 100644 index 0000000..38b9b23 --- /dev/null +++ b/tests/test_r160_product_boundary_contract.py @@ -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()