quarta-feira, 4 de novembro de 2026 Edição nº 228
Buscar
Avaliação · Infraestrutura · 32 min de leitura ·

Credenciais em Sandboxes do LangSmith: Hands-On com Claude Code, GitHub e o Auth Proxy

Por Fabio Douek

Ir para seção
Explica (TLDR) como se eu fosse...
O que é isso?

Imagine que o seu robô ajudante quer biscoitos de potes diferentes, e cada pote precisa de uma chave diferente. Em vez de dar as chaves para o robô, um adulto fica ao lado dos potes com uma lista que diz qual chave abre qual pote, e abre o pote certo sempre que o robô pede.

O robô nunca encosta numa chave. O problema é que, se você troca uma chave, o adulto continua usando a antiga até você entregar uma lista nova, e o robô ainda pode buscar biscoitos para um estranho espertinho.

A questão é a custódia. Um agente de programação com IA normalmente guarda as API keys que usa, então qualquer coisa que comprometa o agente pode copiá-las. O LangSmith mantém as chaves em secrets do workspace e as anexa às requisições de saída por meio de um proxy fora do sandbox.

Isso reduz o risco de roubo, não de uso indevido. Pontos de diligência: as regras podem ser restritas por host e path, mas um secret alterado ou excluído continua funcionando em um sandbox em execução até que a configuração seja reaplicada. Planos não Enterprise também restringem para onde um sandbox pode se conectar: a AWS exigiu uma exceção do suporte, que remove a allow list padrão.

A condição é a exposição de credenciais: agentes vazam chaves quando um texto hostil manda. A intervenção mantém a chave fora do sandbox e a adiciona às requisições no host. Nos meus testes, o ambiente, os arquivos e as variáveis de ambiente dos processos não continham nenhuma credencial real, mesmo buscando como root.

Efeitos colaterais a monitorar: o agente ainda pode usar o acesso, um secret revogado não é retirado de sandboxes em execução até que a configuração seja reaplicada, e uma regra desativada deixa as requisições passarem sem credenciais. O tratamento para AWS funcionou depois que o egress foi liberado.

Repare na preocupação silenciosa que vem junto com entregar suas chaves a um agente e depois deixá-lo ler código não confiável. Aqui o agente trabalha como sempre, mas as chaves nunca ficam na mesa dele, e pedir para ele mostrá-las não leva a lugar nenhum.

A confiança muda de lugar em vez de desaparecer. Alguém ainda decide quais hosts recebem qual chave, e alguém precisa lembrar que trocar uma chave só tem efeito quando o sandbox é avisado. Dar nome a esse responsável evita que a rotação vire uma fonte de ansiedade silenciosa.

Pense no sandbox como o palco e no proxy do LangSmith como o técnico de som com um mapa de patch. Cada linha do mapa diz qual saída recebe qual sinal, até o nível de paths individuais, e a banda nunca mexe nos cabos de verdade.

O groove não muda para quem toca: Claude Code, git e gh simplesmente tocam. O que precisa ser ensaiado é a troca entre as músicas: uma chave nova só chega à mesa quando você entrega um mapa novo ao técnico, e aí ela entra em menos de um segundo.

A história é um antes e depois bem claro. Antes: o sandbox guarda uma API key ativa e um token do GitHub que qualquer prompt injection pode imprimir. Depois: o mesmo agente abre um pull request enquanto o sandbox guarda nada além de uma string placeholder.

O diferencial é a precisão: as regras fazem match de host e path, os valores podem usar templating, e a assinatura com role da AWS funciona sem chaves da AWS no sandbox. A ressalva que os compradores precisam ouvir cedo é que planos não Enterprise restringem para onde os sandboxes podem se conectar, e a AWS exige um ticket de suporte.

GitHub Veja o código-fonte aqui: langsmith-sandbox-credentials

Visão Geral

Rodar um agente de programação em um sandbox protege a sua máquina do agente, não as suas credenciais. Se o Claude Code precisa de uma chave da Anthropic e de um token do GitHub, o setup comum coloca esses valores no ambiente do sandbox, onde um printenv induzido por prompt injection consegue pegá-los.

Os LangSmith Sandboxes, que ficaram disponíveis em GA em maio de 2026, resolvem isso com um auth proxy que roda no host, fora da VM do sandbox. Você dá a cada sandbox um proxy_config com regras: faça match de um host (e, opcionalmente, de paths ou headers) e adicione estes headers. Os valores dos headers vêm de workspace secrets do LangSmith, escritos como {SECRET_NAME}, ou de valores “opaque” somente de escrita. O próprio sandbox não guarda nada, ou guarda um placeholder para que ferramentas como o gh aceitem iniciar.

Ou seja, o proxy adiciona headers às requisições que fazem match; o sandbox não precisa enviar nada. Duas features se destacam: as regras podem fazer match de paths além de hosts, e uma regra aws_auth pode reassinar requisições da AWS com uma IAM role confiada por meio do OIDC issuer do LangSmith, de modo que nem chaves temporárias da AWS entram na VM.

