feat(resource): add deferred physical resource types #662

Merged
hamza.khyari merged 1 commits from feature/post-resource-types-physical into master 2026-03-19 14:12:44 +00:00
27 changed files with 2508 additions and 720 deletions
+7
View File
@@ -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()
+54
View File
@@ -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`.
+8 -3
View File
@@ -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"
+16
View File
@@ -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"
+16
View File
@@ -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"
+22
View File
@@ -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"
+24
View File
@@ -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"
+16
View File
@@ -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"
+16
View File
@@ -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"
+18
View File
@@ -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"
+25
View File
@@ -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"
+44
View File
@@ -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"
+1 -2
View File
@@ -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
-5
View File
@@ -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)}"
+1 -1
View File
@@ -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}
+101
View File
@@ -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)
# ---------------------------------------------------------------------------
1
@@ -149,6 +90,7 @@ _GIT_FS_CONTAINER_TYPES: list[dict[str, Any]] = [
],
"parent_types": [],
"child_types": [
"git",
"fs-directory",
"fs-file",
"devcontainer-instance",
1
@@ -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,
3
@@ -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": {
Outdated
Review

P3: "enabled": True is set on every auto_discovery config but no code reads it. Either add a consumer or remove it to avoid misleading future contributors.

P3: `"enabled": True` is set on every auto_discovery config but no code reads it. Either add a consumer or remove it to avoid misleading future contributors.
"enabled": True,
"scan_depth": 1,
Outdated
Review

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.

**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).",
Outdated
Review

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.

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",
Outdated
Review

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.

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(
...,
1
@@ -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:
1
@@ -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
1