diff --git a/CHANGELOG.md b/CHANGELOG.md index b8be9db68..c3897172c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +- Added LSP resource types: `executable`, `lsp-server`, `lsp-workspace`, + `lsp-document` with parent/child hierarchy, auto-discovery rules, and + handler references. Registered in bootstrap, with YAML configurations, + Behave BDD tests (21 scenarios), and Robot integration tests (6 tests). (#832) - Implemented functional LSP runtime replacing local-mode stubs with real LSP protocol support. `StdioTransport` manages server subprocesses via JSON-RPC over stdin/stdout. `LspClient` implements initialize/shutdown/ diff --git a/examples/resource-types/executable.yaml b/examples/resource-types/executable.yaml new file mode 100644 index 000000000..fae10cde5 --- /dev/null +++ b/examples/resource-types/executable.yaml @@ -0,0 +1,29 @@ +# Spec reference: docs/adr/ADR-039-container-resource-types.md lines 207-226 +schema_version: "1.0" +name: executable +description: "System executable (language runtime, compiler, LSP server binary). Discovered from container or host PATH." +resource_kind: physical +sandbox_strategy: none +user_addable: true +built_in: true +cli_args: + - name: path + type: string + required: true + description: Absolute path to the executable + - name: version + type: string + required: false + description: Executable version if detectable +parent_types: ["container-exec-env", "fs-directory"] +child_types: [] +handler: "cleveragents.resource.handlers.executable:ExecutableHandler" +auto_discovery: + trigger_types: ["container-exec-env", "fs-directory"] + scan_paths: [] + lazy: true +capabilities: + read: true + write: false + sandbox: false + checkpoint: false diff --git a/examples/resource-types/lsp-document.yaml b/examples/resource-types/lsp-document.yaml new file mode 100644 index 000000000..9eeff4d50 --- /dev/null +++ b/examples/resource-types/lsp-document.yaml @@ -0,0 +1,20 @@ +# Spec reference: docs/adr/ADR-040-lsp-resource-types.md lines 109-131 +schema_version: "1.0" +name: lsp-document +description: "Text document tracked by an LSP server. Provides semantic access: types, diagnostics, completions, references, symbols." +resource_kind: physical +sandbox_strategy: none +user_addable: false +built_in: true +cli_args: [] +parent_types: ["lsp-workspace"] +child_types: [] +handler: "cleveragents.resource.handlers.lsp:LSPDocumentHandler" +auto_discovery: + trigger_types: ["lsp-workspace"] + scan_paths: [] +capabilities: + read: true + write: true + sandbox: false + checkpoint: false diff --git a/examples/resource-types/lsp-server.yaml b/examples/resource-types/lsp-server.yaml new file mode 100644 index 000000000..8b53417ae --- /dev/null +++ b/examples/resource-types/lsp-server.yaml @@ -0,0 +1,45 @@ +# Spec reference: docs/adr/ADR-040-lsp-resource-types.md lines 69-88 +schema_version: "1.0" +name: lsp-server +description: "LSP server definition and running process. Provides semantic services for one or more languages." +resource_kind: physical +sandbox_strategy: none +user_addable: true +built_in: true +cli_args: + - name: server-name + type: string + required: true + description: Unique server name + - name: command + type: string + required: true + description: Path to server executable or command + - name: language-ids + type: string + required: true + description: "Comma-separated language identifiers (e.g. python,typescript)" + - name: transport + type: string + required: false + description: "Transport protocol: stdio | tcp | pipe (default: stdio)" + - name: args + type: string + required: false + description: Additional command-line arguments + - name: port + type: string + required: false + description: TCP port (required when transport=tcp) + - name: initialization-options + type: string + required: false + description: JSON initialization options for the server +parent_types: ["container-instance", "container-exec-env"] +child_types: ["lsp-workspace"] +handler: "cleveragents.resource.handlers.lsp:LSPServerHandler" +capabilities: + read: true + write: false + sandbox: false + checkpoint: false diff --git a/examples/resource-types/lsp-workspace.yaml b/examples/resource-types/lsp-workspace.yaml new file mode 100644 index 000000000..a00af050d --- /dev/null +++ b/examples/resource-types/lsp-workspace.yaml @@ -0,0 +1,20 @@ +# Spec reference: docs/adr/ADR-040-lsp-resource-types.md lines 90-107 +schema_version: "1.0" +name: lsp-workspace +description: "Workspace root tracked by an LSP server. Corresponds to a workspaceFolder in the LSP specification." +resource_kind: physical +sandbox_strategy: none +user_addable: false +built_in: true +cli_args: [] +parent_types: ["lsp-server"] +child_types: ["lsp-document"] +handler: "cleveragents.resource.handlers.lsp:LSPWorkspaceHandler" +auto_discovery: + trigger_types: ["lsp-server"] + scan_paths: [] +capabilities: + read: true + write: false + sandbox: false + checkpoint: false diff --git a/features/resource_type_lsp.feature b/features/resource_type_lsp.feature new file mode 100644 index 000000000..ad09affd6 --- /dev/null +++ b/features/resource_type_lsp.feature @@ -0,0 +1,137 @@ +@m7 @resource_type @lsp +Feature: LSP Resource Types + As a developer using language tooling + I want LSP-related resource types registered in the resource DAG + So that actors can discover and use language servers + + # ── YAML loading ───────────────────────────────────────────── + + Scenario Outline: LSP type loads from YAML for lsp_rt + Given the built-in YAML file for lsp_rt + When I load the YAML via schema for lsp_rt + Then the lsp_rt schema should be valid + And the lsp_rt schema name should be "" + And the lsp_rt schema resource_kind should be "physical" + And the lsp_rt schema sandbox_strategy should be "none" + + Examples: + | type_name | + | executable | + | lsp-server | + | lsp-workspace | + | lsp-document | + + # ── User-addable flags ──────────────────────────────────────── + + Scenario: executable is user-addable for lsp_rt + Given the built-in executable YAML file for lsp_rt + When I load the YAML via schema for lsp_rt + Then the lsp_rt schema user_addable should be true + + Scenario: lsp-server is user-addable for lsp_rt + Given the built-in lsp-server YAML file for lsp_rt + When I load the YAML via schema for lsp_rt + Then the lsp_rt schema user_addable should be true + + Scenario: lsp-workspace is not user-addable for lsp_rt + Given the built-in lsp-workspace YAML file for lsp_rt + When I load the YAML via schema for lsp_rt + Then the lsp_rt schema user_addable should be false + + Scenario: lsp-document is not user-addable for lsp_rt + Given the built-in lsp-document YAML file for lsp_rt + When I load the YAML via schema for lsp_rt + Then the lsp_rt schema user_addable should be false + + # ── Capabilities ────────────────────────────────────────────── + + Scenario: lsp-document has write capability for lsp_rt + Given the built-in lsp-document YAML file for lsp_rt + When I load the YAML via schema for lsp_rt + Then the lsp_rt capability read should be true + And the lsp_rt capability write should be true + + Scenario: executable has read-only capabilities for lsp_rt + Given the built-in executable YAML file for lsp_rt + When I load the YAML via schema for lsp_rt + Then the lsp_rt capability read should be true + And the lsp_rt capability write should be false + + # ── Parent/child hierarchy ──────────────────────────────────── + + Scenario: lsp-server has lsp-workspace as child type for lsp_rt + Given the built-in lsp-server YAML file for lsp_rt + When I load the YAML via schema for lsp_rt + Then the lsp_rt child_types should contain "lsp-workspace" + + Scenario: lsp-workspace has lsp-document as child type for lsp_rt + Given the built-in lsp-workspace YAML file for lsp_rt + When I load the YAML via schema for lsp_rt + Then the lsp_rt child_types should contain "lsp-document" + + Scenario: lsp-workspace has lsp-server as parent type for lsp_rt + Given the built-in lsp-workspace YAML file for lsp_rt + When I load the YAML via schema for lsp_rt + Then the lsp_rt parent_types should contain "lsp-server" + + Scenario: lsp-document has lsp-workspace as parent type for lsp_rt + Given the built-in lsp-document YAML file for lsp_rt + When I load the YAML via schema for lsp_rt + Then the lsp_rt parent_types should contain "lsp-workspace" + + # ── Auto-discovery ──────────────────────────────────────────── + + Scenario: executable has auto-discovery from container-exec-env for lsp_rt + Given the built-in executable YAML file for lsp_rt + When I load the YAML via schema for lsp_rt + Then the lsp_rt auto_discovery should be present + And the lsp_rt auto_discovery trigger_types should contain "container-exec-env" + + Scenario: lsp-workspace has auto-discovery from lsp-server for lsp_rt + Given the built-in lsp-workspace YAML file for lsp_rt + When I load the YAML via schema for lsp_rt + Then the lsp_rt auto_discovery should be present + And the lsp_rt auto_discovery trigger_types should contain "lsp-server" + + # ── BUILTIN_NAMES ───────────────────────────────────────────── + + Scenario: BUILTIN_NAMES includes all LSP types for lsp_rt + Given the ResourceTypeSpec BUILTIN_NAMES set for lsp_rt + Then BUILTIN_NAMES should contain "executable" for lsp_rt + And BUILTIN_NAMES should contain "lsp-server" for lsp_rt + And BUILTIN_NAMES should contain "lsp-workspace" for lsp_rt + And BUILTIN_NAMES should contain "lsp-document" for lsp_rt + + # ── Bootstrap DB roundtrip ──────────────────────────────────── + + Scenario: LSP types survive DB roundtrip after bootstrap for lsp_rt + Given a lsp_rt fresh in-memory resource registry with bootstrap + When I query the lsp_rt registry for type "executable" + Then the lsp_rt db type "executable" should exist + And the lsp_rt db type "executable" should have kind "physical" + When I query the lsp_rt registry for type "lsp-server" + Then the lsp_rt db type "lsp-server" should exist + When I query the lsp_rt registry for type "lsp-workspace" + Then the lsp_rt db type "lsp-workspace" should exist + When I query the lsp_rt registry for type "lsp-document" + Then the lsp_rt db type "lsp-document" should exist + + # ── Auto-discovery: lsp-document ────────────────────────────── + + Scenario: lsp-document has auto-discovery from lsp-workspace for lsp_rt + Given the built-in lsp-document YAML file for lsp_rt + When I load the YAML via schema for lsp_rt + Then the lsp_rt auto_discovery should be present + And the lsp_rt auto_discovery trigger_types should contain "lsp-workspace" + + # ── Negative tests ──────────────────────────────────────────── + + Scenario: lsp-workspace rejects manual registration for lsp_rt + Given a lsp_rt fresh in-memory resource registry with bootstrap + When I attempt to manually register "lsp-workspace" for lsp_rt + Then the lsp_rt manual registration should be rejected + + Scenario: lsp-document rejects manual registration for lsp_rt + Given a lsp_rt fresh in-memory resource registry with bootstrap + When I attempt to manually register "lsp-document" for lsp_rt + Then the lsp_rt manual registration should be rejected diff --git a/features/steps/resource_type_lsp_steps.py b/features/steps/resource_type_lsp_steps.py new file mode 100644 index 000000000..3e4e5aaa3 --- /dev/null +++ b/features/steps/resource_type_lsp_steps.py @@ -0,0 +1,224 @@ +"""Step definitions for resource_type_lsp.feature.""" + +from __future__ import annotations + +from pathlib import Path + +import yaml +from behave import given, then, when + +from cleveragents.domain.models.core.resource_type import ResourceTypeSpec +from cleveragents.resource.schema import ResourceTypeConfigSchema + +_EXAMPLES_DIR = Path(__file__).resolve().parents[2] / "examples" / "resource-types" + + +# ── Given steps ────────────────────────────────────────────── + + +@given("the built-in {type_name} YAML file for lsp_rt") +def step_load_yaml_file_lsp_rt(context: object, type_name: str) -> None: + """Load the built-in YAML file for the given type.""" + yaml_path = _EXAMPLES_DIR / f"{type_name}.yaml" + assert yaml_path.exists(), f"YAML file not found: {yaml_path}" + with yaml_path.open() as f: + context.lsp_rt_config = yaml.safe_load(f) # type: ignore[attr-defined] + + +@given("the ResourceTypeSpec BUILTIN_NAMES set for lsp_rt") +def step_load_builtin_names_lsp_rt(context: object) -> None: + """Load the BUILTIN_NAMES frozenset.""" + context.lsp_rt_builtin_names = ResourceTypeSpec.BUILTIN_NAMES # type: ignore[attr-defined] + + +@given("a lsp_rt fresh in-memory resource registry with bootstrap") +def step_fresh_registry_lsp_rt(context: object) -> None: + """Create an in-memory SQLite DB, bootstrap all built-in types.""" + from sqlalchemy import create_engine + from sqlalchemy.orm import sessionmaker + + from cleveragents.application.services.resource_registry_service import ( + ResourceRegistryService, + ) + from cleveragents.infrastructure.database.models import Base + + engine = create_engine("sqlite:///:memory:", echo=False) + Base.metadata.create_all(engine) + factory = sessionmaker(bind=engine, expire_on_commit=False) + context.lsp_rt_service = ResourceRegistryService( # type: ignore[attr-defined] + session_factory=factory, + ) + + +# ── When steps ─────────────────────────────────────────────── + + +@when("I load the YAML via schema for lsp_rt") +def step_load_schema_lsp_rt(context: object) -> None: + """Parse the YAML config into a ResourceTypeConfigSchema.""" + config = context.lsp_rt_config # type: ignore[attr-defined] + context.lsp_rt_schema = ResourceTypeConfigSchema(**config) # type: ignore[attr-defined] + + +@when('I query the lsp_rt registry for type "{name}"') +def step_query_registry_lsp_rt(context: object, name: str) -> None: + """Query the bootstrapped registry for a specific type.""" + from cleveragents.core.exceptions import NotFoundError + + service = context.lsp_rt_service # type: ignore[attr-defined] + try: + context.lsp_rt_db_spec = service.show_type(name) # type: ignore[attr-defined] + context.lsp_rt_db_found = True # type: ignore[attr-defined] + except NotFoundError: + context.lsp_rt_db_spec = None # type: ignore[attr-defined] + context.lsp_rt_db_found = False # type: ignore[attr-defined] + + +# ── Then steps ─────────────────────────────────────────────── + + +@then("the lsp_rt schema should be valid") +def step_schema_valid_lsp_rt(context: object) -> None: + """Assert the schema loaded successfully.""" + schema = context.lsp_rt_schema # type: ignore[attr-defined] + assert schema is not None, "Schema should not be None" + assert schema.name, "Schema must have a non-empty name" + + +@then('the lsp_rt schema name should be "{name}"') +def step_schema_name_lsp_rt(context: object, name: str) -> None: + """Assert the schema name matches.""" + actual = context.lsp_rt_schema.name # type: ignore[attr-defined] + assert actual == name, f"Expected name '{name}', got '{actual}'" + + +@then('the lsp_rt schema resource_kind should be "{kind}"') +def step_schema_kind_lsp_rt(context: object, kind: str) -> None: + """Assert the resource kind matches.""" + actual = context.lsp_rt_schema.resource_kind # type: ignore[attr-defined] + assert actual == kind, f"Expected kind '{kind}', got '{actual}'" + + +@then('the lsp_rt schema sandbox_strategy should be "{strategy}"') +def step_schema_sandbox_lsp_rt(context: object, strategy: str) -> None: + """Assert the sandbox strategy matches.""" + actual = context.lsp_rt_schema.sandbox_strategy # type: ignore[attr-defined] + assert actual == strategy, f"Expected strategy '{strategy}', got '{actual}'" + + +@then("the lsp_rt schema user_addable should be true") +def step_user_addable_true_lsp_rt(context: object) -> None: + """Assert user_addable is True.""" + actual = context.lsp_rt_schema.user_addable # type: ignore[attr-defined] + assert actual is True, f"Expected user_addable True, got {actual}" + + +@then("the lsp_rt schema user_addable should be false") +def step_user_addable_false_lsp_rt(context: object) -> None: + """Assert user_addable is False.""" + actual = context.lsp_rt_schema.user_addable # type: ignore[attr-defined] + assert actual is False, f"Expected user_addable False, got {actual}" + + +@then("the lsp_rt capability read should be true") +def step_cap_read_true_lsp_rt(context: object) -> None: + """Assert read capability is True.""" + caps = context.lsp_rt_schema.capabilities # type: ignore[attr-defined] + assert caps["read"] is True, f"Expected read=True, got {caps['read']}" + + +@then("the lsp_rt capability write should be true") +def step_cap_write_true_lsp_rt(context: object) -> None: + """Assert write capability is True.""" + caps = context.lsp_rt_schema.capabilities # type: ignore[attr-defined] + assert caps["write"] is True, f"Expected write=True, got {caps['write']}" + + +@then("the lsp_rt capability write should be false") +def step_cap_write_false_lsp_rt(context: object) -> None: + """Assert write capability is False.""" + caps = context.lsp_rt_schema.capabilities # type: ignore[attr-defined] + assert caps["write"] is False, f"Expected write=False, got {caps['write']}" + + +@then('the lsp_rt child_types should contain "{child}"') +def step_child_types_lsp_rt(context: object, child: str) -> None: + """Assert child_types contains the given type.""" + types = context.lsp_rt_schema.child_types # type: ignore[attr-defined] + assert child in types, f"Expected '{child}' in child_types {types}" + + +@then('the lsp_rt parent_types should contain "{parent}"') +def step_parent_types_lsp_rt(context: object, parent: str) -> None: + """Assert parent_types contains the given type.""" + types = context.lsp_rt_schema.parent_types # type: ignore[attr-defined] + assert parent in types, f"Expected '{parent}' in parent_types {types}" + + +@then("the lsp_rt auto_discovery should be present") +def step_auto_discovery_present_lsp_rt(context: object) -> None: + """Assert auto_discovery is present.""" + ad = context.lsp_rt_schema.auto_discovery # type: ignore[attr-defined] + assert ad is not None, "auto_discovery should not be None" + + +@then('the lsp_rt auto_discovery trigger_types should contain "{trigger}"') +def step_auto_discovery_trigger_lsp_rt(context: object, trigger: str) -> None: + """Assert auto_discovery trigger_types contains the given type.""" + ad = context.lsp_rt_schema.auto_discovery # type: ignore[attr-defined] + triggers = ad.get("trigger_types", []) + assert trigger in triggers, f"Expected '{trigger}' in trigger_types {triggers}" + + +@then('BUILTIN_NAMES should contain "{name}" for lsp_rt') +def step_builtin_names_contains_lsp_rt(context: object, name: str) -> None: + """Assert BUILTIN_NAMES contains the given name.""" + names = context.lsp_rt_builtin_names # type: ignore[attr-defined] + assert name in names, f"'{name}' not in BUILTIN_NAMES: {sorted(names)}" + + +@then('the lsp_rt db type "{name}" should exist') +def step_db_type_exists_lsp_rt(context: object, name: str) -> None: + """Assert the type was found in the DB.""" + assert context.lsp_rt_db_found is True, ( # type: ignore[attr-defined] + f"Type '{name}' not found after bootstrap" + ) + + +@then('the lsp_rt db type "{name}" should have kind "{kind}"') +def step_db_type_kind_lsp_rt(context: object, name: str, kind: str) -> None: + """Assert the DB type has the expected resource kind.""" + spec = context.lsp_rt_db_spec # type: ignore[attr-defined] + actual = ( + str(spec.resource_kind.value) + if hasattr(spec.resource_kind, "value") + else str(spec.resource_kind) + ) + assert actual == kind, f"Expected '{name}' kind '{kind}', got '{actual}'" + + +# ── When/Then steps: negative tests ───────────────────────── + + +@when('I attempt to manually register "{type_name}" for lsp_rt') +def step_attempt_manual_register_lsp_rt(context: object, type_name: str) -> None: + """Attempt to register a resource instance of a non-user-addable type.""" + service = context.lsp_rt_service # type: ignore[attr-defined] + context.lsp_rt_register_error = None # type: ignore[attr-defined] + try: + service.register_resource( + type_name=type_name, + name=f"test-manual-{type_name}", + ) + except Exception as exc: + context.lsp_rt_register_error = exc # type: ignore[attr-defined] + + +@then("the lsp_rt manual registration should be rejected") +def step_manual_registration_rejected_lsp_rt(context: object) -> None: + """Assert that manual registration was rejected.""" + err = context.lsp_rt_register_error # type: ignore[attr-defined] + assert err is not None, "Expected registration to be rejected but it succeeded" + assert "not user-addable" in str(err), ( + f"Expected 'not user-addable' in error, got: {err}" + ) diff --git a/robot/helper_resource_type_lsp.py b/robot/helper_resource_type_lsp.py new file mode 100644 index 000000000..004179b3e --- /dev/null +++ b/robot/helper_resource_type_lsp.py @@ -0,0 +1,178 @@ +"""Helper for LSP resource type Robot integration tests. + +Issue: #832 +""" + +from __future__ import annotations + +import sys +from typing import Any + + +def _import_lsp_types() -> None: + """Verify LSP_RESOURCE_TYPES can be imported.""" + from cleveragents.application.services._resource_registry_lsp import ( + LSP_RESOURCE_TYPES, + ) + + assert len(LSP_RESOURCE_TYPES) == 4, f"Expected 4, got {len(LSP_RESOURCE_TYPES)}" + names = [t["name"] for t in LSP_RESOURCE_TYPES] + for expected in ("executable", "lsp-server", "lsp-workspace", "lsp-document"): + assert expected in names, f"{expected} not in LSP_RESOURCE_TYPES: {names}" + + print("import-lsp-types-ok") + + +def _check_builtin_names() -> None: + """Verify all 4 LSP types are in BUILTIN_NAMES.""" + from cleveragents.domain.models.core.resource_type import ResourceTypeSpec + + for name in ("executable", "lsp-server", "lsp-workspace", "lsp-document"): + assert name in ResourceTypeSpec.BUILTIN_NAMES, f"{name} not in BUILTIN_NAMES" + + print("check-builtin-names-ok") + + +def _db_roundtrip() -> None: + """Bootstrap in-memory DB and verify all 4 LSP types are retrievable.""" + from sqlalchemy import create_engine + from sqlalchemy.orm import sessionmaker + + from cleveragents.application.services.resource_registry_service import ( + ResourceRegistryService, + ) + from cleveragents.infrastructure.database.models import Base + + engine = create_engine("sqlite:///:memory:", echo=False) + Base.metadata.create_all(engine) + factory = sessionmaker(bind=engine, expire_on_commit=False) + svc = ResourceRegistryService(session_factory=factory) + + for name in ("executable", "lsp-server", "lsp-workspace", "lsp-document"): + spec = svc.show_type(name) + assert spec is not None, f"{name} not found after bootstrap" + assert str(spec.resource_kind) == "physical", ( + f"{name} kind: {spec.resource_kind}" + ) + assert str(spec.sandbox_strategy) == "none", ( + f"{name} strategy: {spec.sandbox_strategy}" + ) + + print("db-roundtrip-ok") + + +def _check_hierarchy() -> None: + """Verify parent/child relationships: server->workspace->document.""" + from cleveragents.application.services._resource_registry_lsp import ( + LSP_RESOURCE_TYPES, + ) + + types = {t["name"]: t for t in LSP_RESOURCE_TYPES} + + # lsp-server has lsp-workspace as child + assert "lsp-workspace" in types["lsp-server"]["child_types"], ( + "lsp-server missing lsp-workspace child" + ) + # lsp-workspace has lsp-server as parent + assert "lsp-server" in types["lsp-workspace"]["parent_types"], ( + "lsp-workspace missing lsp-server parent" + ) + # lsp-workspace has lsp-document as child + assert "lsp-document" in types["lsp-workspace"]["child_types"], ( + "lsp-workspace missing lsp-document child" + ) + # lsp-document has lsp-workspace as parent + assert "lsp-workspace" in types["lsp-document"]["parent_types"], ( + "lsp-document missing lsp-workspace parent" + ) + # executable has no children + assert types["executable"]["child_types"] == [], ( + "executable should have no children" + ) + + print("check-hierarchy-ok") + + +def _check_auto_discovery() -> None: + """Verify executable has auto_discovery with trigger_types.""" + from cleveragents.application.services._resource_registry_lsp import ( + LSP_RESOURCE_TYPES, + ) + + types = {t["name"]: t for t in LSP_RESOURCE_TYPES} + + # executable has auto_discovery + ad = types["executable"].get("auto_discovery") + assert ad is not None, "executable missing auto_discovery" + assert "container-exec-env" in ad.get("trigger_types", []), ( + "executable missing container-exec-env trigger" + ) + assert ad.get("lazy") is True, "executable auto_discovery should be lazy" + + # lsp-workspace has auto_discovery from lsp-server + ad_ws = types["lsp-workspace"].get("auto_discovery") + assert ad_ws is not None, "lsp-workspace missing auto_discovery" + assert "lsp-server" in ad_ws.get("trigger_types", []), ( + "lsp-workspace missing lsp-server trigger" + ) + + print("check-auto-discovery-ok") + + +def _check_user_addable_guard() -> None: + """Verify lsp-workspace and lsp-document reject manual registration.""" + from sqlalchemy import create_engine + from sqlalchemy.orm import sessionmaker + + from cleveragents.application.services.resource_registry_service import ( + ResourceRegistryService, + ) + from cleveragents.infrastructure.database.models import Base + + engine = create_engine("sqlite:///:memory:", echo=False) + Base.metadata.create_all(engine) + factory = sessionmaker(bind=engine, expire_on_commit=False) + svc = ResourceRegistryService(session_factory=factory) + + for non_addable in ("lsp-workspace", "lsp-document"): + try: + svc.register_resource(type_name=non_addable, name=f"test-{non_addable}") + print(f"FAIL: {non_addable} registration should have been rejected") + sys.exit(1) + except Exception as exc: + assert "not user-addable" in str(exc), ( + f"{non_addable}: expected 'not user-addable' in error, got: {exc}" + ) + + print("check-user-addable-guard-ok") + + +_COMMANDS: dict[str, Any] = { + "import-lsp-types": _import_lsp_types, + "check-builtin-names": _check_builtin_names, + "db-roundtrip": _db_roundtrip, + "check-hierarchy": _check_hierarchy, + "check-auto-discovery": _check_auto_discovery, + "check-user-addable-guard": _check_user_addable_guard, +} + +if __name__ == "__main__": + if len(sys.argv) < 2: + print(f"Usage: {sys.argv[0]} ") + print(f"Commands: {', '.join(sorted(_COMMANDS))}") + sys.exit(1) + + cmd = sys.argv[1] + fn = _COMMANDS.get(cmd) + if fn is None: + print(f"Unknown command: {cmd}") + sys.exit(1) + + try: + fn() + except Exception as exc: + print(f"FAIL [{cmd}]: {type(exc).__name__}: {exc}") + import traceback + + traceback.print_exc() + sys.exit(1) diff --git a/robot/resource_type_lsp.robot b/robot/resource_type_lsp.robot new file mode 100644 index 000000000..015302b81 --- /dev/null +++ b/robot/resource_type_lsp.robot @@ -0,0 +1,54 @@ +*** Settings *** +Documentation Integration tests for LSP resource types: executable, +... lsp-server, lsp-workspace, lsp-document. +... Spec reference: ADR-039, ADR-040 +... Issue: #832 +Resource ${CURDIR}/common.resource +Suite Setup Setup Test Environment +Suite Teardown Cleanup Test Environment + +*** Variables *** +${HELPER} robot/helper_resource_type_lsp.py + +*** Test Cases *** +LSP Resource Types Are Importable + [Documentation] Verify LSP_RESOURCE_TYPES can be imported from the data module + [Tags] lsp resource import + ${result}= Run Process ${PYTHON} ${HELPER} import-lsp-types cwd=${WORKSPACE} + Should Be Equal As Integers ${result.rc} 0 Import failed: ${result.stderr} + Should Contain ${result.stdout} import-lsp-types-ok + +LSP Types Present In BUILTIN_NAMES + [Documentation] Verify all 4 LSP types are in BUILTIN_NAMES + [Tags] lsp resource builtin + ${result}= Run Process ${PYTHON} ${HELPER} check-builtin-names cwd=${WORKSPACE} + Should Be Equal As Integers ${result.rc} 0 BUILTIN_NAMES check failed: ${result.stderr} + Should Contain ${result.stdout} check-builtin-names-ok + +LSP Types Survive DB Bootstrap Roundtrip + [Documentation] Bootstrap in-memory DB and verify all 4 LSP types are retrievable + [Tags] lsp resource bootstrap database + ${result}= Run Process ${PYTHON} ${HELPER} db-roundtrip cwd=${WORKSPACE} + Should Be Equal As Integers ${result.rc} 0 DB roundtrip failed: ${result.stderr} + Should Contain ${result.stdout} db-roundtrip-ok + +LSP Type Hierarchy Is Correct + [Documentation] Verify parent/child relationships: server->workspace->document + [Tags] lsp resource hierarchy + ${result}= Run Process ${PYTHON} ${HELPER} check-hierarchy cwd=${WORKSPACE} + Should Be Equal As Integers ${result.rc} 0 Hierarchy check failed: ${result.stderr} + Should Contain ${result.stdout} check-hierarchy-ok + +Executable Has Auto-Discovery Configuration + [Documentation] Verify executable type has auto_discovery with trigger_types + [Tags] lsp resource discovery + ${result}= Run Process ${PYTHON} ${HELPER} check-auto-discovery cwd=${WORKSPACE} + Should Be Equal As Integers ${result.rc} 0 Discovery check failed: ${result.stderr} + Should Contain ${result.stdout} check-auto-discovery-ok + +Non-User-Addable LSP Types Reject Manual Registration + [Documentation] Verify lsp-workspace and lsp-document reject manual registration + [Tags] lsp resource guard + ${result}= Run Process ${PYTHON} ${HELPER} check-user-addable-guard cwd=${WORKSPACE} + Should Be Equal As Integers ${result.rc} 0 User-addable guard failed: ${result.stderr} + Should Contain ${result.stdout} check-user-addable-guard-ok diff --git a/src/cleveragents/application/services/_resource_registry_data.py b/src/cleveragents/application/services/_resource_registry_data.py index 3a6da3f91..fd6864e2c 100644 --- a/src/cleveragents/application/services/_resource_registry_data.py +++ b/src/cleveragents/application/services/_resource_registry_data.py @@ -22,6 +22,9 @@ from cleveragents.application.services._resource_registry_cloud import ( CLOUD_BASE_TYPES, GCP_AZURE_TYPES, ) +from cleveragents.application.services._resource_registry_lsp import ( + LSP_RESOURCE_TYPES, +) from cleveragents.application.services._resource_registry_physical import ( DEFERRED_PHYSICAL_TYPES, ) @@ -253,6 +256,8 @@ BUILTIN_TYPES: list[dict[str, Any]] = [ *VIRTUAL_CORE_TYPES, # Deferred virtual types -- _resource_registry_virtual_deferred (#331) *VIRTUAL_DEFERRED_TYPES, + # LSP resource types -- _resource_registry_lsp (#832) + *LSP_RESOURCE_TYPES, ] diff --git a/src/cleveragents/application/services/_resource_registry_lsp.py b/src/cleveragents/application/services/_resource_registry_lsp.py new file mode 100644 index 000000000..3f6b594f6 --- /dev/null +++ b/src/cleveragents/application/services/_resource_registry_lsp.py @@ -0,0 +1,196 @@ +"""LSP resource type definitions for the Resource Registry. + +Contains the built-in LSP-related type entries (``executable``, +``lsp-server``, ``lsp-workspace``, ``lsp-document``) that are appended +to ``BUILTIN_TYPES`` in ``_resource_registry_data``. + +These types bridge the resource system with the LSP runtime, allowing +actors to discover available language tooling and the LSP system to know +which resources it operates on. + +Spec reference: docs/adr/ADR-039-container-resource-types.md (executable), + docs/adr/ADR-040-lsp-resource-types.md (lsp-server, + lsp-workspace, lsp-document) + +.. note:: + + This is an **internal** module (``_``-prefixed). External code + should import ``BUILTIN_TYPES`` from ``_resource_registry_data`` + instead. +""" + +from __future__ import annotations + +from typing import Any + +__all__ = ["LSP_RESOURCE_TYPES"] + +# === LSP Resource Types (#832) === +# +# These types model language tooling infrastructure in the resource DAG. +# - executable: system binary / interpreter / LSP server binary +# - lsp-server: a configured or running LSP server instance +# - lsp-workspace: workspace root tracked by an LSP server +# - lsp-document: text document tracked by an LSP server + +LSP_RESOURCE_TYPES: list[dict[str, Any]] = [ + # ── executable ─────────────────────────────────────────────── + { + "name": "executable", + "description": ( + "System executable (language runtime, compiler, LSP server " + "binary). Discovered from container or host PATH." + ), + "resource_kind": "physical", + "sandbox_strategy": "none", + "user_addable": True, + "built_in": True, + "cli_args": [ + { + "name": "path", + "type": "string", + "required": True, + "description": "Absolute path to the executable", + }, + { + "name": "version", + "type": "string", + "required": False, + "description": "Executable version if detectable", + }, + ], + "parent_types": ["container-exec-env", "fs-directory"], + "child_types": [], + "handler": "cleveragents.resource.handlers.executable:ExecutableHandler", + "auto_discovery": { + "trigger_types": ["container-exec-env", "fs-directory"], + "scan_paths": [], + "lazy": True, + }, + "capabilities": { + "read": True, + "write": False, + "sandbox": False, + "checkpoint": False, + }, + }, + # ── lsp-server ─────────────────────────────────────────────── + { + "name": "lsp-server", + "description": ( + "LSP server definition and running process. Provides " + "semantic services for one or more languages." + ), + "resource_kind": "physical", + "sandbox_strategy": "none", + "user_addable": True, + "built_in": True, + "cli_args": [ + { + "name": "server-name", + "type": "string", + "required": True, + "description": "Unique server name", + }, + { + "name": "command", + "type": "string", + "required": True, + "description": "Path to server executable or command", + }, + { + "name": "language-ids", + "type": "string", + "required": True, + "description": ( + "Comma-separated language identifiers (e.g. python,typescript)" + ), + }, + { + "name": "transport", + "type": "string", + "required": False, + "description": ("Transport: stdio | tcp | pipe (default: stdio)"), + }, + { + "name": "args", + "type": "string", + "required": False, + "description": "Additional command-line arguments", + }, + { + "name": "port", + "type": "string", + "required": False, + "description": "TCP port (required when transport=tcp)", + }, + { + "name": "initialization-options", + "type": "string", + "required": False, + "description": "JSON initialization options for the server", + }, + ], + "parent_types": ["container-instance", "container-exec-env"], + "child_types": ["lsp-workspace"], + "handler": "cleveragents.resource.handlers.lsp:LSPServerHandler", + "capabilities": { + "read": True, + "write": False, + "sandbox": False, + "checkpoint": False, + }, + }, + # ── lsp-workspace ──────────────────────────────────────────── + { + "name": "lsp-workspace", + "description": ( + "Workspace root tracked by an LSP server. Corresponds to " + "a workspaceFolder in the LSP specification." + ), + "resource_kind": "physical", + "sandbox_strategy": "none", + "user_addable": False, + "built_in": True, + "cli_args": [], + "parent_types": ["lsp-server"], + "child_types": ["lsp-document"], + "handler": "cleveragents.resource.handlers.lsp:LSPWorkspaceHandler", + "auto_discovery": { + "trigger_types": ["lsp-server"], + "scan_paths": [], + }, + "capabilities": { + "read": True, + "write": False, + "sandbox": False, + "checkpoint": False, + }, + }, + # ── lsp-document ───────────────────────────────────────────── + { + "name": "lsp-document", + "description": ( + "Text document tracked by an LSP server. Provides semantic " + "access: types, diagnostics, completions, references, symbols." + ), + "resource_kind": "physical", + "sandbox_strategy": "none", + "user_addable": False, + "built_in": True, + "cli_args": [], + "parent_types": ["lsp-workspace"], + "child_types": [], + "handler": "cleveragents.resource.handlers.lsp:LSPDocumentHandler", + "auto_discovery": { + "trigger_types": ["lsp-workspace"], + "scan_paths": [], + }, + "capabilities": { + "read": True, + "write": True, + "sandbox": False, + "checkpoint": False, + }, + }, +] diff --git a/src/cleveragents/domain/models/core/_resource_type_validation.py b/src/cleveragents/domain/models/core/_resource_type_validation.py index 1a4dda32a..14cc20b16 100644 --- a/src/cleveragents/domain/models/core/_resource_type_validation.py +++ b/src/cleveragents/domain/models/core/_resource_type_validation.py @@ -117,6 +117,11 @@ BUILTIN_TYPE_NAMES: frozenset[str] = frozenset( "remote", "submodule", "symlink", + # LSP resource types (#832) + "executable", + "lsp-server", + "lsp-workspace", + "lsp-document", } )