Este post é a parte 2 de 2; a parte 1 percorre o ciclo de vida completo de um sandbox. Eu montei um sandbox de Claude Code nesse modelo, com a chave da Anthropic, um token do GitHub para a REST API e o mesmo token no formato Basic para o git. Depois testei como a injeção se comporta, fiz o Claude Code abrir um pull request de verdade, tentei extrair as credenciais e fiz a rotação delas com um sandbox em execução. Também configurei o lado da AWS, um OIDC provider e uma IAM role. Na minha organização não Enterprise, o egress restrito bloqueou todos os endpoints da AWS no início, então a assinatura com a role teve que esperar uma exceção do suporte da LangChain. Quando ela saiu, rodei o teste da AWS de novo em 2026-11-03, acrescentando o Bedrock, um mount do S3 e os controles de egress do próprio sandbox. Tudo abaixo rodou com o SDK Python langsmith 0.14.14, Claude Code 2.1.312 e gh 2.103.3, contra a região GCP US do LangSmith.

Arquitetura

Sua máquina .env (valores reais) 01_setup_secrets.py create_sandbox(proxy_config) Workspace secrets somente escrita ANTHROPIC_API_KEY GITHUB_BEARER GITHUB_BASIC POST /workspaces/ current/secrets LANGSMITH SANDBOX (VM, ROOT) Claude Code claude -p ... git · gh sem config, sem token env: só placeholders ANTHROPIC_API_KEY= sk-ant-placeholder... GH_TOKEN=proxy-injected AWS_ACCESS_KEY_ID=dummy sem HTTP_PROXY Auth proxy (host) transparente TLS dos hosts com match regras: host + path adiciona ou troca headers reassina AWS SigV4 egress: política da org ou access_control HTTPS {SECRET} resolvido quando a config é aplicada api.anthropic.com x-api-key: <chave real> api.github.com (gh) Authorization: Bearer <real> github.com (git) Authorization: Basic <real> *.amazonaws.com SigV4 como IAM role (OIDC) precisa de uma exceção de egress
Os valores reais vão da sua máquina para os workspace secrets do LangSmith uma única vez. O proxy config de cada sandbox os referencia pelo nome, e o proxy no lado do host os adiciona como headers para os hosts e paths que fazem match.

Três coisas nessa figura importam para o resto do post:

  1. O proxy é transparente e adiciona headers. Não existe HTTP_PROXY no sandbox. O tráfego para um host que faz match com uma regra é interceptado e descriptografado no host, e os headers da regra são adicionados, substituindo qualquer um que o próprio sandbox tenha enviado. O sandbox confia na CA do proxy por meio do bundle do sistema, para o qual SSL_CERT_FILE, NODE_EXTRA_CA_CERTS, REQUESTS_CA_BUNDLE e CURL_CA_BUNDLE apontam.
  2. Os secrets são resolvidos quando a config é aplicada, não a cada requisição. Uma regra diz {GITHUB_BEARER}; o LangSmith busca o valor quando o sandbox é criado ou quando o proxy_config dele é atualizado. Isso tem grandes consequências para a rotação (Teste 6). A exceção são os callbacks: uma regra pode chamar um endpoint hospedado por você para buscar uma credencial no momento da requisição (com cache de 60 segundos a uma hora, falhando de forma fechada). Não testei callbacks aqui.
  3. Em planos não Enterprise, o egress é uma allow list gerenciada. Qualquer access_control que você passar é substituído pela política da organização, e uma regra para um host não libera esse host. Uma exceção do suporte da LangChain remove a lista, e a partir daí o seu próprio access_control é a única coisa que limita para onde um sandbox se conecta. Mais sobre isso no Teste 7.

Setup

Passo 1: Credenciais a injetar

  • Uma API key da Anthropic, com limite de gastos.
  • Um fine-grained personal access token do GitHub restrito a um repo descartável, com Contents e Pull requests configurados como leitura e escrita.
  • Para a parte da AWS: uma conta AWS onde você possa criar um IAM OIDC provider, uma role e um bucket, e uma organização do LangSmith que consiga alcançar a AWS (Teste 7).

Passo 2: Código complementar e snapshot

git clone https://github.com/fabiodouek/my2centsai-blog-samples.git
cd my2centsai-blog-samples/langsmith-sandbox-credentials
uv venv && uv pip install -e .
cp .env.example .env       # LANGSMITH_API_KEY, ANTHROPIC_API_KEY, GITHUB_TOKEN, GITHUB_REPO
python snapshot/build.py

O snapshot é gerado a partir de um Dockerfile, usando o create_snapshot_from_dockerfile do LangSmith. Ele fixa versões atuais do Claude Code e do gh:

