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/server.py ADDED
@@ -0,0 +1,175 @@
1
+ """
2
+ Executor -- FastAPI server on Unix Domain Socket.
3
+
4
+ Wraps the ExecutorGateway with HTTP endpoints, consistent with
5
+ the other IntentFrame services. Replaces the raw length-prefixed
6
+ Unix socket transport with standard HTTP.
7
+
8
+ Endpoints:
9
+ POST /execute -- gateway.handle(request)
10
+ POST /rollback -- gateway.handle_rollback(rollback_id)
11
+ GET /health -- health check
12
+
13
+ Startup:
14
+ uvicorn executor.server:app --uds ~/.intentframe/run/executor.sock
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import logging
20
+ import os
21
+ from contextlib import asynccontextmanager
22
+ from typing import Any
23
+
24
+ from fastapi import FastAPI, HTTPException
25
+ from pydantic import BaseModel
26
+
27
+ from executor.config import load_config
28
+ from executor_sdk.exceptions import ConfigurationError
29
+ from executor_sdk.packs import ENTRY_POINT_GROUP
30
+ from executor.main import build_gateway
31
+ from executor_sdk.models import (
32
+ ExecutionRequest,
33
+ ExecutionResult,
34
+ )
35
+
36
+ logger = logging.getLogger(__name__)
37
+
38
+ _gateway = None
39
+ _worker_pool = None
40
+
41
+
42
+ def _load_pack(ref: str) -> None:
43
+ """Load one executor pack by entry-point name or importable module path.
44
+
45
+ A pack is anything exposing a module-level ``register_all()`` (see
46
+ ``executor_sdk.packs``). There are no built-in or platform-default packs;
47
+ deployments wire packs purely through ``packs:`` in executor.yaml.
48
+
49
+ Resolution order for each ``ref``:
50
+ 1. A distribution advertising ``ref`` under the
51
+ ``intentframe.executor_packs`` entry-point group (third-party packs
52
+ referenced by their short name).
53
+ 2. An importable module path exposing ``register_all()`` (first-party
54
+ packs and anything referenced by full dotted module path).
55
+ """
56
+ from importlib import import_module
57
+ from importlib.metadata import entry_points
58
+
59
+ by_name = {ep.name: ep for ep in entry_points(group=ENTRY_POINT_GROUP)}
60
+ if ref in by_name:
61
+ register = by_name[ref].load()
62
+ else:
63
+ module = import_module(ref)
64
+ register = getattr(module, "register_all", None)
65
+ if register is None:
66
+ raise ConfigurationError(
67
+ f"Executor pack '{ref}' has no register_all() entry point and "
68
+ f"is not advertised under the '{ENTRY_POINT_GROUP}' group.",
69
+ )
70
+
71
+ register()
72
+ logger.info("Executor pack registered: %s", ref)
73
+
74
+
75
+ def _register_packs(config) -> None:
76
+ """Load every configured executor pack once, in order (fail-closed).
77
+
78
+ Packs are entirely config-driven: ``executor.yaml`` must list the packs it
79
+ needs under ``packs:``. Nothing is loaded implicitly.
80
+ """
81
+ refs = list(config.packs)
82
+ if not refs:
83
+ raise ConfigurationError(
84
+ "No executor packs configured. Set `packs:` in executor.yaml, e.g.\n"
85
+ " packs:\n"
86
+ " - intentframe_native_kit.intentframe_executor_pack_posix # portable base\n"
87
+ " - intentframe_native_kit.intentframe_executor_pack_console # console / simulated user_io\n"
88
+ "(or a pack advertised under the "
89
+ f"'{ENTRY_POINT_GROUP}' entry-point group).",
90
+ )
91
+
92
+ for ref in refs:
93
+ try:
94
+ _load_pack(ref)
95
+ except ConfigurationError:
96
+ raise
97
+ except Exception as exc:
98
+ raise ConfigurationError(
99
+ f"Failed to load executor pack '{ref}': {exc}",
100
+ ) from exc
101
+
102
+
103
+ @asynccontextmanager
104
+ async def lifespan(app: FastAPI):
105
+ global _gateway, _worker_pool
106
+ config = load_config(config_path=os.environ.get("EXECUTOR_CONFIG"))
107
+ _register_packs(config)
108
+
109
+ # Platform-specific startup checks (e.g. macOS TCC permissions) are owned by
110
+ # the relevant executor pack and run when its adapters are constructed in
111
+ # build_gateway() -- core executor stays deployment-agnostic.
112
+ _gateway, _transport, _worker_pool = build_gateway(config)
113
+ logger.info("Executor gateway ready")
114
+ yield
115
+ if _worker_pool:
116
+ await _worker_pool.shutdown()
117
+ if _gateway:
118
+ _gateway.close()
119
+ logger.info("Executor gateway shut down")
120
+
121
+
122
+ app = FastAPI(
123
+ title="IntentFrame Executor",
124
+ version="0.1.0",
125
+ lifespan=lifespan,
126
+ )
127
+
128
+
129
+ class HealthResponse(BaseModel):
130
+ status: str = "ok"
131
+ service: str = "executor"
132
+ uid: int = 0
133
+ euid: int = 0
134
+ running_as_root: bool = False
135
+ pid: int = 0
136
+
137
+
138
+ class RollbackRequest(BaseModel):
139
+ rollback_id: str
140
+
141
+
142
+ @app.get("/health", response_model=HealthResponse)
143
+ async def health() -> HealthResponse:
144
+ euid = os.geteuid()
145
+ # ``running_as_root`` reflects machine-level root capability for
146
+ # RUN_COMMAND, not the executor process's own privilege. Two paths
147
+ # to ``True``:
148
+ # 1. The executor actually runs as UID 0 (rare -- only if the
149
+ # whole stack was launched with sudo).
150
+ # 2. The gateway advertised ``INTENTFRAME_ESCALATION_ARMED=1`` in
151
+ # our env at spawn time, meaning ``/etc/sudoers.d/intentframe-run``
152
+ # is installed and the sandbox engine is allowed to wrap
153
+ # sandbox-exec with ``sudo -n``.
154
+ # No runtime probe, no YAML coupling -- the YAML only decides
155
+ # whether each individual command opts into the escalation.
156
+ env_armed = os.environ.get("INTENTFRAME_ESCALATION_ARMED") == "1"
157
+ return HealthResponse(
158
+ uid=os.getuid(),
159
+ euid=euid,
160
+ running_as_root=(euid == 0) or env_armed,
161
+ pid=os.getpid(),
162
+ )
163
+
164
+ @app.post("/execute", response_model=ExecutionResult)
165
+ async def execute(request: ExecutionRequest) -> ExecutionResult:
166
+ if _gateway is None:
167
+ raise HTTPException(status_code=503, detail="Gateway not initialized")
168
+ return await _gateway.handle(request)
169
+
170
+
171
+ @app.post("/rollback", response_model=ExecutionResult)
172
+ async def rollback(req: RollbackRequest) -> ExecutionResult:
173
+ if _gateway is None:
174
+ raise HTTPException(status_code=503, detail="Gateway not initialized")
175
+ return await _gateway.handle_rollback(req.rollback_id)
@@ -0,0 +1,174 @@
1
+ """
2
+ Worker pool -- manages concurrent adapter execution with limits and timeouts.
3
+
4
+ The worker pool enforces:
5
+ - Concurrency limits (semaphore-based, configurable max_workers)
6
+ - Timeout enforcement (via adapter's safe_execute, which uses asyncio.wait_for)
7
+ - Clean shutdown (drain in-flight work before stopping)
8
+
9
+ Design:
10
+ The skeleton uses asyncio.Semaphore for concurrency control.
11
+ All adapters run in the gateway's event loop via async execution.
12
+
13
+ Future enhancement (platform-specific):
14
+ ProcessPoolExecutor for crash isolation -- if an adapter crashes
15
+ the worker process, the gateway survives. This requires running
16
+ a new event loop per worker process and marshalling results back.
17
+ The interface stays the same; only the internal execution changes.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import asyncio
23
+ import logging
24
+ import time
25
+
26
+ from executor_sdk.adapters.base import CapabilityAdapter
27
+ from executor_sdk.constants import DEFAULT_ADAPTER_TIMEOUT, DEFAULT_MAX_WORKERS
28
+ from executor_sdk.models import ExecutionResult
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+ __all__ = ["WorkerPool"]
33
+
34
+
35
+ class WorkerPool:
36
+ """Manages concurrent adapter execution with limits and timeouts.
37
+
38
+ Usage:
39
+ pool = WorkerPool(max_workers=4, default_timeout=30.0)
40
+ result = await pool.submit(adapter, "SEND_EMAIL", params, credentials)
41
+ # result is always an ExecutionResult (never raises)
42
+
43
+ await pool.shutdown() # drain and stop
44
+
45
+ Guarantees:
46
+ - At most max_workers adapters execute concurrently.
47
+ - Each execution respects its timeout (via safe_execute).
48
+ - submit() always returns ExecutionResult (never raises).
49
+ - shutdown() waits for in-flight work to complete.
50
+ """
51
+
52
+ def __init__(
53
+ self,
54
+ max_workers: int = DEFAULT_MAX_WORKERS,
55
+ default_timeout: float = DEFAULT_ADAPTER_TIMEOUT,
56
+ ) -> None:
57
+ self._max_workers = max_workers
58
+ self._default_timeout = default_timeout
59
+ self._semaphore = asyncio.Semaphore(max_workers)
60
+ self._active_count = 0
61
+ self._shutdown_event = asyncio.Event()
62
+ self._is_shutdown = False
63
+
64
+ @property
65
+ def max_workers(self) -> int:
66
+ """Maximum number of concurrent workers."""
67
+ return self._max_workers
68
+
69
+ @property
70
+ def active_count(self) -> int:
71
+ """Number of currently executing workers."""
72
+ return self._active_count
73
+
74
+ @property
75
+ def is_shutdown(self) -> bool:
76
+ """Whether the pool has been shut down."""
77
+ return self._is_shutdown
78
+
79
+ async def submit(
80
+ self,
81
+ adapter: CapabilityAdapter,
82
+ action: str,
83
+ params: dict,
84
+ credentials: dict | None = None,
85
+ timeout: float | None = None,
86
+ ) -> ExecutionResult:
87
+ """Submit an action for execution with concurrency control.
88
+
89
+ Blocks (via semaphore) if max_workers are already executing.
90
+ Delegates to adapter.safe_execute() which handles timeout and
91
+ exception catching.
92
+
93
+ Args:
94
+ adapter: The capability adapter to execute.
95
+ action: The action type (e.g., "SEND_EMAIL").
96
+ params: Action-specific parameters.
97
+ credentials: Credentials from vault (if needed).
98
+ timeout: Override timeout for this execution. Uses default if None.
99
+
100
+ Returns:
101
+ ExecutionResult -- always, even on failure or pool shutdown.
102
+ """
103
+ if self._is_shutdown:
104
+ return ExecutionResult(
105
+ success=False,
106
+ error="Worker pool is shut down",
107
+ )
108
+
109
+ effective_timeout = timeout or self._default_timeout
110
+ adapter_id = adapter.manifest().adapter_id
111
+
112
+ async with self._semaphore:
113
+ self._active_count += 1
114
+ start_time = time.monotonic()
115
+
116
+ try:
117
+ logger.debug(
118
+ "Worker starting: adapter=%s action=%s active=%d/%d",
119
+ adapter_id,
120
+ action,
121
+ self._active_count,
122
+ self._max_workers,
123
+ )
124
+
125
+ result = await adapter.safe_execute(
126
+ action=action,
127
+ params=params,
128
+ credentials=credentials,
129
+ timeout=effective_timeout,
130
+ )
131
+
132
+ elapsed_ms = int((time.monotonic() - start_time) * 1000)
133
+ result.duration_ms = elapsed_ms
134
+
135
+ logger.debug(
136
+ "Worker completed: adapter=%s action=%s success=%s duration=%dms",
137
+ adapter_id,
138
+ action,
139
+ result.success,
140
+ elapsed_ms,
141
+ )
142
+
143
+ return result
144
+
145
+ finally:
146
+ self._active_count -= 1
147
+ if self._active_count == 0 and self._is_shutdown:
148
+ self._shutdown_event.set()
149
+
150
+ async def shutdown(self, timeout: float = 10.0) -> None:
151
+ """Gracefully shut down the worker pool.
152
+
153
+ Stops accepting new work and waits for in-flight executions
154
+ to complete (up to timeout seconds).
155
+
156
+ Args:
157
+ timeout: Maximum seconds to wait for in-flight work.
158
+ """
159
+ self._is_shutdown = True
160
+
161
+ if self._active_count > 0:
162
+ logger.info(
163
+ "Worker pool shutting down: waiting for %d active workers",
164
+ self._active_count,
165
+ )
166
+ try:
167
+ await asyncio.wait_for(self._shutdown_event.wait(), timeout=timeout)
168
+ except asyncio.TimeoutError:
169
+ logger.warning(
170
+ "Worker pool shutdown timed out with %d active workers",
171
+ self._active_count,
172
+ )
173
+ else:
174
+ logger.info("Worker pool shut down (no active workers)")
@@ -0,0 +1,37 @@
1
+ Metadata-Version: 2.4
2
+ Name: intentframe-executor
3
+ Version: 0.1.0
4
+ Summary: IntentFrame executor service — dispatches approved actions to capability adapters
5
+ Project-URL: Homepage, https://github.com/intentframe/intentframe
6
+ Project-URL: Repository, https://github.com/intentframe/intentframe
7
+ Project-URL: Issues, https://github.com/intentframe/intentframe/issues
8
+ Author: IntentFrame Contributors
9
+ License-Expression: AGPL-3.0-only
10
+ License-File: LICENSE
11
+ Keywords: agents,ai,executor,intentframe,security
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Topic :: Security
16
+ Requires-Python: >=3.14
17
+ Requires-Dist: fastapi
18
+ Requires-Dist: intentframe-executor-sdk==0.1.0
19
+ Requires-Dist: pydantic>=2.12.5
20
+ Requires-Dist: pyyaml>=6.0.3
21
+ Requires-Dist: uvicorn[standard]
22
+ Description-Content-Type: text/markdown
23
+
24
+ # intentframe-executor
25
+
26
+ The executor service for [IntentFrame](https://github.com/intentframe/intentframe).
27
+ It receives approved actions from the runtime, dispatches them to capability
28
+ adapters via a worker pool, and enforces the credential/audit boundary. It runs
29
+ as a FastAPI app on a Unix domain socket (`executor.server:app`).
30
+
31
+ Capability adapters are loaded as executor packs through the
32
+ `intentframe.executor_packs` entry-point group, so deployments select packs
33
+ without modifying this service. Built on `intentframe-executor-sdk`.
34
+
35
+ ```bash
36
+ pip install intentframe-executor
37
+ ```
@@ -0,0 +1,13 @@
1
+ executor/__init__.py,sha256=uAEz0NxFkmWHWps5I5uP5vwgh5w592vBX91pNTGJ9Hw,1030
2
+ executor/dispatch.py,sha256=M1qnrZUlKB1JdnNTzII0IZXLU4KZScmjJxg9VPAxWSw,4296
3
+ executor/gateway.py,sha256=ygNjpJBX0hZlt2NepIJ6o2W3MEPbNT-KKPrKwwg90Us,15534
4
+ executor/main.py,sha256=hhSZ-ShUofmrSO3XxGXEBpXnDJoGe18zxy0_SGKmy3s,11747
5
+ executor/server.py,sha256=8-MBQb4h6er-_msvZ5wGvys-hqmqdqpgwS36EHxpsCM,5936
6
+ executor/worker_pool.py,sha256=jkZRtOEeJykGeIUvTqE7woGkyaRWnYkm8iT5D_CIQL0,5887
7
+ executor/config/__init__.py,sha256=SHtnvvzfB9TJPURmjDdNR7APwZ473Y3U4_7_W7ZXBo0,3211
8
+ executor/config/executor.yaml,sha256=wjWN-Ary5fw3m8XdUF2U-gl_DZ65RlfYKp0gQPqN6H0,7411
9
+ executor/config/schema.py,sha256=M1Hzyn418qiFV4YbBAx_m7VzWhFX5tvzcv34soyfSlQ,7047
10
+ intentframe_executor-0.1.0.dist-info/METADATA,sha256=bjKwT3xdw--KPFUR9rkvq3185XnlJZg_RUcbTwdJHsg,1480
11
+ intentframe_executor-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
12
+ intentframe_executor-0.1.0.dist-info/licenses/LICENSE,sha256=DZak_2itbUtvHzD3E7GNUYSRK6jdOJ-GqncQ2weavLA,34523
13
+ intentframe_executor-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any