AWS Lambda Coding Agent Sandbox: Safe Tool Isolation for LLM Agents

AWS Lambda Coding Agent Sandbox: Safe Tool Isolation for LLM Agents
9 views
A coding agent that can npm test, edit files, and open PRs is useful. The same agent with long-lived credentials, outbound internet, and a shared EFS volume is a ransomware kit with a chatbot UI. On AWS, the unfair advantage is not “more tools” — it is an AWS Lambda coding agent sandbox that makes every tool call a sealed, auditable transaction.
⚡ TL;DR: Run each agent tool (lint, test, patch apply, repo read) inside an AWS Lambda coding agent sandbox: dedicated Lambda, tiny IAM policy, no standing secrets in env, and network locked to VPC endpoints you choose. Pass a per-session workspace via S3 (or ephemeral /tmp) — never a shared writable disk across tenants. Enforce wall-clock timeouts (e.g. 30–120s), memory caps, and concurrency limits. Return structured tool results (exit code, truncated stdout) to the model — not raw root shells. Illustrative: a 1024MB Node sandbox cold-starting in ~200–600ms, warm path under ~50–150ms for simple file ops.
For orchestration and privilege boundaries, see Amazon Bedrock Agents guardrails, AI code review bot IAM, and Lambda warm pools for agent tools.

Why an AWS Lambda coding agent sandbox beats always-on VMs

Long-running agent hosts feel convenient until one session leaves a malicious postinstall running. Lambda gives you:
  • Process isolation per invocation — crash and malware die with the execution environment recycle.
  • IAM as the real sandbox boundary — the model cannot escalate past the role.
  • Cost that tracks tool use — idle agents cost $0; a misbehaving loop hits reserved concurrency and stops.
text
User → Orchestrator (Bedrock / custom) → Tool router
        ├─ ReadRepoLambda     (S3 GetObject only)
        ├─ ApplyPatchLambda   (S3 read/write session prefix only)
        ├─ RunTestsLambda     (no AWS APIs; VPC deny egress)
        └─ OpenPRLambda       (GitHub App token from Secrets Manager, scoped)
❌ Don’t give one “AgentRuntime” role AdministratorAccess and hope prompt instructions hold.
✅ Split tools; each Lambda’s policy is the capability the model is allowed to have.

Session workspaces: S3 prefixes, not shared disks

