Claude Platform Docs
MessagesOutils

Utilisation du navigateur et de l'ordinateur avec les ensembles d'outils du SDK

Exécutez l'outil d'utilisation du navigateur ou l'outil d'utilisation de l'ordinateur depuis le SDK Python ou TypeScript. Le SDK exécute la boucle et les vérifications que vous configurez, et vous fournissez le navigateur ou le bureau.

Avec les « toolsets » (ensembles d'outils) du navigateur et de l'ordinateur des SDK Python et TypeScript, vous pouvez créer un agent qui travaille dans un navigateur web ou sur un bureau. Il peut utiliser des applications dépourvues d'interface programmatique. Grâce à l'outil d'utilisation du navigateur, Claude navigue, lit et interagit avec des pages web. Grâce à l'outil d'utilisation de l'ordinateur, Claude voit un bureau sous forme de captures d'écran et utilise la souris et le clavier.

Le SDK connecte Claude à votre navigateur ou à votre bureau. Il exécute les vérifications que vous configurez, comme une « URL policy » (politique d'URL) pour navigate ou un callback d'approbation. Il envoie à votre « driver » (pilote) chaque appel qui passe ces vérifications et construit le tool_result à partir de ce que le pilote renvoie. Le « tool runner » (exécuteur d'outils) peut exécuter la « agentic loop » (boucle agentique) pour vous.

Vous fournissez le pilote, qui fait fonctionner le navigateur ou le bureau. Commencez par le pilote d'un fournisseur. Avec l'un d'eux, un agent ne demande que quelques lignes de code. Si aucun ne convient, écrivez le vôtre avec une bibliothèque d'automatisation de navigateur comme Playwright.

La majeure partie de cette page porte sur l'ensemble d'outils du navigateur. Ensemble d'outils de l'ordinateur couvre ce qui diffère.

Écrire votre propre pilote est un cas avancé, traité dans Implémenter un pilote. Le dépôt claude-quickstarts contient également un exemple de pilote de navigateur en Python et en TypeScript. Il contrôle Chromium via le Chrome DevTools Protocol (CDP), et ce n'est pas du code de production.

Utiliser le pilote d'un fournisseur

Avec le pilote d'un fournisseur, comme ceux présentés sous Intégrations partenaires, vous pouvez créer un agent en quelques lignes de code. Le fournisseur de cet exemple est fictif. Remplacez example_browser_provider et ExampleBrowser par le paquet et la classe de pilote d'un vrai fournisseur, et configurez-les comme l'indique la documentation du fournisseur.

from anthropic import Anthropic
from example_browser_provider import ExampleBrowser  # made-up provider

client = Anthropic()
with ExampleBrowser() as browser:
    runner = client.beta.messages.tool_runner(
        model="claude-opus-5-5",
        max_tokens=1024,
        tools=[browser],
        messages=[
            {"role": "user", "content": "Open example.com and tell me its heading."}
        ],
    )
    for message in runner:
        print(message)

Passez l'instance du pilote elle-même comme entrée de tools. Le runner ne ferme jamais l'ensemble d'outils, de sorte qu'une même instance peut servir à plusieurs exécutions. Fermez-la lorsque vous avez terminé.

Pour écrire votre propre pilote, consultez Implémenter un pilote.

Intégrations partenaires

Browser Use, Browserbase, Daytona et E2B publient leurs propres pilotes pour les ensembles d'outils du SDK.

Pilote Browser Use pour l'ensemble d'outils du navigateur.

Pilote Browserbase Stagehand pour l'ensemble d'outils du navigateur.

Pilote Daytona pour l'ensemble d'outils de l'ordinateur.

E2B

Pilotes E2B pour les ensembles d'outils du navigateur et de l'ordinateur.

Nos partenaires publient également du code sur GitHub : le démarrage rapide Browser Use, l'ensemble d'outils Browserbase et l'exemple E2B.

Personnaliser un pilote

Les exemples de cette section utilisent MyBrowser, le pilote de l'exemple avancé présenté plus loin sur cette page.

Activer ou désactiver des membres

configs accepte les paramètres par membre décrits sous Configurer l'ensemble d'outils. Ne listez que les membres que vous modifiez :

# Un MyBrowser qui implémente également read_console.
browser = MyBrowser(
    backend, configs={"read_console": {"enabled": True}, "navigate": {"enabled": False}}
)

Le SDK refuse un appel à un membre désactivé avant que votre code ne s'exécute. Activer un membre que votre classe n'implémente pas est une erreur de configuration, sauf si la classe redéfinit execute.

Ajouter des hooks avant et après

Redéfinissez execute et appelez le execute du parent. Le code placé avant cet appel s'exécute après la politique d'URL, la politique de fichiers et confirm, et il peut modifier l'entrée. Le SDK ne vérifie pas à nouveau l'entrée modifiée. Le code placé après l'appel reçoit le résultat et peut le modifier. Levez ToolError (lancez-la avec throw en TypeScript) pour refuser l'appel.

Redéfinir execute change les membres proposés à Claude. Le SDK considère chaque membre comme implémenté, de sorte que Claude se voit proposer tous les membres activés par défaut. Le MyBrowser de l'exemple avancé sert trois membres, donc le TracedBrowser suivant propose à Claude des membres qu'il ne peut pas servir. Désactivez ces membres avec configs avant de l'utiliser.

import time


class TracedBrowser(MyBrowser):
    def execute(self, context, name, input):
        started = time.monotonic()
        result = super().execute(context, name, input)
        elapsed_ms = (time.monotonic() - started) * 1000
        call_id = context.tool_use.id if context.tool_use else "-"
        log.info("%s %s %.0fms", call_id, name, elapsed_ms)
        return redact(result) if name == "get_page_text" else result

Implémenter un pilote

Un pilote est votre sous-classe de BetaAbstractBrowserToolset20260801. Dans celle-ci, redéfinissez les membres que votre navigateur prend en charge. Un membre que vous n'implémentez pas est envoyé à l'API comme désactivé. Si Claude l'appelle malgré tout, le SDK renvoie une erreur et l'exécution continue. Redéfinir execute change les membres envoyés comme désactivés (Ajouter des hooks avant et après). Chaque membre reçoit le contexte de l'appel et l'entrée du membre sous forme d'objet typé, comme BetaBrowserNavigateInput. Les types d'entrée proviennent de anthropic.types.beta. Outils membres liste les champs de chaque entrée.

En TypeScript, écrivez les membres sous forme de méthodes, et non de champs de fonctions fléchées, car le SDK les trouve sur le prototype. Écrivez le membre type sous la forme type_ en TypeScript. En Python, c'est type.

Renvoyer des résultats

Ce que renvoie un membre détermine ce que Claude lit. Un résultat réussi se termine par un bloc browser_state construit à partir de votre rapport d'état. Un résultat d'erreur ne contient aucun bloc.

MembreRenvoieClaude lit
screenshot, zoomBetaScreenshotResultUn bloc image
navigateBetaBrowserNavigateResultNavigated to {url} — {title} (HTTP {status})
new_tab, switch_tab, list_tabs, close_tabUne entrée d'onglet (new_tab, switch_tab), une liste d'entrées d'onglets (list_tabs), ou rien (close_tab)Le bloc browser_state seul
read_page, get_page_text, find, read_console, read_network, javascript_execUne chaîneLa chaîne, ou (empty) pour une chaîne vide
Tous les autres membresRien, ou une ligne de texteUne courte confirmation, comme Clicked., puis la ligne renvoyée dans un bloc de texte distinct

Signaler l'état du navigateur

Le SDK appelle _browser_state après chaque appel qui renvoie un résultat, y compris les appels refusés et échoués. Renvoyez chaque onglet ouvert, ainsi que ce qui a changé depuis le dernier rapport :

  • Les onglets ouverts et les événements de téléchargement.
  • Un BetaNavigationRefused pour chaque navigation bloquée par votre hook de requête.
  • Un BetaDialogDismissed pour chaque boîte de dialogue native fermée par votre pilote.

Placez tous ces éléments dans state_changes. En Python, BetaNavigationRefused() et BetaDialogDismissed(kind=..., message=...) proviennent de anthropic.tools.browser. En TypeScript, ce sont { type: "navigation_refused" } et { type: "dialog_dismissed", kind, message }.

Les deux derniers ne sont pas des changements d'état de l'API. Le SDK les signale à Claude sous forme de texte en dehors du bloc browser_state : une ligne pour toutes les navigations refusées, qui n'inclut pas d'URL, et une ligne pour chacune des trois premières boîtes de dialogue fermées, puis le nombre des autres.

Lorsqu'un onglet est ouvert, exactement un doit être actif. Chaque membre qui accepte un tab_id doit agir sur l'onglet qu'il désigne. Les limites de l'API concernant le rapport sont listées dans Suivre les onglets avec browser_state.

Claude lit l'URL de chaque onglet, l'URL de chaque téléchargement et l'URL que renvoie votre navigate, telles que votre pilote les a écrites. Le SDK n'analyse pas ces URL. Il transforme les sauts de ligne et autres caractères de contrôle en espaces, supprime les espaces aux extrémités et coupe chaque URL à 4 096 caractères. Ainsi, le contenu d'une URL data:, le chemin d'une URL file:, et un nom d'utilisateur ou un mot de passe dans une URL parviennent tous à Claude.

Gérer les erreurs

Levée par un membre ou le SDKClaude litL'exécution
ToolErrorSon message, sous forme de résultat d'erreurContinue
ToolsetUsageErrorRienS'arrête
Toute autre exceptionClassName: message en Python ou Error: message en TypeScript, sous forme de résultat d'erreurContinue

Le SDK lève ToolsetUsageError pour une erreur de configuration, une mauvaise utilisation du SDK pendant un appel, ou un appel après close. Il en lève également une lorsque _browser_state lève une exception quelconque, même une ToolError. Le tool runner et tool_result ne l'interceptent pas, elle parvient donc à leur appelant.

Le SDK ne masque pas les chemins locaux dans le texte d'erreur d'un membre, dans la ligne que renvoie une action comme left_click, dans l'erreur d'un téléchargement échoué, ni dans le message d'une boîte de dialogue fermée.

Une ToolError provenant de votre politique d'URL, de votre politique de fichiers ou de votre callable confirm parvient à Claude telle qu'elle est écrite, donc n'incluez pas de chemins locaux dans son texte. Interceptez les exceptions dans vos membres, et levez ToolError (lancez-la avec throw en TypeScript) avec votre propre texte.

Démarrer les appels pendant le streaming de la réponse

Par défaut, le tool runner exécute les appels d'un tour après la fin de la réponse de Claude. Avec le démarrage anticipé, il peut démarrer un appel au navigateur ou à l'ordinateur pendant que la réponse est encore en streaming.

Pour l'activer, passez stream=True et run_tools_eagerly=True au tool runner, comme le fait l'exemple avancé. Avec stream, chaque passage dans votre boucle sur le runner vous donne un stream au lieu d'un message.

Le démarrage anticipé modifie le moment où un appel peut démarrer, et non la façon dont l'ensemble d'outils l'exécute :

  • Les vérifications s'exécutent toujours en premier : Le SDK appelle votre politique d'URL, votre politique de fichiers et confirm avant l'exécution d'un appel. Avec le démarrage anticipé, il peut appeler confirm pendant que la réponse est encore en streaming.
  • Les appels s'exécutent toujours un par un : Un ensemble d'outils exécute ses appels dans l'ordre où Claude les a écrits. Un appel échoué arrête toujours les appels suivants de cet ensemble d'outils dans le tour.
  • Un appel qui a démarré ne peut pas être annulé : Si la réponse est ensuite interrompue, par exemple à max_tokens, ou si votre boucle s'arrête prématurément, l'action a quand même lieu, et Claude ne lit jamais son résultat.

Exécuter sans le tool runner

Passez l'instance dans tools (browser.toJSON() en TypeScript), et répondez à chaque appel de membre avec tool_result. L'outil d'utilisation du navigateur exige que vous vous arrêtiez au premier appel échoué (Actions par lot).

Cette boucle utilise MyBrowser et la politique d'URL de l'exemple avancé. Après un appel échoué, elle répond aux appels suivants du tour sans les exécuter :

from anthropic.types.beta import BetaMessageParam, BetaToolResultBlockParam

NOT_EXECUTED = "Not executed: an earlier action in this turn failed."
MAX_TURNS = 10

with MyBrowser(backend, url_policy=url_policy) as browser:
    messages: list[BetaMessageParam] = [{"role": "user", "content": "Open example.com"}]
    for _ in range(MAX_TURNS):
        response = client.beta.messages.create(
            model="claude-opus-5-5",
            max_tokens=1024,
            tools=[browser],
            messages=messages,
        )
        messages.append({"role": "assistant", "content": response.content})
        calls = [
            block
            for block in response.content
            if block.type == "tool_use" and block.toolset_name == browser.toolset_name
        ]
        if not calls:
            break
        results: list[BetaToolResultBlockParam] = []
        failed = False
        for call in calls:
            if failed:
                # Après un appel en échec, le reste du tour reçoit une réponse sans être exécuté.
                results.append(
                    {
                        "type": "tool_result",
                        "tool_use_id": call.id,
                        "toolset_name": call.toolset_name,
                        "content": NOT_EXECUTED,
                        "is_error": True,
                    }
                )
                continue
            result = browser.tool_result(call)
            failed = bool(result.get("is_error"))
            results.append(result)
        messages.append({"role": "user", "content": results})

Chaque réponse à un appel ignoré comporte is_error, le toolset_name de l'appel et le texte exact qu'exige la section Actions par lot. Le tool runner envoie la même réponse.

Exemple avancé : écrire votre propre pilote

Cet exemple réunit les principaux éléments d'Implémenter un pilote dans un seul pilote, MyBrowser, et l'exécute avec le tool runner et le démarrage anticipé. MyBrowser implémente navigate, screenshot et left_click, ainsi que _browser_state, le rapport d'état dont chaque pilote a besoin.

backend représente votre propre wrapper autour d'une bibliothèque d'automatisation de navigateur, comme Playwright. La politique d'URL est un exemple. Écrire une politique d'URL l'explique.

import re
from urllib.parse import urlsplit

from anthropic import Anthropic
from anthropic.tools import ToolError
from anthropic.tools.browser import (
    BetaAbstractBrowserToolset20260801,
    BetaBrowserNavigateResult,
    BetaBrowserState,
    BetaScreenshotResult,
    BetaToolsetCallContext,
    BetaURLContext,
)
from anthropic.types.beta import (
    BetaBrowserLeftClickInput,
    BetaBrowserNavigateInput,
    BetaBrowserScreenshotInput,
    BetaBrowserStateTabEntryParam,
)


class MyBrowser(BetaAbstractBrowserToolset20260801):
    def __init__(self, backend, **options):
        super().__init__(**options)
        self.backend = backend

    def _browser_state(self, context: BetaToolsetCallContext) -> BetaBrowserState:
        return BetaBrowserState(
            tabs=[
                BetaBrowserStateTabEntryParam(
                    tab_id=tab.id,
                    title=tab.title,
                    url=tab.url,
                    active=tab.id == self.backend.active,
                )
                for tab in self.backend.tabs()
            ],
            state_changes=self.backend.drain_changes(),
        )

    def navigate(
        self, context: BetaToolsetCallContext, input: BetaBrowserNavigateInput
    ) -> BetaBrowserNavigateResult:
        # input.url vaut "back", "forward", "reload", ou une URL que votre politique d'URL
        # a autorisée, telle que Claude l'a écrite. Comme is_allowed, backend.goto ajoute https://
        # à une URL qui ne commence pas par un schéma.
        page = self.backend.goto(input.url, input.tab_id)
        return BetaBrowserNavigateResult(
            url=page.url, status=page.status, title=page.title
        )

    def screenshot(
        self, context: BetaToolsetCallContext, input: BetaBrowserScreenshotInput
    ) -> BetaScreenshotResult:
        data = self.backend.png_base64(input.tab_id)
        return BetaScreenshotResult(data=data, media_type="image/png")

    def left_click(
        self, context: BetaToolsetCallContext, input: BetaBrowserLeftClickInput
    ) -> None:
        # Rien à renvoyer : Claude lit "Clicked."
        self.backend.click(input.target, input.tab_id)

    def close(self) -> None:
        super().close()  # first, so no call is still using the browser when it closes
        if not self.backend.closed:
            self.backend.close()


ALLOWED_HOSTS = ("example.com", "iana.org")
SCHEME_PREFIX = re.compile(r"[a-z][a-z0-9+.-]*:", re.IGNORECASE)


def is_allowed(url: str) -> bool:
    # Un exemple, pas une politique de production. Voir "Write a URL policy".
    if url.lower() == "about:blank":
        return True
    with_scheme = url if SCHEME_PREFIX.match(url) else f"https://{url}"
    # Un navigateur lit "\" comme "/" dans une URL web.
    try:
        parts = urlsplit(with_scheme.replace("\\", "/"))
    except ValueError:
        return False
    host = parts.hostname or ""
    listed = any(host == name or host.endswith(f".{name}") for name in ALLOWED_HOSTS)
    return parts.scheme in ("http", "https") and listed


def url_policy(context: BetaURLContext, url: str) -> None:
    if not is_allowed(url):
        raise ToolError(f"blocked: {url} is not on an allowed host")


client = Anthropic()
with MyBrowser(backend, url_policy=url_policy) as browser:
    runner = client.beta.messages.tool_runner(
        model="claude-opus-5-5",
        max_tokens=1024,
        tools=[browser],
        messages=[
            {
                "role": "user",
                "content": "Open example.com and tell me the page heading.",
            }
        ],
        stream=True,
        run_tools_eagerly=True,  # so a call can start before the response ends
    )
    for stream in runner:
        print(stream.get_final_message())

Exécuter l'ensemble d'outils en toute sécurité

La prochaine action de Claude dépend des pages qu'il lit. Une page, ou du texte injecté dans une page, peut tenter d'atteindre des services internes ou d'extraire des fichiers de l'hôte. Elle peut aussi tenter de déclencher des actions aux effets réels. Avant d'exécuter un pilote sur autre chose qu'un navigateur jetable, suivez ces six étapes :

  1. Écrire une politique d'URL qui refuse chaque URL de navigate dont la tâche n'a pas besoin.
  2. Intercepter les requêtes dans le pilote, et vérifier chaque requête avec les mêmes règles.
  3. Appliquer une « egress policy » (politique de sortie) au conteneur, afin que le réseau bloque ce que le pilote ne peut pas voir.
  4. Confiner les téléversements et les téléchargements, ou laisser les téléversements désactivés.
  5. Contrôler les membres lourds de conséquences avec confirm.
  6. Isoler l'hôte du navigateur dans un conteneur ou une VM dédié(e) pour chaque session.

Le SDK applique les étapes 1, 4 et 5 via les politiques et le callable que vous transmettez. Les étapes 2, 3 et 6 relèvent de votre pilote et de votre déploiement. Les précautions décrites dans Considérations de sécurité s'appliquent également.

Écrire une politique d'URL

Une politique d'URL est une fonction que vous transmettez en tant que url_policy. Vous écrivez la politique, et il vous incombe de la rendre prête pour la production. Avant l'exécution de votre navigate, le SDK appelle la politique avec le contexte de l'appel et l'URL telle que Claude l'a écrite.

Ne renvoyez rien pour autoriser l'URL. Pour la refuser, levez ToolError (lancez-la avec throw en TypeScript). Claude lit le message de l'erreur, et navigate ne s'exécute pas. La politique ne vérifie que l'URL de chaque appel navigate. Ce n'est pas un contrôle réseau.

Voici la politique de l'exemple avancé. Elle autorise about:blank, ainsi que deux sites et leurs sous-domaines en http et https :

import re
from urllib.parse import urlsplit

from anthropic.tools import ToolError
from anthropic.tools.browser import BetaURLContext

ALLOWED_HOSTS = ("example.com", "iana.org")
SCHEME_PREFIX = re.compile(r"[a-z][a-z0-9+.-]*:", re.IGNORECASE)


def is_allowed(url: str) -> bool:
    # Un exemple, pas une politique de production.
    if url.lower() == "about:blank":
        return True
    with_scheme = url if SCHEME_PREFIX.match(url) else f"https://{url}"
    # Un navigateur interprète « \ » comme « / » dans une URL web.
    try:
        parts = urlsplit(with_scheme.replace("\\", "/"))
    except ValueError:
        return False
    host = parts.hostname or ""
    listed = any(host == name or host.endswith(f".{name}") for name in ALLOWED_HOSTS)
    return parts.scheme in ("http", "https") and listed


def url_policy(context: BetaURLContext, url: str) -> None:
    if not is_allowed(url):
        raise ToolError(f"blocked: {url} is not on an allowed host")


browser = MyBrowser(backend, url_policy=url_policy)

Le SDK n'appelle la politique que pour navigate, et non pour "back", "forward" ou "reload". Claude peut omettre le schéma, comme dans example.com, donc avant de lire l'hôte, l'exemple ajoute https:// à une URL qui ne commence pas par un schéma. Votre navigate reçoit l'URL telle que Claude l'a écrite. Ajoutez-y le schéma avec le même test, afin que le navigateur ouvre l'URL que votre politique a vérifiée.

L'exemple n'est pas de qualité production. Une politique de production nécessite bien davantage, dans des domaines comme ceux-ci :

  • Les liens sur lesquels Claude clique, les formulaires qu'il soumet, les redirections et les requêtes qu'une page effectue d'elle-même. Seul votre pilote les voit. Consultez Intercepter les requêtes dans le pilote.
  • Les noms d'hôte et les URL provenant du contenu des pages, qu'un attaquant peut choisir.
  • L'analyse de chaque URL de la même manière que le navigateur, et non la recherche du nom d'hôte par un simple découpage de chaîne.
  • Les schémas d'URL. Le SDK ne les vérifie pas, refusez donc chaque schéma que votre pilote n'a pas l'intention d'ouvrir.
  • Les règles de sortie sur le conteneur, pour ce qu'aucune vérification d'URL ne peut voir. Consultez Appliquer une politique de sortie au conteneur.

Si vous transmettez url_policy=None, le SDK refuse chaque navigate vers une URL, de sorte qu'un None lu depuis votre configuration ne peut pas laisser des URL non vérifiées par accident. En TypeScript, undefined laisse l'option non définie, comme si vous ne la transmettiez pas.

Intercepter les requêtes dans le pilote

La politique d'URL ne juge que l'URL d'un appel navigate. Un lien sur lequel Claude clique, un formulaire qu'il soumet, une redirection, ou une image ou un script sur une page autorisée peut toujours atteindre un hôte que la politique refuserait. Utilisez des règles de sortie pour bloquer ces hôtes.

Un pilote doté de l'interception des requêtes peut également vérifier ces requêtes dans son hook de requête, la fonction que votre bibliothèque d'automatisation appelle avant chaque requête. Appelez la fonction qu'utilise votre politique, afin d'écrire les règles une seule fois :

class MyBrowser(BetaAbstractBrowserToolset20260801):
    ...

    # Enregistré sur un contexte de navigateur Playwright avec context.route("**/*", self._guard).
    def _guard(self, route):
        # is_allowed est la fonction de « Write a URL policy ».
        if not is_allowed(route.request.url):
            return route.abort("blockedbyclient")
        route.continue_()

Un hook de requête comme celui-ci ne voit pas les handshakes WebSocket, les requêtes des service workers ni les étapes de redirection. Bloquez les service workers. Vérifiez les handshakes WebSocket et les étapes de redirection avec un hook qui les voit. L'exemple de politique refuse les URL ws:// et wss://, donc remplacez ws:// par http:// et wss:// par https:// avant de transmettre une URL WebSocket à is_allowed.

Appliquer une politique de sortie au conteneur

L'interception ne peut pas voir chaque requête que fait le navigateur, et la politique d'URL ne voit pas vers quoi un nom se résout. Une politique de sortie appliquée par le réseau du conteneur couvre les deux :

  • Bloquez les plages d'adresses link-local et privées en IPv4 et IPv6. Cela inclut l'adresse de métadonnées cloud 169.254.169.254.
  • N'autorisez les connexions sortantes que vers les hôtes dont la tâche a besoin. Si vos règles portent sur des adresses IP, résolvez les noms d'hôte que vous autorisez au démarrage du conteneur.
  • N'autorisez le DNS que vers le résolveur du conteneur.

Les règles appliquées en dehors du conteneur du navigateur, comme une NetworkPolicy Kubernetes ou un pare-feu cloud, ne voient pas le loopback à l'intérieur de celui-ci. Avec ces seules règles, une page peut atteindre tout ce qui y écoute, y compris un port DevTools, qui affiche la liste des onglets ouverts du navigateur et l'adresse qui contrôle chacun d'eux.

Confiner les téléversements et les téléchargements

file_upload est désactivé par défaut. Sans file_policy, le SDK refuse tout téléversement qui fournit un chemin ou un ID de document. Pour activer les téléversements, passez une BetaLocalFilePolicy avec un répertoire de téléversement qui ne contient que les fichiers de la tâche :

from anthropic.tools.browser import BetaLocalFilePolicy

# Un MyBrowser qui implémente également file_upload.
browser = MyBrowser(
    backend,
    configs={"file_upload": {"enabled": True}},
    confirm=make_confirm(),  # required for file_upload; see Gate consequential members
    url_policy=url_policy,
    file_policy=BetaLocalFilePolicy(
        upload_roots=["/task/uploads"],
        download_dir="/task/downloads",
        expose_download_paths=False,
    ),
)

Le SDK résout chaque chemin de téléversement, en suivant les liens symboliques, et refuse tout chemin situé en dehors des racines de téléversement. La politique de fichiers lève une erreur de configuration pour un répertoire de téléchargement qui est une racine de téléversement, qui se trouve à l'intérieur de l'une d'elles ou qui en contient une. Cette vérification peut manquer un lien symbolique, donc ne laissez pas un lien symbolique rendre le répertoire de téléchargement identique à une racine de téléversement, ni placer l'un à l'intérieur de l'autre. Le SDK n'inclut le path d'un téléchargement dans le rapport d'état que lorsque expose_download_paths est vrai et que le fichier se trouve dans le répertoire de téléchargement.

La politique de fichiers fournie vérifie les chemins sur le système de fichiers du processus qui exécute le SDK. Elle ne protège qu'un navigateur qui partage ce système de fichiers. Pour un navigateur distant, suivez plutôt Navigateurs distants et hébergés.

Configurez les téléchargements de cette façon :

  • Créez vous-même le répertoire de téléchargement avec le mode 0700, et montez-le en noexec,nosuid,nodev.
  • Gardez le répertoire hors de portée des autres outils que Claude peut appeler, comme un shell ou un outil de fichiers.
  • Dans un changement d'état download_failed, écrivez error sous forme de phrase fixe. Le texte d'une exception peut contenir le chemin ou l'URL.
  • Ne lisez pas un fichier téléchargé dans la conversation, et ne l'exécutez pas, tant qu'une personne n'en a pas décidé ainsi.

Contrôler les membres lourds de conséquences

javascript_exec et file_upload sont désactivés par défaut. Si vous activez l'un ou l'autre sans callable confirm, le constructeur lève une erreur de configuration. Avec un callable confirm, le SDK l'appelle avant chaque appel sur le point de s'exécuter. Sans callable, rien n'est demandé. En TypeScript, confirm: null ne lève pas l'erreur de configuration, et le SDK refuse chaque appel à n'importe quel membre.

Renvoyez True pour exécuter l'appel, ou False pour le refuser. Lorsque votre callable interroge une personne, montrez-lui le membre, l'URL de la page et l'entrée de l'appel. Au préalable, échappez chaque caractère situé en dehors de l'ASCII imprimable dans l'URL et l'entrée, car les deux peuvent contenir du texte provenant de la page.

Cet exemple demande confirmation pour les deux membres soumis à approbation via votre propre fonction ask_user, et approuve le reste :

import json
from collections.abc import Callable
from urllib.parse import urlsplit

from anthropic.tools.browser import BetaConfirmContext

GATED = {"javascript_exec", "file_upload"}


def shown(value: object) -> str:
    """A value as JSON, with every character outside printable ASCII escaped."""
    return json.dumps(value, ensure_ascii=True, indent=2)


def make_confirm() -> Callable[[BetaConfirmContext], bool]:
    granted: set[tuple[str, str, str]] = set()

    def confirm(context: BetaConfirmContext) -> bool:
        name = context.member
        if name not in GATED:
            return True
        detail = shown(context.input.to_dict())
        page = context.tab_url
        where = shown(page) if page else "a tab with no URL"
        question = f"Allow {name} on {where}?\n{detail}"
        if page is None or urlsplit(page).scheme not in ("http", "https"):
            return ask_user(question)  # no web origin: ask every time
        # Une approbation ne couvre que cette entrée exacte sur cette page.
        key = (name, page, detail)
        if key not in granted and ask_user(question):
            granted.add(key)
        return key in granted

    return confirm


# Un MyBrowser qui implémente aussi javascript_exec et file_upload
# file_upload nécessite aussi la file_policy de « Confine uploads and downloads »
browser = MyBrowser(
    backend,
    configs={"javascript_exec": {"enabled": True}, "file_upload": {"enabled": True}},
    confirm=make_confirm(),
    url_policy=url_policy,
)

Chaque appel à make_confirm() renvoie un callable sans aucune approbation. Appelez-le une fois pour chaque ensemble d'outils, et donnez à chaque utilisateur son propre ensemble d'outils.

Une approbation couvre la page telle que le dernier rapport d'état l'a montrée, et la page peut changer avant que l'appel ne s'exécute. Les achats, les messages envoyés et les conditions acceptées passent par des membres ordinaires comme left_click et type, donc confirm ne peut pas les identifier par leur nom. Pour qu'une personne les approuve, interrogez-la aussi sur ces membres.

N'activez pas javascript_exec à moins que des règles de sortie ne limitent les connexions sortantes du navigateur aux hôtes dont la tâche a besoin. Un script peut envoyer le contenu de la page à n'importe quelle adresse que le navigateur peut atteindre.

Une URL javascript: dans un appel navigate exécute également un script dans la page courante, même lorsque javascript_exec est désactivé. confirm voit cet appel comme un navigate, donc le callable confirm de l'exemple l'approuverait. Votre pilote doit refuser les URL javascript:. Dans l'exemple, la politique d'URL les refuse avant l'exécution de confirm.

Isoler l'hôte du navigateur

Exécutez le navigateur dans un conteneur ou une VM dédié(e), à privilèges minimaux, pour chaque session :

  • Exécutez-le en tant qu'utilisateur non root, avec un système de fichiers racine en lecture seule lorsque le navigateur le permet.
  • Ne montez rien depuis l'hôte au-delà des répertoires de téléversement et de téléchargement que vous avez configurés, le cas échéant.
  • Gardez les identifiants hors de l'environnement, et partez d'un profil de navigateur vierge.
  • Ne partagez aucun système de fichiers avec d'autres outils que Claude peut appeler.

Exécutez le code qui appelle l'API en dehors du conteneur du navigateur, car ce code détient votre clé API et la conversation. Le tool runner et tool_result exécutent tous deux l'ensemble d'outils dans le processus de ce code, de sorte que le navigateur ne partage pas le système de fichiers de l'ensemble d'outils. Traitez le navigateur comme distant : Navigateurs distants et hébergés s'applique.

Traitez tout ce qu'une page renvoie comme non fiable, y compris le texte de la page, les captures d'écran, les entrées de console et de réseau, les titres d'onglets et les noms de téléchargements.

Navigateurs distants et hébergés

Certains navigateurs ne partagent pas de système de fichiers avec le processus qui exécute le SDK. C'est le cas d'un navigateur dans un autre conteneur, d'un navigateur que vous atteignez via une URL DevTools ou d'un navigateur fourni par un service de navigateur hébergé. Avec ceux-ci, la politique d'URL, l'interception des requêtes et confirm s'exécutent toujours dans votre processus.

Avec un navigateur hébergé, le fournisseur contrôle la sortie réseau et l'isolation de l'hôte. Votre propre politique de sortie ne s'y applique pas, donc le hook de requête du pilote est votre seule vérification des requêtes du navigateur. Renseignez-vous sur ce que le réseau du navigateur peut atteindre.

Les vérifications de chemins fournies ne protègent pas un navigateur distant. BetaLocalFilePolicy vérifie les chemins sur le système de fichiers du processus qui exécute le SDK, alors que le navigateur lit et écrit sur son propre système de fichiers.

Le SDK ne peut pas détecter qu'un navigateur est distant. Ainsi, pour un navigateur distant, laissez file_upload désactivé, sauf si votre pilote vérifie les chemins de téléversement là où le navigateur s'exécute, avec son propre BetaFilePolicy. Un BetaFilePolicy contrôle les chemins et les ID de document de chaque téléversement, et décide si Claude voit le chemin d'un téléchargement.

Faites en sorte qu'un navigateur distant refuse les téléchargements, sauf si son propre hôte dispose de la configuration de téléchargement décrite dans Confiner les téléversements et les téléchargements.

Gardez la clé API du fournisseur et l'URL de connexion de la session, qui peut contenir une clé, hors des journaux, des résultats d'outils et des textes d'erreur. Si le fournisseur enregistre les sessions, l'enregistrement est une autre copie de tout ce que Claude a vu et tapé, et les conditions de conservation du fournisseur s'y appliquent.

Ensemble d'outils de l'ordinateur

BetaAbstractComputerToolset20260801 est la classe pour l'outil d'utilisation de l'ordinateur. Un pilote est une sous-classe avec une méthode par outil, comme screenshot ou left_click. Partez du pilote d'un partenaire dans Intégrations partenaires. Pour écrire le vôtre, le dépôt claude-quickstarts contient un exemple VNC minimal en Python et TypeScript. Il contrôle un bureau via VNC, et ce n'est pas du code de production.

La classe accepte configs, confirm et tool_configs, et vous les transmettez comme pour la classe du navigateur. Personnaliser un pilote, Gérer les erreurs, Démarrer les appels pendant le streaming de la réponse et Exécuter sans le tool runner s'y appliquent également, avec ces différences :

  • Options propres au navigateur : La classe n'a ni politique d'URL, ni politique de fichiers, ni rapport d'état.
  • configs : Chaque outil que vous implémentez est activé par défaut. Les paramètres par outil sont listés dans Paramètres de l'outil.
  • confirm : Si votre classe implémente type, key ou hold_key, le constructeur lève une erreur de configuration, sauf si vous transmettez un callable confirm ou désactivez ces outils avec configs. Une classe qui redéfinit execute est considérée comme implémentant les trois.
  • Appels ignorés : Après un appel à l'ordinateur échoué, le tool runner répond aux appels à l'ordinateur suivants du tour avec Not executed: an earlier computer action in this turn failed. Les appels au navigateur reçoivent un texte différent. Dans une boucle que vous écrivez, répondez aux appels à l'ordinateur ignorés avec ce texte exact (Actions groupées).
  • execute : En TypeScript, une redéfinition de execute type name comme BetaComputerMemberName et input comme BetaComputerMemberInput. Les deux proviennent de @anthropic-ai/sdk/resources/beta.
  • Texte d'erreur : Le SDK ne masque pas les chemins locaux dans le texte d'erreur d'un outil de l'ordinateur ni dans le texte que renvoie un outil. Claude lit les deux, coupés à 4 096 caractères. Interceptez les exceptions dans vos outils, et levez ToolError (lancez-la avec throw en TypeScript) avec votre propre texte.

Écrire un pilote de bureau

Ce pilote implémente cinq des 17 outils listés dans Actions disponibles. Un outil que vous n'implémentez pas est envoyé à l'API comme désactivé, comme l'explique Implémenter un pilote. Dans l'exemple, display représente votre propre wrapper autour de ce qui contrôle le bureau, comme un client VNC ou xdotool, et ask_user représente votre propre fonction qui interroge une personne.

from anthropic import Anthropic
from anthropic.tools.computer import (
    BetaAbstractComputerToolset20260801,
    BetaComputerCursorPositionResult,
    BetaScreenshotResult,
    BetaToolsetCallContext,
)
from anthropic.types.beta import (
    BetaComputerCursorPositionInput,
    BetaComputerKeyInput,
    BetaComputerLeftClickInput,
    BetaComputerScreenshotInput,
    BetaComputerTypeInput,
)


class MyDesktop(BetaAbstractComputerToolset20260801):
    def __init__(self, display, **options):
        super().__init__(**options)
        self.display = display

    def screenshot(
        self, context: BetaToolsetCallContext, input: BetaComputerScreenshotInput
    ) -> BetaScreenshotResult:
        # Les coordonnées de Claude sont des positions en pixels dans cette image.
        data = self.display.png_base64()
        return BetaScreenshotResult(data=data, media_type="image/png")

    def cursor_position(
        self, context: BetaToolsetCallContext, input: BetaComputerCursorPositionInput
    ) -> BetaComputerCursorPositionResult:
        x, y = self.display.cursor()
        return BetaComputerCursorPositionResult(x=x, y=y)

    def left_click(
        self, context: BetaToolsetCallContext, input: BetaComputerLeftClickInput
    ) -> None:
        # input.coordinate vaut [x, y], ou None pour cliquer au curseur. input.text est une
        # touche de modification à maintenir, comme "shift". Rien à renvoyer : Claude lit "Clicked."
        self.display.click(input.coordinate, input.text)

    def type(
        self, context: BetaToolsetCallContext, input: BetaComputerTypeInput
    ) -> None:
        self.display.type(input.text)

    def key(self, context: BetaToolsetCallContext, input: BetaComputerKeyInput) -> None:
        self.display.press(input.text, input.repeat or 1)

    def close(self) -> None:
        super().close()  # first, so no call is still using the desktop when it closes
        self.display.close()


client = Anthropic()
with MyDesktop(
    display,
    # requis pour type et key ; voir Exécuter l'ensemble d'outils computer en toute sécurité.
    confirm=lambda context: ask_user(f'Allow the "{context.member}" tool?'),
) as desktop:
    runner = client.beta.messages.tool_runner(
        model="claude-opus-5-5",
        max_tokens=1024,
        tools=[desktop],
        messages=[
            {"role": "user", "content": "Open the calculator and compute 17 * 23."}
        ],
    )
    for message in runner:
        print(message)

Quatre outils n'ont aucun champ d'entrée : screenshot, cursor_position, left_mouse_down et left_mouse_up. Chaque type d'entrée, y compris les leurs, provient de anthropic.types.beta.

En TypeScript, écrivez les outils sous forme de méthodes, et non de champs de fonctions fléchées. Implémenter un pilote en donne la raison.

Mettre à l'échelle les coordonnées et dimensionner les images

Chaque point que Claude envoie est une position en pixels dans les captures d'écran que vous renvoyez : chaque coordinate et start_coordinate, ainsi que les deux coins d'une region de zoom. Le SDK les transmet à vos outils sans modification.

Si vos captures d'écran ont une taille différente de celle de l'écran, effectuez la conversion dans les deux sens dans votre pilote : les points de Claude vers les pixels de l'écran, et la position du curseur que vous signalez vers les pixels de la capture d'écran. Conservez la même taille de capture d'écran pendant toute la session, et donnez-lui le rapport d'aspect de l'écran :

from anthropic.tools import ToolError

SCREENSHOT_SIZE = (1280, 720)  # the size of every full screenshot the driver returns
DISPLAY_SIZE = (2560, 1440)


def to_display(coordinate: list[int]) -> tuple[int, int]:
    x, y = coordinate
    width, height = SCREENSHOT_SIZE
    if not (0 <= x < width and 0 <= y < height):
        # Refuser le point. Le ramener dans la plage cliquerait là où Claude n'a pas choisi.
        raise ToolError(f"[{x}, {y}] is outside the {width}x{height} screenshot")
    return x * DISPLAY_SIZE[0] // width, y * DISPLAY_SIZE[1] // height


def to_screenshot(x: int, y: int) -> tuple[int, int]:
    width, height = DISPLAY_SIZE
    return x * SCREENSHOT_SIZE[0] // width, y * SCREENSHOT_SIZE[1] // height


to_display([640, 360])  # (1280, 720)

Appelez to_display sur chaque coordinate et start_coordinate avant d'agir en conséquence, et to_screenshot sur la position que cursor_position signale. Un clic ou un défilement sans coordinate agit à l'emplacement du curseur, il n'y a donc rien à convertir. Mettez à l'échelle une region de zoom selon le même facteur. Une image de zoom ne change rien à cela : les points suivants que Claude envoie restent des positions en pixels dans la capture d'écran complète.

Le SDK ne lit ni ne redimensionne jamais une image. L'API rejette une image qui dépasse les limites du modèle, ce qui met fin à l'exécution. Mettez à l'échelle chaque capture d'écran et chaque image de zoom dans le pilote. Consultez Dimensionner les captures d'écran pour respecter les limites d'image. Lors d'une longue exécution, une requête peut contenir plus de 20 images, et l'API applique alors à chacune une limite plus stricte. Consultez Gérer l'historique des captures d'écran.

Vérifications des entrées et résultats

Ne comptez pas sur le SDK pour vérifier l'entrée d'un appel. Faites en sorte que votre pilote vérifie les champs qu'il utilise et refuse un appel qu'il ne peut pas exécuter : levez ToolError (lancez-la avec throw en TypeScript). Claude lit le message et peut envoyer un appel corrigé.

Aucun des deux SDK ne limite une duration, un nombre de repeat ou un scroll_amount. L'étape 4 de Exécuter l'ensemble d'outils de l'ordinateur en toute sécurité les traite.

Ce qu'un outil renvoie détermine ce que Claude lit :

OutilRenvoieClaude lit
screenshot, zoomBetaScreenshotResultUn bloc image
cursor_positionBetaComputerCursorPositionResultX={x},Y={y}
Tous les autres outilsRien, ou une ligne de texteUne courte confirmation, telle que Clicked., puis la ligne renvoyée dans un bloc de texte distinct

Exécuter l'ensemble d'outils de l'ordinateur en toute sécurité

Ce qui s'affiche à l'écran oriente la prochaine action de Claude. Une page ou une notification peut tenter de faire taper Claude dans la mauvaise fenêtre, ou de déclencher une action aux effets réels.

Avant d'exécuter un pilote sur autre chose qu'une machine jetable, suivez ces cinq étapes :

  1. Isolez le bureau. Exécutez-le dans un conteneur ou une VM dédié(e) à privilèges minimaux pour chaque session, sans identifiants ni montages de l'hôte. N'installez que les applications dont la tâche a besoin. Exécutez le code qui appelle l'API en dehors de celui-ci.
  2. Soumettez les actions aux effets réels à confirm. Un achat ou un message envoyé est un left_click, un type ou un key ordinaire, et confirm reçoit l'outil et son entrée, pas l'écran. Choisissez donc les outils qui nécessitent une approbation en fonction des applications que Claude peut atteindre sur le bureau.
  3. Gardez les terminaux, les boîtes de dialogue d'exécution et les lanceurs hors du bureau, sauf si la tâche en a besoin. Lorsque l'un d'eux a le focus clavier, type, key et hold_key exécutent tout ce que Claude tape, et un clic peut lancer un programme. Si la tâche en a besoin, demandez à une personne avant toute saisie au clavier et tout clic, et isolez le bureau afin qu'une commande ne puisse pas aller plus loin que ce dont la tâche a besoin.
  4. Refusez les entrées que votre bureau ne devrait pas accepter, comme un point en dehors de la capture d'écran, un wait de plusieurs minutes, ou un repeat ou scroll_amount très élevé. Levez ToolError (lancez-la avec throw en TypeScript) au lieu de ramener la valeur dans la plage autorisée.
  5. Considérez tout ce qui s'affiche à l'écran comme non fiable, y compris les titres de fenêtres et le texte du presse-papiers. Ne l'exécutez pas et ne le transmettez pas sans vérification.

Le SDK applique l'étape 2 par le biais du callable que vous transmettez. Les autres étapes relèvent de votre pilote et de votre déploiement. Les précautions décrites dans Considérations de sécurité s'appliquent également.

Le confirm suivant demande à une personne avant toute saisie au clavier, via votre propre fonction ask_user, et approuve tous les autres appels. context.member est le nom de l'outil. Pour qu'une personne approuve également les clics, définissez ASK_FIRST sur KEYBOARD | CLICKS.

from anthropic.tools.computer import BetaComputerConfirmContext

KEYBOARD = {"type", "key", "hold_key"}
CLICKS = {
    "left_click",
    "right_click",
    "middle_click",
    "double_click",
    "triple_click",
    "left_click_drag",
    "left_mouse_down",
    "left_mouse_up",
}
ASK_FIRST = KEYBOARD


def confirm(context: BetaComputerConfirmContext) -> bool:
    if context.member not in ASK_FIRST:
        return True
    # la fonction présentée est celle de « Gate consequential members ».
    detail = shown(context.input.to_dict())
    return ask_user(f'Allow the "{context.member}" tool with this input?\n{detail}')


desktop = MyDesktop(display, confirm=confirm)

Un appel refusé reçoit un résultat d'erreur, et l'exécution se poursuit. Pour un appel type refusé, Claude lit The user did not grant permission to run 'type'. Do not retry it unless the user asks you to.

Ce confirm approuve tous les outils qui ne figurent pas dans ASK_FIRST, y compris tout outil que vous ajouterez plus tard. Pour échouer de manière fermée, n'approuvez que les outils sans effet sur le bureau (screenshot, zoom, cursor_position et wait) et demandez pour tous les autres outils.

Utiliser les deux ensembles d'outils dans une même requête

Transmettez les deux instances dans tools. Chaque appel porte son toolset_name, browser ou computer, et le tool runner envoie l'appel à l'instance correspondante. Après un appel en échec, le tool runner ignore uniquement les appels ultérieurs du tour vers le même ensemble d'outils, de sorte qu'un appel à l'ordinateur en échec n'entraîne pas l'omission des appels au navigateur du tour.

# MyBrowser, backend et url_policy proviennent de l'exemple avancé.
# confirm est la fonction de « Exécuter le jeu d'outils computer en toute sécurité ».
with (
    MyBrowser(backend, url_policy=url_policy) as browser,
    MyDesktop(display, confirm=confirm) as desktop,
):
    runner = client.beta.messages.tool_runner(
        model="claude-opus-5-5",
        max_tokens=1024,
        tools=[browser, desktop],
        messages=[
            {
                "role": "user",
                "content": "Copy the heading on example.com into the open text editor.",
            }
        ],
        stream=True,
        run_tools_eagerly=True,  # so a call can start before the response ends
    )
    for stream in runner:
        print(stream.get_final_message())

Dans une boucle que vous écrivez, répondez à chaque appel avec tool_result sur l'instance dont le toolset_name correspond au toolset_name de l'appel. À chaque tour, suivez séparément les appels en échec pour chaque ensemble d'outils, et répondez à un appel ignoré avec le texte propre à cet ensemble d'outils. Exécuter sans le tool runner contient le texte du navigateur, et la puce Appels ignorés en haut de cette section contient celui de l'ordinateur.

Référence

Les options du constructeur ont la même signification dans les deux SDK :

PythonTypeScriptDéfinit
configsconfigsLes membres activés
confirmconfirmLe callable qui approuve ou refuse chaque appel
url_policyurlPolicyVotre politique d'URL, que le SDK appelle avant chaque navigate vers une URL
file_policyfilePolicyLes racines de téléversement et l'exposition du chemin de téléchargement
tool_configstoolConfigsLes champs de l'entrée tools, tels que cache_control
La méthode _browser_statebrowserStateLe rapport d'état

La classe du navigateur accepte toutes les options du tableau. La classe de l'ordinateur accepte configs, confirm et tool_configs.

Vous ne pouvez pas modifier une option après la construction. Le guide de l'ensemble d'outils du navigateur pour Python, le guide de l'ensemble d'outils du navigateur pour TypeScript, le guide de l'ensemble d'outils de l'ordinateur pour Python et le guide de l'ensemble d'outils de l'ordinateur pour TypeScript décrivent chaque option. Les guides Python couvrent également les classes asynchrones, BetaAsyncAbstractBrowserToolset20260801 et BetaAsyncAbstractComputerToolset20260801.

Limitations

  • Le SDK n'inclut ni navigateur, ni bureau, ni pilote prêt à l'emploi, ni politique d'URL : C'est à vous de les fournir. Consultez Intégrations partenaires, Implémenter un pilote et Écrire une politique d'URL.
  • La politique d'URL ne voit que les appels navigate : Elle ne voit pas les requêtes qu'une page effectue, ni les pages qui s'ouvrent d'une autre manière, par exemple à partir d'un lien sur lequel Claude clique ou d'une redirection. Consultez Intercepter les requêtes dans le pilote.
  • Une approbation est basée sur le dernier rapport d'état : La page peut changer après ce rapport. Le SDK ne vérifie pas à nouveau la page avant que l'appel ne s'exécute.
  • Les appels sur un même ensemble d'outils s'exécutent un par un : Vous ne pouvez pas désactiver ce comportement.
  • Le SDK ne vérifie pas que les identifiants d'onglets sont uniques ni le nombre d'onglets, et ne vérifie pas toujours qu'un seul onglet est actif : L'API rejette un rapport qui enfreint ces règles.
  • Pour un appel à l'ordinateur, confirm reçoit l'outil et son entrée, pas l'écran : Vous ne pouvez pas en déduire ce que fait un clic à un point donné.
  • Le SDK ne redimensionne pas les images d'un ensemble d'outils de l'ordinateur : L'API rejette une image qui dépasse les limites du modèle. Consultez Mettre à l'échelle les coordonnées et dimensionner les images.

Étapes suivantes

Les outils membres, le bloc browser_state et les considérations de sécurité de l'outil.

Les 17 outils, les coordonnées et les limites d'image, et les considérations de sécurité de l'outil.

Comment le SDK exécute la boucle, et comment modifier les messages qu'il envoie.

Des garde-fous pour toute application qui lit du contenu non fiable.

Was this page helpful?