Exécution de code Python (Monty)
ADK-Rust exécute du Python écrit par le modèle dans le processus via l’interpréteur Pydantic Monty — sans conteneur, sans sous-processus, avec un démarrage en quelques microsecondes. Cette fonctionnalité est fournie en deux couches :
adk-code(fonctionnalitéembedded-python) —MontyExecutorBuilderet les deux produits d’exécution,MontyOneShotExecutoretMontyReplExecutor, qui implémentent tous deuxCodeExecutor.adk-tool(fonctionnalitécode-embedded-python) —MontyPythonCodeTool(monty_python_code), l’outil destiné à l’agent qui s’appuie sur ces exécuteurs.
Il s’agit d’un complément à PythonCodeTool (python_code), basé sur un conteneur,
qui exécute CPython complet dans Docker — utilisez-le lorsque les scripts ont
besoin du véritable écosystème Python (packages pip, extensions C, bibliothèque
standard complète). Monty implémente un sous-ensemble de Python, en échange
d’une exécution rapide dans le processus, d’un état d’interpréteur
sérialisable et d’une garantie d’absence de réseau et de sous-processus,
assurée par construction.
[dependencies]
adk-tool = { version = "2.1.0", features = ["code-embedded-python"] }
Ou via le crate parapluie :
[dependencies]
adk-rust = { version = "2.1.0", features = ["minimal", "code-embedded-python"] }
Exécution unique ou REPL
Un seul générateur produit les deux produits ; le mode est encodé dans le type, et non dans un indicateur :
| Mode | Compilation | État | Concurrence |
|---|---|---|---|
| Exécution unique | build_one_shot() | Interpréteur vierge à chaque appel | Sûr en concurrence |
| REPL | build_repl() | Les variables, fonctions et imports persistent entre les appels | Les appels sont sérialisés par session |
use adk_code::{MontyExecutorBuilder, PathAccess};
let builder = MontyExecutorBuilder::new()
.allow_path("/data", "/srv/agent/data", PathAccess::ReadOnly)
.allow_path("/out", "/srv/agent/out", PathAccess::ReadWrite)
.environ_var("PROJECT", "acme")
.system_clock();
let one_shot = builder.clone().build_one_shot()?;
let repl = builder.build_repl()?;
L’exécuteur REPL stocke l’interpréteur sérialisé entre les appels. Monty
préserve la session lors des exceptions au niveau Python, de sorte qu’un
extrait ayant échoué ne détruit pas l’état accumulé. Les méthodes de cycle de
vie de CodeExecutor gèrent la session : start() l’initialise, stop() la
supprime, restart() la réinitialise, et execute() l’initialise paresseusement
avant start().
Modèle de sécurité
L’isolation combine une politique explicite et une mise en œuvre par omission :
- Système de fichiers. Seuls les répertoires accordés avec
allow_pathsont accessibles, chacun en lecture seule ou en lecture-écriture, viapathlib.Pathsur le chemin de montage virtuel. La table de montage de Monty applique la limite (canonicalisation + détection des échappements par liens symboliques). Tout autre chemin lève uneOSErrorinterceptable (les vérifications d’existence renvoientFalse). - Environnement.
os.getenv/os.environne lisent que la map explicite accordée lors de la construction — l’environnement du processus hôte n’est jamais exposé. - Horloge.
date.today()/datetime.now()ne fonctionnent que lorsque.system_clock()a été accordé ; sinon, ils lèventOSError. - Réseau et sous-processus. Monty ne fournit aucune surface pour l’un ou l’autre — ils sont impossibles quelle que soit la configuration.
- Délais d’expiration.
SandboxPolicy::timeoutcorrespond àResourceLimits::max_durationde Monty (préemption réelle dans la VM, par appel). Une limite de mémoire (256 MiB par défaut) borne le tas ; en mode REPL, elle borne le tas cumulé de la session.
Autorisations par rapport à la politique de requête. Les autorisations du constructeur constituent l'accès maximal dont peut disposer un script. La SandboxPolicy par requête ne peut que les restreindre — une requête dépassant les autorisations est rejetée de manière sécurisée avec ExecutionError::UnsupportedPolicy indiquant l'excédent, avant l'exécution de tout code.
Une autorisation couvre toute sa sous-arborescence de répertoires : demander un point de montage autorisé ou n'importe quel sous-répertoire de celui-ci réussit, et le point de montage effectif correspond au chemin demandé, adossé au sous-répertoire hôte correspondant. Utilisez granted_policy() pour demander exactement ce que l'exécuteur propose.
La politique effective d'une session REPL ne doit pas varier d'un appel à l'autre ; un appel dont la politique diffère de celle établie pour la session est rejeté, avec des instructions invitant à restart().
Fonctions hôte
Les fonctions Rust enregistrées (synchrones ou asynchrones) deviennent des fonctions Python appelables, visibles par les scripts sous leur nom simple :
use adk_code::MontyExecutorBuilder;
use serde_json::json;
let executor = MontyExecutorBuilder::new()
.function_fn("row_count", "Count rows in the loaded dataset.", |args, _kwargs| async move {
Ok(json!(args.len()))
})
.build_one_shot()?;
Pour utiliser la forme complète du trait, implémentez HostFunction (name, description, signature facultatif pour l'invite LLM, et call asynchrone avec des arguments positionnels et nommés convertis par JSON). La validation du registre a lieu lors de build_*() : les noms doivent être des identifiants Python valides, uniques, et ne doivent pas entrer en collision avec les fonctions intégrées de Python.
Dans un script, les fonctions hôte sont appelées de manière synchrone — jamais avec await. Un Err renvoyé devient une exception Python interceptable contenant le message ; l'appel d'un nom non enregistré lève une exception corrective listant les noms enregistrés. L'exécution des fonctions hôte dispose de sa propre limite de temps réel (host_function_timeout, 30 s par défaut), afin qu'une fonction bloquée ne puisse pas bloquer execute().
Remarque : les fonctions hôte s'exécutent comme du code hôte. Elles relèvent de la propre limite de confiance de l'utilisateur, et non de celle de Monty — le bac à sable de l'interpréteur n'isole pas leurs effets de bord.
Exécuteurs auto-descriptifs
Les deux exécuteurs implémentent CodeExecutor::prompt_snippet(), en restituant leurs
capacités intégrées : la sémantique des modes, les racines du système de fichiers avec leurs niveaux d'accès,
les noms des variables d'environnement (les valeurs ne sont jamais restituées), la disponibilité de l'horloge,
la garantie d'absence de réseau et de sous-processus, le contrat de sortie, ainsi qu'un bloc d'initialisation Python
pour les fonctions hôtes enregistrées. MontyPythonCodeTool ajoute l'extrait à sa description destinée à LLM,
de sorte que l'invite et le comportement dans l'interpréteur dérivent de la même configuration et ne puissent pas diverger.
MontyPythonCodeTool
L'outil destiné à l'agent (monty_python_code, portée code:execute) reproduit
JavaScriptCodeTool : les erreurs comme informations JSON, les clés de sortie en camelCase et un
repli "rejected" structuré lorsque la fonctionnalité est désactivée.
use adk_code::PathAccess;
use adk_tool::MontyPythonCodeTool;
use serde_json::json;
use std::sync::Arc;
let tool = MontyPythonCodeTool::builder()
.allow_path("/out", "/srv/agent/out", PathAccess::ReadWrite)
.environ_var("PROJECT", "acme")
.system_clock()
.function_fn("get_weather", "Current weather for a city.", |args, _kwargs| async move {
Ok(json!({ "temp_c": 21 }))
})
.build_repl()?;
let agent = LlmAgentBuilder::new("data_agent")
.instruction("Use monty_python_code for calculations and data work.")
.model(model)
.tool(Arc::new(tool))
.build()?;
MontyPythonCodeTool::new() construit un outil ponctuel entièrement sandboxé ;
MontyPythonCodeTool::repl() un outil REPL entièrement sandboxé.
Portée des sessions
En mode REPL, les sessions de l'interpréteur sont indexées par l'identité complète de session ADK
— nom de l'application, identifiant utilisateur et identifiant de session — afin que l'état ne soit jamais partagé entre utilisateurs, même lorsque les chaînes d'identifiant de session se répètent d'un utilisateur à l'autre. Toutes les sessions partagent les mêmes autorisations et le même registre de fonctions hôtes ; seul l'état de l'interpréteur est propre à chaque session.
La table des sessions est limitée par une capacité LRU (max_sessions, 100 par défaut ; 0 est traité comme 1) ; lors de l'appel suivant d'une session évincée, un nouvel interpréteur est créé de manière transparente.
Arguments de l'outil
| Argument | Type | Description |
|---|---|---|
code | chaîne (obligatoire) | Code source Python à exécuter |
input | quelconque | Valeur JSON facultative liée à la variable input |
timeout_secs | entier | Budget de temps de l’interpréteur (30 par défaut, limité entre 1 et 300) |
reset | booléen | Mode REPL uniquement : supprimer la session persistante avant l’exécution |
Enveloppe de sortie
{ "status": "success", "stdout": "", "stderr": "", "output": {"n": 42},
"stdoutTruncated": false, "stderrTruncated": false, "durationMs": 3 }
Il n'y a pas de exitCode — l'exécution se fait dans le processus, aucun processus n'est créé ;
status est le signal de réussite ou d'échec. stdoutTruncated / stderrTruncated
indiquent que la sortie capturée a été tronquée à la limite en octets
imposée par la politique du bac à sable (1 Mo
par défaut).
La valeur de l'expression finale du script est renvoyée sous forme de output ; la sortie
print() est capturée sous forme de stdout. Statuts d'échec : "failed" (exception
Python — trace d'exécution dans stderr, y compris les exceptions levées par les fonctions
hôtes), "timeout" (budget temporel dépassé), "rejected" (arguments incorrects ou fonctionnalité
désactivée). Jamais de ToolError.
L'enveloppe est fixe dans les deux modes ; l'outil la déclare donc via
Tool::response_schema() — les fournisseurs qui exposent des schémas de réponse la reçoivent
dans la déclaration de l'outil aux côtés de parameters.
Relation avec CodeAct
Le parcours CodeActAgent + adk-codeact-monty exécute également du Python via Monty,
mais avec une distribution ADK Tool depuis l'intérieur des scripts (call_tool(...)) et
une suspension/reprise entre les tours de l'agent. MontyPythonCodeTool exclut délibérément
les deux — il s'agit d'un outil autonome d'exécution de code dont le point d'extension est
le registre de fonctions hôtes. Voir Coding Agent pour
CodeAct.
Exemple
examples/monty_python_code_tool exécute un LlmAgent avec un
MontyPythonCodeTool en mode REPL configuré avec un montage en lecture-écriture, une variable
d'environnement et une fonction hôte enregistrée — illustrant la persistance des variables
sur plusieurs tours et les appels de fonctions hôtes depuis du Python écrit par le modèle.