Treat every agent session as s3://agent-workspaces/{tenantId}/{sessionId}/. The orchestrator copies the repo snapshot (or sparse checkout artifact) into that prefix; tools only see that prefix.
typescript
// apply-patch/handler.ts
import { S3Client, GetObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3";
import { createHash } from "crypto";
import { promises as fs } from "fs";
import path from "path";
import { execFile } from "child_process";
import { promisify } from "util";

const execFileAsync = promisify(execFile);
const s3 = new S3Client({});
const BUCKET = process.env.WORKSPACE_BUCKET!;
const MAX_DIFF_BYTES = 200_000; // ✅ hard cap — illustrative

type PatchEvent = {
  tenantId: string;
  sessionId: string;
  relativePath: string;
  unifiedDiff: string;
};

export const handler = async (event: PatchEvent) => {
  assertIds(event.tenantId, event.sessionId);
  if (Buffer.byteLength(event.unifiedDiff, "utf8") > MAX_DIFF_BYTES) {
    return { ok: false, error: "diff_too_large" };
  }
  if (event.relativePath.includes("..") || path.isAbsolute(event.relativePath)) {
    return { ok: false, error: "invalid_path" }; // ❌ path traversal
  }

  const prefix = `tenants/${event.tenantId}/sessions/${event.sessionId}/`;
  const key = `${prefix}${event.relativePath}`;
  const local = `/tmp/work/${event.relativePath}`;
  await fs.mkdir(path.dirname(local), { recursive: true });

  const obj = await s3.send(new GetObjectCommand({ Bucket: BUCKET, Key: key }));
  const body = await obj.Body!.transformToByteArray();
  await fs.writeFile(local, body);

  // Apply with `patch` binary — no shell metacharacters
  const diffPath = "/tmp/change.diff";
  await fs.writeFile(diffPath, event.unifiedDiff);
  try {
    // ✅ execFile argv array — never exec(`patch < ${user}`)
    await execFileAsync("patch", ["-p0", "--batch", "-i", diffPath], {
      cwd: "/tmp/work",
      timeout: 10_000,
      maxBuffer: 2_000_000,
    });
  } catch (e: any) {
    return { ok: false, error: "patch_failed", detail: String(e?.stderr || e).slice(0, 2000) };
  }

  const patched = await fs.readFile(local);
  await s3.send(
    new PutObjectCommand({
      Bucket: BUCKET,
      Key: key,
      Body: patched,
      Metadata: {
        sha256: createHash("sha256").update(patched).digest("hex"),
        session: event.sessionId,
      },
    })
  );

  return { ok: true, bytes: patched.length };
};

function assertIds(tenantId: string, sessionId: string) {
  const re = /^[a-zA-Z0-9_-]{8,64}$/;
  if (!re.test(tenantId) || !re.test(sessionId)) throw new Error("bad_ids");
}
IAM for ApplyPatch:
json
{
  "Effect": "Allow",
  "Action": ["s3:GetObject", "s3:PutObject"],
  "Resource": "arn:aws:s3:::agent-workspaces/tenants/${aws:PrincipalTag/tenantId}/sessions/*"
}
Pass tenantId as a session tag on the assumed role so the condition key works. Illustrative pattern: orchestrator assumes AgentSessionRole with sts:TagSession.

Network deny-by-default for “run tests” sandboxes

RunTestsLambda should not call the public internet unless you explicitly allow a package mirror. Prefer:
  • Bundle dependencies in the workspace artifact (CI already resolved lockfiles).
  • Or allowlist an internal CodeArtifact / npm mirror via VPC endpoint / PrivateLink.
  • Block SSM, STS, and cloud metadata misuse by not attaching broad AWS permissions at all.
hcl
resource "aws_security_group" "test_sandbox" {
  name   = "agent-test-sandbox"
  vpc_id = var.vpc_id

  # ❌ No egress 0.0.0.0/0
  # ✅ Optional: allow HTTPS only to VPC endpoint SGs for CodeArtifact/S3
  egress {
    from_port       = 443
    to_port         = 443
    protocol        = "tcp"
    security_groups = [var.codeartifact_vpce_sg]
  }
}

resource "aws_lambda_function" "run_tests" {
  function_name = "agent-run-tests"
  runtime       = "nodejs20.x"
  handler       = "index.handler"
  role          = aws_iam_role.tests_no_aws_apis.arn
  timeout       = 120          # illustrative wall clock
  memory_size   = 2048         # tests often need CPU ~ proportional to memory
  reserved_concurrent_executions = 20  # blast-radius cap

  vpc_config {
    subnet_ids         = var.private_subnet_ids
    security_group_ids = [aws_security_group.test_sandbox.id]
  }

  environment {
    variables = {
      # ❌ No GITHUB_TOKEN here
      MAX_STDOUT_BYTES = "100000"
    }
  }
}
typescript
// run-tests — return truncated structured output to the LLM
export const handler = async (evt: { sessionPath: string; cmd: "test" | "lint" }) => {
  const allowed = {
    test: ["npm", ["test", "--", "--runInBand"]],
    lint: ["npm", ["run", "lint"]],
  } as const;
  const [bin, args] = allowed[evt.cmd];

  try {
    const { stdout, stderr } = await execFileAsync(bin, args, {
      cwd: evt.sessionPath,
      timeout: 90_000,
      maxBuffer: 100_000,
      env: { PATH: "/usr/local/bin:/usr/bin", HOME: "/tmp", CI: "1" }, // ✅ minimal env
    });
    return {
      ok: true,
      exitCode: 0,
      stdout: stdout.slice(0, 100_000),
      stderr: stderr.slice(0, 20_000),
    };
  } catch (e: any) {
    return {
      ok: false,
      exitCode: e.code ?? 1,
      stdout: String(e.stdout || "").slice(0, 100_000),
      stderr: String(e.stderr || e.message).slice(0, 20_000),
    };
  }
};
Illustrative latency: warm lint tool ~0.5–2s; full unit suite 15–90s — set the agent’s UX to “job started” rather than blocking a chat bubble for 90s. Fan long tests out via Step Functions or SQS if the orchestrator needs to stay snappy.

Secrets: short-lived, task-scoped, never in prompts

Opening a PR needs a GitHub App installation token — mint it inside OpenPRLambda, use once, discard.
python
# open_pr/handler.py — illustrative
import os, time, jwt, urllib.request, json, boto3

sm = boto3.client("secretsmanager")

def handler(event, _ctx):
    # event: {tenantId, sessionId, branch, title, body}
    secret = json.loads(sm.get_secret_value(SecretId=os.environ["GH_APP_SECRET_ARN"])["SecretString"])
    token = mint_installation_token(secret, event["installation_id"])
    # ✅ token lives only in this invocation's memory
    # ❌ never return token to the model / never log Authorization headers
    pr = create_pull_request(token, event)
    return {"ok": True, "prUrl": pr["html_url"], "number": pr["number"]}

def mint_installation_token(secret, installation_id: int) -> str:
    now = int(time.time())
    payload = {"iat": now - 60, "exp": now + 540, "iss": secret["app_id"]}
    app_jwt = jwt.encode(payload, secret["private_key"], algorithm="RS256")
    req = urllib.request.Request(
        f"https://api.github.com/app/installations/{installation_id}/access_tokens",
        method="POST",
        headers={
            "Authorization": f"Bearer {app_jwt}",
            "Accept": "application/vnd.github+json",
        },
    )
    with urllib.request.urlopen(req, timeout=10) as resp:
        return json.load(resp)["token"]
Pair with Secrets Manager rotation and CloudTrail data events on the secret ARN. If the model “needs” credentials, it needs a tool, not a string in the system prompt.

Orchestrator contract: structured tools only

Expose tools to the LLM as JSON schemas with enums — the model picks cmd: "lint" | "test", never a free-form shell string.
json
{
  "name": "run_checks",
  "description": "Run lint or unit tests in the session sandbox",
  "parameters": {
    "type": "object",
    "additionalProperties": false,
    "required": ["cmd"],
    "properties": {
      "cmd": { "type": "string", "enum": ["lint", "test"] }
    }
  }
}
✅ Allowlist binaries and argv.
❌ {"cmd": "bash", "args": "-c", "script": user_text} is how you fail a security review.

Closing checklist

✅ Dos – ✅ One Lambda role per tool capability; session-tagged S3 prefixes – ✅ execFile + allowlists; truncate stdout/stderr before returning to the model – ✅ Deny-by-default egress; reserved concurrency as a kill switch – ✅ Mint SCM tokens inside the PR tool; never persist them in DynamoDB chat logs – ✅ Timeouts that match the tool (10s patch, 120s tests) — not 900s everywhere ❌ Don’ts – ❌ Don’t run coding agents on a shared EC2 with Docker-in-Docker “for flexibility” – ❌ Don’t put long-lived PATs in Lambda environment variables – ❌ Don’t let the model supply shell strings or absolute paths – ❌ Don’t share /tmp or EFS across tenants without ironclad isolation – ❌ Don’t skip CloudWatch Logs redaction for Authorization and cookie headers

Related reading

Last updated on September 10, 2026

Watch: 100 Days of AI on YouTube

Short videos from CheatCoders, one AI engineering topic per day.

Open the playlist on YouTube · CheatCoders on YouTube

Deep-dive PDF

Get the expanded guide for this post — extra diagrams-style checklists, failure modes, and a production walkthrough. Free when you subscribe to CheatCoders.

Already subscribed? or open the subscribe page.


Discover more from CheatCoders

Subscribe to get the latest posts sent to your email.

4 Comments

  1. So I’ve been wondering what exactly an SEO consultant spends their day doing, and after working with one for a few months I think I finally get it. They basically dig through your website, figure out what’s broken or slow, check what competitors are ranking for, and then give you a plan to fix things step by step. It’s less magic and more spreadsheets than people assume.

    I guess the short version is they make sure Google and actual users can find and trust your site, through a mix of technical tweaks, content advice, and link building. It’s not an overnight fix though, mine warned me it’d take months before seeing real movement, and he was right. Has anyone had a consultant who delivered faster results, or is that just unrealistic?

    So I hired an SEO consultant last year because I was drowning in Google Analytics and had no clue what half those metrics meant. Honestly the best money I spent was just getting someone to explain what actually matters instead of chasing every shiny metric. He kept telling me to focus on three things and ignore the rest, which felt weird at first but made sense later.

    If I could go back I’d ask way more questions before signing anything, like how they measure success and what tools they actually use. A lot of this industry runs on jargon that sounds impressive but means nothing in practice. What questions do you guys usually ask before trusting someone with your site?
    entity SEO explained [url=https://yourhomeyourwayltd.co.uk/what-an-seo-consultant-actually-does-advice/#entity-SEO-explained]https://yourhomeyourwayltd.co.uk/what-an-seo-consultant-actually-does-advice/[/url] .

Leave a comment

No account needed. Name and email are optional.