intentframe-executor 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- executor/__init__.py +24 -0
- executor/config/__init__.py +106 -0
- executor/config/executor.yaml +156 -0
- executor/config/schema.py +223 -0
- executor/dispatch.py +122 -0
- executor/gateway.py +399 -0
- executor/main.py +284 -0
- executor/server.py +175 -0
- executor/worker_pool.py +174 -0
- intentframe_executor-0.1.0.dist-info/METADATA +37 -0
- intentframe_executor-0.1.0.dist-info/RECORD +13 -0
- intentframe_executor-0.1.0.dist-info/WHEEL +4 -0
- intentframe_executor-0.1.0.dist-info/licenses/LICENSE +661 -0
executor/__init__.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""
|
|
2
|
+
IntentFrame Executor - A Standalone, Protocol-Driven Capability Service.
|
|
3
|
+
|
|
4
|
+
"The Hands" -- the only entity that touches the real world.
|
|
5
|
+
|
|
6
|
+
The Executor is an OS Capability Bridge that runs as a standalone,
|
|
7
|
+
process-isolated service. It communicates exclusively through validated
|
|
8
|
+
protocols (gRPC, REST, Unix socket) and secure channels.
|
|
9
|
+
|
|
10
|
+
Core Invariants:
|
|
11
|
+
1. Fail-Closed: Any failure -> rejection, never silent approval
|
|
12
|
+
2. Authorization Required: Every request must carry valid auth proof
|
|
13
|
+
3. Credentials Never Leave: Creds stay in executor process memory
|
|
14
|
+
4. Virtual Paths Only: Agents see virtual paths, never real paths
|
|
15
|
+
|
|
16
|
+
Architecture:
|
|
17
|
+
Transport Layer -> How requests arrive (pluggable)
|
|
18
|
+
Auth Layer -> How authorization is verified (pluggable)
|
|
19
|
+
Gateway -> Orchestration: verify -> validate -> route -> execute
|
|
20
|
+
Adapter Layer -> How capabilities are executed (pluggable per platform)
|
|
21
|
+
Services Layer -> Cross-cutting: audit, credentials, state, VFS
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration loading and validation for the IntentFrame Executor.
|
|
3
|
+
|
|
4
|
+
The executor is fully config-driven. A single executor.yaml file
|
|
5
|
+
determines which transport, auth verifier, credential backend,
|
|
6
|
+
and adapter set to load at startup.
|
|
7
|
+
|
|
8
|
+
No code changes needed to switch deployment profiles.
|
|
9
|
+
Just change the config file.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
import yaml
|
|
17
|
+
from pydantic import ValidationError
|
|
18
|
+
|
|
19
|
+
from executor.config.schema import ExecutorConfig
|
|
20
|
+
from executor_sdk.constants import DEFAULT_CONFIG_FILENAME
|
|
21
|
+
from executor_sdk.exceptions import ConfigurationError
|
|
22
|
+
|
|
23
|
+
__all__ = ["ExecutorConfig", "load_config"]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def load_config(
|
|
27
|
+
config_path: str | Path | None = None,
|
|
28
|
+
config_dict: dict | None = None,
|
|
29
|
+
) -> ExecutorConfig:
|
|
30
|
+
"""Load and validate executor configuration.
|
|
31
|
+
|
|
32
|
+
Accepts either a file path to a YAML config or a pre-loaded dict.
|
|
33
|
+
The config is validated against the Pydantic schema and returned
|
|
34
|
+
as a typed ExecutorConfig object.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
config_path: Path to executor.yaml. If None and config_dict is None,
|
|
38
|
+
searches default locations.
|
|
39
|
+
config_dict: Pre-loaded config dict (e.g., from tests). Takes
|
|
40
|
+
priority over config_path if both are provided.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
Validated ExecutorConfig.
|
|
44
|
+
|
|
45
|
+
Raises:
|
|
46
|
+
ConfigurationError: If the config file is missing, unreadable,
|
|
47
|
+
or fails schema validation.
|
|
48
|
+
"""
|
|
49
|
+
if config_dict is not None:
|
|
50
|
+
raw = config_dict
|
|
51
|
+
elif config_path is not None:
|
|
52
|
+
raw = _load_yaml(Path(config_path))
|
|
53
|
+
else:
|
|
54
|
+
raw = _load_yaml(_find_default_config())
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
return ExecutorConfig.model_validate(raw)
|
|
58
|
+
except ValidationError as exc:
|
|
59
|
+
raise ConfigurationError(
|
|
60
|
+
f"Invalid executor configuration: {exc.error_count()} validation errors",
|
|
61
|
+
details={"errors": exc.errors()},
|
|
62
|
+
) from exc
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _load_yaml(path: Path) -> dict:
|
|
66
|
+
"""Load a YAML file and return as dict."""
|
|
67
|
+
if not path.exists():
|
|
68
|
+
raise ConfigurationError(f"Config file not found: {path}")
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
with open(path) as f:
|
|
72
|
+
data = yaml.safe_load(f)
|
|
73
|
+
except yaml.YAMLError as exc:
|
|
74
|
+
raise ConfigurationError(f"Invalid YAML in {path}: {exc}") from exc
|
|
75
|
+
|
|
76
|
+
if not isinstance(data, dict):
|
|
77
|
+
raise ConfigurationError(f"Config file must be a YAML mapping: {path}")
|
|
78
|
+
|
|
79
|
+
return data
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _find_default_config() -> Path:
|
|
83
|
+
"""Search for executor.yaml in default locations.
|
|
84
|
+
|
|
85
|
+
Search order:
|
|
86
|
+
1. Current working directory
|
|
87
|
+
2. executor/config/ (relative to package)
|
|
88
|
+
3. ~/.config/intentframe/
|
|
89
|
+
|
|
90
|
+
Returns the first path that exists.
|
|
91
|
+
Raises ConfigurationError if none found.
|
|
92
|
+
"""
|
|
93
|
+
candidates = [
|
|
94
|
+
Path.cwd() / DEFAULT_CONFIG_FILENAME,
|
|
95
|
+
Path(__file__).parent / DEFAULT_CONFIG_FILENAME,
|
|
96
|
+
Path.home() / ".config" / "intentframe" / DEFAULT_CONFIG_FILENAME,
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
for path in candidates:
|
|
100
|
+
if path.exists():
|
|
101
|
+
return path
|
|
102
|
+
|
|
103
|
+
locations = "\n ".join(str(p) for p in candidates)
|
|
104
|
+
raise ConfigurationError(
|
|
105
|
+
f"No {DEFAULT_CONFIG_FILENAME} found. Searched:\n {locations}"
|
|
106
|
+
)
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
2
|
+
# IntentFrame Executor Configuration
|
|
3
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
4
|
+
#
|
|
5
|
+
# This file configures the executor service for a specific deployment.
|
|
6
|
+
# Only three things change between deployments:
|
|
7
|
+
# 1. Transport protocol (how requests arrive)
|
|
8
|
+
# 2. Auth verification (how authorization is checked)
|
|
9
|
+
# 3. Credential backend (where secrets are stored)
|
|
10
|
+
#
|
|
11
|
+
# Everything else (gateway logic, adapter pattern, audit trail) is the same.
|
|
12
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
13
|
+
|
|
14
|
+
# ─── Executor packs: which implementations to load ───────────────────────────
|
|
15
|
+
# Packs are explicit and required -- the executor ships no built-in or
|
|
16
|
+
# platform-default packs. Each pack registers transport / auth / credential /
|
|
17
|
+
# storage / adapter implementations. List the packs your deployment needs, in
|
|
18
|
+
# order. An entry is either an importable module path exposing register_all(),
|
|
19
|
+
# or the short name of a pack advertised under the 'intentframe.executor_packs'
|
|
20
|
+
# entry-point group (how a third-party org plugs in its own pack).
|
|
21
|
+
#
|
|
22
|
+
# First-party packs available in this repo:
|
|
23
|
+
# intentframe_native_kit.intentframe_executor_pack_posix portable base (files VFS, sqlite, hmac, uds)
|
|
24
|
+
# intentframe_native_kit.intentframe_executor_pack_console console_user_io + simulated_user_io (headless)
|
|
25
|
+
# intentframe_native_kit.intentframe_executor_pack_macos native macOS adapters (+ portable base)
|
|
26
|
+
packs:
|
|
27
|
+
- intentframe_native_kit.intentframe_executor_pack_posix
|
|
28
|
+
|
|
29
|
+
# ─── Transport: How requests arrive ──────────────────────────────────────────
|
|
30
|
+
# Types: unix_socket (device default), grpc (cloud/device), rest (admin/debug)
|
|
31
|
+
transport:
|
|
32
|
+
type: unix_socket
|
|
33
|
+
options:
|
|
34
|
+
socket_path: /tmp/intentframe/executor.sock
|
|
35
|
+
# ── gRPC options (when type: grpc) ──
|
|
36
|
+
# host: "0.0.0.0"
|
|
37
|
+
# port: 50051
|
|
38
|
+
# ── REST options (when type: rest) ──
|
|
39
|
+
# host: "0.0.0.0"
|
|
40
|
+
# port: 8080
|
|
41
|
+
|
|
42
|
+
# ─── Auth: How authorization is verified ─────────────────────────────────────
|
|
43
|
+
# Types: guardian_hmac (IntentFrame default), mtls (cloud), bearer (admin/CI)
|
|
44
|
+
auth:
|
|
45
|
+
type: guardian_hmac
|
|
46
|
+
options: {}
|
|
47
|
+
# ── guardian_hmac options ──
|
|
48
|
+
# secret_key_ref: "intentframe/guardian-hmac-key"
|
|
49
|
+
# ── mtls options ──
|
|
50
|
+
# ca_cert: "/path/to/ca.pem"
|
|
51
|
+
# ── bearer options ──
|
|
52
|
+
# issuer: "https://auth.intentframe.com"
|
|
53
|
+
# audience: "executor"
|
|
54
|
+
|
|
55
|
+
# ─── Credentials: Where secrets are stored ───────────────────────────────────
|
|
56
|
+
# Backends: service (vault service over UDS, default), keyring (OS native),
|
|
57
|
+
# hashicorp (HashiCorp Vault KV v2, headless/cloud), env (dev/test ONLY)
|
|
58
|
+
credentials:
|
|
59
|
+
backend: service
|
|
60
|
+
options: {}
|
|
61
|
+
# ── keyring options (if backend: keyring) ──
|
|
62
|
+
# Uses OS-native keyring directly, bypassing the vault service.
|
|
63
|
+
#
|
|
64
|
+
# ── service options (default) ──
|
|
65
|
+
# Talks to the supervisor-managed vault over UDS.
|
|
66
|
+
# socket_path: "~/.intentframe/run/credential-vault.sock"
|
|
67
|
+
#
|
|
68
|
+
# ── hashicorp options (if backend: hashicorp) ──
|
|
69
|
+
# Stores secrets in HashiCorp Vault KV v2. Options override env vars
|
|
70
|
+
# (VAULT_ADDR, VAULT_TOKEN / VAULT_ROLE_ID+VAULT_SECRET_ID, etc.).
|
|
71
|
+
# addr: "https://vault.mycorp.com:8200"
|
|
72
|
+
# kv_mount: "secret"
|
|
73
|
+
# path_prefix: "intentframe"
|
|
74
|
+
# The vault SERVICE itself selects storage via IF_VAULT_BACKEND=hashicorp.
|
|
75
|
+
|
|
76
|
+
# ─── Worker Pool: Concurrency limits ────────────────────────────────────────
|
|
77
|
+
worker_pool:
|
|
78
|
+
max_workers: 4
|
|
79
|
+
default_timeout_seconds: 30.0
|
|
80
|
+
|
|
81
|
+
# ─── Adapters: Which capabilities to load ────────────────────────────────────
|
|
82
|
+
# Each ID must be registered by a platform-specific adapter module.
|
|
83
|
+
# Uncomment the adapters available on your platform.
|
|
84
|
+
adapters:
|
|
85
|
+
enabled: []
|
|
86
|
+
# ── macOS adapters ──
|
|
87
|
+
# - files
|
|
88
|
+
# - terminal
|
|
89
|
+
# - http_api
|
|
90
|
+
# - user_io
|
|
91
|
+
# - mail
|
|
92
|
+
# - calendar
|
|
93
|
+
# - contacts
|
|
94
|
+
# - notes
|
|
95
|
+
# - reminders
|
|
96
|
+
# - browser
|
|
97
|
+
# - messages
|
|
98
|
+
# - notifications
|
|
99
|
+
# - system
|
|
100
|
+
# - clipboard
|
|
101
|
+
# - shortcuts
|
|
102
|
+
# - spotlight
|
|
103
|
+
# - filesystem_watch
|
|
104
|
+
|
|
105
|
+
# ── Cloud adapters ──
|
|
106
|
+
# - s3
|
|
107
|
+
# - ses
|
|
108
|
+
# - lambda_invoke
|
|
109
|
+
# - cloud_storage
|
|
110
|
+
|
|
111
|
+
# ─── Executor pack options ──────────────────────────────────────────────────
|
|
112
|
+
# Opaque config slices owned and validated by executor packs.
|
|
113
|
+
pack_options:
|
|
114
|
+
# VFS mount table for the files adapter.
|
|
115
|
+
# List virtual-to-real path mappings here when the ``files`` adapter is enabled.
|
|
116
|
+
# files:
|
|
117
|
+
# base_path: null # base for relative real_path values; null = home dir
|
|
118
|
+
# mounts:
|
|
119
|
+
# - virtual_path: /home/
|
|
120
|
+
# real_path: ~/
|
|
121
|
+
# writable: true
|
|
122
|
+
# workspace_id: null # optional: resolve mounts from resource registry instead
|
|
123
|
+
|
|
124
|
+
# Real-path allowlist for host-file tools.
|
|
125
|
+
# Empty lists + no ``host_files`` adapter means "no host-file access".
|
|
126
|
+
host_files:
|
|
127
|
+
allowed_read_paths: []
|
|
128
|
+
allowed_write_paths: []
|
|
129
|
+
|
|
130
|
+
# Kernel-enforced sandbox for shell commands (macOS Seatbelt / sandbox-exec).
|
|
131
|
+
# Write scope is controlled by allowed_write_paths below (independent of VFS).
|
|
132
|
+
# All commands run under the highest-privilege template in allowed_templates.
|
|
133
|
+
# If the sandbox engine is unavailable, every RUN_COMMAND is rejected (fail-closed).
|
|
134
|
+
sandbox:
|
|
135
|
+
enabled: true
|
|
136
|
+
working_directory: ~/
|
|
137
|
+
allowed_write_paths:
|
|
138
|
+
- ~/
|
|
139
|
+
allowed_templates:
|
|
140
|
+
- pure_compute
|
|
141
|
+
- file_read_only
|
|
142
|
+
- file_read_write
|
|
143
|
+
# - network_outbound # uncomment to allow outbound network in sandboxed commands
|
|
144
|
+
# - network_full # uncomment to allow port binding
|
|
145
|
+
# - unrestricted # NOT recommended — bypasses most sandbox restrictions
|
|
146
|
+
|
|
147
|
+
# ─── Storage: Database and log paths ────────────────────────────────────────
|
|
148
|
+
# null = platform default (macOS: ~/Library/Application Support/IntentFrame/)
|
|
149
|
+
storage:
|
|
150
|
+
database_path: null
|
|
151
|
+
log_path: null
|
|
152
|
+
|
|
153
|
+
# ─── Logging ─────────────────────────────────────────────────────────────────
|
|
154
|
+
logging:
|
|
155
|
+
level: INFO
|
|
156
|
+
format: json # json (structured) or console (human-readable)
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pydantic schema for executor.yaml configuration.
|
|
3
|
+
|
|
4
|
+
Every configurable aspect of the executor is represented here.
|
|
5
|
+
The schema provides:
|
|
6
|
+
- Type validation (catch typos at startup, not at 3am)
|
|
7
|
+
- Default values (sensible defaults for common deployments)
|
|
8
|
+
- Documentation (field descriptions serve as config docs)
|
|
9
|
+
|
|
10
|
+
The config is intentionally flat where possible. Nesting is used
|
|
11
|
+
only for logical grouping (transport, auth, etc.), not for
|
|
12
|
+
hierarchy's sake.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
20
|
+
|
|
21
|
+
from executor_sdk.constants import (
|
|
22
|
+
DEFAULT_ADAPTER_TIMEOUT,
|
|
23
|
+
DEFAULT_GRPC_PORT,
|
|
24
|
+
DEFAULT_MAX_WORKERS,
|
|
25
|
+
DEFAULT_REST_PORT,
|
|
26
|
+
DEFAULT_UNIX_SOCKET_PATH,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"ExecutorConfig",
|
|
31
|
+
"TransportConfig",
|
|
32
|
+
"AuthConfig",
|
|
33
|
+
"CredentialConfig",
|
|
34
|
+
"WorkerPoolConfig",
|
|
35
|
+
"AdapterConfig",
|
|
36
|
+
"StorageConfig",
|
|
37
|
+
"LoggingConfig",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class TransportConfig(BaseModel):
|
|
42
|
+
"""Configuration for the transport layer.
|
|
43
|
+
|
|
44
|
+
Only ONE transport is active per executor instance.
|
|
45
|
+
|
|
46
|
+
Types:
|
|
47
|
+
unix_socket: Local IPC (device default)
|
|
48
|
+
grpc: Cross-machine, high-performance
|
|
49
|
+
rest: Admin tools, debugging, broad compatibility
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
model_config = ConfigDict(extra="forbid")
|
|
53
|
+
|
|
54
|
+
type: str = Field(
|
|
55
|
+
default="unix_socket",
|
|
56
|
+
description="Transport type: unix_socket, grpc, rest",
|
|
57
|
+
)
|
|
58
|
+
options: dict[str, Any] = Field(
|
|
59
|
+
default_factory=lambda: {"socket_path": DEFAULT_UNIX_SOCKET_PATH},
|
|
60
|
+
description="Transport-specific configuration options",
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class AuthConfig(BaseModel):
|
|
65
|
+
"""Configuration for authorization verification.
|
|
66
|
+
|
|
67
|
+
Only ONE auth verifier is active per executor instance.
|
|
68
|
+
|
|
69
|
+
Types:
|
|
70
|
+
guardian_hmac: IntentFrame default (HMAC signature from Guardian)
|
|
71
|
+
mtls: Mutual TLS certificate verification (cloud)
|
|
72
|
+
bearer: JWT or opaque bearer token (admin/CI)
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
model_config = ConfigDict(extra="forbid")
|
|
76
|
+
|
|
77
|
+
type: str = Field(
|
|
78
|
+
default="guardian_hmac",
|
|
79
|
+
description="Auth verifier type: guardian_hmac, mtls, bearer",
|
|
80
|
+
)
|
|
81
|
+
options: dict[str, Any] = Field(
|
|
82
|
+
default_factory=dict,
|
|
83
|
+
description="Auth-specific configuration options",
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class CredentialConfig(BaseModel):
|
|
88
|
+
"""Configuration for the credential vault backend.
|
|
89
|
+
|
|
90
|
+
Backends:
|
|
91
|
+
service: Vault service over UDS (default — uses the supervisor-managed vault)
|
|
92
|
+
keyring: OS native keyring directly (macOS Keychain, Windows Credential Locker)
|
|
93
|
+
hashicorp: HashiCorp Vault KV v2 over HTTP (headless / cloud / on-prem)
|
|
94
|
+
env: Environment variables (development/testing ONLY)
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
model_config = ConfigDict(extra="forbid")
|
|
98
|
+
|
|
99
|
+
backend: str = Field(
|
|
100
|
+
default="service",
|
|
101
|
+
description="Credential backend: service, keyring, hashicorp, env",
|
|
102
|
+
)
|
|
103
|
+
options: dict[str, Any] = Field(
|
|
104
|
+
default_factory=dict,
|
|
105
|
+
description="Backend-specific configuration options",
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class WorkerPoolConfig(BaseModel):
|
|
110
|
+
"""Configuration for the capability worker pool."""
|
|
111
|
+
|
|
112
|
+
model_config = ConfigDict(extra="forbid")
|
|
113
|
+
|
|
114
|
+
max_workers: int = Field(
|
|
115
|
+
default=DEFAULT_MAX_WORKERS,
|
|
116
|
+
ge=1,
|
|
117
|
+
le=32,
|
|
118
|
+
description="Maximum concurrent adapter executions",
|
|
119
|
+
)
|
|
120
|
+
default_timeout_seconds: float = Field(
|
|
121
|
+
default=DEFAULT_ADAPTER_TIMEOUT,
|
|
122
|
+
gt=0,
|
|
123
|
+
description="Default timeout per adapter execution (seconds)",
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class AdapterConfig(BaseModel):
|
|
128
|
+
"""Configuration for capability adapters.
|
|
129
|
+
|
|
130
|
+
The enabled list determines which adapters are loaded at startup.
|
|
131
|
+
Each adapter ID must be registered via register_adapter() by
|
|
132
|
+
a platform-specific module.
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
model_config = ConfigDict(extra="forbid")
|
|
136
|
+
|
|
137
|
+
enabled: list[str] = Field(
|
|
138
|
+
default_factory=list,
|
|
139
|
+
description="List of adapter IDs to load at startup",
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class StorageConfig(BaseModel):
|
|
144
|
+
"""Configuration for database, log file paths, and backend selection.
|
|
145
|
+
|
|
146
|
+
If paths are null/None, platform-specific defaults are used:
|
|
147
|
+
macOS: ~/Library/Application Support/IntentFrame/
|
|
148
|
+
Linux: ~/.local/share/intentframe/
|
|
149
|
+
Cloud: /var/lib/intentframe/
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
model_config = ConfigDict(extra="forbid")
|
|
153
|
+
|
|
154
|
+
audit_backend: str = Field(
|
|
155
|
+
default="sqlite",
|
|
156
|
+
description="Audit log backend: sqlite, cloud",
|
|
157
|
+
)
|
|
158
|
+
state_backend: str = Field(
|
|
159
|
+
default="sqlite",
|
|
160
|
+
description="State store backend: sqlite, redis",
|
|
161
|
+
)
|
|
162
|
+
database_path: str | None = Field(
|
|
163
|
+
default=None,
|
|
164
|
+
description="Path to SQLite database. None = platform default.",
|
|
165
|
+
)
|
|
166
|
+
log_path: str | None = Field(
|
|
167
|
+
default=None,
|
|
168
|
+
description="Path to log file. None = platform default.",
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class LoggingConfig(BaseModel):
|
|
173
|
+
"""Configuration for structured logging."""
|
|
174
|
+
|
|
175
|
+
model_config = ConfigDict(extra="forbid")
|
|
176
|
+
|
|
177
|
+
level: str = Field(
|
|
178
|
+
default="INFO",
|
|
179
|
+
description="Log level: DEBUG, INFO, WARNING, ERROR, CRITICAL",
|
|
180
|
+
)
|
|
181
|
+
format: str = Field(
|
|
182
|
+
default="json",
|
|
183
|
+
description="Log format: json (structured) or console (human-readable)",
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class ExecutorConfig(BaseModel):
|
|
188
|
+
"""Root configuration schema for the IntentFrame Executor.
|
|
189
|
+
|
|
190
|
+
Loaded from executor.yaml and validated at startup.
|
|
191
|
+
Any validation error prevents the executor from starting (fail-closed).
|
|
192
|
+
"""
|
|
193
|
+
|
|
194
|
+
model_config = ConfigDict(extra="forbid")
|
|
195
|
+
|
|
196
|
+
packs: list[str] = Field(
|
|
197
|
+
default_factory=list,
|
|
198
|
+
description=(
|
|
199
|
+
"Executor packs to load at startup, in order (required -- there are "
|
|
200
|
+
"no built-in or platform-default packs). Each entry is either an "
|
|
201
|
+
"importable module path exposing register_all() (e.g. "
|
|
202
|
+
"'intentframe_native_kit.intentframe_executor_pack_posix'), or the short name of a pack "
|
|
203
|
+
"advertised via the 'intentframe.executor_packs' entry-point group "
|
|
204
|
+
"(e.g. an external org's installed pack). Packs register the "
|
|
205
|
+
"transport, auth, credential, storage and adapter implementations "
|
|
206
|
+
"this executor uses, so at least one base pack is required."
|
|
207
|
+
),
|
|
208
|
+
)
|
|
209
|
+
transport: TransportConfig = Field(default_factory=TransportConfig)
|
|
210
|
+
auth: AuthConfig = Field(default_factory=AuthConfig)
|
|
211
|
+
credentials: CredentialConfig = Field(default_factory=CredentialConfig)
|
|
212
|
+
worker_pool: WorkerPoolConfig = Field(default_factory=WorkerPoolConfig)
|
|
213
|
+
adapters: AdapterConfig = Field(default_factory=AdapterConfig)
|
|
214
|
+
pack_options: dict[str, dict[str, Any]] = Field(
|
|
215
|
+
default_factory=dict,
|
|
216
|
+
description=(
|
|
217
|
+
"Opaque executor-pack/adaptor options keyed by pack-owned feature "
|
|
218
|
+
"or adapter ID. Core validates only that each slice is a mapping; "
|
|
219
|
+
"packs own their own schema validation."
|
|
220
|
+
),
|
|
221
|
+
)
|
|
222
|
+
storage: StorageConfig = Field(default_factory=StorageConfig)
|
|
223
|
+
logging: LoggingConfig = Field(default_factory=LoggingConfig)
|
executor/dispatch.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Action dispatcher -- routes action types to capability adapters.
|
|
3
|
+
|
|
4
|
+
The dispatcher maintains a registry of action_type -> CapabilityAdapter
|
|
5
|
+
mappings. When the gateway receives an ExecutionRequest, it asks the
|
|
6
|
+
dispatcher to resolve the action_type to the correct adapter.
|
|
7
|
+
|
|
8
|
+
Design:
|
|
9
|
+
- Simple dict-based dispatch. No magic, no reflection.
|
|
10
|
+
- Each action type maps to exactly ONE adapter.
|
|
11
|
+
- Action types must be globally unique across all registered adapters.
|
|
12
|
+
- Unknown action types cause immediate rejection (fail-closed).
|
|
13
|
+
- Adapters register their supported actions at startup.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import logging
|
|
19
|
+
|
|
20
|
+
from executor_sdk.adapters.base import CapabilityAdapter
|
|
21
|
+
from executor_sdk.exceptions import AdapterNotFoundError
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
__all__ = ["ActionDispatcher"]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ActionDispatcher:
|
|
29
|
+
"""Routes action types to their registered capability adapters.
|
|
30
|
+
|
|
31
|
+
Usage:
|
|
32
|
+
dispatcher = ActionDispatcher()
|
|
33
|
+
dispatcher.register(mail_adapter)
|
|
34
|
+
dispatcher.register(files_adapter)
|
|
35
|
+
|
|
36
|
+
adapter = dispatcher.resolve("SEND_EMAIL")
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(self) -> None:
|
|
40
|
+
self._action_to_adapter: dict[str, CapabilityAdapter] = {}
|
|
41
|
+
self._adapter_registry: dict[str, CapabilityAdapter] = {}
|
|
42
|
+
|
|
43
|
+
def register(self, adapter: CapabilityAdapter) -> None:
|
|
44
|
+
"""Register an adapter and all its supported actions.
|
|
45
|
+
|
|
46
|
+
Each action type in adapter.supported_actions() is mapped to
|
|
47
|
+
this adapter. Duplicate action types raise ValueError to catch
|
|
48
|
+
configuration errors at startup (not at runtime).
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
adapter: The adapter instance to register.
|
|
52
|
+
|
|
53
|
+
Raises:
|
|
54
|
+
ValueError: If any action type is already registered
|
|
55
|
+
to a different adapter.
|
|
56
|
+
"""
|
|
57
|
+
manifest = adapter.manifest()
|
|
58
|
+
adapter_id = manifest.adapter_id
|
|
59
|
+
|
|
60
|
+
for action in adapter.supported_actions():
|
|
61
|
+
existing = self._action_to_adapter.get(action)
|
|
62
|
+
if existing is not None:
|
|
63
|
+
existing_id = existing.manifest().adapter_id
|
|
64
|
+
if existing_id != adapter_id:
|
|
65
|
+
raise ValueError(
|
|
66
|
+
f"Action '{action}' is already registered to adapter "
|
|
67
|
+
f"'{existing_id}'. Cannot register to '{adapter_id}'. "
|
|
68
|
+
f"Each action type must map to exactly one adapter."
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
self._action_to_adapter[action] = adapter
|
|
72
|
+
|
|
73
|
+
self._adapter_registry[adapter_id] = adapter
|
|
74
|
+
|
|
75
|
+
logger.info(
|
|
76
|
+
"Registered adapter: id=%s actions=%s",
|
|
77
|
+
adapter_id,
|
|
78
|
+
adapter.supported_actions(),
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
def resolve(self, action_type: str) -> CapabilityAdapter:
|
|
82
|
+
"""Resolve an action type to its registered adapter.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
action_type: The action to look up (e.g., "SEND_EMAIL").
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
The CapabilityAdapter registered for this action.
|
|
89
|
+
|
|
90
|
+
Raises:
|
|
91
|
+
AdapterNotFoundError: If no adapter handles this action type.
|
|
92
|
+
"""
|
|
93
|
+
adapter = self._action_to_adapter.get(action_type)
|
|
94
|
+
if adapter is None:
|
|
95
|
+
registered = ", ".join(sorted(self._action_to_adapter)) or "(none)"
|
|
96
|
+
raise AdapterNotFoundError(
|
|
97
|
+
f"No adapter registered for action: '{action_type}'. "
|
|
98
|
+
f"Registered actions: {registered}",
|
|
99
|
+
)
|
|
100
|
+
return adapter
|
|
101
|
+
|
|
102
|
+
def get_adapter(self, adapter_id: str) -> CapabilityAdapter | None:
|
|
103
|
+
"""Look up an adapter by its ID. Returns None if not found."""
|
|
104
|
+
return self._adapter_registry.get(adapter_id)
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def registered_actions(self) -> list[str]:
|
|
108
|
+
"""All action types that have a registered adapter."""
|
|
109
|
+
return sorted(self._action_to_adapter.keys())
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def registered_adapters(self) -> list[str]:
|
|
113
|
+
"""All registered adapter IDs."""
|
|
114
|
+
return sorted(self._adapter_registry.keys())
|
|
115
|
+
|
|
116
|
+
def __contains__(self, action_type: str) -> bool:
|
|
117
|
+
"""Check if an action type is registered."""
|
|
118
|
+
return action_type in self._action_to_adapter
|
|
119
|
+
|
|
120
|
+
def __len__(self) -> int:
|
|
121
|
+
"""Number of registered action types."""
|
|
122
|
+
return len(self._action_to_adapter)
|