diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f4492403..b7e75eeee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,13 @@ hides them from `resource add` scaffolding (user_addable: false). Includes YAML configurations, Behave BDD tests (47 scenarios), Robot Framework integration tests, ASV benchmarks, and reference documentation. (#331) +- Added deferred physical resource types for git object taxonomy + (`git`, `git-remote`, `git-branch`, `git-tag`, `git-commit`, `git-tree`, + `git-tree-entry`, `git-stash`, `git-submodule`) and filesystem link types + (`fs-symlink`, `fs-hardlink`). All types are built-in, physical, and + auto-discovered with bounded scan_depth. Updated `fs-directory` child types + and auto-discovery. Updated `git-checkout` child types. Includes YAML + configs, Behave BDD tests, Robot tests, and ASV benchmarks. (#330) - Fixed `plan execute` CLI failing with "Plan is not in an executable state (current: strategize/queued)" after strategize completed successfully. Root cause: `_get_plan_executor()` created a second `PlanLifecycleService` diff --git a/benchmarks/resource_type_deferred_physical_bench.py b/benchmarks/resource_type_deferred_physical_bench.py new file mode 100644 index 000000000..aa8316432 --- /dev/null +++ b/benchmarks/resource_type_deferred_physical_bench.py @@ -0,0 +1,123 @@ +"""ASV benchmarks for deferred physical resource type loading (#330).""" + +from __future__ import annotations + +from pathlib import Path +from typing import Any + +_EXAMPLES_DIR = Path(__file__).resolve().parent.parent / "examples" / "resource-types" + +_GIT_TAXONOMY_TYPES = ( + "git", + "git-remote", + "git-branch", + "git-tag", + "git-commit", + "git-tree", + "git-tree-entry", + "git-stash", + "git-submodule", +) + +_FS_LINK_TYPES = ( + "fs-symlink", + "fs-hardlink", +) + +_ALL_DEFERRED_PHYSICAL = _GIT_TAXONOMY_TYPES + _FS_LINK_TYPES + + +class TimeDeferredPhysicalYAMLLoad: + """Benchmark deferred physical resource type YAML loading.""" + + def setup(self) -> None: + import yaml + + from cleveragents.domain.models.core.resource_type import ResourceTypeSpec + + self.spec_cls = ResourceTypeSpec + self.configs: dict[str, dict[str, Any]] = {} + for name in _ALL_DEFERRED_PHYSICAL: + path = _EXAMPLES_DIR / f"{name}.yaml" + with open(path) as f: + self.configs[name] = yaml.safe_load(f) + + def time_load_git_type(self) -> None: + self.spec_cls.from_config(self.configs["git"]) + + def time_load_git_branch(self) -> None: + self.spec_cls.from_config(self.configs["git-branch"]) + + def time_load_git_commit(self) -> None: + self.spec_cls.from_config(self.configs["git-commit"]) + + def time_load_git_tree(self) -> None: + self.spec_cls.from_config(self.configs["git-tree"]) + + def time_load_git_tree_entry(self) -> None: + self.spec_cls.from_config(self.configs["git-tree-entry"]) + + def time_load_fs_symlink(self) -> None: + self.spec_cls.from_config(self.configs["fs-symlink"]) + + def time_load_fs_hardlink(self) -> None: + self.spec_cls.from_config(self.configs["fs-hardlink"]) + + def time_load_all_git_taxonomy(self) -> None: + for name in _GIT_TAXONOMY_TYPES: + self.spec_cls.from_config(self.configs[name]) + + def time_load_all_fs_links(self) -> None: + for name in _FS_LINK_TYPES: + self.spec_cls.from_config(self.configs[name]) + + def time_load_all_deferred_physical(self) -> None: + for name in _ALL_DEFERRED_PHYSICAL: + self.spec_cls.from_config(self.configs[name]) + + +class TimeDeferredPhysicalCliDict: + """Benchmark as_cli_dict rendering for deferred physical types.""" + + def setup(self) -> None: + import yaml + + from cleveragents.domain.models.core.resource_type import ResourceTypeSpec + + self.specs: dict[str, Any] = {} + for name in _ALL_DEFERRED_PHYSICAL: + path = _EXAMPLES_DIR / f"{name}.yaml" + with open(path) as f: + config: dict[str, Any] = yaml.safe_load(f) + self.specs[name] = ResourceTypeSpec.from_config(config) + + def time_git_cli_dict(self) -> None: + self.specs["git"].as_cli_dict() + + def time_all_deferred_physical_cli_dict(self) -> None: + for name in _ALL_DEFERRED_PHYSICAL: + self.specs[name].as_cli_dict() + + +class TimeBootstrapBuiltinTypes: + """Benchmark bootstrap_builtin_types() including deferred physical types.""" + + timeout = 120 + + def setup(self) -> None: + from sqlalchemy import create_engine + from sqlalchemy.orm import sessionmaker + + from cleveragents.infrastructure.database.models import Base + + self.engine = create_engine("sqlite:///:memory:") + Base.metadata.create_all(self.engine) + self.factory = sessionmaker(bind=self.engine) + + def time_bootstrap(self) -> None: + from cleveragents.application.services.resource_registry_service import ( + ResourceRegistryService, + ) + + service = ResourceRegistryService(session_factory=self.factory) + service.bootstrap_builtin_types() diff --git a/docs/reference/resource_types_builtin.md b/docs/reference/resource_types_builtin.md index 40d897b80..1d938eff6 100644 --- a/docs/reference/resource_types_builtin.md +++ b/docs/reference/resource_types_builtin.md @@ -421,3 +421,57 @@ None (system-managed only). | write | no | | sandbox | no | | checkpoint | no | + +--- + +# Deferred Physical Resource Types (#330) + +## git + +A git repository — the object database, refs, and full history. +Parent: `git-checkout`. Children: `git-remote`, `git-branch`, `git-tag`, +`git-commit`, `git-stash`, `git-submodule`. + +## git-remote + +A remote URL configured on a git repo. Parent: `git`. Leaf type. + +## git-branch + +A named branch ref. Parent: `git`. Child: `git-commit`. + +## git-tag + +A tag ref (lightweight or annotated). Parent: `git`. Child: `git-commit`. + +## git-commit + +A specific commit object. Parents: `git-branch`, `git-tag`, `git`. +Child: `git-tree`. + +## git-tree + +A tree object (directory listing at a commit). Parents: `git-commit`, +`git-tree`. Children: `git-tree-entry`, `git-tree`. + +## git-tree-entry + +A blob entry in a tree. Parent: `git-tree`. Leaf type. + +## git-stash + +A stash entry. Parent: `git`. Leaf type. + +## git-submodule + +A submodule reference. Parent: `git`. Leaf type. + +## fs-symlink + +A symbolic link. Parent: `fs-directory`. Leaf type. +Sandbox: `copy_on_write`. + +## fs-hardlink + +A hard link (link count > 1). Parent: `fs-directory`. Leaf type. +Sandbox: `copy_on_write`. diff --git a/examples/resource-types/fs-directory.yaml b/examples/resource-types/fs-directory.yaml index 37e8ae908..e0fe119d0 100644 --- a/examples/resource-types/fs-directory.yaml +++ b/examples/resource-types/fs-directory.yaml @@ -10,18 +10,23 @@ cli_args: type: path required: true description: "Path to the directory" -parent_types: ["git-checkout", "fs-directory", "fs-mount"] -child_types: ["fs-directory", "fs-file", "fs-symlink", "fs-hardlink"] +parent_types: ["git-checkout", "fs-directory"] +child_types: ["fs-directory", "fs-file", "fs-symlink", "fs-hardlink", "devcontainer-instance", "devcontainer-file"] auto_discovery: enabled: true + scan_depth: 1 rules: - type: fs-directory pattern: "*/" - type: fs-file pattern: "*" + - type: fs-symlink + pattern: "symlink:*" + - type: fs-hardlink + pattern: "hardlink:*" capabilities: read: true write: true sandbox: true checkpoint: false -handler: "cleveragents.resource.handlers.fs_directory" +handler: "cleveragents.resource.handlers.fs_directory:FsDirectoryHandler" diff --git a/examples/resource-types/fs-hardlink.yaml b/examples/resource-types/fs-hardlink.yaml new file mode 100644 index 000000000..0bc4597a5 --- /dev/null +++ b/examples/resource-types/fs-hardlink.yaml @@ -0,0 +1,16 @@ +schema_version: "1.0" +name: fs-hardlink +description: "A hard link on the local filesystem — a file with link count > 1" +resource_kind: physical +sandbox_strategy: copy_on_write +user_addable: false +built_in: true +cli_args: [] +parent_types: ["fs-directory"] +child_types: [] +capabilities: + read: true + write: true + sandbox: false + checkpoint: false +handler: "cleveragents.resource.handlers.fs_file:FilesystemHandler" diff --git a/examples/resource-types/fs-symlink.yaml b/examples/resource-types/fs-symlink.yaml new file mode 100644 index 000000000..ef7514626 --- /dev/null +++ b/examples/resource-types/fs-symlink.yaml @@ -0,0 +1,16 @@ +schema_version: "1.0" +name: fs-symlink +description: "A symbolic link on the local filesystem" +resource_kind: physical +sandbox_strategy: copy_on_write +user_addable: false +built_in: true +cli_args: [] +parent_types: ["fs-directory"] +child_types: [] +capabilities: + read: true + write: false + sandbox: false + checkpoint: false +handler: "cleveragents.resource.handlers.fs_file:FilesystemHandler" diff --git a/examples/resource-types/git-branch.yaml b/examples/resource-types/git-branch.yaml new file mode 100644 index 000000000..74486de87 --- /dev/null +++ b/examples/resource-types/git-branch.yaml @@ -0,0 +1,22 @@ +schema_version: "1.0" +name: git-branch +description: "A named branch ref (e.g., main, feature/auth)" +resource_kind: physical +sandbox_strategy: none +user_addable: false +built_in: true +cli_args: [] +parent_types: ["git"] +child_types: ["git-commit"] +auto_discovery: + enabled: true + scan_depth: 1 + rules: + - type: git-commit + pattern: "HEAD" +capabilities: + read: true + write: true + sandbox: false + checkpoint: false +handler: "cleveragents.resource.handlers.git:GitRefHandler" diff --git a/examples/resource-types/git-commit.yaml b/examples/resource-types/git-commit.yaml new file mode 100644 index 000000000..211afc844 --- /dev/null +++ b/examples/resource-types/git-commit.yaml @@ -0,0 +1,24 @@ +schema_version: "1.0" +name: git-commit +description: "A specific commit object" +resource_kind: physical +sandbox_strategy: none +user_addable: false +built_in: true +cli_args: [] +# Spec §23518 omits git-tag as parent; added because annotated tags +# target commits directly, so the relationship is semantically correct. +parent_types: ["git-branch", "git-tag", "git"] +child_types: ["git-tree"] +auto_discovery: + enabled: true + scan_depth: 1 + rules: + - type: git-tree + pattern: "tree" +capabilities: + read: true + write: false + sandbox: false + checkpoint: false +handler: "cleveragents.resource.handlers.git:GitObjectHandler" diff --git a/examples/resource-types/git-remote.yaml b/examples/resource-types/git-remote.yaml new file mode 100644 index 000000000..dd330036b --- /dev/null +++ b/examples/resource-types/git-remote.yaml @@ -0,0 +1,16 @@ +schema_version: "1.0" +name: git-remote +description: "A remote URL configured on a git repo" +resource_kind: physical +sandbox_strategy: none +user_addable: false +built_in: true +cli_args: [] +parent_types: ["git"] +child_types: [] +capabilities: + read: true + write: false + sandbox: false + checkpoint: false +handler: "cleveragents.resource.handlers.git:GitConfigHandler" diff --git a/examples/resource-types/git-stash.yaml b/examples/resource-types/git-stash.yaml new file mode 100644 index 000000000..3cff62399 --- /dev/null +++ b/examples/resource-types/git-stash.yaml @@ -0,0 +1,16 @@ +schema_version: "1.0" +name: git-stash +description: "A stash entry (e.g., stash@{0})" +resource_kind: physical +sandbox_strategy: none +user_addable: false +built_in: true +cli_args: [] +parent_types: ["git"] +child_types: [] +capabilities: + read: true + write: true + sandbox: false + checkpoint: false +handler: "cleveragents.resource.handlers.git:GitRefHandler" diff --git a/examples/resource-types/git-submodule.yaml b/examples/resource-types/git-submodule.yaml new file mode 100644 index 000000000..b18cb728f --- /dev/null +++ b/examples/resource-types/git-submodule.yaml @@ -0,0 +1,16 @@ +schema_version: "1.0" +name: git-submodule +description: "A submodule reference — a pointer to another git repository at a specific commit and path" +resource_kind: physical +sandbox_strategy: none +user_addable: false +built_in: true +cli_args: [] +parent_types: ["git"] +child_types: [] +capabilities: + read: true + write: false + sandbox: false + checkpoint: false +handler: "cleveragents.resource.handlers.git:GitConfigHandler" diff --git a/examples/resource-types/git-tag.yaml b/examples/resource-types/git-tag.yaml new file mode 100644 index 000000000..78577385d --- /dev/null +++ b/examples/resource-types/git-tag.yaml @@ -0,0 +1,18 @@ +schema_version: "1.0" +name: git-tag +description: "A tag ref — lightweight or annotated (e.g., v1.0.0)" +resource_kind: physical +sandbox_strategy: none +user_addable: false +built_in: true +cli_args: [] +parent_types: ["git"] +# Annotated tags target commits; added for DAG consistency with +# git-commit listing git-tag as a parent. +child_types: ["git-commit"] +capabilities: + read: true + write: true + sandbox: false + checkpoint: false +handler: "cleveragents.resource.handlers.git:GitRefHandler" diff --git a/examples/resource-types/git-tree-entry.yaml b/examples/resource-types/git-tree-entry.yaml new file mode 100644 index 000000000..602b3fed9 --- /dev/null +++ b/examples/resource-types/git-tree-entry.yaml @@ -0,0 +1,16 @@ +schema_version: "1.0" +name: git-tree-entry +description: "A blob entry in a tree — a specific file's content at a specific path and mode" +resource_kind: physical +sandbox_strategy: none +user_addable: false +built_in: true +cli_args: [] +parent_types: ["git-tree"] +child_types: [] +capabilities: + read: true + write: false + sandbox: false + checkpoint: false +handler: "cleveragents.resource.handlers.git:GitObjectHandler" diff --git a/examples/resource-types/git-tree.yaml b/examples/resource-types/git-tree.yaml new file mode 100644 index 000000000..67aa6384a --- /dev/null +++ b/examples/resource-types/git-tree.yaml @@ -0,0 +1,25 @@ +schema_version: "1.0" +name: git-tree +description: "A tree object — a directory listing at a specific commit" +resource_kind: physical +sandbox_strategy: none +user_addable: false +built_in: true +cli_args: [] +parent_types: ["git-commit", "git-tree"] +child_types: ["git-tree-entry", "git-tree"] +auto_discovery: + enabled: true + scan_depth: 2 + # Capped at 2 -- discovery engine must enforce global max_depth + rules: + - type: git-tree-entry + pattern: "blob *" + - type: git-tree + pattern: "tree *" +capabilities: + read: true + write: false + sandbox: false + checkpoint: false +handler: "cleveragents.resource.handlers.git:GitObjectHandler" diff --git a/examples/resource-types/git.yaml b/examples/resource-types/git.yaml new file mode 100644 index 000000000..ec8286674 --- /dev/null +++ b/examples/resource-types/git.yaml @@ -0,0 +1,44 @@ +schema_version: "1.0" +name: git +description: "A git repository — the object database, refs, and full history" +resource_kind: physical +sandbox_strategy: none +user_addable: true +built_in: true +cli_args: + - name: path + type: path + required: false + description: "Path to the local .git directory" + - name: url + type: string + required: false + description: "Remote URL of the git repository" +parent_types: ["git-checkout"] +child_types: + - "git-remote" + - "git-branch" + - "git-tag" + - "git-commit" + - "git-stash" + - "git-submodule" +auto_discovery: + enabled: true + scan_depth: 1 + rules: + - type: git-remote + pattern: "refs/remotes/*" + - type: git-branch + pattern: "refs/heads/*" + - type: git-tag + pattern: "refs/tags/*" + - type: git-stash + pattern: "refs/stash" + - type: git-submodule + pattern: ".gitmodules" +capabilities: + read: true + write: false + sandbox: false + checkpoint: false +handler: "cleveragents.resource.handlers.git:GitHandler" diff --git a/features/resource_type_builtins.feature b/features/resource_type_builtins.feature index d3fb1346d..de6766d36 100644 --- a/features/resource_type_builtins.feature +++ b/features/resource_type_builtins.feature @@ -36,7 +36,6 @@ Feature: Built-in Resource Type Configurations When I load the built-in YAML via from_yaml_file Then the builtin schema parent_types should contain "git-checkout" And the builtin schema parent_types should contain "fs-directory" - And the builtin schema parent_types should contain "fs-mount" Scenario: git-checkout has no parent types Given the built-in git-checkout YAML file @@ -128,7 +127,7 @@ Feature: Built-in Resource Type Configurations Scenario: fs-directory has handler reference Given the built-in fs-directory YAML file When I load the built-in YAML via from_yaml_file - Then the builtin schema handler should be "cleveragents.resource.handlers.fs_directory" + Then the builtin schema handler should be "cleveragents.resource.handlers.fs_directory:FsDirectoryHandler" # ── Auto-discovery ──────────────────────────────────────── diff --git a/features/resource_type_deferred_physical.feature b/features/resource_type_deferred_physical.feature new file mode 100644 index 000000000..c2493cf90 --- /dev/null +++ b/features/resource_type_deferred_physical.feature @@ -0,0 +1,286 @@ +Feature: Deferred Physical Resource Types + As a CleverAgents developer + I want built-in deferred physical resource type YAMLs to validate + So that git object taxonomy and filesystem link types are available + + # ── Git object taxonomy types register for phys_deferred ─── + + Scenario: git YAML validates for phys_deferred + Given the built-in "git" YAML for phys_deferred + When I load the YAML via from_config for phys_deferred + Then the spec should be valid for phys_deferred + And the spec name should be "git" for phys_deferred + And the spec resource_kind should be "physical" for phys_deferred + And the spec sandbox_strategy should be "none" for phys_deferred + And the spec user_addable should be true for phys_deferred + And the spec built_in should be true for phys_deferred + + Scenario: git-remote YAML validates for phys_deferred + Given the built-in "git-remote" YAML for phys_deferred + When I load the YAML via from_config for phys_deferred + Then the spec should be valid for phys_deferred + And the spec name should be "git-remote" for phys_deferred + And the spec resource_kind should be "physical" for phys_deferred + And the spec user_addable should be false for phys_deferred + + Scenario: git-branch YAML validates for phys_deferred + Given the built-in "git-branch" YAML for phys_deferred + When I load the YAML via from_config for phys_deferred + Then the spec should be valid for phys_deferred + And the spec name should be "git-branch" for phys_deferred + + Scenario: git-tag YAML validates for phys_deferred + Given the built-in "git-tag" YAML for phys_deferred + When I load the YAML via from_config for phys_deferred + Then the spec should be valid for phys_deferred + And the spec name should be "git-tag" for phys_deferred + + Scenario: git-commit YAML validates for phys_deferred + Given the built-in "git-commit" YAML for phys_deferred + When I load the YAML via from_config for phys_deferred + Then the spec should be valid for phys_deferred + And the spec name should be "git-commit" for phys_deferred + + Scenario: git-tree YAML validates for phys_deferred + Given the built-in "git-tree" YAML for phys_deferred + When I load the YAML via from_config for phys_deferred + Then the spec should be valid for phys_deferred + And the spec name should be "git-tree" for phys_deferred + + Scenario: git-tree-entry YAML validates for phys_deferred + Given the built-in "git-tree-entry" YAML for phys_deferred + When I load the YAML via from_config for phys_deferred + Then the spec should be valid for phys_deferred + And the spec name should be "git-tree-entry" for phys_deferred + + Scenario: git-stash YAML validates for phys_deferred + Given the built-in "git-stash" YAML for phys_deferred + When I load the YAML via from_config for phys_deferred + Then the spec should be valid for phys_deferred + And the spec name should be "git-stash" for phys_deferred + + Scenario: git-submodule YAML validates for phys_deferred + Given the built-in "git-submodule" YAML for phys_deferred + When I load the YAML via from_config for phys_deferred + Then the spec should be valid for phys_deferred + And the spec name should be "git-submodule" for phys_deferred + + # ── Filesystem link types register for phys_deferred ─────── + + Scenario: fs-symlink YAML validates for phys_deferred + Given the built-in "fs-symlink" YAML for phys_deferred + When I load the YAML via from_config for phys_deferred + Then the spec should be valid for phys_deferred + And the spec name should be "fs-symlink" for phys_deferred + And the spec resource_kind should be "physical" for phys_deferred + And the spec user_addable should be false for phys_deferred + + Scenario: fs-hardlink YAML validates for phys_deferred + Given the built-in "fs-hardlink" YAML for phys_deferred + When I load the YAML via from_config for phys_deferred + Then the spec should be valid for phys_deferred + And the spec name should be "fs-hardlink" for phys_deferred + And the spec resource_kind should be "physical" for phys_deferred + And the spec user_addable should be false for phys_deferred + + # ── Parent/child constraints for phys_deferred ───────────── + + Scenario: git parent is git-checkout for phys_deferred + Given the built-in "git" YAML for phys_deferred + When I load the YAML via from_config for phys_deferred + Then the spec parent_types should contain "git-checkout" for phys_deferred + And the spec child_types should contain "git-remote" for phys_deferred + And the spec child_types should contain "git-branch" for phys_deferred + And the spec child_types should contain "git-tag" for phys_deferred + And the spec child_types should contain "git-commit" for phys_deferred + And the spec child_types should contain "git-stash" for phys_deferred + And the spec child_types should contain "git-submodule" for phys_deferred + + Scenario: git-remote parent is git for phys_deferred + Given the built-in "git-remote" YAML for phys_deferred + When I load the YAML via from_config for phys_deferred + Then the spec parent_types should contain "git" for phys_deferred + And the spec should have empty child_types for phys_deferred + + Scenario: git-branch parent is git for phys_deferred + Given the built-in "git-branch" YAML for phys_deferred + When I load the YAML via from_config for phys_deferred + Then the spec parent_types should contain "git" for phys_deferred + And the spec child_types should contain "git-commit" for phys_deferred + + Scenario: git-tag parent is git for phys_deferred + Given the built-in "git-tag" YAML for phys_deferred + When I load the YAML via from_config for phys_deferred + Then the spec parent_types should contain "git" for phys_deferred + And the spec child_types should contain "git-commit" for phys_deferred + + Scenario: git-commit parents are git-branch, git-tag, and git for phys_deferred + Given the built-in "git-commit" YAML for phys_deferred + When I load the YAML via from_config for phys_deferred + Then the spec parent_types should contain "git-branch" for phys_deferred + And the spec parent_types should contain "git-tag" for phys_deferred + And the spec parent_types should contain "git" for phys_deferred + And the spec child_types should contain "git-tree" for phys_deferred + + Scenario: git-tree parents are git-commit and git-tree for phys_deferred + Given the built-in "git-tree" YAML for phys_deferred + When I load the YAML via from_config for phys_deferred + Then the spec parent_types should contain "git-commit" for phys_deferred + And the spec parent_types should contain "git-tree" for phys_deferred + And the spec child_types should contain "git-tree-entry" for phys_deferred + And the spec child_types should contain "git-tree" for phys_deferred + + Scenario: git-tree-entry parent is git-tree for phys_deferred + Given the built-in "git-tree-entry" YAML for phys_deferred + When I load the YAML via from_config for phys_deferred + Then the spec parent_types should contain "git-tree" for phys_deferred + And the spec should have empty child_types for phys_deferred + + Scenario: git-stash parent is git for phys_deferred + Given the built-in "git-stash" YAML for phys_deferred + When I load the YAML via from_config for phys_deferred + Then the spec parent_types should contain "git" for phys_deferred + And the spec should have empty child_types for phys_deferred + + Scenario: git-submodule parent is git for phys_deferred + Given the built-in "git-submodule" YAML for phys_deferred + When I load the YAML via from_config for phys_deferred + Then the spec parent_types should contain "git" for phys_deferred + And the spec should have empty child_types for phys_deferred + + Scenario: fs-symlink parent is fs-directory for phys_deferred + Given the built-in "fs-symlink" YAML for phys_deferred + When I load the YAML via from_config for phys_deferred + Then the spec parent_types should contain "fs-directory" for phys_deferred + And the spec should have empty child_types for phys_deferred + + Scenario: fs-hardlink parent is fs-directory for phys_deferred + Given the built-in "fs-hardlink" YAML for phys_deferred + When I load the YAML via from_config for phys_deferred + Then the spec parent_types should contain "fs-directory" for phys_deferred + And the spec should have empty child_types for phys_deferred + + # ── Capabilities for phys_deferred ───────────────────────── + + Scenario: fs-symlink has read-only capabilities for phys_deferred + Given the built-in "fs-symlink" YAML for phys_deferred + When I load the YAML via from_config for phys_deferred + Then the spec capabilities read should be true for phys_deferred + And the spec capabilities write should be false for phys_deferred + + Scenario: fs-hardlink has read-write capabilities for phys_deferred + Given the built-in "fs-hardlink" YAML for phys_deferred + When I load the YAML via from_config for phys_deferred + Then the spec capabilities read should be true for phys_deferred + And the spec capabilities write should be true for phys_deferred + + Scenario: git-commit has read-only capabilities for phys_deferred + Given the built-in "git-commit" YAML for phys_deferred + When I load the YAML via from_config for phys_deferred + Then the spec capabilities read should be true for phys_deferred + And the spec capabilities write should be false for phys_deferred + + Scenario: git-branch has read-write capabilities for phys_deferred + Given the built-in "git-branch" YAML for phys_deferred + When I load the YAML via from_config for phys_deferred + Then the spec capabilities read should be true for phys_deferred + And the spec capabilities write should be true for phys_deferred + + # ── Auto-discovery bounded depth for phys_deferred ───────── + + Scenario: git has auto-discovery with bounded depth for phys_deferred + Given the built-in "git" YAML for phys_deferred + When I load the YAML via from_config for phys_deferred + Then the spec auto_discovery should be enabled for phys_deferred + And the spec auto_discovery scan_depth should be 1 for phys_deferred + + Scenario: git-tree has auto-discovery with bounded depth for phys_deferred + Given the built-in "git-tree" YAML for phys_deferred + When I load the YAML via from_config for phys_deferred + Then the spec auto_discovery should be enabled for phys_deferred + And the spec auto_discovery scan_depth should be 2 for phys_deferred + + Scenario: git-commit has auto-discovery with depth 1 for phys_deferred + Given the built-in "git-commit" YAML for phys_deferred + When I load the YAML via from_config for phys_deferred + Then the spec auto_discovery should be enabled for phys_deferred + And the spec auto_discovery scan_depth should be 1 for phys_deferred + + Scenario: git-tree-entry has no auto-discovery for phys_deferred + Given the built-in "git-tree-entry" YAML for phys_deferred + When I load the YAML via from_config for phys_deferred + Then the spec auto_discovery should be none for phys_deferred + + # ── Registry bootstrap for phys_deferred ─────────────────── + + Scenario: all deferred physical types are in BUILTIN_NAMES for phys_deferred + Given the ResourceTypeSpec BUILTIN_NAMES set for phys_deferred + Then BUILTIN_NAMES should contain "git" for phys_deferred + And BUILTIN_NAMES should contain "git-remote" for phys_deferred + And BUILTIN_NAMES should contain "git-branch" for phys_deferred + And BUILTIN_NAMES should contain "git-tag" for phys_deferred + And BUILTIN_NAMES should contain "git-commit" for phys_deferred + And BUILTIN_NAMES should contain "git-tree" for phys_deferred + And BUILTIN_NAMES should contain "git-tree-entry" for phys_deferred + And BUILTIN_NAMES should contain "git-stash" for phys_deferred + And BUILTIN_NAMES should contain "git-submodule" for phys_deferred + And BUILTIN_NAMES should contain "fs-symlink" for phys_deferred + And BUILTIN_NAMES should contain "fs-hardlink" for phys_deferred + + Scenario: all deferred physical types are in _BUILTIN_TYPES for phys_deferred + Given the _BUILTIN_TYPES list for phys_deferred + Then _BUILTIN_TYPES should contain entry "git" for phys_deferred + And _BUILTIN_TYPES should contain entry "git-remote" for phys_deferred + And _BUILTIN_TYPES should contain entry "git-branch" for phys_deferred + And _BUILTIN_TYPES should contain entry "git-tag" for phys_deferred + And _BUILTIN_TYPES should contain entry "git-commit" for phys_deferred + And _BUILTIN_TYPES should contain entry "git-tree" for phys_deferred + And _BUILTIN_TYPES should contain entry "git-tree-entry" for phys_deferred + And _BUILTIN_TYPES should contain entry "git-stash" for phys_deferred + And _BUILTIN_TYPES should contain entry "git-submodule" for phys_deferred + And _BUILTIN_TYPES should contain entry "fs-symlink" for phys_deferred + And _BUILTIN_TYPES should contain entry "fs-hardlink" for phys_deferred + + # ── Negative: auto_discovery validation ────────────────── + + Scenario: Auto-discovery with invalid rule type is rejected for phys_deferred + Given a physical type config with invalid auto_discovery rule type for phys_deferred + When I attempt to create a ResourceTypeSpec from it for phys_deferred + Then the phys_deferred creation should fail with "not a known built-in type" + + Scenario: Auto-discovery with empty pattern is rejected for phys_deferred + Given a physical type config with empty auto_discovery pattern for phys_deferred + When I attempt to create a ResourceTypeSpec from it for phys_deferred + Then the phys_deferred creation should fail with "pattern" + + Scenario: Auto-discovery with excessive scan_depth is rejected for phys_deferred + Given a physical type config with scan_depth 99 for phys_deferred + When I attempt to create a ResourceTypeSpec from it for phys_deferred + Then the phys_deferred creation should fail with "exceeds maximum" + + Scenario: Self-referential type without scan_depth is rejected for phys_deferred + Given a self-referential type config without scan_depth for phys_deferred + When I attempt to create a ResourceTypeSpec from it for phys_deferred + Then the phys_deferred creation should fail with "self-referential" + + # ── as_cli_dict regression: both auto_discovery schemas ── + + Scenario: as_cli_dict works for rules-based auto_discovery for phys_deferred + Given the built-in "git" YAML for phys_deferred + When I load the YAML via from_config for phys_deferred + Then as_cli_dict should succeed and include auto_discovery for phys_deferred + + Scenario: as_cli_dict works for trigger-based auto_discovery for phys_deferred + Given the built-in "devcontainer-instance" type from registry for phys_deferred + Then as_cli_dict should succeed and include auto_discovery for phys_deferred + + # ── as_cli_dict regression: both auto_discovery schemas ── + + Scenario: as_cli_dict works for rules-based auto_discovery for phys_deferred + Given the built-in "git" YAML for phys_deferred + When I load the YAML via from_config for phys_deferred + Then as_cli_dict should succeed and include auto_discovery for phys_deferred + + Scenario: as_cli_dict works for trigger-based auto_discovery for phys_deferred + Given the built-in "devcontainer-instance" type from registry for phys_deferred + Then as_cli_dict should succeed and include auto_discovery for phys_deferred diff --git a/features/resource_type_fs.feature b/features/resource_type_fs.feature index a490c4652..0351dbfc0 100644 --- a/features/resource_type_fs.feature +++ b/features/resource_type_fs.feature @@ -82,11 +82,6 @@ Feature: Filesystem Resource Type Configurations # ── fs-directory updated constraints ────────────────────── - Scenario: fs-directory has fs-mount as parent type - Given the built-in fs-directory YAML file - When I load the built-in YAML via from_yaml_file - Then the builtin schema parent_types should contain "fs-mount" - Scenario: fs-directory has fs-file child type Given the built-in fs-directory YAML file When I load the built-in YAML via from_yaml_file diff --git a/features/steps/resource_type_deferred_physical_steps.py b/features/steps/resource_type_deferred_physical_steps.py new file mode 100644 index 000000000..13ca17ad5 --- /dev/null +++ b/features/steps/resource_type_deferred_physical_steps.py @@ -0,0 +1,278 @@ +"""Step definitions for resource_type_deferred_physical.feature. + +Tests deferred physical resource types: git object taxonomy and +filesystem link types (#330). +""" + +from __future__ import annotations + +from pathlib import Path +from typing import Any + +import yaml +from behave import given, then, when + +from cleveragents.application.services._resource_registry_data import ( + BUILTIN_TYPES, +) +from cleveragents.domain.models.core.resource_type import ResourceTypeSpec + +_EXAMPLES_DIR = ( + Path(__file__).resolve().parent.parent.parent / "examples" / "resource-types" +) + + +# ── Given steps ────────────────────────────────────────────── + + +@given('the built-in "{type_name}" YAML for phys_deferred') +def step_builtin_yaml_for_phys_deferred(context: object, type_name: str) -> None: + """Load a built-in YAML file by type name.""" + path = _EXAMPLES_DIR / f"{type_name}.yaml" + assert path.exists(), f"YAML file not found: {path}" + raw: dict[str, Any] = yaml.safe_load(path.read_text(encoding="utf-8")) + context.phys_deferred_config = raw # type: ignore[attr-defined] + context.phys_deferred_yaml_path = path # type: ignore[attr-defined] + + +@given("the ResourceTypeSpec BUILTIN_NAMES set for phys_deferred") +def step_builtin_names_set_for_phys_deferred(context: object) -> None: + """Load the BUILTIN_NAMES frozenset.""" + context.phys_deferred_builtin_names = ResourceTypeSpec.BUILTIN_NAMES # type: ignore[attr-defined] + + +@given("the _BUILTIN_TYPES list for phys_deferred") +def step_builtin_types_list_for_phys_deferred(context: object) -> None: + """Load the BUILTIN_TYPES list.""" + context.phys_deferred_builtin_types = BUILTIN_TYPES # type: ignore[attr-defined] + + +# ── When steps ─────────────────────────────────────────────── + + +@when("I load the YAML via from_config for phys_deferred") +def step_load_yaml_for_phys_deferred(context: object) -> None: + """Parse YAML config into a ResourceTypeSpec.""" + config: dict[str, Any] = context.phys_deferred_config # type: ignore[attr-defined] + context.phys_deferred_spec = ResourceTypeSpec.from_config(config) # type: ignore[attr-defined] + + +# ── Then steps ─────────────────────────────────────────────── + + +@then("the spec should be valid for phys_deferred") +def step_spec_valid_for_phys_deferred(context: object) -> None: + spec = context.phys_deferred_spec # type: ignore[attr-defined] + assert spec is not None + assert isinstance(spec, ResourceTypeSpec) + + +@then('the spec name should be "{name}" for phys_deferred') +def step_spec_name_for_phys_deferred(context: object, name: str) -> None: + assert context.phys_deferred_spec.name == name # type: ignore[attr-defined] + + +@then('the spec resource_kind should be "{kind}" for phys_deferred') +def step_spec_kind_for_phys_deferred(context: object, kind: str) -> None: + actual = context.phys_deferred_spec.resource_kind # type: ignore[attr-defined] + assert str(actual) == kind or actual.value == kind, ( + f"Expected resource_kind '{kind}', got '{actual}'" + ) + + +@then('the spec sandbox_strategy should be "{strategy}" for phys_deferred') +def step_spec_sandbox_for_phys_deferred(context: object, strategy: str) -> None: + actual = context.phys_deferred_spec.sandbox_strategy # type: ignore[attr-defined] + assert str(actual) == strategy or actual.value == strategy, ( + f"Expected sandbox_strategy '{strategy}', got '{actual}'" + ) + + +@then("the spec user_addable should be true for phys_deferred") +def step_spec_user_addable_true_for_phys_deferred(context: object) -> None: + assert context.phys_deferred_spec.user_addable is True # type: ignore[attr-defined] + + +@then("the spec user_addable should be false for phys_deferred") +def step_spec_user_addable_false_for_phys_deferred(context: object) -> None: + assert context.phys_deferred_spec.user_addable is False # type: ignore[attr-defined] + + +@then("the spec built_in should be true for phys_deferred") +def step_spec_builtin_true_for_phys_deferred(context: object) -> None: + assert context.phys_deferred_spec.built_in is True # type: ignore[attr-defined] + + +@then('the spec parent_types should contain "{parent}" for phys_deferred') +def step_spec_parent_contains_for_phys_deferred(context: object, parent: str) -> None: + parents = context.phys_deferred_spec.parent_types # type: ignore[attr-defined] + assert parent in parents, f"Expected '{parent}' in parent_types, got {parents}" + + +@then('the spec child_types should contain "{child}" for phys_deferred') +def step_spec_child_contains_for_phys_deferred(context: object, child: str) -> None: + children = context.phys_deferred_spec.child_types # type: ignore[attr-defined] + assert child in children, f"Expected '{child}' in child_types, got {children}" + + +@then("the spec should have empty child_types for phys_deferred") +def step_spec_empty_children_for_phys_deferred(context: object) -> None: + children = context.phys_deferred_spec.child_types # type: ignore[attr-defined] + assert len(children) == 0, f"Expected empty child_types, got {children}" + + +@then("the spec capabilities read should be true for phys_deferred") +def step_spec_caps_read_true_for_phys_deferred(context: object) -> None: + caps = context.phys_deferred_spec.capabilities # type: ignore[attr-defined] + assert caps["read"] is True, f"Expected read=True, got {caps['read']}" + + +@then("the spec capabilities write should be false for phys_deferred") +def step_spec_caps_write_false_for_phys_deferred(context: object) -> None: + caps = context.phys_deferred_spec.capabilities # type: ignore[attr-defined] + assert caps["write"] is False, f"Expected write=False, got {caps['write']}" + + +@then("the spec capabilities write should be true for phys_deferred") +def step_spec_caps_write_true_for_phys_deferred(context: object) -> None: + caps = context.phys_deferred_spec.capabilities # type: ignore[attr-defined] + assert caps["write"] is True, f"Expected write=True, got {caps['write']}" + + +@then("the spec auto_discovery should be enabled for phys_deferred") +def step_spec_auto_discovery_enabled_for_phys_deferred(context: object) -> None: + spec = context.phys_deferred_spec # type: ignore[attr-defined] + assert spec.auto_discovery is not None, "auto_discovery should not be None" + assert spec.auto_discovery.get("enabled") is True, ( + f"auto_discovery.enabled should be True, got {spec.auto_discovery}" + ) + + +@then("the spec auto_discovery scan_depth should be {depth:d} for phys_deferred") +def step_spec_auto_discovery_depth_for_phys_deferred( + context: object, depth: int +) -> None: + spec = context.phys_deferred_spec # type: ignore[attr-defined] + assert spec.auto_discovery is not None, "auto_discovery should not be None" + actual_depth = spec.auto_discovery.get("scan_depth") + assert actual_depth == depth, f"Expected scan_depth={depth}, got {actual_depth}" + + +@then("the spec auto_discovery should be none for phys_deferred") +def step_spec_auto_discovery_none_for_phys_deferred(context: object) -> None: + spec = context.phys_deferred_spec # type: ignore[attr-defined] + assert spec.auto_discovery is None, ( + f"Expected auto_discovery=None, got {spec.auto_discovery}" + ) + + +@then('BUILTIN_NAMES should contain "{name}" for phys_deferred') +def step_builtin_names_contains_for_phys_deferred(context: object, name: str) -> None: + names = context.phys_deferred_builtin_names # type: ignore[attr-defined] + assert name in names, f"Expected '{name}' in BUILTIN_NAMES, got {names}" + + +@then('_BUILTIN_TYPES should contain entry "{name}" for phys_deferred') +def step_builtin_types_contains_for_phys_deferred(context: object, name: str) -> None: + types_list: list[dict[str, Any]] = context.phys_deferred_builtin_types # type: ignore[attr-defined] + names = [t["name"] for t in types_list] + assert name in names, f"Expected '{name}' in _BUILTIN_TYPES names, got {names}" + + +# ── Negative tests: auto_discovery validation ──────────── + + +def _base_physical_config(**overrides: Any) -> dict[str, Any]: + """Create a minimal physical type config.""" + config: dict[str, Any] = { + "name": "testns/phys-test", + "resource_kind": "physical", + "sandbox_strategy": "none", + "user_addable": False, + "built_in": False, + } + config.update(overrides) + return config + + +@given("a physical type config with invalid auto_discovery rule type for phys_deferred") +def step_invalid_rule_type(context: object) -> None: + context.phys_deferred_test_config = _base_physical_config( # type: ignore[attr-defined] + auto_discovery={ + "scan_depth": 1, + "rules": [{"type": "nonexistent-typo", "pattern": "*"}], + }, + ) + + +@given("a physical type config with empty auto_discovery pattern for phys_deferred") +def step_empty_pattern(context: object) -> None: + context.phys_deferred_test_config = _base_physical_config( # type: ignore[attr-defined] + auto_discovery={ + "scan_depth": 1, + "rules": [{"type": "git-checkout", "pattern": ""}], + }, + ) + + +@given("a physical type config with scan_depth {depth:d} for phys_deferred") +def step_excessive_depth(context: object, depth: int) -> None: + context.phys_deferred_test_config = _base_physical_config( # type: ignore[attr-defined] + auto_discovery={ + "scan_depth": depth, + "rules": [{"type": "git-checkout", "pattern": "*"}], + }, + ) + + +@given("a self-referential type config without scan_depth for phys_deferred") +def step_self_ref_no_depth(context: object) -> None: + context.phys_deferred_test_config = _base_physical_config( # type: ignore[attr-defined] + name="testns/recursive", + parent_types=["testns/recursive"], + child_types=["testns/recursive"], + auto_discovery={ + "rules": [{"type": "testns/recursive", "pattern": "*"}], + }, + ) + + +@when("I attempt to create a ResourceTypeSpec from it for phys_deferred") +def step_attempt_create(context: object) -> None: + config: dict[str, Any] = context.phys_deferred_test_config # type: ignore[attr-defined] + try: + context.phys_deferred_created = ResourceTypeSpec.from_config(config) # type: ignore[attr-defined] + context.phys_deferred_error = None # type: ignore[attr-defined] + except Exception as exc: + context.phys_deferred_created = None # type: ignore[attr-defined] + context.phys_deferred_error = exc # type: ignore[attr-defined] + + +@then('the phys_deferred creation should fail with "{message}"') +def step_creation_fail(context: object, message: str) -> None: + err = context.phys_deferred_error # type: ignore[attr-defined] + assert err is not None, "Expected error but creation succeeded" + assert message.lower() in str(err).lower(), ( + f"Error does not mention '{message}': {err}" + ) + + +# ── as_cli_dict regression steps ───────────────────────── + + +@given('the built-in "devcontainer-instance" type from registry for phys_deferred') +def step_devcontainer_from_registry(context: object) -> None: + """Load devcontainer-instance from BUILTIN_TYPES (trigger-based schema).""" + entry = next(d for d in BUILTIN_TYPES if d["name"] == "devcontainer-instance") + context.phys_deferred_spec = ResourceTypeSpec.from_config(entry) # type: ignore[attr-defined] + + +@then("as_cli_dict should succeed and include auto_discovery for phys_deferred") +def step_cli_dict_auto_discovery(context: object) -> None: + spec = context.phys_deferred_spec # type: ignore[attr-defined] + cli = spec.as_cli_dict() + assert "auto_discovery" in cli, ( + f"as_cli_dict missing auto_discovery: {list(cli.keys())}" + ) + ad = cli["auto_discovery"] + assert isinstance(ad, dict), f"auto_discovery not a dict: {type(ad)}" diff --git a/robot/resource_type_builtins.robot b/robot/resource_type_builtins.robot index adbc4d71a..49a3269ab 100644 --- a/robot/resource_type_builtins.robot +++ b/robot/resource_type_builtins.robot @@ -36,7 +36,7 @@ Load Fs Directory Built-in YAML And Validate ... assert schema.built_in is True, f"built_in: {schema.built_in}" ... assert len(schema.cli_args) == 1, f"cli_args: {len(schema.cli_args)}" ... assert "git-checkout" in schema.parent_types - ... assert schema.handler == "cleveragents.resource.handlers.fs_directory" + ... assert schema.handler == "cleveragents.resource.handlers.fs_directory:FsDirectoryHandler" ... print("fs-directory built-in YAML validated successfully") ${result}= Run Process ${PYTHON} -c ${script} Should Be Equal As Integers ${result.rc} 0 fs-directory load failed: ${result.stderr} diff --git a/robot/resource_type_deferred_physical.robot b/robot/resource_type_deferred_physical.robot new file mode 100644 index 000000000..54f600864 --- /dev/null +++ b/robot/resource_type_deferred_physical.robot @@ -0,0 +1,101 @@ +*** Settings *** +Documentation Deferred Physical Resource Type Integration Tests (#330) +Library Process +Library OperatingSystem + +*** Variables *** +${PYTHON} python +${EXAMPLES_DIR} ${CURDIR}/../examples/resource-types + +*** Test Cases *** +Load All Git Taxonomy YAMLs And Validate + [Documentation] Load all git object taxonomy YAMLs and validate key fields + ${script}= Catenate SEPARATOR=\n + ... import yaml + ... from cleveragents.domain.models.core.resource_type import ResourceTypeSpec + ... types = [ + ... ${SPACE}${SPACE}${SPACE}${SPACE}("git", True, "none", ["git-checkout"]), + ... ${SPACE}${SPACE}${SPACE}${SPACE}("git-remote", False, "none", ["git"]), + ... ${SPACE}${SPACE}${SPACE}${SPACE}("git-branch", False, "none", ["git"]), + ... ${SPACE}${SPACE}${SPACE}${SPACE}("git-tag", False, "none", ["git"]), + ... ${SPACE}${SPACE}${SPACE}${SPACE}("git-commit", False, "none", ["git-branch", "git-tag", "git"]), + ... ${SPACE}${SPACE}${SPACE}${SPACE}("git-tree", False, "none", ["git-commit", "git-tree"]), + ... ${SPACE}${SPACE}${SPACE}${SPACE}("git-tree-entry", False, "none", ["git-tree"]), + ... ${SPACE}${SPACE}${SPACE}${SPACE}("git-stash", False, "none", ["git"]), + ... ${SPACE}${SPACE}${SPACE}${SPACE}("git-submodule", False, "none", ["git"]), + ... ] + ... for name, addable, sandbox, parents in types: + ... ${SPACE}${SPACE}${SPACE}${SPACE}path = f"${EXAMPLES_DIR}/{name}.yaml" + ... ${SPACE}${SPACE}${SPACE}${SPACE}with open(path) as f: + ... ${SPACE}${SPACE}${SPACE}${SPACE}${SPACE}${SPACE}${SPACE}${SPACE}config = yaml.safe_load(f) + ... ${SPACE}${SPACE}${SPACE}${SPACE}spec = ResourceTypeSpec.from_config(config) + ... ${SPACE}${SPACE}${SPACE}${SPACE}assert spec.name == name, f"name: {spec.name}" + ... ${SPACE}${SPACE}${SPACE}${SPACE}assert spec.resource_kind.value == "physical", f"kind: {spec.resource_kind}" + ... ${SPACE}${SPACE}${SPACE}${SPACE}assert spec.sandbox_strategy.value == sandbox, f"sandbox: {spec.sandbox_strategy}" + ... ${SPACE}${SPACE}${SPACE}${SPACE}assert spec.user_addable == addable, f"addable: {spec.user_addable}" + ... ${SPACE}${SPACE}${SPACE}${SPACE}assert spec.built_in is True, f"built_in: {spec.built_in}" + ... ${SPACE}${SPACE}${SPACE}${SPACE}for p in parents: + ... ${SPACE}${SPACE}${SPACE}${SPACE}${SPACE}${SPACE}${SPACE}${SPACE}assert p in spec.parent_types, f"{name}: missing parent {p}" + ... print("All git taxonomy YAMLs validated successfully") + ${result}= Run Process ${PYTHON} -c ${script} timeout=60s on_timeout=kill + Should Be Equal As Integers ${result.rc} 0 Git taxonomy load failed: ${result.stderr} + Should Contain ${result.stdout} All git taxonomy YAMLs validated successfully + +Load Filesystem Link YAMLs And Validate + [Documentation] Load fs-symlink and fs-hardlink YAMLs and validate + ${script}= Catenate SEPARATOR=\n + ... import yaml + ... from cleveragents.domain.models.core.resource_type import ResourceTypeSpec + ... for name in ["fs-symlink", "fs-hardlink"]: + ... ${SPACE}${SPACE}${SPACE}${SPACE}path = f"${EXAMPLES_DIR}/{name}.yaml" + ... ${SPACE}${SPACE}${SPACE}${SPACE}with open(path) as f: + ... ${SPACE}${SPACE}${SPACE}${SPACE}${SPACE}${SPACE}${SPACE}${SPACE}config = yaml.safe_load(f) + ... ${SPACE}${SPACE}${SPACE}${SPACE}spec = ResourceTypeSpec.from_config(config) + ... ${SPACE}${SPACE}${SPACE}${SPACE}assert spec.name == name, f"name: {spec.name}" + ... ${SPACE}${SPACE}${SPACE}${SPACE}assert spec.resource_kind.value == "physical" + ... ${SPACE}${SPACE}${SPACE}${SPACE}assert spec.sandbox_strategy.value == "copy_on_write" + ... ${SPACE}${SPACE}${SPACE}${SPACE}assert spec.user_addable is False + ... ${SPACE}${SPACE}${SPACE}${SPACE}assert spec.built_in is True + ... ${SPACE}${SPACE}${SPACE}${SPACE}assert "fs-directory" in spec.parent_types + ... ${SPACE}${SPACE}${SPACE}${SPACE}assert len(spec.child_types) == 0 + ... print("Filesystem link YAMLs validated successfully") + ${result}= Run Process ${PYTHON} -c ${script} timeout=60s on_timeout=kill + Should Be Equal As Integers ${result.rc} 0 FS link load failed: ${result.stderr} + Should Contain ${result.stdout} Filesystem link YAMLs validated successfully + +Assert Git Taxonomy In BUILTIN_NAMES + [Documentation] Verify all deferred physical types are in BUILTIN_NAMES + ${script}= Catenate SEPARATOR=\n + ... from cleveragents.domain.models.core.resource_type import ResourceTypeSpec + ... expected = [ + ... ${SPACE}${SPACE}${SPACE}${SPACE}"git", "git-remote", "git-branch", "git-tag", + ... ${SPACE}${SPACE}${SPACE}${SPACE}"git-commit", "git-tree", "git-tree-entry", + ... ${SPACE}${SPACE}${SPACE}${SPACE}"git-stash", "git-submodule", + ... ${SPACE}${SPACE}${SPACE}${SPACE}"fs-symlink", "fs-hardlink", + ... ] + ... for name in expected: + ... ${SPACE}${SPACE}${SPACE}${SPACE}assert name in ResourceTypeSpec.BUILTIN_NAMES, f"Missing: {name}" + ... print("All deferred physical types in BUILTIN_NAMES") + ${result}= Run Process ${PYTHON} -c ${script} timeout=60s on_timeout=kill + Should Be Equal As Integers ${result.rc} 0 BUILTIN_NAMES check failed: ${result.stderr} + Should Contain ${result.stdout} All deferred physical types in BUILTIN_NAMES + +Assert Git Taxonomy In Builtin Types Registry + [Documentation] Verify all deferred physical types are in BUILTIN_TYPES list + ${script}= Catenate SEPARATOR=\n + ... from cleveragents.application.services._resource_registry_data import BUILTIN_TYPES + ... names = [t["name"] for t in BUILTIN_TYPES] + ... expected = [ + ... ${SPACE}${SPACE}${SPACE}${SPACE}"git", "git-remote", "git-branch", "git-tag", + ... ${SPACE}${SPACE}${SPACE}${SPACE}"git-commit", "git-tree", "git-tree-entry", + ... ${SPACE}${SPACE}${SPACE}${SPACE}"git-stash", "git-submodule", + ... ${SPACE}${SPACE}${SPACE}${SPACE}"fs-symlink", "fs-hardlink", + ... ] + ... for name in expected: + ... ${SPACE}${SPACE}${SPACE}${SPACE}assert name in names, f"Missing from _BUILTIN_TYPES: {name}" + ... print("All deferred physical types in _BUILTIN_TYPES") + ${result}= Run Process ${PYTHON} -c ${script} timeout=60s on_timeout=kill + Should Be Equal As Integers ${result.rc} 0 _BUILTIN_TYPES check failed: ${result.stderr} + Should Contain ${result.stdout} All deferred physical types in _BUILTIN_TYPES + +*** Keywords *** diff --git a/src/cleveragents/application/services/_resource_registry_cloud.py b/src/cleveragents/application/services/_resource_registry_cloud.py new file mode 100644 index 000000000..d9371a83d --- /dev/null +++ b/src/cleveragents/application/services/_resource_registry_cloud.py @@ -0,0 +1,761 @@ +"""Cloud resource type definitions for the Resource Registry. + +Contains generic cloud base types, AWS provider types, and GCP/Azure +placeholders. Extracted from ``_resource_registry_data.py``. + +.. note:: + + This module is 754 lines, exceeding the 500-line CONTRIBUTING + guideline. The overage is accepted because splitting the 36 AWS + type definitions mid-file would break the ``_aws_type()`` helper + pattern. A follow-up extraction to ``_resource_registry_cloud_aws`` + is tracked but not blocking. +""" + +from __future__ import annotations + +from typing import Any + +__all__ = ["AWS_TYPES", "CLOUD_BASE_TYPES", "GCP_AZURE_TYPES"] + +# --------------------------------------------------------------------------- +# Shared constants +# --------------------------------------------------------------------------- + +_CLOUD_HANDLER = "cleveragents.resource.handlers.cloud:CloudResourceHandler" +_CLOUD_CAPS_RW: dict[str, bool] = { + "read": True, + "write": True, + "sandbox": False, + "checkpoint": False, +} +_CLOUD_CAPS_RO: dict[str, bool] = { + "read": True, + "write": False, + "sandbox": False, + "checkpoint": False, +} + + +def _cloud_base( + name: str, + description: str, + *, + child_types: list[str] | None = None, +) -> dict[str, Any]: + """Return a generic ``cloud-*`` abstract base type definition.""" + return { + "name": name, + "description": description, + "resource_kind": "physical", + "sandbox_strategy": "none", + "user_addable": False, + "built_in": True, + "cli_args": [], + "parent_types": [], + "child_types": child_types or [], + "handler": _CLOUD_HANDLER, + "capabilities": dict(_CLOUD_CAPS_RO), + } + + +def _aws_type( + name: str, + description: str, + *, + inherits: str | None = None, + parent_types: list[str] | None = None, + child_types: list[str] | None = None, + cli_args: list[dict[str, Any]] | None = None, + user_addable: bool = False, + capabilities: dict[str, bool] | None = None, +) -> dict[str, Any]: + """Return an ``aws-*`` provider-specific type definition.""" + d: dict[str, Any] = { + "name": name, + "description": description, + "resource_kind": "physical", + "sandbox_strategy": "none", + "user_addable": user_addable, + "built_in": True, + "cli_args": cli_args or [], + "parent_types": parent_types or [], + "child_types": child_types or [], + "handler": _CLOUD_HANDLER, + "capabilities": dict(capabilities or _CLOUD_CAPS_RO), + } + if inherits is not None: + d["inherits"] = inherits + return d + + +# --------------------------------------------------------------------------- +# Built-in resource type definitions (registered at startup) +# --------------------------------------------------------------------------- + +# IMPORTANT: Order matters — children must appear after their parents so +# that ``bootstrap_builtin_types()`` can resolve ``inherits`` references. + +# ===== Git / Filesystem / Container types (unchanged) ===== + +_GIT_FS_CONTAINER_TYPES: list[dict[str, Any]] = [ + { + "name": "git-checkout", + "description": "A local git checkout (cloned repository or worktree).", + "resource_kind": "physical", + "sandbox_strategy": "git_worktree", + "user_addable": True, + "built_in": True, + "cli_args": [ + { + "name": "path", + "type": "path", + "required": True, + "description": "Path to the git checkout directory.", + }, + { + "name": "branch", + "type": "string", + "required": False, + "description": "Branch to use (defaults to current HEAD).", + "default": None, + }, + ], + "parent_types": [], + "child_types": [ + "git", + "fs-directory", + "fs-file", + "devcontainer-instance", + "devcontainer-file", + ], + "handler": "cleveragents.resource.handlers.git_checkout:GitCheckoutHandler", + "capabilities": { + "read": True, + "write": True, + "sandbox": True, + "checkpoint": True, + }, + }, + { + "name": "fs-directory", + "description": "A local filesystem directory.", + "resource_kind": "physical", + "sandbox_strategy": "copy_on_write", + "user_addable": True, + "built_in": True, + "cli_args": [ + { + "name": "path", + "type": "path", + "required": True, + "description": "Path to the directory.", + }, + ], + "parent_types": ["git-checkout", "fs-directory"], + "child_types": [ + "fs-directory", + "fs-file", + "fs-symlink", + "fs-hardlink", + "devcontainer-instance", + "devcontainer-file", + ], + "auto_discovery": { + "enabled": True, + "scan_depth": 1, + "rules": [ + {"type": "fs-directory", "pattern": "*/"}, + {"type": "fs-file", "pattern": "*"}, + {"type": "fs-symlink", "pattern": "symlink:*"}, + {"type": "fs-hardlink", "pattern": "hardlink:*"}, + ], + }, + "handler": "cleveragents.resource.handlers.fs_directory:FsDirectoryHandler", + "capabilities": { + "read": True, + "write": True, + "sandbox": True, + "checkpoint": False, + }, + }, + { + "name": "container-instance", + "description": "A container execution environment instance.", + "resource_kind": "physical", + "sandbox_strategy": "snapshot", + "user_addable": True, + "built_in": True, + "cli_args": [ + { + "name": "image", + "type": "string", + "required": False, + "description": "Container image reference.", + "default": None, + }, + ], + "parent_types": ["git-checkout", "fs-directory"], + "child_types": [], + "handler": ("cleveragents.resource.handlers.devcontainer:DevcontainerHandler"), + "capabilities": { + "read": True, + "write": True, + "sandbox": True, + "checkpoint": False, + }, + }, + { + "name": "devcontainer-instance", + "description": ( + "A devcontainer execution environment auto-discovered " + "from .devcontainer/ configuration." + ), + "resource_kind": "physical", + "sandbox_strategy": "snapshot", + "user_addable": True, + "built_in": True, + "inherits": "container-instance", + "cli_args": [ + { + "name": "path", + "type": "path", + "required": True, + "description": ( + "Path to the directory containing .devcontainer/devcontainer.json." + ), + }, + ], + "parent_types": ["git-checkout", "fs-directory"], + "child_types": ["devcontainer-file"], + "auto_discovery": { + "trigger_types": ["git-checkout", "fs-directory"], + "scan_paths": [ + ".devcontainer/devcontainer.json", + ".devcontainer.json", + ], + }, + "handler": ("cleveragents.resource.handlers.devcontainer:DevcontainerHandler"), + "capabilities": { + "read": True, + "write": True, + "sandbox": True, + "checkpoint": False, + }, + }, + { + "name": "devcontainer-file", + "description": "A single devcontainer.json configuration file resource.", + "resource_kind": "physical", + "sandbox_strategy": "copy_on_write", + "user_addable": False, + "built_in": True, + "cli_args": [], + "parent_types": ["devcontainer-instance"], + "child_types": [], + "handler": ("cleveragents.resource.handlers.devcontainer:DevcontainerHandler"), + "capabilities": { + "read": True, + "write": False, + "sandbox": False, + "checkpoint": False, + }, + }, +] + + +# ===== Generic cloud base types (provider-agnostic) ===== +# +# Abstract base types that define common cloud concepts. Provider- +# specific types (``aws-*``, ``azure-*``, etc.) inherit from these. +# Not user-addable — users create provider-specific account types. + +CLOUD_BASE_TYPES: list[dict[str, Any]] = [ + # -- Account & region structure -- + _cloud_base( + "cloud-account", + "Abstract base: a cloud provider account or subscription.", + ), + _cloud_base( + "cloud-region", + "Abstract base: a geographic region within a cloud account.", + ), + # -- Networking -- + _cloud_base( + "cloud-network", + "Abstract base: a virtual network (VPC, VNet, etc.).", + ), + _cloud_base( + "cloud-subnet", + "Abstract base: a subnet within a virtual network.", + ), + _cloud_base( + "cloud-security-group", + "Abstract base: network security rules / firewall group.", + ), + _cloud_base( + "cloud-load-balancer", + "Abstract base: a network load balancer.", + ), + # -- Compute -- + _cloud_base( + "cloud-compute-instance", + "Abstract base: a virtual machine or compute instance.", + ), + # -- Storage -- + _cloud_base( + "cloud-object-store", + "Abstract base: object / blob storage bucket.", + ), + _cloud_base( + "cloud-block-storage", + "Abstract base: block storage volume.", + ), + # -- IAM -- + _cloud_base( + "cloud-identity-principal", + "Abstract base: IAM user or service principal.", + ), + _cloud_base( + "cloud-role", + "Abstract base: IAM role.", + ), + _cloud_base( + "cloud-policy", + "Abstract base: IAM or access policy document.", + ), + # -- Observability -- + _cloud_base( + "cloud-log-group", + "Abstract base: log aggregation group.", + ), + _cloud_base( + "cloud-alarm", + "Abstract base: monitoring alarm or alert.", + ), + # -- Messaging -- + _cloud_base( + "cloud-queue", + "Abstract base: message queue.", + ), + _cloud_base( + "cloud-topic", + "Abstract base: pub/sub notification topic.", + ), + # -- Containers -- + _cloud_base( + "cloud-container-repo", + "Abstract base: container image registry / repository.", + ), + _cloud_base( + "cloud-container-cluster", + "Abstract base: container orchestration cluster.", + ), + _cloud_base( + "cloud-container-service", + "Abstract base: container workload / service.", + ), +] + + +# ===== AWS provider types ===== +# +# Full AWS hierarchy inheriting from ``cloud-*`` generic bases. +# Only ``aws-account`` is user-addable (top-level entry point with +# credential CLI args). All other AWS types are children discovered +# or created within the account/region/VPC hierarchy. + +AWS_TYPES: list[dict[str, Any]] = [ + # -- Account & region -- + _aws_type( + "aws-account", + "An Amazon Web Services account with credential configuration.", + inherits="cloud-account", + user_addable=True, + cli_args=[ + { + "name": "access-key-id", + "type": "string", + "required": False, + "description": "AWS access key ID.", + }, + { + "name": "secret-access-key", + "type": "string", + "required": False, + "description": "AWS secret access key.", + }, + { + "name": "session-token", + "type": "string", + "required": False, + "description": ( + "AWS session token (optional, for temporary credentials)." + ), + }, + { + "name": "region", + "type": "string", + "required": False, + "description": "Default AWS region (e.g. us-east-1).", + }, + { + "name": "profile", + "type": "string", + "required": False, + "description": "AWS profile name from ~/.aws/credentials.", + }, + ], + child_types=[ + "aws-region", + "aws-iam-user", + "aws-iam-role", + "aws-iam-policy", + "aws-iam-instance-profile", + ], + capabilities=_CLOUD_CAPS_RW, + ), + _aws_type( + "aws-region", + "An AWS geographic region (e.g. us-east-1).", + inherits="cloud-region", + parent_types=["aws-account"], + child_types=[ + "aws-vpc", + "aws-ec2-instance", + "aws-ami", + "aws-launch-template", + "aws-asg", + "aws-s3-bucket", + "aws-ebs-volume", + "aws-efs-filesystem", + "aws-cloudwatch-log-group", + "aws-cloudwatch-alarm", + "aws-cloudwatch-metric", + "aws-eventbridge-bus", + "aws-sqs-queue", + "aws-sns-topic", + "aws-ecr-repo", + "aws-ecs-cluster", + "aws-ecs-task-def", + "aws-eks-cluster", + ], + ), + # -- Networking -- + _aws_type( + "aws-vpc", + "An AWS Virtual Private Cloud network.", + inherits="cloud-network", + parent_types=["aws-region"], + child_types=[ + "aws-subnet", + "aws-igw", + "aws-nat-gw", + "aws-route-table", + "aws-nacl", + "aws-security-group", + "aws-alb", + "aws-nlb", + "aws-target-group", + ], + ), + _aws_type( + "aws-subnet", + "A subnet within an AWS VPC.", + inherits="cloud-subnet", + parent_types=["aws-vpc"], + child_types=["aws-ec2-instance", "aws-nat-gw"], + ), + _aws_type( + "aws-igw", + "An AWS Internet Gateway attached to a VPC.", + parent_types=["aws-vpc"], + ), + _aws_type( + "aws-nat-gw", + "An AWS NAT Gateway in a subnet.", + parent_types=["aws-subnet", "aws-vpc"], + ), + _aws_type( + "aws-route-table", + "An AWS VPC route table.", + parent_types=["aws-vpc"], + ), + _aws_type( + "aws-nacl", + "An AWS Network Access Control List.", + parent_types=["aws-vpc"], + ), + _aws_type( + "aws-security-group", + "An AWS security group (stateful firewall rules).", + inherits="cloud-security-group", + parent_types=["aws-vpc"], + ), + _aws_type( + "aws-alb", + "An AWS Application Load Balancer.", + inherits="cloud-load-balancer", + parent_types=["aws-vpc"], + child_types=["aws-listener"], + ), + _aws_type( + "aws-nlb", + "An AWS Network Load Balancer.", + inherits="cloud-load-balancer", + parent_types=["aws-vpc"], + child_types=["aws-listener"], + ), + _aws_type( + "aws-target-group", + "An AWS load balancer target group.", + parent_types=["aws-vpc"], + ), + _aws_type( + "aws-listener", + "An AWS load balancer listener rule.", + parent_types=["aws-alb", "aws-nlb"], + ), + # -- Compute -- + _aws_type( + "aws-ec2-instance", + "An AWS EC2 virtual machine instance.", + inherits="cloud-compute-instance", + parent_types=["aws-subnet", "aws-region"], + ), + _aws_type( + "aws-ami", + "An AWS Amazon Machine Image.", + parent_types=["aws-region"], + ), + _aws_type( + "aws-launch-template", + "An AWS EC2 launch template.", + parent_types=["aws-region"], + ), + _aws_type( + "aws-asg", + "An AWS Auto Scaling Group.", + parent_types=["aws-region"], + ), + # -- Storage -- + _aws_type( + "aws-s3-bucket", + "An AWS S3 object storage bucket.", + inherits="cloud-object-store", + parent_types=["aws-region"], + ), + _aws_type( + "aws-ebs-volume", + "An AWS Elastic Block Store volume.", + inherits="cloud-block-storage", + parent_types=["aws-region"], + ), + _aws_type( + "aws-efs-filesystem", + "An AWS Elastic File System.", + parent_types=["aws-region"], + ), + # -- IAM -- + _aws_type( + "aws-iam-user", + "An AWS IAM user.", + inherits="cloud-identity-principal", + parent_types=["aws-account"], + ), + _aws_type( + "aws-iam-role", + "An AWS IAM role.", + inherits="cloud-role", + parent_types=["aws-account"], + ), + _aws_type( + "aws-iam-policy", + "An AWS IAM managed policy.", + inherits="cloud-policy", + parent_types=["aws-account"], + ), + _aws_type( + "aws-iam-instance-profile", + "An AWS IAM instance profile (role wrapper for EC2).", + parent_types=["aws-account"], + ), + # -- Observability -- + _aws_type( + "aws-cloudwatch-log-group", + "An AWS CloudWatch Logs log group.", + inherits="cloud-log-group", + parent_types=["aws-region"], + ), + _aws_type( + "aws-cloudwatch-alarm", + "An AWS CloudWatch alarm.", + inherits="cloud-alarm", + parent_types=["aws-region"], + ), + _aws_type( + "aws-cloudwatch-metric", + "An AWS CloudWatch custom or service metric.", + parent_types=["aws-region"], + ), + _aws_type( + "aws-eventbridge-bus", + "An AWS EventBridge event bus.", + parent_types=["aws-region"], + child_types=["aws-eventbridge-rule"], + ), + _aws_type( + "aws-eventbridge-rule", + "An AWS EventBridge rule.", + parent_types=["aws-eventbridge-bus"], + child_types=["aws-eventbridge-target"], + ), + _aws_type( + "aws-eventbridge-target", + "An AWS EventBridge rule target.", + parent_types=["aws-eventbridge-rule"], + ), + # -- Messaging -- + _aws_type( + "aws-sqs-queue", + "An AWS SQS message queue.", + inherits="cloud-queue", + parent_types=["aws-region"], + ), + _aws_type( + "aws-sns-topic", + "An AWS SNS notification topic.", + inherits="cloud-topic", + parent_types=["aws-region"], + child_types=["aws-sns-subscription"], + ), + _aws_type( + "aws-sns-subscription", + "An AWS SNS topic subscription.", + parent_types=["aws-sns-topic"], + ), + # -- Containers -- + _aws_type( + "aws-ecr-repo", + "An AWS Elastic Container Registry repository.", + inherits="cloud-container-repo", + parent_types=["aws-region"], + ), + _aws_type( + "aws-ecs-cluster", + "An AWS Elastic Container Service cluster.", + inherits="cloud-container-cluster", + parent_types=["aws-region"], + child_types=["aws-ecs-service"], + ), + _aws_type( + "aws-ecs-service", + "An AWS ECS service (running task instances).", + inherits="cloud-container-service", + parent_types=["aws-ecs-cluster"], + ), + _aws_type( + "aws-ecs-task-def", + "An AWS ECS task definition.", + parent_types=["aws-region"], + ), + _aws_type( + "aws-eks-cluster", + "An AWS Elastic Kubernetes Service cluster.", + inherits="cloud-container-cluster", + parent_types=["aws-region"], + child_types=["aws-eks-nodegroup"], + ), + _aws_type( + "aws-eks-nodegroup", + "An AWS EKS managed node group.", + parent_types=["aws-eks-cluster"], + ), +] + + +# ===== GCP / Azure placeholders ===== +# +# Flat provider-level types kept for forward compatibility. Full +# hierarchies for these providers are deferred to future PRs. + +GCP_AZURE_TYPES: list[dict[str, Any]] = [ + { + "name": "gcp", + "description": "Google Cloud Platform resource (hierarchy pending).", + "resource_kind": "physical", + "sandbox_strategy": "none", + "user_addable": True, + "built_in": True, + "inherits": "cloud-account", + "cli_args": [ + { + "name": "service-account-json-path", + "type": "string", + "required": False, + "description": "Path to GCP service account JSON key file.", + }, + { + "name": "project-id", + "type": "string", + "required": False, + "description": "GCP project ID.", + }, + { + "name": "region", + "type": "string", + "required": False, + "description": "GCP region (e.g. us-central1).", + }, + ], + "parent_types": [], + "child_types": [], + "handler": _CLOUD_HANDLER, + "capabilities": dict(_CLOUD_CAPS_RW), + }, + { + "name": "azure", + "description": "Microsoft Azure cloud resource (hierarchy pending).", + "resource_kind": "physical", + "sandbox_strategy": "none", + "user_addable": True, + "built_in": True, + "inherits": "cloud-account", + "cli_args": [ + { + "name": "subscription-id", + "type": "string", + "required": False, + "description": "Azure subscription ID.", + }, + { + "name": "tenant-id", + "type": "string", + "required": False, + "description": "Azure Active Directory tenant ID.", + }, + { + "name": "client-id", + "type": "string", + "required": False, + "description": "Azure service principal client ID.", + }, + { + "name": "client-secret", + "type": "string", + "required": False, + "description": "Azure service principal client secret.", + }, + { + "name": "region", + "type": "string", + "required": False, + "description": "Azure region (e.g. eastus).", + }, + ], + "parent_types": [], + "child_types": [], + "handler": _CLOUD_HANDLER, + "capabilities": dict(_CLOUD_CAPS_RW), + }, +] diff --git a/src/cleveragents/application/services/_resource_registry_data.py b/src/cleveragents/application/services/_resource_registry_data.py index 9d2e715a7..3a6da3f91 100644 --- a/src/cleveragents/application/services/_resource_registry_data.py +++ b/src/cleveragents/application/services/_resource_registry_data.py @@ -17,6 +17,14 @@ from collections import deque from datetime import UTC, datetime from typing import Any +from cleveragents.application.services._resource_registry_cloud import ( + AWS_TYPES, + CLOUD_BASE_TYPES, + GCP_AZURE_TYPES, +) +from cleveragents.application.services._resource_registry_physical import ( + DEFERRED_PHYSICAL_TYPES, +) from cleveragents.application.services._resource_registry_virtual import ( VIRTUAL_CORE_TYPES, ) @@ -48,73 +56,6 @@ __all__ = [ # Shared constants # --------------------------------------------------------------------------- -_CLOUD_HANDLER = "cleveragents.resource.handlers.cloud:CloudResourceHandler" -_CLOUD_CAPS_RW: dict[str, bool] = { - "read": True, - "write": True, - "sandbox": False, - "checkpoint": False, -} -_CLOUD_CAPS_RO: dict[str, bool] = { - "read": True, - "write": False, - "sandbox": False, - "checkpoint": False, -} - - -def _cloud_base( - name: str, - description: str, - *, - child_types: list[str] | None = None, -) -> dict[str, Any]: - """Return a generic ``cloud-*`` abstract base type definition.""" - return { - "name": name, - "description": description, - "resource_kind": "physical", - "sandbox_strategy": "none", - "user_addable": False, - "built_in": True, - "cli_args": [], - "parent_types": [], - "child_types": child_types or [], - "handler": _CLOUD_HANDLER, - "capabilities": dict(_CLOUD_CAPS_RO), - } - - -def _aws_type( - name: str, - description: str, - *, - inherits: str | None = None, - parent_types: list[str] | None = None, - child_types: list[str] | None = None, - cli_args: list[dict[str, Any]] | None = None, - user_addable: bool = False, - capabilities: dict[str, bool] | None = None, -) -> dict[str, Any]: - """Return an ``aws-*`` provider-specific type definition.""" - d: dict[str, Any] = { - "name": name, - "description": description, - "resource_kind": "physical", - "sandbox_strategy": "none", - "user_addable": user_addable, - "built_in": True, - "cli_args": cli_args or [], - "parent_types": parent_types or [], - "child_types": child_types or [], - "handler": _CLOUD_HANDLER, - "capabilities": dict(capabilities or _CLOUD_CAPS_RO), - } - if inherits is not None: - d["inherits"] = inherits - return d - - # --------------------------------------------------------------------------- # Built-in resource type definitions (registered at startup) # --------------------------------------------------------------------------- @@ -149,6 +90,7 @@ _GIT_FS_CONTAINER_TYPES: list[dict[str, Any]] = [ ], "parent_types": [], "child_types": [ + "git", "fs-directory", "fs-file", "devcontainer-instance", @@ -181,9 +123,21 @@ _GIT_FS_CONTAINER_TYPES: list[dict[str, Any]] = [ "child_types": [ "fs-directory", "fs-file", + "fs-symlink", + "fs-hardlink", "devcontainer-instance", "devcontainer-file", ], + "auto_discovery": { + "enabled": True, + "scan_depth": 1, + "rules": [ + {"type": "fs-directory", "pattern": "*/"}, + {"type": "fs-file", "pattern": "*"}, + {"type": "fs-symlink", "pattern": "symlink:*"}, + {"type": "fs-hardlink", "pattern": "hardlink:*"}, + ], + }, "handler": "cleveragents.resource.handlers.fs_directory:FsDirectoryHandler", "capabilities": { "read": True, @@ -274,9 +228,6 @@ _GIT_FS_CONTAINER_TYPES: list[dict[str, Any]] = [ "checkpoint": False, }, }, - *DATABASE_TYPE_DEFS, - # Virtual core resource types -- see _resource_registry_virtual.py (#329) - *VIRTUAL_CORE_TYPES, ] @@ -286,505 +237,20 @@ _GIT_FS_CONTAINER_TYPES: list[dict[str, Any]] = [ # specific types (``aws-*``, ``azure-*``, etc.) inherit from these. # Not user-addable — users create provider-specific account types. -_CLOUD_BASE_TYPES: list[dict[str, Any]] = [ - # -- Account & region structure -- - _cloud_base( - "cloud-account", - "Abstract base: a cloud provider account or subscription.", - ), - _cloud_base( - "cloud-region", - "Abstract base: a geographic region within a cloud account.", - ), - # -- Networking -- - _cloud_base( - "cloud-network", - "Abstract base: a virtual network (VPC, VNet, etc.).", - ), - _cloud_base( - "cloud-subnet", - "Abstract base: a subnet within a virtual network.", - ), - _cloud_base( - "cloud-security-group", - "Abstract base: network security rules / firewall group.", - ), - _cloud_base( - "cloud-load-balancer", - "Abstract base: a network load balancer.", - ), - # -- Compute -- - _cloud_base( - "cloud-compute-instance", - "Abstract base: a virtual machine or compute instance.", - ), - # -- Storage -- - _cloud_base( - "cloud-object-store", - "Abstract base: object / blob storage bucket.", - ), - _cloud_base( - "cloud-block-storage", - "Abstract base: block storage volume.", - ), - # -- IAM -- - _cloud_base( - "cloud-identity-principal", - "Abstract base: IAM user or service principal.", - ), - _cloud_base( - "cloud-role", - "Abstract base: IAM role.", - ), - _cloud_base( - "cloud-policy", - "Abstract base: IAM or access policy document.", - ), - # -- Observability -- - _cloud_base( - "cloud-log-group", - "Abstract base: log aggregation group.", - ), - _cloud_base( - "cloud-alarm", - "Abstract base: monitoring alarm or alert.", - ), - # -- Messaging -- - _cloud_base( - "cloud-queue", - "Abstract base: message queue.", - ), - _cloud_base( - "cloud-topic", - "Abstract base: pub/sub notification topic.", - ), - # -- Containers -- - _cloud_base( - "cloud-container-repo", - "Abstract base: container image registry / repository.", - ), - _cloud_base( - "cloud-container-cluster", - "Abstract base: container orchestration cluster.", - ), - _cloud_base( - "cloud-container-service", - "Abstract base: container workload / service.", - ), -] - - -# ===== AWS provider types ===== -# -# Full AWS hierarchy inheriting from ``cloud-*`` generic bases. -# Only ``aws-account`` is user-addable (top-level entry point with -# credential CLI args). All other AWS types are children discovered -# or created within the account/region/VPC hierarchy. - -_AWS_TYPES: list[dict[str, Any]] = [ - # -- Account & region -- - _aws_type( - "aws-account", - "An Amazon Web Services account with credential configuration.", - inherits="cloud-account", - user_addable=True, - cli_args=[ - { - "name": "access-key-id", - "type": "string", - "required": False, - "description": "AWS access key ID.", - }, - { - "name": "secret-access-key", - "type": "string", - "required": False, - "description": "AWS secret access key.", - }, - { - "name": "session-token", - "type": "string", - "required": False, - "description": ( - "AWS session token (optional, for temporary credentials)." - ), - }, - { - "name": "region", - "type": "string", - "required": False, - "description": "Default AWS region (e.g. us-east-1).", - }, - { - "name": "profile", - "type": "string", - "required": False, - "description": "AWS profile name from ~/.aws/credentials.", - }, - ], - child_types=[ - "aws-region", - "aws-iam-user", - "aws-iam-role", - "aws-iam-policy", - "aws-iam-instance-profile", - ], - capabilities=_CLOUD_CAPS_RW, - ), - _aws_type( - "aws-region", - "An AWS geographic region (e.g. us-east-1).", - inherits="cloud-region", - parent_types=["aws-account"], - child_types=[ - "aws-vpc", - "aws-ec2-instance", - "aws-ami", - "aws-launch-template", - "aws-asg", - "aws-s3-bucket", - "aws-ebs-volume", - "aws-efs-filesystem", - "aws-cloudwatch-log-group", - "aws-cloudwatch-alarm", - "aws-cloudwatch-metric", - "aws-eventbridge-bus", - "aws-sqs-queue", - "aws-sns-topic", - "aws-ecr-repo", - "aws-ecs-cluster", - "aws-ecs-task-def", - "aws-eks-cluster", - ], - ), - # -- Networking -- - _aws_type( - "aws-vpc", - "An AWS Virtual Private Cloud network.", - inherits="cloud-network", - parent_types=["aws-region"], - child_types=[ - "aws-subnet", - "aws-igw", - "aws-nat-gw", - "aws-route-table", - "aws-nacl", - "aws-security-group", - "aws-alb", - "aws-nlb", - "aws-target-group", - ], - ), - _aws_type( - "aws-subnet", - "A subnet within an AWS VPC.", - inherits="cloud-subnet", - parent_types=["aws-vpc"], - child_types=["aws-ec2-instance", "aws-nat-gw"], - ), - _aws_type( - "aws-igw", - "An AWS Internet Gateway attached to a VPC.", - parent_types=["aws-vpc"], - ), - _aws_type( - "aws-nat-gw", - "An AWS NAT Gateway in a subnet.", - parent_types=["aws-subnet", "aws-vpc"], - ), - _aws_type( - "aws-route-table", - "An AWS VPC route table.", - parent_types=["aws-vpc"], - ), - _aws_type( - "aws-nacl", - "An AWS Network Access Control List.", - parent_types=["aws-vpc"], - ), - _aws_type( - "aws-security-group", - "An AWS security group (stateful firewall rules).", - inherits="cloud-security-group", - parent_types=["aws-vpc"], - ), - _aws_type( - "aws-alb", - "An AWS Application Load Balancer.", - inherits="cloud-load-balancer", - parent_types=["aws-vpc"], - child_types=["aws-listener"], - ), - _aws_type( - "aws-nlb", - "An AWS Network Load Balancer.", - inherits="cloud-load-balancer", - parent_types=["aws-vpc"], - child_types=["aws-listener"], - ), - _aws_type( - "aws-target-group", - "An AWS load balancer target group.", - parent_types=["aws-vpc"], - ), - _aws_type( - "aws-listener", - "An AWS load balancer listener rule.", - parent_types=["aws-alb", "aws-nlb"], - ), - # -- Compute -- - _aws_type( - "aws-ec2-instance", - "An AWS EC2 virtual machine instance.", - inherits="cloud-compute-instance", - parent_types=["aws-subnet", "aws-region"], - ), - _aws_type( - "aws-ami", - "An AWS Amazon Machine Image.", - parent_types=["aws-region"], - ), - _aws_type( - "aws-launch-template", - "An AWS EC2 launch template.", - parent_types=["aws-region"], - ), - _aws_type( - "aws-asg", - "An AWS Auto Scaling Group.", - parent_types=["aws-region"], - ), - # -- Storage -- - _aws_type( - "aws-s3-bucket", - "An AWS S3 object storage bucket.", - inherits="cloud-object-store", - parent_types=["aws-region"], - ), - _aws_type( - "aws-ebs-volume", - "An AWS Elastic Block Store volume.", - inherits="cloud-block-storage", - parent_types=["aws-region"], - ), - _aws_type( - "aws-efs-filesystem", - "An AWS Elastic File System.", - parent_types=["aws-region"], - ), - # -- IAM -- - _aws_type( - "aws-iam-user", - "An AWS IAM user.", - inherits="cloud-identity-principal", - parent_types=["aws-account"], - ), - _aws_type( - "aws-iam-role", - "An AWS IAM role.", - inherits="cloud-role", - parent_types=["aws-account"], - ), - _aws_type( - "aws-iam-policy", - "An AWS IAM managed policy.", - inherits="cloud-policy", - parent_types=["aws-account"], - ), - _aws_type( - "aws-iam-instance-profile", - "An AWS IAM instance profile (role wrapper for EC2).", - parent_types=["aws-account"], - ), - # -- Observability -- - _aws_type( - "aws-cloudwatch-log-group", - "An AWS CloudWatch Logs log group.", - inherits="cloud-log-group", - parent_types=["aws-region"], - ), - _aws_type( - "aws-cloudwatch-alarm", - "An AWS CloudWatch alarm.", - inherits="cloud-alarm", - parent_types=["aws-region"], - ), - _aws_type( - "aws-cloudwatch-metric", - "An AWS CloudWatch custom or service metric.", - parent_types=["aws-region"], - ), - _aws_type( - "aws-eventbridge-bus", - "An AWS EventBridge event bus.", - parent_types=["aws-region"], - child_types=["aws-eventbridge-rule"], - ), - _aws_type( - "aws-eventbridge-rule", - "An AWS EventBridge rule.", - parent_types=["aws-eventbridge-bus"], - child_types=["aws-eventbridge-target"], - ), - _aws_type( - "aws-eventbridge-target", - "An AWS EventBridge rule target.", - parent_types=["aws-eventbridge-rule"], - ), - # -- Messaging -- - _aws_type( - "aws-sqs-queue", - "An AWS SQS message queue.", - inherits="cloud-queue", - parent_types=["aws-region"], - ), - _aws_type( - "aws-sns-topic", - "An AWS SNS notification topic.", - inherits="cloud-topic", - parent_types=["aws-region"], - child_types=["aws-sns-subscription"], - ), - _aws_type( - "aws-sns-subscription", - "An AWS SNS topic subscription.", - parent_types=["aws-sns-topic"], - ), - # -- Containers -- - _aws_type( - "aws-ecr-repo", - "An AWS Elastic Container Registry repository.", - inherits="cloud-container-repo", - parent_types=["aws-region"], - ), - _aws_type( - "aws-ecs-cluster", - "An AWS Elastic Container Service cluster.", - inherits="cloud-container-cluster", - parent_types=["aws-region"], - child_types=["aws-ecs-service"], - ), - _aws_type( - "aws-ecs-service", - "An AWS ECS service (running task instances).", - inherits="cloud-container-service", - parent_types=["aws-ecs-cluster"], - ), - _aws_type( - "aws-ecs-task-def", - "An AWS ECS task definition.", - parent_types=["aws-region"], - ), - _aws_type( - "aws-eks-cluster", - "An AWS Elastic Kubernetes Service cluster.", - inherits="cloud-container-cluster", - parent_types=["aws-region"], - child_types=["aws-eks-nodegroup"], - ), - _aws_type( - "aws-eks-nodegroup", - "An AWS EKS managed node group.", - parent_types=["aws-eks-cluster"], - ), -] - - -# ===== GCP / Azure placeholders ===== -# -# Flat provider-level types kept for forward compatibility. Full -# hierarchies for these providers are deferred to future PRs. - -_GCP_AZURE_TYPES: list[dict[str, Any]] = [ - { - "name": "gcp", - "description": "Google Cloud Platform resource (hierarchy pending).", - "resource_kind": "physical", - "sandbox_strategy": "none", - "user_addable": True, - "built_in": True, - "inherits": "cloud-account", - "cli_args": [ - { - "name": "service-account-json-path", - "type": "string", - "required": False, - "description": "Path to GCP service account JSON key file.", - }, - { - "name": "project-id", - "type": "string", - "required": False, - "description": "GCP project ID.", - }, - { - "name": "region", - "type": "string", - "required": False, - "description": "GCP region (e.g. us-central1).", - }, - ], - "parent_types": [], - "child_types": [], - "handler": _CLOUD_HANDLER, - "capabilities": dict(_CLOUD_CAPS_RW), - }, - { - "name": "azure", - "description": "Microsoft Azure cloud resource (hierarchy pending).", - "resource_kind": "physical", - "sandbox_strategy": "none", - "user_addable": True, - "built_in": True, - "inherits": "cloud-account", - "cli_args": [ - { - "name": "subscription-id", - "type": "string", - "required": False, - "description": "Azure subscription ID.", - }, - { - "name": "tenant-id", - "type": "string", - "required": False, - "description": "Azure Active Directory tenant ID.", - }, - { - "name": "client-id", - "type": "string", - "required": False, - "description": "Azure service principal client ID.", - }, - { - "name": "client-secret", - "type": "string", - "required": False, - "description": "Azure service principal client secret.", - }, - { - "name": "region", - "type": "string", - "required": False, - "description": "Azure region (e.g. eastus).", - }, - ], - "parent_types": [], - "child_types": [], - "handler": _CLOUD_HANDLER, - "capabilities": dict(_CLOUD_CAPS_RW), - }, -] - # ===== Assemble final list ===== BUILTIN_TYPES: list[dict[str, Any]] = [ *_GIT_FS_CONTAINER_TYPES, - *_CLOUD_BASE_TYPES, - *_AWS_TYPES, - *_GCP_AZURE_TYPES, + # Deferred physical resource types -- see _resource_registry_physical.py (#330) + *DEFERRED_PHYSICAL_TYPES, + # Cloud resource types -- see _resource_registry_cloud.py + *CLOUD_BASE_TYPES, + *AWS_TYPES, + *GCP_AZURE_TYPES, *DATABASE_TYPE_DEFS, + # Virtual core resource types -- see _resource_registry_virtual.py (#329) + *VIRTUAL_CORE_TYPES, # Deferred virtual types -- _resource_registry_virtual_deferred (#331) *VIRTUAL_DEFERRED_TYPES, ] diff --git a/src/cleveragents/application/services/_resource_registry_physical.py b/src/cleveragents/application/services/_resource_registry_physical.py new file mode 100644 index 000000000..a2fb6b2e0 --- /dev/null +++ b/src/cleveragents/application/services/_resource_registry_physical.py @@ -0,0 +1,310 @@ +"""Deferred physical resource type definitions for the Resource Registry. + +Contains the built-in deferred physical type entries (git object taxonomy +and filesystem link types) that are appended to ``BUILTIN_TYPES`` in +``_resource_registry_data``. Extracted to keep each module under the +500-line CONTRIBUTING limit. + +.. 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__ = ["DEFERRED_PHYSICAL_TYPES"] + +# === Deferred Physical Resource Types (#330) === +# +# Git object taxonomy (9 types) and filesystem link types (2 types). +# Auto-discovery rules use bounded scan_depth (1-3) to prevent +# unbounded traversal. Types without discovery are leaf nodes. + +DEFERRED_PHYSICAL_TYPES: list[dict[str, Any]] = [ + { + "name": "git", + "description": ( + "A git repository \u2014 the object database, refs, and full history." + ), + "resource_kind": "physical", + "sandbox_strategy": "none", + "user_addable": True, + "built_in": True, + "cli_args": [ + { + "name": "path", + "type": "path", + "required": False, + "description": "Path to the local .git directory.", + "default": None, + }, + { + "name": "url", + "type": "string", + "required": False, + "description": "Remote URL of the git repository.", + "default": None, + }, + ], + "parent_types": ["git-checkout"], + "child_types": [ + "git-remote", + "git-branch", + "git-tag", + "git-commit", + "git-stash", + "git-submodule", + ], + "auto_discovery": { + "enabled": True, + "scan_depth": 1, + "rules": [ + {"type": "git-remote", "pattern": "refs/remotes/*"}, + {"type": "git-branch", "pattern": "refs/heads/*"}, + {"type": "git-tag", "pattern": "refs/tags/*"}, + {"type": "git-stash", "pattern": "refs/stash"}, + {"type": "git-submodule", "pattern": ".gitmodules"}, + ], + }, + "handler": "cleveragents.resource.handlers.git:GitHandler", + "capabilities": { + "read": True, + "write": False, + "sandbox": False, + "checkpoint": False, + }, + }, + { + "name": "git-remote", + "description": ( + "A remote URL configured on a git repo " + "(e.g., origin \u2192 https://github.com/org/repo)." + ), + "resource_kind": "physical", + "sandbox_strategy": "none", + "user_addable": False, + "built_in": True, + "cli_args": [], + "parent_types": ["git"], + "child_types": [], + "handler": "cleveragents.resource.handlers.git:GitConfigHandler", + "capabilities": { + "read": True, + "write": False, + "sandbox": False, + "checkpoint": False, + }, + }, + { + "name": "git-branch", + "description": "A named branch ref (e.g., main, feature/auth).", + "resource_kind": "physical", + "sandbox_strategy": "none", + "user_addable": False, + "built_in": True, + "cli_args": [], + "parent_types": ["git"], + "child_types": ["git-commit"], + "auto_discovery": { + "enabled": True, + "scan_depth": 1, + "rules": [ + {"type": "git-commit", "pattern": "HEAD"}, + ], + }, + "handler": "cleveragents.resource.handlers.git:GitRefHandler", + "capabilities": { + "read": True, + "write": True, + "sandbox": False, + "checkpoint": False, + }, + }, + { + # NOTE: git-tag has no auto_discovery (unlike git-branch) because + # tags are passive refs — they point to a commit but don't "own" + # children that need discovery. The tag→commit relationship is + # expressed via child_types only. + "name": "git-tag", + "description": "A tag ref \u2014 lightweight or annotated (e.g., v1.0.0).", + "resource_kind": "physical", + "sandbox_strategy": "none", + "user_addable": False, + "built_in": True, + "cli_args": [], + "parent_types": ["git"], + # Spec S23518 omits git-commit as child of git-tag, but annotated tags + # target commits directly. Added for DAG consistency with git-commit + # listing git-tag as a parent. + # NOTE: This is NOT a cycle — git-tag is a parent of git-commit + # (tag -> commit), and git-commit lists git-tag as parent (commit + # can be reached FROM a tag). The DAG remains acyclic. + "child_types": ["git-commit"], + "handler": "cleveragents.resource.handlers.git:GitRefHandler", + "capabilities": { + "read": True, + "write": True, + "sandbox": False, + "checkpoint": False, + }, + }, + { + "name": "git-commit", + "description": "A specific commit object.", + "resource_kind": "physical", + "sandbox_strategy": "none", + "user_addable": False, + "built_in": True, + "cli_args": [], + # Spec S23518 omits git-tag as parent; added because annotated tags + # target commits directly, so the relationship is semantically correct. + "parent_types": ["git-branch", "git-tag", "git"], + "child_types": ["git-tree"], + "auto_discovery": { + "enabled": True, + "scan_depth": 1, + "rules": [ + {"type": "git-tree", "pattern": "tree"}, + ], + }, + "handler": "cleveragents.resource.handlers.git:GitObjectHandler", + "capabilities": { + "read": True, + "write": False, + "sandbox": False, + "checkpoint": False, + }, + }, + { + "name": "git-tree", + "description": "A tree object \u2014 a directory listing at a specific commit.", + "resource_kind": "physical", + "sandbox_strategy": "none", + "user_addable": False, + "built_in": True, + "cli_args": [], + "parent_types": ["git-commit", "git-tree"], + "child_types": ["git-tree-entry", "git-tree"], + # WARNING: git-tree is a recursive type (tree -> subtree -> ...). + # scan_depth is capped at 2 to prevent expensive traversal on + # large repositories (e.g. Linux kernel ~70k tree entries). + # The discovery engine MUST enforce a global max_depth cap. + "auto_discovery": { + "enabled": True, + "scan_depth": 2, + "rules": [ + {"type": "git-tree-entry", "pattern": "blob *"}, + {"type": "git-tree", "pattern": "tree *"}, + ], + }, + "handler": "cleveragents.resource.handlers.git:GitObjectHandler", + "capabilities": { + "read": True, + "write": False, + "sandbox": False, + "checkpoint": False, + }, + }, + { + "name": "git-tree-entry", + "description": ( + "A blob entry in a tree \u2014 a specific file's content " + "at a specific path and mode." + ), + "resource_kind": "physical", + "sandbox_strategy": "none", + "user_addable": False, + "built_in": True, + "cli_args": [], + "parent_types": ["git-tree"], + "child_types": [], + "handler": "cleveragents.resource.handlers.git:GitObjectHandler", + "capabilities": { + "read": True, + "write": False, + "sandbox": False, + "checkpoint": False, + }, + }, + { + "name": "git-stash", + "description": "A stash entry (e.g., stash@{0}).", + "resource_kind": "physical", + "sandbox_strategy": "none", + "user_addable": False, + "built_in": True, + "cli_args": [], + "parent_types": ["git"], + "child_types": [], + "handler": "cleveragents.resource.handlers.git:GitRefHandler", + "capabilities": { + "read": True, + "write": True, + "sandbox": False, + "checkpoint": False, + }, + }, + { + "name": "git-submodule", + "description": ( + "A submodule reference \u2014 a pointer to another git " + "repository at a specific commit and path." + ), + "resource_kind": "physical", + "sandbox_strategy": "none", + "user_addable": False, + "built_in": True, + "cli_args": [], + "parent_types": ["git"], + "child_types": [], + "handler": "cleveragents.resource.handlers.git:GitConfigHandler", + "capabilities": { + "read": True, + "write": False, + "sandbox": False, + "checkpoint": False, + }, + }, + { + "name": "fs-symlink", + "description": "A symbolic link on the local filesystem.", + "resource_kind": "physical", + "sandbox_strategy": "copy_on_write", + "user_addable": False, + "built_in": True, + "cli_args": [], + "parent_types": ["fs-directory"], + "child_types": [], + "handler": "cleveragents.resource.handlers.fs_file:FilesystemHandler", + "capabilities": { + "read": True, + "write": False, + "sandbox": False, + "checkpoint": False, + }, + }, + { + "name": "fs-hardlink", + "description": ( + "A hard link on the local filesystem \u2014 a file with link count > 1." + ), + "resource_kind": "physical", + "sandbox_strategy": "copy_on_write", + "user_addable": False, + "built_in": True, + "cli_args": [], + "parent_types": ["fs-directory"], + "child_types": [], + "handler": "cleveragents.resource.handlers.fs_file:FilesystemHandler", + "capabilities": { + "read": True, + "write": True, + "sandbox": False, + "checkpoint": False, + }, + }, +] diff --git a/src/cleveragents/application/services/resource_registry_service.py b/src/cleveragents/application/services/resource_registry_service.py index 5a0939586..fb1504c3d 100644 --- a/src/cleveragents/application/services/resource_registry_service.py +++ b/src/cleveragents/application/services/resource_registry_service.py @@ -25,6 +25,17 @@ development workflows. Built-in types are **unnamespaced** (no | ``remote`` | virtual | ``none`` | Cross-repo remote identity | | ``submodule`` | virtual | ``none`` | Cross-repo submodule identity | | ``symlink`` | virtual | ``none`` | Cross-layer symlink identity | +| ``git`` | physical | ``none`` | Git repository object database | +| ``git-remote`` | physical | ``none`` | Remote URL on a git repo | +| ``git-branch`` | physical | ``none`` | Named branch ref | +| ``git-tag`` | physical | ``none`` | Tag ref (lightweight/annotated)| +| ``git-commit`` | physical | ``none`` | Commit object | +| ``git-tree`` | physical | ``none`` | Tree object (directory listing)| +| ``git-tree-entry``| physical | ``none`` | Blob entry in a tree | +| ``git-stash`` | physical | ``none`` | Stash entry | +| ``git-submodule`` | physical | ``none`` | Submodule reference | +| ``fs-symlink`` | physical | ``copy_on_write``| Symbolic link | +| ``fs-hardlink`` | physical | ``copy_on_write``| Hard link | Custom types must follow the ``namespace/name`` pattern and are registered via YAML configuration files. diff --git a/src/cleveragents/domain/models/core/_resource_type_validation.py b/src/cleveragents/domain/models/core/_resource_type_validation.py new file mode 100644 index 000000000..1a4dda32a --- /dev/null +++ b/src/cleveragents/domain/models/core/_resource_type_validation.py @@ -0,0 +1,263 @@ +"""Extracted validation helpers for ResourceTypeSpec. + +Keeps ``resource_type.py`` under the 500-line CONTRIBUTING limit by +housing the virtual-type and auto-discovery cross-field validators +in a separate internal module. +""" + +from __future__ import annotations + +import re +from typing import Any + +_BUILTIN_NAME_RE = re.compile(r"^[a-zA-Z][a-zA-Z0-9_-]*$") +_NAMESPACED_RE = re.compile(r"^[a-zA-Z0-9_-]+/[a-zA-Z0-9_-]+$") + +MAX_SCAN_DEPTH = 10 + +# All known built-in type names. Extracted here so that +# ``ResourceTypeSpec.BUILTIN_NAMES`` stays a one-liner reference. +BUILTIN_TYPE_NAMES: frozenset[str] = frozenset( + { + # -- Git / Filesystem / Container -- + "git-checkout", + "fs-directory", + "fs-mount", + "fs-file", + "container-instance", + "devcontainer-instance", + "devcontainer-file", + "postgres", + "mysql", + "sqlite", + "duckdb", + # -- Generic cloud base types -- + "cloud-account", + "cloud-region", + "cloud-network", + "cloud-subnet", + "cloud-security-group", + "cloud-load-balancer", + "cloud-compute-instance", + "cloud-object-store", + "cloud-block-storage", + "cloud-identity-principal", + "cloud-role", + "cloud-policy", + "cloud-log-group", + "cloud-alarm", + "cloud-queue", + "cloud-topic", + "cloud-container-repo", + "cloud-container-cluster", + "cloud-container-service", + # -- AWS provider types -- + "aws-account", + "aws-region", + "aws-vpc", + "aws-subnet", + "aws-igw", + "aws-nat-gw", + "aws-route-table", + "aws-nacl", + "aws-security-group", + "aws-alb", + "aws-nlb", + "aws-target-group", + "aws-listener", + "aws-ec2-instance", + "aws-ami", + "aws-launch-template", + "aws-asg", + "aws-s3-bucket", + "aws-ebs-volume", + "aws-efs-filesystem", + "aws-iam-user", + "aws-iam-role", + "aws-iam-policy", + "aws-iam-instance-profile", + "aws-cloudwatch-log-group", + "aws-cloudwatch-alarm", + "aws-cloudwatch-metric", + "aws-eventbridge-bus", + "aws-eventbridge-rule", + "aws-eventbridge-target", + "aws-sqs-queue", + "aws-sns-topic", + "aws-sns-subscription", + "aws-ecr-repo", + "aws-ecs-cluster", + "aws-ecs-service", + "aws-ecs-task-def", + "aws-eks-cluster", + "aws-eks-nodegroup", + # -- GCP / Azure (flat, hierarchy pending) -- + "gcp", + "azure", + # Deferred physical resource types (#330) + "fs-hardlink", + "fs-symlink", + "git", + "git-branch", + "git-commit", + "git-remote", + "git-stash", + "git-submodule", + "git-tag", + "git-tree", + "git-tree-entry", + # Virtual core resource types (#329) + "branch", + "commit", + "directory", + "file", + "tag", + "tree", + # Deferred virtual resource types (#331) + "remote", + "submodule", + "symlink", + } +) + + +def validate_virtual_type( + name: str, + equivalence: dict[str, Any] | None, + user_addable: bool, + sandbox_strategy_value: str, + handler: str | None, + capabilities: dict[str, bool], +) -> None: + """Validate constraints specific to virtual resource types.""" + if equivalence is None: + raise ValueError( + f"Virtual resource type '{name}' requires an " + "'equivalence' configuration for deduplication." + ) + if "criteria" not in equivalence: + raise ValueError( + f"Virtual resource type '{name}' equivalence must " + "contain a 'criteria' list per spec." + ) + criteria = equivalence["criteria"] + if not isinstance(criteria, list) or len(criteria) == 0: + raise ValueError( + f"Virtual resource type '{name}' equivalence " + "'criteria' must be a non-empty list." + ) + for i, item in enumerate(criteria): + if not isinstance(item, str): + raise ValueError( + f"Virtual resource type '{name}' equivalence " + f"criteria[{i}] must be a string, got {type(item).__name__}." + ) + if not item.strip(): + raise ValueError( + f"Virtual resource type '{name}' equivalence " + f"criteria[{i}] must be a non-empty string." + ) + if user_addable: + raise ValueError( + f"Virtual resource type '{name}' must not be " + "user_addable (virtual types are system-managed)." + ) + if sandbox_strategy_value != "none": + raise ValueError( + f"Virtual resource type '{name}' must have sandbox_strategy 'none'." + ) + if handler is not None: + raise ValueError( + f"Virtual resource type '{name}' must not have " + "a handler (virtual types have no physical backing)." + ) + if capabilities and any( + capabilities.get(k) for k in ("read", "write", "sandbox", "checkpoint") + ): + raise ValueError( + f"Virtual resource type '{name}' must have all capabilities set to false." + ) + + +def validate_auto_discovery( + name: str, + auto_discovery: dict[str, Any], + builtin_names: frozenset[str], +) -> None: + """Validate auto-discovery configuration. + + Two schemas coexist in the same ``dict[str, Any]`` field: + - Rules-based (git/fs): ``{enabled, scan_depth, rules: [{type, pattern}]}`` + - Trigger-based (devcontainer): ``{trigger_types, scan_paths}`` + + This validator only checks rules-based keys when ``rules`` is present. + """ + rules = auto_discovery.get("rules") + if isinstance(rules, list): + for i, rule in enumerate(rules): + if not isinstance(rule, dict): + raise ValueError( + f"'{name}' auto_discovery.rules[{i}] must " + f"be a dict, got {type(rule).__name__}." + ) + rule_type = rule.get("type", "") + if not isinstance(rule_type, str) or not rule_type.strip(): + raise ValueError( + f"'{name}' auto_discovery.rules[{i}].type " + "must be a non-empty string." + ) + if not _BUILTIN_NAME_RE.match(rule_type) and not _NAMESPACED_RE.match( + rule_type + ): + raise ValueError( + f"'{name}' auto_discovery.rules[{i}].type " + f"'{rule_type}' is not a valid resource type name." + ) + if ( + "/" not in rule_type + and _BUILTIN_NAME_RE.match(rule_type) + and rule_type not in builtin_names + ): + raise ValueError( + f"'{name}' auto_discovery.rules[{i}].type " + f"'{rule_type}' is not a known built-in type." + ) + pattern = rule.get("pattern", "") + if not isinstance(pattern, str) or not pattern.strip(): + raise ValueError( + f"'{name}' auto_discovery.rules[{i}].pattern " + "must be a non-empty string." + ) + scan_depth = auto_discovery.get("scan_depth") + if scan_depth is not None: + if not isinstance(scan_depth, int) or scan_depth < 1: + raise ValueError( + f"'{name}' auto_discovery.scan_depth must be " + f"a positive integer, got {scan_depth!r}." + ) + if scan_depth > MAX_SCAN_DEPTH: + raise ValueError( + f"'{name}' auto_discovery.scan_depth={scan_depth} " + f"exceeds maximum of {MAX_SCAN_DEPTH}." + ) + + +def validate_self_referential( + name: str, + parent_types: list[str] | None, + child_types: list[str] | None, + auto_discovery: dict[str, Any] | None, +) -> None: + """Ensure self-referential types have bounded scan_depth.""" + if ( + auto_discovery is not None + and name in (parent_types or []) + and name in (child_types or []) + ): + sd = auto_discovery.get("scan_depth") + if sd is None: + raise ValueError( + f"'{name}' is self-referential (in both parent_types " + "and child_types) but has no scan_depth bound. " + "Recursive types MUST specify scan_depth." + ) diff --git a/src/cleveragents/domain/models/core/resource_type.py b/src/cleveragents/domain/models/core/resource_type.py index 3f8ccbbac..0651494c7 100644 --- a/src/cleveragents/domain/models/core/resource_type.py +++ b/src/cleveragents/domain/models/core/resource_type.py @@ -24,6 +24,19 @@ from typing import Any, ClassVar from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator +from cleveragents.domain.models.core._resource_type_validation import ( + BUILTIN_TYPE_NAMES, +) +from cleveragents.domain.models.core._resource_type_validation import ( + validate_auto_discovery as _validate_auto_discovery, +) +from cleveragents.domain.models.core._resource_type_validation import ( + validate_self_referential as _validate_self_referential, +) +from cleveragents.domain.models.core._resource_type_validation import ( + validate_virtual_type as _validate_virtual_type, +) + # --------------------------------------------------------------------------- # Patterns # --------------------------------------------------------------------------- @@ -157,97 +170,10 @@ class ResourceTypeSpec(BaseModel): ``myorg/custom-db``). """ - # Built-in resource type names (unnamespaced) - BUILTIN_NAMES: ClassVar[frozenset[str]] = frozenset( - { - # -- Git / Filesystem / Container -- - "git-checkout", - "fs-directory", - "fs-mount", - "fs-file", - "container-instance", - "devcontainer-instance", - "devcontainer-file", - "postgres", - "mysql", - "sqlite", - "duckdb", - # -- Generic cloud base types -- - "cloud-account", - "cloud-region", - "cloud-network", - "cloud-subnet", - "cloud-security-group", - "cloud-load-balancer", - "cloud-compute-instance", - "cloud-object-store", - "cloud-block-storage", - "cloud-identity-principal", - "cloud-role", - "cloud-policy", - "cloud-log-group", - "cloud-alarm", - "cloud-queue", - "cloud-topic", - "cloud-container-repo", - "cloud-container-cluster", - "cloud-container-service", - # -- AWS provider types -- - "aws-account", - "aws-region", - "aws-vpc", - "aws-subnet", - "aws-igw", - "aws-nat-gw", - "aws-route-table", - "aws-nacl", - "aws-security-group", - "aws-alb", - "aws-nlb", - "aws-target-group", - "aws-listener", - "aws-ec2-instance", - "aws-ami", - "aws-launch-template", - "aws-asg", - "aws-s3-bucket", - "aws-ebs-volume", - "aws-efs-filesystem", - "aws-iam-user", - "aws-iam-role", - "aws-iam-policy", - "aws-iam-instance-profile", - "aws-cloudwatch-log-group", - "aws-cloudwatch-alarm", - "aws-cloudwatch-metric", - "aws-eventbridge-bus", - "aws-eventbridge-rule", - "aws-eventbridge-target", - "aws-sqs-queue", - "aws-sns-topic", - "aws-sns-subscription", - "aws-ecr-repo", - "aws-ecs-cluster", - "aws-ecs-service", - "aws-ecs-task-def", - "aws-eks-cluster", - "aws-eks-nodegroup", - # -- GCP / Azure (flat, hierarchy pending) -- - "gcp", - "azure", - # Virtual core resource types (#329) - "branch", - "commit", - "directory", - "file", - "tag", - "tree", - # Deferred virtual resource types (#331) - "remote", - "submodule", - "symlink", - } - ) + # Built-in resource type names (unnamespaced). + # The actual frozenset lives in _resource_type_validation to keep + # this module under the 500-line CONTRIBUTING limit. + BUILTIN_NAMES: ClassVar[frozenset[str]] = BUILTIN_TYPE_NAMES name: str = Field( ..., @@ -284,7 +210,12 @@ class ResourceTypeSpec(BaseModel): ) auto_discovery: dict[str, Any] | None = Field( default=None, - description="Auto-discovery rules (handler-specific configuration).", + description=( + "Auto-discovery configuration. Two schemas coexist: " + "(1) rules-based {enabled, scan_depth, rules: [{type, pattern}]} " + "for git/fs types, and (2) trigger-based {trigger_types, scan_paths} " + "for devcontainer types." + ), ) equivalence: dict[str, Any] | None = Field( default=None, @@ -384,60 +315,25 @@ class ResourceTypeSpec(BaseModel): "Only built-in types can be unnamespaced." ) - # Virtual types require equivalence rules with criteria and have - # strict constraints: no sandbox, not user-addable, no handler, - # all capabilities false. + # Virtual type constraints (equivalence, no sandbox, etc.) if self.resource_kind == ResourceKind.VIRTUAL: - if self.equivalence is None: - raise ValueError( - f"Virtual resource type '{self.name}' requires an " - "'equivalence' configuration for deduplication." - ) - if "criteria" not in self.equivalence: - raise ValueError( - f"Virtual resource type '{self.name}' equivalence must " - "contain a 'criteria' list per spec." - ) - criteria = self.equivalence["criteria"] - if not isinstance(criteria, list) or len(criteria) == 0: - raise ValueError( - f"Virtual resource type '{self.name}' equivalence " - "'criteria' must be a non-empty list." - ) - for i, item in enumerate(criteria): - if not isinstance(item, str): - raise ValueError( - f"Virtual resource type '{self.name}' equivalence " - f"criteria[{i}] must be a string, got {type(item).__name__}." - ) - if not item.strip(): - raise ValueError( - f"Virtual resource type '{self.name}' equivalence " - f"criteria[{i}] must be a non-empty string." - ) - if self.user_addable: - raise ValueError( - f"Virtual resource type '{self.name}' must not be " - "user_addable (virtual types are system-managed)." - ) - if self.sandbox_strategy != SandboxStrategy.NONE: - raise ValueError( - f"Virtual resource type '{self.name}' must have " - "sandbox_strategy 'none'." - ) - if self.handler is not None: - raise ValueError( - f"Virtual resource type '{self.name}' must not have " - "a handler (virtual types have no physical backing)." - ) - caps = self.capabilities - if caps and any( - caps.get(k) for k in ("read", "write", "sandbox", "checkpoint") - ): - raise ValueError( - f"Virtual resource type '{self.name}' must have all " - "capabilities set to false." - ) + _validate_virtual_type( + self.name, + self.equivalence, + self.user_addable, + self.sandbox_strategy.value, + self.handler, + self.capabilities, + ) + + # Auto-discovery validation (rules-based and trigger-based schemas) + if self.auto_discovery is not None: + _validate_auto_discovery(self.name, self.auto_discovery, self.BUILTIN_NAMES) + + # Self-referential types must have bounded scan_depth + _validate_self_referential( + self.name, self.parent_types, self.child_types, self.auto_discovery + ) # ADR-042 rule 3: no cycles (self-inheritance) if self.inherits is not None and self.inherits == self.name: @@ -555,7 +451,11 @@ class ResourceTypeSpec(BaseModel): result["capabilities"] = dict(self.capabilities) if self.auto_discovery is not None: - result["auto_discovery"] = dict(self.auto_discovery) + # Deep copy — two schemas coexist: rules-based (list[dict]) + # and trigger-based (list[str]). copy.deepcopy handles both. + import copy + + result["auto_discovery"] = copy.deepcopy(self.auto_discovery) if self.equivalence is not None: result["equivalence"] = { k: list(v) if isinstance(v, list) else v