Skip to content

Commit ac563a2

Browse files
committed
Modularize telemetry.py into a package with focused sub-modules
- Split telemetry.py into teaagent/telemetry/ package: _availability (HAS_OTEL flag), _config (TelemetryConfig, TelemetryNotAvailable), _audit (OTelAuditSink, configure_telemetry), _metrics (InMemoryMetricsSink, OTelMetricsSink, configure_metrics), _transport (TracingHTTPTransport)
1 parent 77b59bc commit ac563a2

6 files changed

Lines changed: 308 additions & 249 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ All notable changes to TeaAgent are tracked here.
66

77
- Split `teaagent/tui.py` (517 → ~290 lines) by extracting `handle_command` logic to `_commands.py`.
88
- Split `teaagent/mcp_http.py` (575 → ~400 lines) by extracting OAuth endpoint handlers to `_oauth.py`.
9-
- Split `teaagent/telemetry.py` into a `teaagent/telemetry/` package with focused modules: `_deps.py`, `_config.py`, `_audit.py`, `_metrics.py`, and `_transport.py`.
9+
- Split `teaagent/telemetry.py` into a `teaagent/telemetry/` package with focused modules: `_availability.py`, `_config.py`, `_audit.py`, `_metrics.py`, and `_transport.py`.
10+
- Split `teaagent/code_mode.py` into a `teaagent/code_mode/` package with focused modules: `_types.py`, `_validation.py`, `_child_process.py`, and `_container.py`.
1011
- Made `teaagent.cli.main()` accept injectable `_adapter_factory`, `_serve_mcp_http`, `_check_graphqlite`, `_check_llm`, and `_run_model_conformance` keyword arguments, enabling handler extraction without breaking existing tests.
1112
- Split `teaagent/workspace_tools.py` into a `teaagent/workspace_tools/` package with four focused modules: `_config.py`, `_helpers.py`, `_shell.py`, `_files.py`. Backward-compatible public imports preserved via `__init__.py` re-exports.
1213
- Expanded audit string redaction with patterns for JWT tokens, AWS access keys (`AKIA...`), and GitHub personal access tokens (`ghp_...`, `github_pat_...`).