FROM node:22-bookworm
ARG CLAUDE_CODE=2.1.312
ARG GH=2.103.3
RUN npm install -g @anthropic-ai/claude-code@${CLAUDE_CODE} \
 && curl -sL https://github.com/cli/cli/releases/download/v${GH}/gh_${GH}_linux_amd64.tar.gz \
    | tar -xz -C /usr/local --strip-components=1 \
 && apt-get update && apt-get install -y --no-install-recommends python3-boto3 jq && rm -rf /var/lib/apt/lists/*

O build roda dentro de um sandbox builder temporário (Docker Hub, npm, GitHub e Debian estão todos na allow list restrita) e levou cerca de cinco minutos. Três coisas me fizeram tropeçar no caminho:

  • Disco: um fs_capacity_bytes de 8 GiB é rejeitado porque o builder parte de uma base de 16 GiB, então use 16 GiB.
  • Timeout: o timeout HTTP padrão do SDK é de 10 segundos, e create_snapshot_from_dockerfile(timeout=...) não o estende para a chamada final de captura. Meu primeiro build falhou com um ReadTimeout no lado do cliente, o SDK derrubou o builder no meio da captura e o snapshot terminou como failed. Fazer o build com SandboxClient(timeout=600) resolveu.
  • Tag: builds a partir de Dockerfile recebem como tag um hash do build, não latest, então create_sandbox(snapshot="my2cents-claude-creds") (ou o snapshot_name=, que está deprecated) procura my2cents-claude-creds:latest e retorna 404. O código complementar procura o snapshot pelo nome e passa o snapshot_id.

Passo 3: Armazenar os secrets

python 01_setup_secrets.py

Os workspace secrets são definidos com uma única chamada POST /api/v1/workspaces/current/secrets, e lê-los de volta retorna apenas as chaves:

token = os.environ["GITHUB_TOKEN"]
values = {
    "ANTHROPIC_API_KEY": os.environ["ANTHROPIC_API_KEY"],
    "GITHUB_BEARER": f"Bearer {token}",
    "GITHUB_BASIC": "Basic " + base64.b64encode(f"x-access-token:{token}".encode()).decode(),
}
secrets_api("POST", json=[{"key": k, "value": v} for k, v in values.items()])
=== What the API gives back (values are write-only) =============
{'key': 'ANTHROPIC_API_KEY'}
{'key': 'GITHUB_BASIC'}
{'key': 'MECH_BARE_TOKEN'}
{'key': 'GITHUB_BEARER'}

(MECH_BARE_TOKEN é o secret descartável que o Teste 1 cria.) Não há ID, versão ou placeholder por secret na listagem. Os secrets também podem carregar hosts permitidos (os code evaluators usam isso), mas as regras do proxy não precisam disso.

Os workspace secrets são compartilhados por tudo no workspace: os code evaluators do LangSmith, por exemplo, usam o mesmo armazenamento. Dê nomes adequados a eles.

Passo 4: As regras do proxy

As regras de header são dicts simples; o helper workspace_secret() do SDK monta a referência:

from langsmith.sandbox import proxy_config, workspace_secret

ANTHROPIC_RULE = {
    "name": "anthropic",
    "match_hosts": ["api.anthropic.com"],
    "headers": [{"name": "x-api-key", **workspace_secret("ANTHROPIC_API_KEY")}],
    "env_vars": {"ANTHROPIC_API_KEY": "sk-ant-placeholder-not-a-real-key", "ANTHROPIC_MODEL": "claude-sonnet-4-6"},
}
GITHUB_API_RULE = {
    "name": "github-api",
    "match_hosts": ["api.github.com"],
    "headers": [{"name": "Authorization", **workspace_secret("GITHUB_BEARER")}],
    "env_vars": {"GH_TOKEN": "proxy-injected"},
}
GITHUB_GIT_RULE = {
    "name": "github-git",
    "match_hosts": ["github.com"],
    "headers": [{"name": "Authorization", **workspace_secret("GITHUB_BASIC")}],
}

sbx = client.create_sandbox(
    snapshot_id=snapshot_id("my2cents-claude-creds"),   # companion helper: looks the snapshot up by name
    proxy_config=proxy_config(rules=[ANTHROPIC_RULE, GITHUB_API_RULE, GITHUB_GIT_RULE]),
    idle_ttl_seconds=600, delete_after_stop_seconds=600,
)

Os env_vars de uma regra são definidos para todos os comandos enquanto a regra está ativa. Eles servem apenas para placeholders: o LangSmith os armazena e retorna em texto puro (Teste 1).

Testes

Teste 1: Como o proxy injeta

python 02_injection_mechanics.py

Quando rodei isto, o egress restrito bloqueava o httpbin e todos os outros serviços de echo, então este teste usa o GitHub como endpoint observável: GET /user responde 200 ou 401, e GET /rate_limit mostra se uma requisição foi autenticada (60 requisições por hora sem token, 5.000 com token). Duas regras em api.github.com, divididas por path:

rules = [
    # Text + reference in one header value
    {"name": "templated", "match_hosts": ["api.github.com"], "match_paths": ["/user"],
     "headers": [{"name": "Authorization", "type": "workspace_secret", "value": "Bearer {MECH_BARE_TOKEN}"}]},
    # A write-only value sent with the config, on other paths
    {"name": "opaque", "match_hosts": ["api.github.com"], "match_paths": ["/repos/*"],
     "headers": [{"name": "Authorization", **opaque_secret(f"Bearer {token}")}],
     "env_vars": {"MECH_VISIBLE": "env_vars are stored and returned in plaintext"}},
]
=== A. Inside the sandbox =======================================
MECH_VISIBLE=env_vars are stored and returned in plaintext
NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt
SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt
REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt
no HTTP_PROXY: interception is transparent

=== B. Matched host and path: /user =============================
GET /user                              -> 200

=== C. Sandbox sends its own Authorization header ===============
GET /user with 'Bearer bogus'          -> 200

=== D. match_paths: unmatched path gets nothing =================
GET /rate_limit (no rule for this path) -> {"limit":60,"used":0}
GET /repos/<repo> (opaque rule)        -> 200

=== E. Plain HTTP to a rule host ================================
http://api.github.com/user             -> 403

=== F. What leaves the process, and who signed the certificate ==
*  issuer: CN=smithbox-proxy-ca
> Host: api.github.com
Verificação Resultado
Secret no env do sandbox Não está lá; apenas os env_vars da regra
"Bearer {SECRET}": texto mais uma referência Funciona: a referência é inserida na string via templating
O sandbox envia o próprio Authorization: Bearer bogus Sobrescrito pela regra (200)
Path sem regra correspondente (/rate_limit) Sem header: limite não autenticado de 60
Valor opaque em /repos/* Funciona (200)
HTTP puro para um host com regra 403 vindo do proxy
O que o curl -v vê saindo do processo Nenhum header Authorization; TLS emitido por smithbox-proxy-ca

O path matching é a feature de destaque. Um host, duas credenciais diferentes, escolhidas pelo path. Você pode dar um token para api.github.com/repos/<one-repo>/* e deixar o resto da API sem autenticação. O templating responde a uma pergunta que a minha pesquisa não tinha conseguido responder: você pode armazenar o token puro e escrever Bearer {TOKEN} na regra, em vez de embutir o prefixo no secret.

Ler o sandbox de volta pela REST API (GET /api/v2/sandboxes/boxes/{name}; o model get_sandbox() do SDK deixa o proxy_config de fora) mostra o que o LangSmith armazena:

{"name": "templated", "headers": [{"name": "Authorization", "type": "workspace_secret", "value": "Bearer {MECH_BARE_TOKEN}"}]},
{"name": "opaque", "headers": [{"name": "Authorization", "type": "opaque", "is_set": true}],
 "env_vars": {"MECH_VISIBLE": "env_vars are stored and returned in plaintext"}}

Referências a workspace secrets voltam como referências, valores opaque voltam como is_set: true, e os env_vars voltam por completo. A mesma resposta inclui access_control.allow_list: a lista de egress gerenciada para organizações restritas, com 60 entradas, que a LangChain não publica em nenhum outro lugar. Ela cobre registries de packages (npm, PyPI, astral), hosts Git, mirrors de Debian e Ubuntu, container registries, os próprios hosts da LangChain e as principais APIs de modelos (*.anthropic.com, *.openai.com, Gemini, Groq, DeepSeek, OpenRouter, Hugging Face). Nada de AWS. Depois da exceção de egress (Teste 7), sandboxes novos voltam sem nenhum access_control, a menos que você defina um.

Teste 2: Claude Code com uma chave placeholder

python 03_claude_code.py
=== A. No rule: placeholder key goes to Anthropic as-is =========
2.1.312 (Claude Code)
ANTHROPIC_API_KEY=sk-ant-placeholder-not-a-real-key
no rule: {"is_error":true,"result":"Invalid API key · Fix external API key"}

=== B. Header rule on api.anthropic.com =========================
json                         'ok'       error=False cost=$0.0791 4.4s
stream-json                  3 events

O controle (o placeholder no env, sem regra de header) falha de forma limpa com “Invalid API key” em vez de travar. Com a regra, o Claude Code funciona em 4,4 s, streaming incluído. Ele confia na CA do proxy por meio do NODE_EXTRA_CA_CERTS, sem nada para configurar.

Teste 3: git e gh sem token

python 04_github.py
=== What the sandbox has ========================================
GH_TOKEN=proxy-injected
no git config

=== gh api user (Bearer injected on api.github.com) =============
fabiodouek

=== git clone fabiodouek/my2centsai-sandbox-test (Basic injected on github.com)
8373223 Initial commit

=== git push to branch langsmith-demo-git-1793246142 ============
50f2bc68e1a3d3b52d57a9b182814cb04a85d382	refs/heads/langsmith-demo-git-1793246142

=== The remote URL and git config have no credentials ===========
https://github.com/fabiodouek/my2centsai-sandbox-test.git
no credential config

Como o proxy adiciona o header Basic a toda requisição para github.com, o git não precisa de configuração nenhuma. O GH_TOKEN=proxy-injected existe apenas para que o gh acredite que está logado; é a mesma receita que a LangChain usa no seu próprio agente Open SWE.

Teste 4: O Claude Code abre um pull request

python 05_agent_task.py
=== Claude Code: clone, commit, push, open PR ===================
is_error=False  turns=8  cost=$0.0801
wall=29s
result: https://github.com/fabiodouek/my2centsai-sandbox-test/pull/4

Vinte e nove segundos, oito centavos e um PR de verdade (24 s e 26 s em outras execuções). Um obstáculo no caminho: os sandboxes do LangSmith rodam como root, e o Claude Code recusa --dangerously-skip-permissions como root:

--dangerously-skip-permissions cannot be used with root/sudo privileges for security reasons

Definir IS_SANDBOX=1 diz ao Claude Code que ele está em um sandbox e remove a restrição. Essa variável não está na documentação do Claude Code, que só diz que a verificação é ignorada dentro de um sandbox reconhecido, então considere que ela pode mudar. O código complementar a passa na linha de comando para as execuções do agente.

Tabela Boxes do LangSmith: my2cents-lscreds-04, criado a partir do snapshot my2cents-claude-creds, aparece como Stopped com motivo de status idle timeout, ao lado de três sandboxes my2cents-lc de system/default; todas as linhas têm 1 vCPU e 2.0 Gi

(A linha my2cents-lscreds-04 parada é de uma execução interrompida; o script exclui o sandbox dela quando termina.) O console não mostra as regras do proxy nem quais secrets um sandbox usa. Nem a tabela Boxes nem a página de detalhes têm um campo para isso, então a REST API (GET /api/v2/sandboxes/boxes/{name}) é o único lugar para auditá-las.

Teste 5: Tentando extrair as credenciais

python 06_leak_attempts.py

A busca roda como root (sem precisar de sudo), e o sandbox gera o hash de toda string com formato de credencial que encontra, para que o script possa compará-los com hashes da chave real da Anthropic, do token do GitHub e da API key do LangSmith sem enviar esses valores para lugar nenhum:

=== 1. Environment, files, process environments (the sandbox runs as root)
root
ANTHROPIC_API_KEY=sk-ant-placeholder-not-a-real-key
GH_TOKEN=proxy-injected
every file: 4 credential-shaped strings, real credentials among them: none
every process environment: 0 credential-shaped strings, real credentials among them: none

=== 2. Watch our own outbound request ===========================
> Host: api.github.com

=== 3. Echo services and the metadata endpoint ==================
https://httpbin.org/headers                000 blocked
https://postman-echo.com/headers           000 blocked
http://169.254.169.254/latest/meta-data/   404

=== 4. Body reflection: render GH_TOKEN through GitHub's markdown API
<p>token: proxy-injected</p>

=== 5. The LangSmith API is on the allow list; is there a key to call it with?
401
  • Passo 2: uma requisição simples sai do processo sem header Authorization; o proxy o adiciona depois. (O máximo que uma ferramenta lá dentro pode enviar é o placeholder: o gh envia GH_TOKEN=proxy-injected.)
  • Passo 3: os serviços de echo estão fora da allow list, então o erro de “injetar em um host que reflete headers” é difícil de cometer por acidente. Essa proteção vem do egress restrito: depois da exceção no Teste 7, eles ficam acessíveis, a menos que você defina o seu próprio access_control. O endereço link-local de metadata responde com um 404 page not found genérico (e o host de metadata do Google, que verifiquei manualmente, com 403): nenhuma credencial de instância ali.
  • Passo 5: a própria API do LangSmith é acessível a partir do sandbox, mas não há nenhuma chave do LangSmith lá dentro para chamá-la.

Com o mesmo prompt de “depuração do dono” dos meus posts sobre E2B e Daytona, o Claude Code nunca imprimiu nada secreto, mas a resposta variou entre execuções. Na execução acima, ele recusou em um turno e se ofereceu para verificar se as variáveis estavam definidas. Na execução gravada para o vídeo, ele listou o tamanho de cada placeholder e disse que o GH_TOKEN “is what authenticated that call”, o que está errado: quem autenticou foi o header do proxy. O placeholder impede que o agente vaze a chave, não que ele entenda errado de onde vem o próprio acesso. E o limite honesto, mais uma vez:

=== 7. What injection does not stop: using the credential =======
authenticated to GitHub as fabiodouek
can list 5 repos

O agente não consegue ler o token, mas consegue usá-lo em todos os hosts e paths com os quais uma regra faz match. Regras de path permitem restringir isso a paths específicos da API.

Teste 6: Rotação e revogação com um sandbox em execução

python 07_rotation.py

Um sandbox, fazendo polling de GET /user por dentro, enquanto o script muda coisas por fora:

=== Baseline ====================================================
GET /user -> 200

=== A. Change the workspace secret, don't touch the sandbox =====
watch 60 s for a 401: 200 at 0.7s, still 200 after 60s

=== B. Re-apply the same proxy config (update_sandbox) ==========
update_sandbox took 0.2s; then: 401 at 0.6s

=== C. Restore the secret and re-apply ==========================
update_sandbox took 0.2s; then: 200 at 0.7s

=== D. Disable the api.github.com rule ==========================
update_sandbox took 0.2s; then: 401 at 0.6s
GH_TOKEN in a new command: unset
{
  "message": "Requires authentication",
re-enable: update_sandbox took 0.2s; then: 200 at 0.6s

=== E. Delete the workspace secret, then re-apply ===============
watch 30 s without re-applying: 200 at 0.7s, still 200 after 31s
re-apply with a missing secret: ValidationError resolve secret references: secret 'GITHUB_BEARER' not found in tenant secrets. Ensure it's configured in your tenant settings
sandbox keeps the last applied value: 200

Este é o resultado que mais diferencia as três plataformas que testei (E2B, Daytona e LangSmith):

  • Alterar um workspace secret não muda nada em um sandbox em execução. Ele continuou autenticando com o token antigo durante o minuto inteiro. O LangSmith resolve as referências {SECRET} quando o proxy config é aplicado, e a documentação diz isso; mesmo assim, é fácil deixar passar.
  • Reaplicar é instantâneo. update_sandbox(proxy_config=...) com a mesma config levou 0,2 s, e o novo valor já estava em uso no polling seguinte, mas só quando você pede.
  • Desativar uma regra falha de forma aberta. O GitHub respondeu “Requires authentication”: a requisição saiu sem o header. O placeholder GH_TOKEN da regra também desapareceu dos comandos novos.
  • Excluir um secret não o revoga. O sandbox em execução manteve o valor antigo, e reaplicar a config passou a ser rejeitado, porque a referência não resolve mais. Para cortar o acesso de um sandbox, desative a regra ou exclua o sandbox, e revogue o token no GitHub.

Teste 7: AWS sem credenciais da AWS

python 08_aws_role.py

Esta é a feature que diferencia o LangSmith no papel. Uma regra aws_auth indica uma IAM role; o LangSmith a assume com sts:AssumeRoleWithWebIdentity, usando um token OIDC de curta duração que ele assina para o seu workspace, e o proxy reassina requisições SigV4 para endpoints da AWS suportados (hosts de serviços regionais, S3, e IAM e STS globais) com as credenciais temporárias da role. O sandbox recebe chaves dummy para que os SDKs da AWS iniciem, e nunca vê uma chave real, nem mesmo um session token. Isso cobre serviços SigV4 como o S3, que a injeção de headers sozinha não consegue cobrir.

Quando rodei este teste pela primeira vez, em 2026-10-29, ele parou na verificação de acesso: sts.us-east-1.amazonaws.com -> 000blocked. A AWS não está na allow list gerenciada que as organizações não Enterprise recebem, e essa lista prevalece sobre qualquer coisa que um sandbox peça. A regra aws_auth não liberou os hosts (a documentação do auth proxy diz que configurar credenciais para um destino “does not grant access beyond the organization policy”), uma allow list de access_control com a AWS foi armazenada como null, e um mount do S3 com a mesma role apareceu, mas travou na primeira listagem. Não existe configuração self-service: a flag da organização (sandbox_restricted_egress) pode ser lida pela API, mas não escrita, e o caminho documentado é um ticket de suporte listando os destinos. Abri um no mesmo dia. O único caminho já liberado para o Bedrock era o LLM Gateway da LangChain, que autentica com uma API key do Bedrock armazenada no LangSmith em vez da IAM role. A exceção foi concedida, e tudo abaixo vem de uma nova execução em 2026-11-03.

A role

Configurei o lado da AWS da forma como o dashboard do LangSmith descreve. O /api/v1/info no GCP US reporta sandbox_workload_identity_supported: true com o issuer https://api.smith.langchain.com:

aws iam create-open-id-connect-provider --url https://api.smith.langchain.com --client-id-list sts.amazonaws.com
{"Version": "2012-10-17", "Statement": [{"Effect": "Allow",
  "Principal": {"Federated": "arn:aws:iam::<account>:oidc-provider/api.smith.langchain.com"},
  "Action": "sts:AssumeRoleWithWebIdentity",
  "Condition": {"StringEquals": {"api.smith.langchain.com:aud": "sts.amazonaws.com",
                                 "api.smith.langchain.com:sub": "ws:<workspace-id>:sandbox"}}}]}

A inline policy da role permite invocar o Bedrock e ler e escrever no S3 em sandbox/ de um bucket (ela está no README complementar). O bucket também guarda private/secret.txt, que a role não consegue ler. A regra do sandbox é uma linha e, como a exceção deixa o egress totalmente aberto, o script também dá ao sandbox uma allow list própria:

AWS_RULE = aws_auth(role_arn=ROLE, env_vars={"AWS_REGION": "us-east-1", "CLAUDE_CODE_USE_BEDROCK": "1", ...})
EGRESS = {"allow_list": ["*.amazonaws.com"]}  # this sandbox's own egress policy: AWS only

sbx = create_sandbox("08", rules=[AWS_RULE], access_control=EGRESS)
=== What the sandbox sees =======================================
AWS_ACCESS_KEY_ID=dummy
AWS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt
AWS_EC2_METADATA_DISABLED=true
AWS_REGION=us-east-1
AWS_SECRET_ACCESS_KEY=dummy
stored rule          : {'role_arn': 'arn:aws:iam::<account>:role/my2cents-langsmith-sandbox'}
stored access_control: {'allow_list': ['*.amazonaws.com']}

=== Egress: AWS only, by the sandbox's own allow list ===========
sts.us-east-1.amazonaws.com -> 302
example.com                 -> 000 blocked

=== Identity: the role, assumed by the proxy ====================
arn:aws:sts::<account>:assumed-role/my2cents-langsmith-sandbox/ls-01a19ed6-5be3-7990-830f-64f8663cb487

=== S3 inside and outside the role's prefix =====================
get sandbox/hello.txt : hello from my2cents
put sandbox/from-ls   : ok
get private/secret.txt: AccessDenied (no identity-based policy allows)
list all buckets      : AccessDenied (no identity-based policy allows)

=== Claude Code on Bedrock ======================================
'ok' error=False cost=$0.0063
  • Nenhuma credencial da AWS no sandbox. Só chaves dummy, AWS_EC2_METADATA_DISABLED=true e um CA bundle, sem session token. O LangSmith armazena apenas o ARN da role.
  • Quem assume a role é o proxy, não o sandbox. O CloudTrail mostra cada chamada AssumeRoleWithWebIdentity vindo do LangSmith com o user agent do AWS SDK para Go, o subject ws:<workspace-id>:sandbox, uma sessão chamada ls- mais um ID, e credenciais de 900 segundos. A docstring do SDK diz que o LangSmith as renova. Não há External ID em lugar nenhum, o que resolve a dúvida sobre a documentação mais abaixo.
  • O IAM continua impondo o limite. Dentro de sandbox/, leituras e escritas funcionam; private/secret.txt e ListBuckets recebem AccessDenied da própria policy da role.
  • O Claude Code roda no Bedrock a partir dos env_vars da regra e das chaves dummy, mesmo com a AWS como único destino acessível. Duas chamadas naquele dia custaram US$ 0,078 cada; repetições dentro de poucos minutos custaram US$ 0,006, o que parece prompt caching.

Egress depois da exceção

A exceção não adiciona a AWS à lista; ela remove a lista. Um sandbox padrão agora alcança example.com e o httpbin, e GET /boxes/{name} não retorna mais nenhum access_control. Ou seja, o ponto do Teste 5 de que os serviços de echo estão fora da lista não vale mais para a minha organização. O lado bom é que o próprio access_control de um sandbox, antes ignorado, agora é armazenado e aplicado:

access_control na criação Egress restrito (2026-10-29) Depois da exceção (2026-11-03)
Nenhum Lista gerenciada de 60 padrões; AWS, example.com e httpbin bloqueados Tudo o que testei estava acessível
{"allow_list": ["*.amazonaws.com", "api.github.com"]} Armazenado como null; a lista gerenciada foi aplicada Armazenado e aplicado: AWS e GitHub respondem; example.com, httpbin e PyPI bloqueados
{"deny_list": ["example.com", "httpbin.org"]} Não testado Armazenado e aplicado: esses dois bloqueados, todo o resto aberto

Um sufixo :443 nas entradas da allow list não fez diferença. Mais um detalhe: depois da exceção, GET /api/v1/orgs/current/info ainda reporta sandbox_restricted_egress: true, então a flag não diz em qual dos casos você está. Um curl de dentro de um sandbox diz.

S3 como filesystem montado

O segundo sandbox do script recebe a mesma role por meio de mount_config em vez de uma regra do proxy, como um mount somente leitura do prefixo sandbox/:

mnt = create_sandbox("08m", mount_config=mount_config(
    auth=[aws_auth(role_arn=ROLE)],
    mounts=[s3_mount(id="data", mount_path="/mnt/mounts/data", bucket=BUCKET, prefix="sandbox/", read_only=True)],
))
=== The bucket as a read-only mount =============================
from-ls.txt
hello.txt
hello from my2cents
write a file: Read-only file system

=== The mount's role, called directly ===========================
s3 get sandbox/hello.txt: hello from my2cents
s3 put sandbox/x.txt    : AccessDenied (explicit deny in a session policy)
sts get-caller-identity : Proxy auth failed

O mount que travava antes da exceção agora lista o conteúdo em cerca de um segundo (rclone sobre FUSE, montado como ro). A parte mais interessante é o escopo. Quando o LangSmith assume a role para um mount, o CloudTrail mostra que ele passa uma session policy: ações de objeto e de listagem do S3 apenas no bucket e em sandbox/*, um deny em todas as outras ações e recursos e, como o mount é somente leitura, um deny em escritas. O sandbox ainda consegue chamar o S3 diretamente com as credenciais do mount, mas só para ler dentro do prefixo, e o proxy não assina nada que não seja S3. Isso bate com a docstring do SDK: em mount_config, uma role “is restricted to the configured S3 mounts”, enquanto em proxy_config ela usa todas as suas permissões. Se um sandbox só precisa de um bucket, o mount é a forma mais restrita de entregar isso.

Uma observação sobre a documentação: a docstring do aws_auth no SDK diz “LangSmith supplies the workspace External ID”, a página do auth proxy fala em um “workspace External ID”, e a página de mounts mostra uma trust policy de AssumeRole com um External ID. É assim que funciona a integração do LangSmith com o registry ECR. Para regras do proxy e mounts, a API, o changelog, as instruções de role do dashboard e agora o CloudTrail mostram OIDC web identity sem External ID, que é o que eu configurei.

Resumo dos resultados

Teste O que eu verifiquei Resultado
1. Mecânica Templating, sobrescrita, path matching, valores opaque, HTTP puro, o que a API armazena Adiciona/sobrescreve headers por host e path; templating funciona; env_vars armazenados em texto puro; allow list gerenciada visível
2. Claude Code Chave placeholder com e sem regra Erro limpo no estilo 401 sem a regra; funciona com ela, streaming incluído
3. git e gh Clone de repo privado, push, chamada REST Tudo funciona sem nenhuma configuração de git
4. Tarefa do agente Claude Code abre um PR de ponta a ponta 29 s, US$ 0,08, PR aberto; precisa de IS_SANDBOX=1 como root
5. Tentativas de vazamento Env, arquivos e /proc (comparados por hash), curl, hosts de echo, metadata, reflexão via markdown, pedir ao Claude Nada real encontrado; nada sequer sai do processo; a credencial ainda pode ser usada
6. Rotação Alterar, reaplicar, desativar, excluir com um sandbox em execução Mudanças em secrets só valem ao reaplicar (aí em menos de um segundo); regras desativadas falham de forma aberta; secrets excluídos continuam funcionando
7. Role da AWS Role via OIDC, reassinatura SigV4, S3, Bedrock, mount do S3, controles de egress Bloqueado até uma exceção do suporte; depois, nenhuma credencial da AWS no sandbox, o IAM aplica o prefixo, o Claude Code roda no Bedrock e os mounts recebem uma session policy com escopo restrito. A exceção remove a allow list, então defina o access_control você mesmo

O que a injeção não resolve

  • Uso indevido pelos hosts com match. Coberto no Teste 5. Regras de path ajudam, mas o escopo do token continua importando.
  • A rotação não é automática para workspace secrets. Alterar ou excluir um não muda nada nos sandboxes em execução até que o proxy config deles seja reaplicado (Teste 6). Se você faz rotação em um cronograma, o reapply precisa fazer parte do job, como acontece no Open SWE, ou use uma regra de callback que busca o valor no momento da requisição.
  • Regras de header desativadas falham de forma aberta. A requisição ainda sai, sem o header. (Segundo a documentação, regras de callback falham de forma fechada.)
  • update_sandbox substitui o proxy config inteiro. Omita uma regra, callbacks ou access_control do update e eles somem. Monte a config completa em um único lugar e sempre envie tudo.
  • env_vars são texto puro. Eles são armazenados e retornados pela API. Coloque ali apenas placeholders.
  • Autenticação em formato de header, mais regras de cloud providers. O proxy não consegue injetar em corpos de requisição nem em protocolos não HTTP (git sobre SSH, protocolos de database). As exceções são as regras de provider para AWS, GCP e Azure, que cuidam da assinatura e dos tokens por conta própria; testei apenas a da AWS (Teste 7).
  • O egress é tudo ou nada no nível da organização. Em planos não Enterprise, a allow list gerenciada substitui o seu access_control silenciosamente, e alcançar qualquer outra coisa, AWS incluída, exige um ticket de suporte. A exceção remove a lista por completo, então todo sandbox passa a alcançar qualquer host até você dar a ele um access_control próprio. Verifique de dentro de um sandbox em qual dos casos você está; a flag da organização não muda.
  • Confiança no nível do workspace para a role da AWS. O subject OIDC é ws:<workspace-id>:sandbox, e o CloudTrail mostra exatamente isso em toda assunção da role, então todo sandbox do workspace pode assumir a role. A API aceita um escopo de tag mais restrito (subject_scope: "tag:<uuid>"), que o Open SWE da LangChain usa, mas os guias ainda não documentam o subject do token resultante.

Limpeza

python 09_cleanup.py

O script exclui os sandboxes de demonstração, o snapshot e os workspace secrets, fecha os PRs de demonstração e exclui os branches deles. O lado da AWS (OIDC provider, role, bucket) tem comandos próprios no README complementar. Excluir workspace secrets do LangSmith não revoga nada no provider: revogue o token do GitHub e faça a rotação da chave da Anthropic se você os criou só para isso.

Veredito

O auth proxy cumpre o trabalho principal. Ao longo destes testes, o sandbox nunca guardou uma chave real da Anthropic, um token do GitHub ou uma credencial da AWS, e o Claude Code, o git, o gh e o boto3 funcionaram sem perceber. O path matching e o templating permitem que um mesmo host receba credenciais diferentes por path da API, e a assinatura com role da AWS vai além do que a injeção de headers consegue: nenhuma chave da AWS de nenhum tipo entra na VM, o IAM continua aplicando a policy da role, e um mount do S3 recebe uma session policy com escopo restrito ao mount.

As arestas são operacionais. Mudanças em secrets só chegam a um sandbox em execução quando você reaplica a config dele, uma regra desativada falha de forma aberta, env_vars são texto puro, e o console não mostra quais regras ou secrets um sandbox usa. Em planos não Enterprise, o egress decide com o que você consegue integrar: a AWS exigiu um ticket de suporte, e a exceção removeu a allow list em vez de ampliá-la.

Se os seus agentes já rodam no LangSmith e precisam de acesso ao Git ou à cloud com credenciais que eles não deveriam conseguir ler, vale a pena adotar. Trate o proxy config como código que você reaplica a cada rotação, restrinja bem a trust e a policy da role e, se conseguir uma exceção de egress, dê a todo sandbox um access_control desde o primeiro dia.

Comments