feat(resource): add deferred physical resource types #662
@@ -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`
|
||||
|
||||
@@ -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()
|
||||
@@ -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`.
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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 ────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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)}"
|
||||
@@ -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}
|
||||
|
||||
@@ -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 ***
|
||||
@@ -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),
|
||||
},
|
||||
]
|
||||
@@ -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,
|
||||
]
|
||||
|
||||
@@ -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,
|
||||
|
brent.edwards
commented
P1-4/5 (still open): **P1-4/5 (still open):** `scan_depth: 3` on the `git` type. All 5 discovery rules (`refs/remotes/*`, `refs/heads/*`, `refs/tags/*`, `refs/stash`, `.gitmodules`) match direct children of the git namespace — depth 1 is sufficient. Depth 3 means recursively discovering children-of-children-of-children for every branch/tag/remote, which is unnecessary and expensive on repos with many refs.
|
||||
"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).",
|
||||
|
brent.edwards
commented
P1: P1: `scan_depth: 3` here for `git-branch` but the only discovery rule is `{"type": "git-commit", "pattern": "HEAD"}`. A branch has one HEAD — `scan_depth: 1` is sufficient. The current value won't cause bugs but is misleading about traversal depth.
|
||||
"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",
|
||||
|
brent.edwards
commented
P2: P2: `git-tree` is self-referential (`parent_types` and `child_types` both contain `git-tree`). The model's cycle detection (ADR-042 rule 3) only checks `inherits`-based self-loops. Consider adding a model-level guard: if `name in parent_types and name in child_types`, require `scan_depth` to be present and <= a configurable max.
|
||||
"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,
|
||||
},
|
||||
},
|
||||
]
|
||||
@@ -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.
|
||||
|
||||
@@ -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."
|
||||
)
|
||||
@@ -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
|
||||
|
||||
P3:
"enabled": Trueis set on every auto_discovery config but no code reads it. Either add a consumer or remove it to avoid misleading future contributors.