teaagent/code_mode/__init__.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
from __future__ import annotations
2+
3+
import ast
4+
from typing import Any
5+
6+
from ._child_process import ChildProcessCodeModeBackend
7+
from ._container import CONTAINER_CODE_MODE_SCRIPT, ContainerCodeModeBackend
8+
from ._types import CodeModeBackend, CodeModeResult, CodeModeSandbox
9+
from ._validation import (
10+
ALLOWED_NODES,
11+
SAFE_BUILTINS,
12+
UnsafeCodeError,
13+
validate_plain_data,
14+
validate_tree,
15+
)
16+
17+
18+
def execute_code_mode(
19+
code: str,
20+
*,
21+
inputs: dict[str, Any] | None = None,
22+
sandbox: CodeModeSandbox | None = None,
23+
backend: CodeModeBackend | None = None,
24+
) -> CodeModeResult:
25+
tree = ast.parse(code, mode='exec')
26+
validate_tree(tree)
27+
safe_inputs = dict(inputs or {})
28+
validate_plain_data(safe_inputs, 'inputs')
29+
30+
active_sandbox = sandbox or CodeModeSandbox()
31+
if active_sandbox.timeout_seconds <= 0:
32+
raise UnsafeCodeError('Code Mode timeout must be positive')
33+
if active_sandbox.cpu_seconds <= 0:
34+
raise UnsafeCodeError('Code Mode CPU limit must be positive')
35+
if active_sandbox.memory_bytes <= 0:
36+
raise UnsafeCodeError('Code Mode memory limit must be positive')
37+
38+
return (backend or ChildProcessCodeModeBackend()).execute(
39+
code, safe_inputs, active_sandbox
40+
)
41+
42+
43+
__all__ = [
44+
'ALLOWED_NODES',
45+
'CONTAINER_CODE_MODE_SCRIPT',
46+
'SAFE_BUILTINS',
47+
'ChildProcessCodeModeBackend',
48+
'CodeModeBackend',
49+
'CodeModeResult',
50+
'CodeModeSandbox',
51+
'ContainerCodeModeBackend',
52+
'UnsafeCodeError',
53+
'execute_code_mode',
54+
]
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
from __future__ import annotations
2+
3+
import multiprocessing
4+
import traceback
5+
from contextlib import suppress
6+
from dataclasses import dataclass
7+
from typing import Any
8+
9+
from ._types import CodeModeResult, CodeModeSandbox
10+
from ._validation import SAFE_BUILTINS, UnsafeCodeError, validate_plain_data
11+
12+
try:
13+
import resource
14+
except ImportError: # pragma: no cover - resource is Unix-only.
15+
resource = None # type: ignore[assignment]
16+
17+
18+
@dataclass(frozen=True)
19+
class ChildProcessCodeModeBackend:
20+
def execute(
21+
self,
22+
code: str,
23+
inputs: dict[str, Any],
24+
sandbox: CodeModeSandbox,
25+
) -> CodeModeResult:
26+
parent_conn, child_conn = multiprocessing.Pipe(duplex=False)
27+
process = multiprocessing.Process(
28+
target=_execute_code_mode_child,
29+
args=(code, inputs, sandbox, child_conn),
30+
)
31+
process.start()
32+
process.join(sandbox.timeout_seconds)
33+
if process.is_alive():
34+
process.terminate()
35+
process.join()
36+
raise UnsafeCodeError('Code Mode timed out')
37+
38+
if parent_conn.poll():
39+
message = parent_conn.recv()
40+
else:
41+
raise UnsafeCodeError('Code Mode sandbox exited without a result')
42+
43+
if message['status'] == 'error':
44+
raise UnsafeCodeError(message['error'])
45+
variables = message['variables']
46+
validate_plain_data(variables, 'variables')
47+
return CodeModeResult(variables=variables)
48+
49+
50+
def _execute_code_mode_child(
51+
code: str,
52+
inputs: dict[str, Any],
53+
sandbox: CodeModeSandbox,
54+
result_pipe: multiprocessing.connection.Connection,
55+
) -> None:
56+
try:
57+
_apply_resource_limits(sandbox)
58+
namespace: dict[str, Any] = {'__builtins__': SAFE_BUILTINS}
59+
namespace.update(inputs)
60+
exec(compile(code, '<teaagent-code-mode>', 'exec'), namespace, namespace)
61+
variables = {
62+
key: value
63+
for key, value in namespace.items()
64+
if key != '__builtins__' and not key.startswith('_')
65+
}
66+
validate_plain_data(variables, 'variables')
67+
except Exception as exc: # pragma: no cover - exercised through parent process.
68+
result_pipe.send(
69+
{
70+
'status': 'error',
71+
'error': f'{type(exc).__name__}: {exc}',
72+
'traceback': traceback.format_exc(),
73+
}
74+
)
75+
else:
76+
result_pipe.send({'status': 'ok', 'variables': variables})
77+
78+
79+
def _apply_resource_limits(sandbox: CodeModeSandbox) -> None:
80+
if resource is None:
81+
return
82+
_, cpu_hard = resource.getrlimit(resource.RLIMIT_CPU)
83+
resource.setrlimit(resource.RLIMIT_CPU, (sandbox.cpu_seconds, cpu_hard))
84+
if hasattr(resource, 'RLIMIT_AS'):
85+
_, hard = resource.getrlimit(resource.RLIMIT_AS)
86+
soft = sandbox.memory_bytes
87+
if hard != resource.RLIM_INFINITY:
88+
soft = min(soft, hard)
89+
with suppress(ValueError):
90+
resource.setrlimit(resource.RLIMIT_AS, (soft, hard))

0 commit comments

Comments
 (0)