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) — MontyExecutorBuilder et les deux produits d’exĂ©cution, MontyOneShotExecutor et MontyReplExecutor, qui implĂ©mentent tous deux CodeExecutor.
  • 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 :

ModeCompilationÉtatConcurrence
Exécution uniquebuild_one_shot()Interpréteur vierge à chaque appelSûr en concurrence
REPLbuild_repl()Les variables, fonctions et imports persistent entre les appelsLes 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_path sont accessibles, chacun en lecture seule ou en lecture-Ă©criture, via pathlib.Path sur 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 une OSError interceptable (les vĂ©rifications d’existence renvoient False).
  • Environnement. os.getenv / os.environ ne 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Ăšvent OSError.
  • 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::timeout correspond Ă  ResourceLimits::max_duration de 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

ArgumentTypeDescription
codechaßne (obligatoire)Code source Python à exécuter
inputquelconqueValeur JSON facultative liée à la variable input
timeout_secsentierBudget de temps de l’interprĂ©teur (30 par dĂ©faut, limitĂ© entre 1 et 300)
resetboolĂ©enMode 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.

Exécution de code Python (Monty) - Documentation ADK-Rust | ADK-Rust