<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Varsni, Notes</title><description>Amutha Varsni Rajkumar, cloud and AIOps engineer. Cloud and AI at Presidio, side projects and writing after hours.</description><link>https://varsni.com</link><language>en-gb</language><item><title>Setting Up Agent Sandboxes in Kubernetes</title><link>https://varsni.com/notes/agent-sandbox-kubernetes</link><guid isPermaLink="true">https://varsni.com/notes/agent-sandbox-kubernetes</guid><description>How I wired kubernetes-sigs/agent-sandbox into a LangGraph agent, dropped the broken official integration, and built a real trust boundary around shell execution</description><pubDate>Fri, 28 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Allowing an autonomous AI agent to run arbitrary shell commands is terrifying. Allowing it inside your primary production Kubernetes cluster is even worse.&lt;/p&gt;
&lt;p&gt;The official documentation directs developers toward &lt;code&gt;langchain-kubernetes&lt;/code&gt; and &lt;code&gt;kubernetes-sigs/agent-sandbox&lt;/code&gt;. When deployed to a live cluster, that path quickly hits broken SDK wrappers and missing runtime components.&lt;/p&gt;
&lt;p&gt;Here is how we achieved local, self-hosted code execution in Kubernetes without exposing cloud credentials or paying for a third-party sandbox SaaS.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;The Architecture and Trust Boundary&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The primary chat agent has read-only AWS access via MCP tools. The sandbox environment must be structurally incapable of accessing those cloud credentials. Rather than relying on soft prompt instructions, isolation is enforced directly through Kubernetes primitives.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://varsni.com/notes/agent-sandbox-kubernetes/trust-boundary.svg&quot; alt=&quot;The API replica tunnels through the sandbox-router into a hardened sandbox pod, where a DNS-only NetworkPolicy structurally blocks any path to AWS&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;Why We Chose Native Kubernetes Sandboxes&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;We evaluated several execution environments before committing to an in-cluster solution:&lt;/p&gt;
&lt;div style=&quot;overflow-x:auto;-webkit-overflow-scrolling:touch;width:100%;max-width:100%;margin:1.25em 0;&quot;&gt;&lt;table style=&quot;width:100%;max-width:100%;border-collapse:collapse;table-layout:fixed;font-size:12px;line-height:1.35;&quot;&gt;
&lt;thead style=&quot;border-bottom:2px solid #d6d7db;&quot;&gt;
&lt;tr&gt;
&lt;th style=&quot;padding:0.45rem 0.5rem;text-align:left;border-bottom:1px solid #d6d7db;font-weight:600;word-break:break-word;overflow-wrap:anywhere;vertical-align:top;&quot;&gt;Solution&lt;/th&gt;
&lt;th style=&quot;padding:0.45rem 0.5rem;text-align:left;border-bottom:1px solid #d6d7db;font-weight:600;word-break:break-word;overflow-wrap:anywhere;vertical-align:top;&quot;&gt;Verdict&lt;/th&gt;
&lt;th style=&quot;padding:0.45rem 0.5rem;text-align:left;border-bottom:1px solid #d6d7db;font-weight:600;word-break:break-word;overflow-wrap:anywhere;vertical-align:top;&quot;&gt;Why It Was Ruled Out&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:0.45rem 0.5rem;text-align:left;border-bottom:1px solid #d6d7db;vertical-align:top;word-break:break-word;overflow-wrap:anywhere;&quot;&gt;&lt;strong&gt;AWS AgentCore&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:0.45rem 0.5rem;text-align:left;border-bottom:1px solid #d6d7db;vertical-align:top;word-break:break-word;overflow-wrap:anywhere;&quot;&gt;Rejected&lt;/td&gt;
&lt;td style=&quot;padding:0.45rem 0.5rem;text-align:left;border-bottom:1px solid #d6d7db;vertical-align:top;word-break:break-word;overflow-wrap:anywhere;&quot;&gt;Managed Bedrock dependency; not native to Kubernetes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:0.45rem 0.5rem;text-align:left;border-bottom:1px solid #d6d7db;vertical-align:top;word-break:break-word;overflow-wrap:anywhere;&quot;&gt;&lt;strong&gt;E2B&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:0.45rem 0.5rem;text-align:left;border-bottom:1px solid #d6d7db;vertical-align:top;word-break:break-word;overflow-wrap:anywhere;&quot;&gt;Rejected&lt;/td&gt;
&lt;td style=&quot;padding:0.45rem 0.5rem;text-align:left;border-bottom:1px solid #d6d7db;vertical-align:top;word-break:break-word;overflow-wrap:anywhere;&quot;&gt;Requires bare metal or Firecracker; does not fit standard managed EKS or Minikube&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:0.45rem 0.5rem;text-align:left;border-bottom:1px solid #d6d7db;vertical-align:top;word-break:break-word;overflow-wrap:anywhere;&quot;&gt;&lt;strong&gt;Daytona&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:0.45rem 0.5rem;text-align:left;border-bottom:1px solid #d6d7db;vertical-align:top;word-break:break-word;overflow-wrap:anywhere;&quot;&gt;Rejected&lt;/td&gt;
&lt;td style=&quot;padding:0.45rem 0.5rem;text-align:left;border-bottom:1px solid #d6d7db;vertical-align:top;word-break:break-word;overflow-wrap:anywhere;&quot;&gt;BYOC model still relies on an external SaaS control plane&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:0.45rem 0.5rem;text-align:left;border-bottom:1px solid #d6d7db;vertical-align:top;word-break:break-word;overflow-wrap:anywhere;&quot;&gt;&lt;strong&gt;Modal / Runloop&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:0.45rem 0.5rem;text-align:left;border-bottom:1px solid #d6d7db;vertical-align:top;word-break:break-word;overflow-wrap:anywhere;&quot;&gt;Rejected&lt;/td&gt;
&lt;td style=&quot;padding:0.45rem 0.5rem;text-align:left;border-bottom:1px solid #d6d7db;vertical-align:top;word-break:break-word;overflow-wrap:anywhere;&quot;&gt;Adds external network hops and introduces vendor lock-in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:0.45rem 0.5rem;text-align:left;border-bottom:1px solid #d6d7db;vertical-align:top;word-break:break-word;overflow-wrap:anywhere;&quot;&gt;&lt;strong&gt;k8s-agent-sandbox&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:0.45rem 0.5rem;text-align:left;border-bottom:1px solid #d6d7db;vertical-align:top;word-break:break-word;overflow-wrap:anywhere;&quot;&gt;&lt;strong&gt;Adopted&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:0.45rem 0.5rem;text-align:left;border-bottom:1px solid #d6d7db;vertical-align:top;word-break:break-word;overflow-wrap:anywhere;&quot;&gt;Native CRDs, fully self-hosted, integrates with standard Kubernetes NetworkPolicies&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;The Implementation: Adapter and Lifecycle&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;deepagents defaults to &lt;code&gt;StateBackend&lt;/code&gt;, an in-memory virtual filesystem that cannot execute bash commands or persist files across turns.&lt;/p&gt;
&lt;p&gt;Because the &lt;code&gt;langchain-kubernetes&lt;/code&gt; package had method and parameter mismatches with the real &lt;code&gt;k8s-agent-sandbox&lt;/code&gt; SDK, we built a 120-line custom adapter directly against &lt;code&gt;BaseSandbox&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;class _AgentSandbox(BaseSandbox):
    def __init__(self, sandbox) -&amp;gt; None:
        self._sandbox = sandbox

    @property
    def id(self) -&amp;gt; str:
        return self._sandbox.claim_name  # Stable across warm-pool rebinds

    def execute(self, command: str, *, timeout: int | None = None) -&amp;gt; ExecuteResponse:
        result = self._sandbox.commands.run(command, timeout=timeout or 60)
        output = result.stdout
        if result.stderr:
            output = f&quot;{output}\n{result.stderr}&quot; if output else result.stderr
        return ExecuteResponse(output=output, exit_code=result.exit_code)

    def upload_files(self, files: list[tuple[str, bytes]]) -&amp;gt; list[FileUploadResponse]:
        responses = []
        for path, content in files:
            try:
                self._sandbox.files.write(path, content)
                responses.append(FileUploadResponse(path=path, error=None))
            except Exception as e:
                responses.append(FileUploadResponse(path=path, error=str(e)))
        return responses

    def download_files(self, paths: list[str]) -&amp;gt; list[FileDownloadResponse]:
        responses = []
        for path in paths:
            try:
                content = self._sandbox.files.read(path)
                responses.append(FileDownloadResponse(path=path, content=content, error=None))
            except Exception as e:
                responses.append(FileDownloadResponse(path=path, content=None, error=str(e)))
        return responses

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To maintain persistent workspaces across conversation turns without locking memory to a single API replica, claim IDs are stored in Redis:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://varsni.com/notes/agent-sandbox-kubernetes/sandbox-lifecycle.svg&quot; alt=&quot;Each conversation id is looked up in Redis: a hit rebinds the existing sandbox claim, a miss creates a pod from the warm pool and writes the new claim name back&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;async def get_sandbox_backend(conversation_id: str) -&amp;gt; _AgentSandbox | None:
    if not settings.sandbox_enabled or not settings.sandbox_warmpool_name:
        return None

    namespace = settings.sandbox_namespace or &quot;default&quot;
    redis_key = f&quot;costops:sandbox:{conversation_id}&quot;
    existing_claim_name = await redis.get(redis_key)

    client = SandboxClient()
    try:
        sandbox = None
        if existing_claim_name:
            try:
                sandbox = await asyncio.to_thread(client.get_sandbox, existing_claim_name, namespace)
            except SandboxNotFoundError:
                pass
        if sandbox is None:
            sandbox = await asyncio.to_thread(
                client.create_sandbox,
                warmpool=settings.sandbox_warmpool_name,
                namespace=namespace,
                shutdown_after_seconds=settings.sandbox_idle_timeout_seconds,
            )
    except Exception:
        return None

    if sandbox.claim_name != existing_claim_name:
        await redis.set(redis_key, sandbox.claim_name)
    return _AgentSandbox(sandbox)

&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;The Gotcha: The Cross-Namespace RBAC Trap&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The most confusing failure during implementation was an RBAC error masked as a broken controller.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;SandboxClient&lt;/code&gt; connects using &lt;code&gt;connection_mode=&quot;tunnel&quot;&lt;/code&gt;, which shells out to a local &lt;code&gt;kubectl port-forward&lt;/code&gt; command. That command must resolve and forward to the &lt;strong&gt;sandbox-router&lt;/strong&gt; running in the &lt;code&gt;agent-sandbox-system&lt;/code&gt; namespace, not just resources in your application namespace.&lt;/p&gt;
&lt;p&gt;Your API ServiceAccount requires two separate RBAC bindings:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;# 1. In your application namespace (manage sandbox lifecycle)
- apiGroups: [&quot;agents.x-k8s.io&quot;]
  resources: [&quot;sandboxes&quot;]
  verbs: [&quot;get&quot;, &quot;list&quot;, &quot;watch&quot;, &quot;create&quot;, &quot;delete&quot;, &quot;patch&quot;]
- apiGroups: [&quot;extensions.agents.x-k8s.io&quot;]
  resources: [&quot;sandboxclaims&quot;]
  verbs: [&quot;get&quot;, &quot;list&quot;, &quot;watch&quot;, &quot;create&quot;, &quot;delete&quot;, &quot;patch&quot;]
- apiGroups: [&quot;extensions.agents.x-k8s.io&quot;]
  resources: [&quot;sandboxwarmpools&quot;]
  verbs: [&quot;get&quot;, &quot;list&quot;, &quot;watch&quot;]
- apiGroups: [&quot;&quot;]
  resources: [&quot;pods&quot;, &quot;pods/portforward&quot;]
  verbs: [&quot;get&quot;, &quot;list&quot;, &quot;watch&quot;]
---
# 2. In agent-sandbox-system (allow port-forwarding to the router)
- apiGroups: [&quot;&quot;]
  resources: [&quot;services&quot;, &quot;pods&quot;, &quot;pods/portforward&quot;]
  verbs: [&quot;get&quot;, &quot;list&quot;, &quot;watch&quot;]

&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;Hardening and Safety Verification&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Security relies on strict infrastructure policies:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;No IAM Role Binding:&lt;/strong&gt; The &lt;code&gt;costops-sandbox&lt;/code&gt; ServiceAccount carries no AWS IAM annotations (&lt;code&gt;[eks.amazonaws.com/role-arn](https://eks.amazonaws.com/role-arn)&lt;/code&gt;) and mounts no cloud tokens.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DNS-Only Egress:&lt;/strong&gt; Network policies deny all outbound internet and VPC traffic except UDP/TCP port 53 for internal cluster DNS.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pod Hardening:&lt;/strong&gt; Containers run as UID 1000 with &lt;code&gt;allowPrivilegeEscalation: false&lt;/code&gt; and all Linux capabilities dropped.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We verified the isolation directly inside a live sandbox pod:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;kubectl -n costops exec -it &amp;lt;sandbox-pod&amp;gt; -- env | grep -i aws
kubectl -n costops exec -it &amp;lt;sandbox-pod&amp;gt; -- aws sts get-caller-identity

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Both commands return empty results and connection timeouts.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;Key Takeaways&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Treat every agent sandbox as an untrusted workload by dropping Linux capabilities and enforcing non-root execution.&lt;/li&gt;
&lt;li&gt;Do not rely on system prompts for security boundaries. Use Kubernetes NetworkPolicies and clean ServiceAccounts instead.&lt;/li&gt;
&lt;li&gt;Store conversation-to-claim mappings in shared storage like Redis rather than local process memory.&lt;/li&gt;
&lt;li&gt;Grant port-forward RBAC permissions in both the application namespace and the system routing namespace.&lt;/li&gt;
&lt;li&gt;Keep the runtime execution container separate from your main backend image to avoid credential leakage.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Building a secure code execution sandbox for AI agents does not require expensive managed platforms or complex bare-metal virtualization. By combining &lt;code&gt;kubernetes-sigs/agent-sandbox&lt;/code&gt; with strict NetworkPolicies, dedicated ServiceAccounts, and a lightweight adapter, you get full control over your infrastructure, complete data privacy, and robust security guarantees right inside your existing cluster.&lt;/p&gt;
</content:encoded></item><item><title>Cost Governance for LLM Applications</title><link>https://varsni.com/notes/llm-cost-governance</link><guid isPermaLink="true">https://varsni.com/notes/llm-cost-governance</guid><description>How to track and control costs for LLM pipelines and applications</description><pubDate>Wed, 12 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Most teams confuse &lt;strong&gt;cost visibility&lt;/strong&gt; with &lt;strong&gt;cost governance&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Seeing a sudden $15,000 spike on a line-item graph is visibility. Preventing a rogue agent loop from spending $15,000 in twenty minutes while you sleep is governance. Confusing the two is how a $2,000/month pilot turns into an eye-watering invoice that ruins your quarter.&lt;/p&gt;
&lt;p&gt;When I finished building the tracing, evals, guardrails, and prompt versioning for an end-to-end LLMOps pipeline, cost was the final item marked &lt;em&gt;&quot;we&apos;ll figure it out later.&quot;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Here is what actually &quot;figuring it out&quot; looks like in production.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;The Default Trapping: One Line Item, Zero Context&lt;/h2&gt;
&lt;p&gt;By default, managed platforms like AWS Bedrock bill LLM usage as a single consolidated line item. You watch the total cost climb, but you can’t answer critical post-mortem questions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Which engineering team owns this burst?&lt;/li&gt;
&lt;li&gt;Which specific agent script spiraled into an infinite retry loop?&lt;/li&gt;
&lt;li&gt;Which bad prompt template inflated our token counts?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Solving this is the key task. Every optimization effort before attribution is just uneducated guessing.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;The 4-Layer LLM Governance Framework&lt;/h2&gt;
&lt;p&gt;To move from passive monitoring to active governance, implement these four layers sequentially.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://varsni.com/notes/llm-cost-governance/layers.svg&quot; alt=&quot;Four layers of LLM cost governance: attribution, alerting, enforcement, and optimization&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Layer 1: Granular Attribution&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Application Inference Profiles (AIPs):&lt;/strong&gt; Wrap Bedrock model ARNs inside tagged profiles. Route calls through these ARNs so metadata automatically flows to AWS Cost Explorer and Cost &amp;amp; Usage Reports (CUR). Tag by team or cost center—never per user (it won&apos;t scale).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;IAM Principal Allocation:&lt;/strong&gt; Enable caller identity data in CUR 2.0 to allocate costs directly to IAM roles and users without building custom middleware.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Per-Request Metadata:&lt;/strong&gt; For sub-penny telemetry (like prompt/completion token ratios), pass a &lt;code&gt;requestMetadata&lt;/code&gt; payload containing your &lt;code&gt;tenantId&lt;/code&gt; or &lt;code&gt;userId&lt;/code&gt; and query it via CloudWatch Logs Insights.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Layer 2: Proactive Alerting&lt;/h3&gt;
&lt;p&gt;Avoid single, org-wide budget caps—nobody takes ownership of them. Set project-level budgets using &lt;strong&gt;AWS Budgets&lt;/strong&gt; paired with &lt;strong&gt;Cost Anomaly Detection&lt;/strong&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Remember:&lt;/strong&gt; An alert only tells you that damage has occurred. It does not stop the bleed.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Layer 3: Hard Enforcement (The Missing Layer)&lt;/h3&gt;
&lt;p&gt;This is the step most engineering teams skip until their first billing incident. An runaway loop burns thousands of dollars in minutes, long before your CloudWatch alarm evaluates or an email alert triggers.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Enforcement requires an in-path check.&lt;/strong&gt; Run a lightweight proxy or API middleware backed by a fast state store (like Redis) that evaluates live token/dollar spend and active-gates incoming requests before they hit the upstream model provider.&lt;/p&gt;
&lt;h3&gt;Layer 4: Tactical Optimization&lt;/h3&gt;
&lt;p&gt;Once you know &lt;em&gt;who&lt;/em&gt; is spending and &lt;em&gt;where&lt;/em&gt;, apply the levers that actually impact the bottom line:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Context &amp;amp; Prompt Caching:&lt;/strong&gt; Cache static system prompts and static RAG context blocks.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dynamic Model Routing:&lt;/strong&gt; Route simple sub-tasks (classification, extraction, rewrites) to lighter, cheaper models.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Batch Inference:&lt;/strong&gt; Offload non-real-time jobs to async batch endpoints for ~50% cost reductions.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Provisioned Throughput:&lt;/strong&gt; Commit to dedicated capacity only after baseline usage stabilizes.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Centralized Governance Gateways: Native vs. Proxy&lt;/h2&gt;
&lt;p&gt;AWS-native tools work well within a single ecosystem. But the moment your application makes calls across OpenAI, Anthropic, and Bedrock, native tagging falls apart.&lt;/p&gt;
&lt;p&gt;You need a centralized &lt;strong&gt;Governance Gateway&lt;/strong&gt; in front of your invocation pipeline.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://varsni.com/notes/llm-cost-governance/gateway.svg&quot; alt=&quot;Your app calls an AI gateway (LiteLLM or Portkey), which fans out to AWS Bedrock, OpenAI, and Anthropic&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Choosing Your Control Plane&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;AWS-Native Only (AIPs + CUR 2.0):&lt;/strong&gt; Best if you are single-cloud, have modest scale, and don&apos;t want to maintain additional infrastructure proxy layers.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AWS-Native + LiteLLM / Portkey:&lt;/strong&gt; Stay entirely on Bedrock infrastructure, but deploy an open-source gateway (LiteLLM) or managed control plane (Portkey) in front of it to gain fallbacks, prompt caching, and dynamic routing logic.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Multi-Provider Proxy (LiteLLM / Portkey / Helicone):&lt;/strong&gt; Standardize all outgoing calls across multiple providers behind a single OpenAI-compatible interface.&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Rule of thumb:&lt;/strong&gt; Place a governance gateway in front of your LLM calls &lt;strong&gt;before&lt;/strong&gt; integrating a second model provider. Refactoring call sites after going multi-provider is painful and prone to governance leaks.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;Production Readiness Checklist&lt;/h2&gt;
&lt;p&gt;Before marking your LLMOps cost governance task as &quot;Done,&quot; verify these items:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;[ ] Every model invocation maps to a defined team, service, or project.&lt;/li&gt;
&lt;li&gt;[ ] Project-level budgets are set with automated anomaly detection alerts.&lt;/li&gt;
&lt;li&gt;[ ] In-path enforcement (circuit breakers/rate limits) exists for volatile workloads.&lt;/li&gt;
&lt;li&gt;[ ] At least one optimization lever (caching, routing, or batching) is active in production.&lt;/li&gt;
&lt;li&gt;[ ] Model and vector store pricing assumptions are updated against current rates.&lt;/li&gt;
&lt;li&gt;[ ] A proxy/gateway layer is architecture-ready before multi-provider expansion.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;The Takeaway&lt;/h2&gt;
&lt;p&gt;Cost dashboards are popular because they are easy to deploy. But visibility only tells you how much money you’ve already lost.&lt;/p&gt;
&lt;p&gt;True governance should be structural: hard gates, real-time attribution, and dynamic routing that protect your infrastructure budgets automatically. Build visibility first, but just don&apos;t stop there.&lt;/p&gt;
</content:encoded></item><item><title>Why I Chose AWS Batch for GPU Workloads (And What It Took to Make It Work)</title><link>https://varsni.com/notes/aws-batch-gpu</link><guid isPermaLink="true">https://varsni.com/notes/aws-batch-gpu</guid><description>How to set up GPU batch processing so it was fast, cheaper, and reliable</description><pubDate>Thu, 18 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Running GPU jobs in the cloud looks simple at first. It rarely is. Queues get slow. Costs creep up. Instances behave in ways you do not expect.&lt;/p&gt;
&lt;p&gt;This post walks through how I set up GPU batch processing so it was fast, cheaper, and reliable. It also covers the three changes that actually moved the needle.&lt;/p&gt;
&lt;h2&gt;The problem: heavy GPU jobs, not constant traffic&lt;/h2&gt;
&lt;p&gt;My workload was not a steady stream. Jobs arrived in bursts. Sometimes a bunch at once. Sometimes nothing for a while. The compute needs were real and big. They just were not 24/7.&lt;/p&gt;
&lt;p&gt;That left a few bad choices:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Keep a GPU instance running all the time and burn money when idle.&lt;/li&gt;
&lt;li&gt;Start EC2 instances by hand for each job and take on all the ops work.&lt;/li&gt;
&lt;li&gt;Write a custom queue with my own logic for retries and concurrency.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;None of that felt right. I needed something that could sit quiet when no work was there. It should wake up on new jobs, run things in order, handle retries, and not need manual care.&lt;/p&gt;
&lt;h2&gt;Why AWS Batch made sense&lt;/h2&gt;
&lt;p&gt;AWS Batch is built for this kind of pattern. It manages the lifecycle of compute for you. Instances come up when jobs arrive. They go away when there is no work.&lt;/p&gt;
&lt;p&gt;For GPU jobs this works well. Batch supports GPU instance families and runs everything as containers. Jobs run in the same environment every time. No custom setup per run.&lt;/p&gt;
&lt;p&gt;A few parts were key.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Job queues with real controls.&lt;/strong&gt; Batch gives you job queues with priorities, retry settings, and job dependencies. No polling scripts. No custom schedulers. No guessing what ran.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Compute environments with launch templates.&lt;/strong&gt; You can attach a launch template to a compute environment. This gives control over userdata, EBS volumes, and ECS agent config. That control became important later.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GPU resource targeting.&lt;/strong&gt; In the job definition you say how many GPUs a job needs. Batch handles the scheduling. For my case the &lt;code&gt;g5&lt;/code&gt; family worked well. Enough GPU memory and good throughput.&lt;/p&gt;
&lt;h2&gt;Step Functions for orchestration&lt;/h2&gt;
&lt;p&gt;Batch runs jobs. It does not decide what should run next.&lt;/p&gt;
&lt;p&gt;For that I used AWS Step Functions. Real workloads are pipelines. They have steps and branches. They need retries and error handling.&lt;/p&gt;
&lt;p&gt;Step Functions gave me a state machine to model that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Run jobs in sequence and pass outputs forward.&lt;/li&gt;
&lt;li&gt;Branch on success or failure without custom code.&lt;/li&gt;
&lt;li&gt;Handle retries with backoff at the workflow level.&lt;/li&gt;
&lt;li&gt;See where a pipeline is stuck or moving.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Batch handled how jobs run. Step Functions handled when they run and in what order.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://varsni.com/notes/aws-batch-gpu/architecture.svg&quot; alt=&quot;Architecture diagram showing jobs flowing from Step Functions orchestration into an AWS Batch job queue and GPU compute environment&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Where things broke and how we fixed them&lt;/h2&gt;
&lt;p&gt;The setup on paper looks clean. In practice the first version was slower and more expensive than it should have been. Some instance behavior was also not obvious until we looked closer.&lt;/p&gt;
&lt;p&gt;Here are the three main problems. And what fixed them.&lt;/p&gt;
&lt;h3&gt;Problem 1: Image pulls slowed every job&lt;/h3&gt;
&lt;p&gt;Each job pulled the Docker image from ECR before it could start. The image was large. It had CUDA, ML libraries, and other tools. The pull took time. For a single job it was already noticeable. For a busy queue it was painful.&lt;/p&gt;
&lt;p&gt;The root cause was ECS image cleanup.&lt;/p&gt;
&lt;p&gt;By default the ECS agent on the Batch instance removes Docker images from time to time. This frees disk space. That is fine for long running services. For batch GPU jobs it hurts. You actually want the image to stay on disk so the next job can reuse it.&lt;/p&gt;
&lt;p&gt;The fix was to disable image cleanup.&lt;/p&gt;
&lt;p&gt;Set &lt;code&gt;ECS_IMAGE_CLEANUP_ENABLED=false&lt;/code&gt; in &lt;code&gt;/etc/ecs/ecs.config&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The clean way to do this is in userdata inside the launch template. The instance boots, reads the config, and starts ECS with cleanup turned off. The image then stays cached.&lt;/p&gt;
&lt;p&gt;After that change the first job on a new instance still pulls the image. Jobs that follow on the same instance skip the pull and start compute right away.&lt;/p&gt;
&lt;h3&gt;Problem 2: Scale-down removed the cache benefit&lt;/h3&gt;
&lt;p&gt;Caching helps only if the instance stays alive long enough. Our compute environment scaled down fast. When a job finished the instance often shut down. The next job then got a fresh instance and a fresh pull. We were back to cold starts.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://varsni.com/notes/aws-batch-gpu/cold-vs-warm.svg&quot; alt=&quot;Comparison of a cold instance that pulls the image on every job versus a warm instance that hits the cache and starts computing immediately&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The fix was to adjust scale-down behavior.&lt;/p&gt;
&lt;p&gt;Instead of killing instances as soon as there were no jobs, we kept them alive for a short window. During this time the image stayed cached. If new jobs arrived, they landed on a warm instance and skipped the pull.&lt;/p&gt;
&lt;p&gt;This is a tradeoff. You pay for some idle time. In return you get lower latency and better throughput during bursts. For workloads that come in clusters this works well.&lt;/p&gt;
&lt;p&gt;I also wanted this to be flexible. So the scale-down window is configurable from the app.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If I expect a quiet phase I reduce the window. Idle instances shut down fast.&lt;/li&gt;
&lt;li&gt;If I expect a spike I increase the window. Instances stay warm for longer.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The control lives next to the usage signal. So the cost of warm instances appears only when it brings value.&lt;/p&gt;
&lt;h3&gt;Problem 3: Multi-GPU instances looked wasteful&lt;/h3&gt;
&lt;p&gt;A &lt;code&gt;g5.12xlarge&lt;/code&gt; has four GPUs. A single GPU job uses one. At first this looked bad. You pay for four GPUs. One is busy. Three are idle.&lt;/p&gt;
&lt;p&gt;The simple view is that multi-GPU instances are not good for single GPU jobs. With caching and warm instances that view changes.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://varsni.com/notes/aws-batch-gpu/multi-gpu.svg&quot; alt=&quot;A single g5.12xlarge with four GPUs: the first job pays the warmup cost while the next three jobs run immediately on the already-cached instance&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Once you:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Keep images cached, and&lt;/li&gt;
&lt;li&gt;Keep instances warm for a short time&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;a multi-GPU node turns into a throughput boost.&lt;/p&gt;
&lt;p&gt;The first job on that node pays the warmup cost. Instance launch plus image pull. After that three GPUs are still free. The next three jobs can start right away.&lt;/p&gt;
&lt;p&gt;No new instances. No extra image pulls. Just scheduling and compute.&lt;/p&gt;
&lt;p&gt;For bursty workloads this is a big shift. You do not pay the warmup cost four times on four machines. You pay once and reuse it across all GPUs on that node.&lt;/p&gt;
&lt;h2&gt;How it behaved after the fixes&lt;/h2&gt;
&lt;p&gt;With all three changes in place the system behaved very differently.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The first job on a new instance still has a warmup cost.&lt;/li&gt;
&lt;li&gt;Jobs after that on the same instance start at compute.&lt;/li&gt;
&lt;li&gt;The scale-down window keeps instances alive just long enough to catch bursts.&lt;/li&gt;
&lt;li&gt;On multi-GPU nodes the warmup cost spreads across several jobs.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Queues moved faster. The cost per job dropped. Bursts felt smoother with less random latency.&lt;/p&gt;
&lt;h2&gt;The bigger takeaway&lt;/h2&gt;
&lt;p&gt;AWS Batch gives you managed compute and native job queues. Step Functions gives you a solid way to build pipelines. The jump from “it works” to &lt;strong&gt;“it works well”&lt;/strong&gt; came from three small but important tweaks:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Turn off image cleanup&lt;/strong&gt; so large GPU images stay cached.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tune scale-down&lt;/strong&gt; so instances stay warm when bursts are likely.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use multi-GPU instances for throughput&lt;/strong&gt; once caching is in place.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;These details do not stand out in the basic docs. In practice they made a clear difference.&lt;/p&gt;
</content:encoded></item><item><title>Designing Reliable Agent Workflows</title><link>https://varsni.com/notes/reliable-agents</link><guid isPermaLink="true">https://varsni.com/notes/reliable-agents</guid><description>How to design reliable workflows that improve agent performance</description><pubDate>Mon, 15 Dec 2025 06:21:01 GMT</pubDate><content:encoded>&lt;p&gt;Hi there!&lt;/p&gt;
&lt;p&gt;In this post, I&apos;ll show you how to design reliable workflows that improve agent performance. We&apos;ll cover how to fix vague roles, enforce strict inputs/outputs, and manage state explicitly.&lt;/p&gt;
&lt;h2&gt;Why agents fail&lt;/h2&gt;
&lt;p&gt;Most failures come from design, not intelligence.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Vague roles confuse agents. If one agent has to plan, do the work, and judge the result, it often loses focus and goes off track.&lt;/li&gt;
&lt;li&gt;Loose inputs and outputs hide problems. Free-form text makes it easy to sound right while being wrong.&lt;/li&gt;
&lt;li&gt;Missing state causes loops. If the system doesn’t track what ran and what failed, it repeats the same mistakes.&lt;/li&gt;
&lt;li&gt;No guardrails means risky actions slip through. With unchecked access, one bad step can cause real damage fast.&lt;/li&gt;
&lt;li&gt;Weak error handling makes failures messy. Blind retries and late escalation lead to quiet breakage or endless retries.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;When do you need this?&lt;/h2&gt;
&lt;p&gt;Not every task needs three agents and a state machine. If you just need to &quot;summarize this text&quot; or &quot;write a haiku,&quot; a single prompt is fine.&lt;/p&gt;
&lt;p&gt;Use this structured workflow when the task is:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Complex and multi-step&lt;/strong&gt;: Requires planning, research, and synthesis (e.g., &quot;Plan a travel itinerary&quot;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;High risk&lt;/strong&gt;: Involves sending emails, making purchases, or modifying data.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Long-running&lt;/strong&gt;: Needs to run for minutes or hours without losing context.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Data-heavy&lt;/strong&gt;: Involves querying databases, analyzing large datasets, or cross-referencing multiple sources.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;A bad workflow (what not to do)&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Goal&lt;/strong&gt;: Pull product metrics, analyze them, and email a summary to the team.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Design&lt;/strong&gt;: A single “analyst agent” with a broad prompt:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“You are an expert analyst. Gather metrics, analyze them, write a summary, and email the team.”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://varsni.com/notes/reliable-agents/placeholder-monolithic-agent.svg&quot; alt=&quot;Flowchart of a single monolithic agent failing to manage multiple tasks&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;What could go wrong&lt;/h3&gt;
&lt;p&gt;The workflow crumbles quickly. Wrong tables are queried because the prompt is vague, and metrics are invented when data access fails. The write-up might sound professional but contains fake numbers. Worse, emails often go to wrong recipients due to parsing errors. When failures occur, the agent simply retries with new wording, entering an endless loop.&lt;/p&gt;
&lt;h3&gt;Root causes&lt;/h3&gt;
&lt;p&gt;This failure stems from a lack of structure. There is no split between planning, execution, and validation. Without clear input/output formats or success rules, the agent guesses. Explicit state like “data fetched” or “email sent” is missing, and there is no write protection or approval for emails. Instead of structured failure, the only strategy is &quot;try again.&quot;&lt;/p&gt;
&lt;h2&gt;A better workflow (step by step)&lt;/h2&gt;
&lt;p&gt;Use three small agents, each with one job:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Planner agent&lt;/strong&gt; – decides steps and defines inputs and outputs&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Executor agent&lt;/strong&gt; – runs steps with limited access&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Validator agent&lt;/strong&gt; – checks outputs and blocks unsafe actions&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://varsni.com/notes/reliable-agents/placeholder-three-agent-workflow.svg&quot; alt=&quot;Diagram showing Planner, Executor, and Validator interaction&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;1. Define clear inputs and outputs&lt;/h3&gt;
&lt;p&gt;Every step needs a small contract:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Input format&lt;/strong&gt;: typed JSON fields&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Output format&lt;/strong&gt;: JSON with a fixed schema&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Success condition&lt;/strong&gt;: true/false with a reason&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Avoid free-form text at the interface level. Natural language can exist inside fields, but the structure must be strict.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Example: Fetch weekly metrics&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Input&lt;/strong&gt;&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{ &quot;week_start&quot;: &quot;ISO date&quot;, &quot;metrics&quot;: [&quot;active_users&quot;, &quot;revenue&quot;] }
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Output&lt;/strong&gt;&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{ &quot;success&quot;: boolean, &quot;data&quot;: { &quot;active_users&quot;: number, &quot;revenue&quot;: number }, &quot;errors&quot;: [string] }
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Success rule&lt;/strong&gt;: Success is true, all fields exist, and values fall within expected ranges.&lt;/p&gt;
&lt;h3&gt;2. Manage state explicitly&lt;/h3&gt;
&lt;p&gt;Store state in a place all agents can read. Keep track of &lt;code&gt;done&lt;/code&gt; steps (outputs and timestamps), &lt;code&gt;failed&lt;/code&gt; attempts (errors with codes), &lt;code&gt;pending&lt;/code&gt; items, and &lt;code&gt;notes&lt;/code&gt; (links/assumptions).&lt;/p&gt;
&lt;p&gt;Never rely on only chat memory. Each agent reads state at the start and writes state at the end. Use IDs for steps and outputs so everything is traceable.&lt;/p&gt;
&lt;h3&gt;3. Guardrails by default&lt;/h3&gt;
&lt;p&gt;Security should be baked in. Start with &lt;strong&gt;read-only access scopes&lt;/strong&gt;, granting write access only when needed. Enforce &lt;strong&gt;tool limits&lt;/strong&gt; by allowing only whitelisted tools with strict schemas. Require &lt;strong&gt;human approval&lt;/strong&gt; for high-risk actions like emails or external calls. Implement &lt;strong&gt;rate limits&lt;/strong&gt; to prevent retry storms, and use &lt;strong&gt;content rules&lt;/strong&gt; to block PII leaks or unsafe text.&lt;/p&gt;
&lt;h3&gt;4. Failure handling that ends cleanly&lt;/h3&gt;
&lt;p&gt;Failures should be expected and designed for. Limit &lt;strong&gt;retries&lt;/strong&gt; by error type with backoff, and allow only &lt;strong&gt;one change per retry&lt;/strong&gt; never rewrite the whole plan. &lt;strong&gt;Escalate&lt;/strong&gt; after N failures, stopping to hand off context. Ensure a &lt;strong&gt;safe exit&lt;/strong&gt; where, if assumptions break, the workflow terminates without loops or made-up data.&lt;/p&gt;
&lt;h2&gt;Putting it together: a concrete flow&lt;/h2&gt;
&lt;h3&gt;A) Planning&lt;/h3&gt;
&lt;p&gt;The planner receives the high-level goal and converts it into a structured checklist.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Input&lt;/strong&gt;: &lt;code&gt;task = &quot;Weekly metrics summary email&quot;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Output&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;plan_id&quot;: &quot;plan_123&quot;,
  &quot;steps&quot;: [
    { &quot;step&quot;: 1, &quot;action&quot;: &quot;fetch_metrics&quot;, &quot;params&quot;: { &quot;range&quot;: &quot;last_7_days&quot; }, &quot;status&quot;: &quot;pending&quot; },
    { &quot;step&quot;: 2, &quot;action&quot;: &quot;analyze_trends&quot;, &quot;params&quot;: { &quot;focus&quot;: [&quot;growth&quot;, &quot;churn&quot;] }, &quot;status&quot;: &quot;pending&quot; },
    { &quot;step&quot;: 3, &quot;action&quot;: &quot;draft_email&quot;, &quot;params&quot;: { &quot;template&quot;: &quot;weekly_summary&quot; }, &quot;status&quot;: &quot;pending&quot; }
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;B) Execution (read-only first)&lt;/h3&gt;
&lt;p&gt;The executor picks up pending steps one by one. It reads the shared state, calls the necessary tools, and writes the output back to state.&lt;/p&gt;
&lt;p&gt;It first &lt;strong&gt;fetches the metrics&lt;/strong&gt;, then &lt;strong&gt;analyzes trends&lt;/strong&gt;, and only then &lt;strong&gt;drafts the email&lt;/strong&gt; using the fetched data.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Action&lt;/strong&gt;: &lt;code&gt;draft_email&lt;/code&gt;
&lt;strong&gt;Result&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;step&quot;: 3,
  &quot;action&quot;: &quot;draft_email&quot;,
  &quot;success&quot;: true,
  &quot;output&quot;: {
    &quot;subject&quot;: &quot;Weekly Metrics: +5% Growth&quot;,
    &quot;body&quot;: &quot;Hi Team, Active users are up to 10,500...&quot;,
    &quot;recipients&quot;: [&quot;team@company.com&quot;]
  },
  &quot;timestamp&quot;: &quot;2025-12-15T10:05:00Z&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(Note: No emails are sent yet. The executor only produces data and drafts.)&lt;/p&gt;
&lt;h3&gt;C) Validation&lt;/h3&gt;
&lt;p&gt;The validator reviews the drafted email against safety rules and data mapping.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Check&lt;/strong&gt;: &lt;code&gt;draft_email&lt;/code&gt; output vs &lt;code&gt;fetch_metrics&lt;/code&gt; data
&lt;strong&gt;Rules&lt;/strong&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Data Mapping&lt;/strong&gt;: Do the numbers in the email body match the fetched JSON data?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Safety&lt;/strong&gt;: No PII (Personally Identifiable Information) or leaked secrets.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Policy&lt;/strong&gt;: Recipients match the internal allow-list etc.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Validation Result&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;validation_id&quot;: &quot;val_456&quot;,
  &quot;plan_id&quot;: &quot;plan_123&quot;,
  &quot;validated_step&quot;: 3,
  &quot;approved&quot;: true,
  &quot;risk_level&quot;: &quot;low&quot;,
  &quot;reason&quot;: &quot;Draft matches source data. No sensitive info found.&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;D) Approval and sending&lt;/h3&gt;
&lt;p&gt;The final gatekeeper decides whether to proceed based on the validation result.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Low risk&lt;/strong&gt;: Auto-send via the API.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;High risk&lt;/strong&gt; (or validation failed): Trigger a human approval request with a diff of the proposed email.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://varsni.com/notes/reliable-agents/placeholder-sequence-flow.svg&quot; alt=&quot;Sequence diagram detailing the flow from Planning to Sending&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Best Practices for Reliability&lt;/h2&gt;
&lt;h3&gt;1. Implementation Tips&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Give the agent a &quot;Task Tracker&quot; tool&lt;/strong&gt;
Instead of asking the agent to remember the plan in its context window, give it a tool to read and write to a persistent checklist.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Map the work&lt;/strong&gt;: The agent calls &lt;code&gt;create_tasks([...])&lt;/code&gt; to lock in the plan.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Track progress&lt;/strong&gt;: It calls &lt;code&gt;complete_task(id)&lt;/code&gt; after every success.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ensure completion&lt;/strong&gt;: Before finishing, it calls &lt;code&gt;get_pending_tasks()&lt;/code&gt; to verify nothing was missed.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This externalizes memory, making the agent robust against distractions and context limits.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Treat prompts like code&lt;/strong&gt;
Prompts are logic. Version control them, review changes in PRs, and test them against golden datasets. Don&apos;t let prompts live in database strings or loose variables.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Prefer deterministic tools&lt;/strong&gt;
Reliability drops when tools are fuzzy. Prefer structured APIs over web scraping. If you must parse text, wrap it in a dedicated &quot;extraction&quot; agent that outputs JSON.&lt;/p&gt;
&lt;h3&gt;2. Common pitfalls to avoid&lt;/h3&gt;
&lt;div style=&quot;overflow-x:auto;-webkit-overflow-scrolling:touch;width:100%;max-width:100%;margin:1.25em 0;&quot;&gt;&lt;table style=&quot;width:100%;max-width:100%;border-collapse:collapse;table-layout:fixed;font-size:12px;line-height:1.35;&quot;&gt;
&lt;thead style=&quot;border-bottom:2px solid #d6d7db;&quot;&gt;
&lt;tr&gt;
&lt;th style=&quot;text-align:left&quot;&gt;Pitfall&lt;/th&gt;
&lt;th style=&quot;text-align:left&quot;&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;“One agent is simpler”&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;It isn’t. Split roles.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;“We’ll pass chat history”&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;Persist real state instead.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;“We need creative freedom”&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;Keep creativity inside strict containers.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;“Retries will fix it”&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;Retry less, with better signals.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;“Humans slow things down”&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;Humans prevent costly mistakes.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h3&gt;3. Reliability Checklist&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Clear roles: Planner, Executor, Validator&lt;/li&gt;
&lt;li&gt;Contracts for every step&lt;/li&gt;
&lt;li&gt;Explicit state, no chat memory&lt;/li&gt;
&lt;li&gt;Read-only by default, limited writes&lt;/li&gt;
&lt;li&gt;Structured failures and safe exits&lt;/li&gt;
&lt;li&gt;Traceable IDs and logs&lt;/li&gt;
&lt;li&gt;Data sanity checks&lt;/li&gt;
&lt;li&gt;Per-step access control&lt;/li&gt;
&lt;li&gt;Tests for bad inputs and edge cases&lt;/li&gt;
&lt;li&gt;Regular audits of failures and near misses&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Closing thought&lt;/h2&gt;
&lt;p&gt;Reliable agent systems are built like reliable software systems. Separation of concerns, Strict interfaces, Explicit state and Careful access.&lt;/p&gt;
&lt;p&gt;Models help, but workflows carry the weight. Design the workflow assuming it will fail and it won&apos;t surprise you when it tries.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;Thanks for reading! 🎉&lt;/p&gt;
&lt;p&gt;If you&apos;re building agent workflows, I hope these patterns help you create more reliable systems. Remember: good design beats clever prompts every time. If you have questions or want to share your own reliability patterns, feel free to reach out!&lt;/p&gt;
</content:encoded></item><item><title>MCP Explained: How AI Tools and Agents Work Together</title><link>https://varsni.com/notes/what-is-mcp</link><guid isPermaLink="true">https://varsni.com/notes/what-is-mcp</guid><description>How MCP enables AI agents to talk to tools and APIs and share information securely, in real time</description><pubDate>Wed, 02 Jul 2025 11:06:17 GMT</pubDate><content:encoded>&lt;h2&gt;What is MCP?&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;MCP stands for Model Context Protocol.&lt;/strong&gt;&lt;br /&gt;
It&apos;s a new open standard that enables AI agents to talk to tools and APIs and share information securely, in real time.&lt;/p&gt;
&lt;p&gt;Think of it like this:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;LLM + Tools = Agent.&lt;/strong&gt;&lt;br /&gt;
MCP is what makes that “+ Tools” part easier, flexible, and secure.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Think of MCP Like a USB-C Cable&lt;/h3&gt;
&lt;p&gt;Let’s break it down with a simple analogy:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You have a &lt;strong&gt;laptop (Agent)&lt;/strong&gt; and a &lt;strong&gt;smartphone (Data Source)&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;You want to transfer photos from your phone to your laptop.&lt;/li&gt;
&lt;li&gt;So, you use a &lt;strong&gt;USB-C cable (MCP)&lt;/strong&gt; to connect them.&lt;/li&gt;
&lt;li&gt;The cable doesn’t care what brand your devices are as long as they speak the same protocol, they can share data seamlessly.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Now imagine the same idea, but with AI tools:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;laptop is an AI agent&lt;/strong&gt; like Amazon Q or Claude.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;phone is a data source&lt;/strong&gt; like a database, API, or local file.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;USB-C cable is MCP&lt;/strong&gt; the protocol that lets these tools talk to each other and get things done.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;MCP acts like the connector between agents and the tools they need to be useful in real-world workflows.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://varsni.com/notes/what-is-mcp/mcp-usb.png&quot; alt=&quot;MCP USB&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;A Real-World Use Case&lt;/h3&gt;
&lt;p&gt;Let’s say your company has important business data in a &lt;strong&gt;PostgreSQL database&lt;/strong&gt;. You’d like a GenAI assistant to read from that data and generate reports or automate tasks.&lt;/p&gt;
&lt;p&gt;With MCP, this becomes very doable:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Set up a &lt;strong&gt;PostgreSQL MCP server&lt;/strong&gt; to expose that data.&lt;/li&gt;
&lt;li&gt;Connect an &lt;strong&gt;agent (like Amazon Q)&lt;/strong&gt; to it using MCP.&lt;/li&gt;
&lt;li&gt;The agent can now securely query the database, generate insights, or automate reporting all while respecting any permissions you configure on the MCP server.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Basically, &lt;strong&gt;MCP works like an API layer between your tools and the agent&lt;/strong&gt;, but with more flexibility and control.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Components of MCP&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://varsni.com/notes/what-is-mcp/mcp-components.png&quot; alt=&quot;MCP Components&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Let’s look at the core pieces that make MCP work:&lt;/p&gt;
&lt;h4&gt;MCP Server:&lt;/h4&gt;
&lt;p&gt;A service that exposes a data source or tool like a database, CLI, or document for an agent to use.&lt;/p&gt;
&lt;h4&gt;MCP Client:&lt;/h4&gt;
&lt;p&gt;Usually the AI agent or model (like Claude or Amazon Q) that initiates the interaction by sending requests.&lt;/p&gt;
&lt;h4&gt;MCP Protocol:&lt;/h4&gt;
&lt;p&gt;The communication standard that defines how the client and server talk. Think of it like the “language” they both understand.&lt;/p&gt;
&lt;h4&gt;MCP Host:&lt;/h4&gt;
&lt;p&gt;Where the MCP client or server runs. This might be:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Your &lt;strong&gt;local machine&lt;/strong&gt; (e.g., Claude Desktop, Cursor IDE)&lt;/li&gt;
&lt;li&gt;An &lt;strong&gt;app or agent&lt;/strong&gt; hosted in the cloud using frameworks like LangChain or Strands&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;Wrapping It All Up&lt;/h3&gt;
&lt;p&gt;As you can see, &lt;strong&gt;MCP acts like the API/backend for agents&lt;/strong&gt;, making them capable of interacting with the real world. Plugging in or removing tools could become as easy as installing a plugin or extension (like in Cursor IDE).&lt;/p&gt;
&lt;p&gt;It’s early days, but the potential is huge and growing fast.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;🚀 Want to Try It Yourself?&lt;/h3&gt;
&lt;p&gt;If you’re curious to explore MCP, here are some next steps you can try out:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Set up your own MCP server using the open-source &lt;code&gt;fastmcp&lt;/code&gt; template: &lt;a href=&quot;https://pypi.org/project/fastmcp/1.0/&quot;&gt;fastmcp on PyPI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Connect it to a database, folder, or API&lt;/li&gt;
&lt;li&gt;Then test how an agent like Claude Desktop or Amazon Q can interact with it&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;Thanks for reading! 🎉&lt;/p&gt;
&lt;p&gt;If this post helped clarify what MCP is all about, feel free to share it, or let me know what you’re building with it. Excited to see where this goes!&lt;/p&gt;
</content:encoded></item><item><title>Build Lightweight MCP Servers Easily with FastMCP</title><link>https://varsni.com/notes/create-mcp-servers</link><guid isPermaLink="true">https://varsni.com/notes/create-mcp-servers</guid><description>How to create MCP servers easily with FastMCP</description><pubDate>Wed, 02 Jul 2025 11:03:12 GMT</pubDate><content:encoded>&lt;p&gt;Hi there!&lt;/p&gt;
&lt;p&gt;In this post, I&apos;ll show you how to create MCP servers easily with FastMCP. To know the basics of MCP, you can read my previous post &lt;a href=&quot;https://varsni.com/notes/what-is-mcp&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;What is FastMCP?&lt;/h2&gt;
&lt;p&gt;FastMCP is a new open-source library that makes it easy to create MCP servers. It&apos;s built on top of the FastAPI framework and uses the latest version of the MCP protocol.&lt;/p&gt;
&lt;h2&gt;How to install FastMCP?&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pip install fastmcp
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;How to create an MCP server with FastMCP?&lt;/h2&gt;
&lt;p&gt;Creating an MCP server with FastMCP is straightforward. Let&apos;s build a simple example that exposes a file system tool and a resource.&lt;/p&gt;
&lt;h3&gt;Basic Setup&lt;/h3&gt;
&lt;p&gt;First, create a new Python file for your MCP server:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from fastmcp import FastMCP

# Create a new MCP server instance
mcp = FastMCP(&quot;My MCP Server&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That&apos;s it! You&apos;ve created an MCP server. Now let&apos;s add some functionality.&lt;/p&gt;
&lt;h3&gt;Adding Tools&lt;/h3&gt;
&lt;p&gt;Tools are functions that the MCP client (like an AI agent) can call. Let&apos;s add a tool to read files:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from fastmcp import FastMCP
import os

mcp = FastMCP(&quot;File System Server&quot;)

@mcp.tool()
def read_file(file_path: str) -&amp;gt; str:
    &quot;&quot;&quot;Read the contents of a file from the local filesystem.
    
    Args:
        file_path: The path to the file to read
        
    Returns:
        The contents of the file as a string
    &quot;&quot;&quot;
    if not os.path.exists(file_path):
        return f&quot;Error: File &apos;{file_path}&apos; not found&quot;
    
    with open(file_path, &apos;r&apos;, encoding=&apos;utf-8&apos;) as f:
        return f.read()
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Adding Resources&lt;/h3&gt;
&lt;p&gt;Resources are data sources that agents can access. Let&apos;s add a resource that lists files in a directory:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;@mcp.resource(&quot;file://directory/{directory}&quot;)
def list_directory(directory: str) -&amp;gt; str:
    &quot;&quot;&quot;List all files in the specified directory.
    
    Args:
        directory: The directory path to list
        
    Returns:
        A formatted string listing all files
    &quot;&quot;&quot;
    if not os.path.isdir(directory):
        return f&quot;Error: &apos;{directory}&apos; is not a valid directory&quot;
    
    files = os.listdir(directory)
    return &quot;\n&quot;.join(f&quot;- {f}&quot; for f in files)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Adding Prompts&lt;/h3&gt;
&lt;p&gt;Prompts are reusable templates that help guide the agent&apos;s behavior:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;@mcp.prompt()
def analyze_codebase() -&amp;gt; str:
    &quot;&quot;&quot;Prompt template for analyzing a codebase.
    
    Returns:
        A prompt string for code analysis
    &quot;&quot;&quot;
    return &quot;&quot;&quot;Analyze the codebase structure:
1. Identify the main entry points
2. List all dependencies
3. Find potential security issues
4. Suggest improvements&quot;&quot;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Complete Example&lt;/h3&gt;
&lt;p&gt;Here&apos;s a complete MCP server that combines all these features:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from fastmcp import FastMCP
import os
import json
from datetime import datetime

mcp = FastMCP(&quot;Development Tools Server&quot;)

@mcp.tool()
def read_file(file_path: str) -&amp;gt; str:
    &quot;&quot;&quot;Read the contents of a file from the local filesystem.&quot;&quot;&quot;
    if not os.path.exists(file_path):
        return f&quot;Error: File &apos;{file_path}&apos; not found&quot;
    
    try:
        with open(file_path, &apos;r&apos;, encoding=&apos;utf-8&apos;) as f:
            return f.read()
    except Exception as e:
        return f&quot;Error reading file: {str(e)}&quot;

@mcp.tool()
def write_file(file_path: str, content: str) -&amp;gt; str:
    &quot;&quot;&quot;Write content to a file on the local filesystem.
    
    Args:
        file_path: The path where to write the file
        content: The content to write
    &quot;&quot;&quot;
    try:
        os.makedirs(os.path.dirname(file_path), exist_ok=True)
        with open(file_path, &apos;w&apos;, encoding=&apos;utf-8&apos;) as f:
            f.write(content)
        return f&quot;Successfully wrote to &apos;{file_path}&apos;&quot;
    except Exception as e:
        return f&quot;Error writing file: {str(e)}&quot;

@mcp.tool()
def get_file_info(file_path: str) -&amp;gt; str:
    &quot;&quot;&quot;Get metadata about a file.
    
    Args:
        file_path: The path to the file
        
    Returns:
        JSON string with file information
    &quot;&quot;&quot;
    if not os.path.exists(file_path):
        return json.dumps({&quot;error&quot;: &quot;File not found&quot;})
    
    stat = os.stat(file_path)
    info = {
        &quot;path&quot;: file_path,
        &quot;size&quot;: stat.st_size,
        &quot;modified&quot;: datetime.fromtimestamp(stat.st_mtime).isoformat(),
        &quot;is_file&quot;: os.path.isfile(file_path),
        &quot;is_dir&quot;: os.path.isdir(file_path)
    }
    return json.dumps(info, indent=2)

@mcp.resource(&quot;file://directory/{directory}&quot;)
def list_directory(directory: str) -&amp;gt; str:
    &quot;&quot;&quot;List all files and subdirectories in the specified directory.&quot;&quot;&quot;
    if not os.path.isdir(directory):
        return f&quot;Error: &apos;{directory}&apos; is not a valid directory&quot;
    
    items = []
    for item in os.listdir(directory):
        item_path = os.path.join(directory, item)
        item_type = &quot;directory&quot; if os.path.isdir(item_path) else &quot;file&quot;
        items.append(f&quot;{item_type}: {item}&quot;)
    
    return &quot;\n&quot;.join(items) if items else &quot;Directory is empty&quot;

@mcp.prompt()
def code_review_prompt() -&amp;gt; str:
    &quot;&quot;&quot;Generate a prompt for code review tasks.&quot;&quot;&quot;
    return &quot;&quot;&quot;Review the provided code and check for:
1. Code quality and best practices
2. Potential bugs or errors
3. Security vulnerabilities
4. Performance optimizations
5. Documentation completeness

Provide specific, actionable feedback.&quot;&quot;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Running Your Server&lt;/h3&gt;
&lt;p&gt;To run your MCP server, add this at the end of your file:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;if __name__ == &quot;__main__&quot;:
    mcp.run()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then run it with:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python your_server.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or use the FastMCP CLI:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;fastmcp serve your_server.py
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Connecting to an Agent&lt;/h3&gt;
&lt;p&gt;Once your server is running, you can connect it to MCP-compatible agents like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Claude Desktop&lt;/strong&gt;: Update claude settings and add your server to the &lt;code&gt;claude_desktop_config.json&lt;/code&gt; file&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Amazon Q&lt;/strong&gt;: Configure it through Q CLI&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cursor IDE&lt;/strong&gt;: Add it to your cursor IDE MCP settings&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Best Practices&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Error Handling&lt;/strong&gt;: Always wrap file operations in try-except blocks and return clear error messages&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Type Hints&lt;/strong&gt;: Use Python type hints in your function signatures for better documentation&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Docstrings&lt;/strong&gt;: Write clear docstrings that explain what each tool/resource does&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Security&lt;/strong&gt;: Be careful about file paths - validate and sanitize inputs to prevent directory traversal attacks&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Resource Limits&lt;/strong&gt;: Consider adding limits on file sizes or operation timeouts for production use&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Advanced Features&lt;/h3&gt;
&lt;p&gt;FastMCP also supports:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Async tools&lt;/strong&gt;: Use &lt;code&gt;async def&lt;/code&gt; for non-blocking operations&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Streaming responses&lt;/strong&gt;: For large data transfers&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Custom schemas&lt;/strong&gt;: Define complex input/output types&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Middleware&lt;/strong&gt;: Add authentication, logging, or rate limiting&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Example: Database MCP Server&lt;/h3&gt;
&lt;p&gt;Here&apos;s a more advanced example that connects to a database:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from fastmcp import FastMCP
import sqlite3
from typing import List, Dict

mcp = FastMCP(&quot;Database Server&quot;)

@mcp.tool()
def query_database(db_path: str, query: str) -&amp;gt; str:
    &quot;&quot;&quot;Execute a SQL query on a SQLite database.
    
    Args:
        db_path: Path to the SQLite database file
        query: SQL query to execute
        
    Returns:
        JSON string with query results
    &quot;&quot;&quot;
    try:
        conn = sqlite3.connect(db_path)
        conn.row_factory = sqlite3.Row
        cursor = conn.cursor()
        cursor.execute(query)
        
        rows = cursor.fetchall()
        results = [dict(row) for row in rows]
        
        conn.close()
        return json.dumps(results, indent=2)
    except Exception as e:
        return json.dumps({&quot;error&quot;: str(e)})

@mcp.resource(&quot;db://tables/{db_path}&quot;)
def list_tables(db_path: str) -&amp;gt; str:
    &quot;&quot;&quot;List all tables in a SQLite database.&quot;&quot;&quot;
    try:
        conn = sqlite3.connect(db_path)
        cursor = conn.cursor()
        cursor.execute(&quot;SELECT name FROM sqlite_master WHERE type=&apos;table&apos;&quot;)
        tables = [row[0] for row in cursor.fetchall()]
        conn.close()
        return &quot;\n&quot;.join(f&quot;- {table}&quot; for table in tables)
    except Exception as e:
        return f&quot;Error: {str(e)}&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Wrapping Up&lt;/h2&gt;
&lt;p&gt;FastMCP makes it incredibly easy to build MCP servers. With just a few decorators, you can expose tools, resources, and prompts that AI agents can use to interact with your systems.&lt;/p&gt;
&lt;p&gt;The key advantages of FastMCP:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Simple API&lt;/strong&gt;: Decorator-based design that&apos;s intuitive to use&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;FastAPI Foundation&lt;/strong&gt;: Built on FastAPI, so you get async support and great performance&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Type Safety&lt;/strong&gt;: Leverages Python type hints for better validation&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Active Development&lt;/strong&gt;: Regularly updated with the latest MCP protocol features&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Next Steps&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Build your own MCP server for a specific use case (database, API, file system, etc.)&lt;/li&gt;
&lt;li&gt;Test it with Claude Desktop or another MCP client&lt;/li&gt;
&lt;li&gt;Share your server with the community or use it in your own agent workflows&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Thanks for reading! 🎉&lt;/p&gt;
&lt;p&gt;If you build something cool with FastMCP, I&apos;d love to hear about it. Happy building!&lt;/p&gt;
</content:encoded></item><item><title>Multitenancy in AWS with account per tenant model</title><link>https://varsni.com/notes/multitenancy-in-aws</link><guid isPermaLink="true">https://varsni.com/notes/multitenancy-in-aws</guid><description>How to implement multitenancy in AWS with account per tenant model</description><pubDate>Wed, 08 Nov 2023 06:21:01 GMT</pubDate><content:encoded>&lt;p&gt;Multi-tenancy in the cloud is akin to running multiple, separate offices within a shared workspace. In technical terms, it means deploying a single software application or service that serves numerous users or clients, all isolated from one another. This approach maximises resource utilisation and cost-efficiency by sharing infrastructure and codebase.&lt;/p&gt;
&lt;h2&gt;MLT Architectures&lt;/h2&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;Single Database, Separate Schemas&lt;/strong&gt;: In this approach, a single database is used to store data for all tenants, but each tenant has its own schema within the database. This architecture provides strong isolation between tenants while allowing efficient data management. It&apos;s suitable for scenarios where tenants have similar data structures but need data separation.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://varsni.com/notes/multitenancy-in-aws/schema_MLT.png&quot; alt=&quot;Schema seperated tenants&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Database-per-Tenant&lt;/strong&gt;: Each tenant has its own dedicated database instance. This approach offers the highest level of data isolation but can be resource-intensive and costly. It&apos;s suitable for scenarios where strict data separation is required.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://varsni.com/notes/multitenancy-in-aws/db_MLT.png&quot; alt=&quot;Schema seperated tenants&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Micro-services with API Gateway&lt;/strong&gt;: Tenants are served by separate microservices, each with its own database. An API gateway manages tenant-specific routing and authentication. This approach provides flexibility, scalability, and strong isolation.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://varsni.com/notes/multitenancy-in-aws/routing_MLT.png&quot; alt=&quot;Schema seperated tenants&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Account level isolation&lt;/strong&gt;: A multi-tenancy architecture with separate AWS accounts involves assigning each tenant or customer their own dedicated AWS account. This architecture ensures robust isolation, security, and scalability between tenants. Each tenant manages their AWS resources independently, with IAM policies enforcing access control. Network isolation is achieved using separate VPCs. Monitoring, billing, and disaster recovery are handled individually for each tenant, allowing for customised solutions. This approach is ideal for scenarios where data separation and security are paramount, such as SaaS applications or managed service providers.&lt;/p&gt;
&lt;p&gt;The Approach we will be looking today is &lt;strong&gt;Account Level Isolation.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Multi-tenancy using separate AWS accounts is a reliable method that ensures strong isolation and security between tenants. Here’s a high-level overview of a multi-tenancy architecture with separate AWS accounts:&lt;/p&gt;
&lt;h3&gt;Tenant Isolation&lt;/h3&gt;
&lt;p&gt;We assign each tenant its own AWS account. Tenant resources, data, and configurations remain completely isolated from each other due to this separation.&lt;/p&gt;
&lt;h3&gt;Identity and Access Management (IAM)&lt;/h3&gt;
&lt;p&gt;AWS Identity and Access Management (IAM) controls access and permissions to each tenant’s AWS resources. IAM policies ensure granular access control, allowing tenants to access only their own resources.&lt;/p&gt;
&lt;h3&gt;Networking – Virtual Private Cloud (VPC) isolation&lt;/h3&gt;
&lt;p&gt;Each tenant typically has its own VPC, which provides network isolation. This prevents tenants from interacting directly with each other’s resources. Each tenant receives outbound internet access through Network Address Translation (NAT) gateways or instances.&lt;/p&gt;
&lt;h3&gt;Resource Scaling&lt;/h3&gt;
&lt;p&gt;Each tenant’s AWS account can independently scale its resources based on its own requirements and usage patterns. This ensures that one tenant’s resource demands do not impact others.&lt;/p&gt;
&lt;h3&gt;Monitoring and Logging&lt;/h3&gt;
&lt;p&gt;We use AWS CloudWatch and CloudTrail to monitor, log, and audit tenant-specific activities. This facilitates troubleshooting and security monitoring.&lt;/p&gt;
&lt;h3&gt;Backup and Disaster Recovery&lt;/h3&gt;
&lt;p&gt;Each tenant can implement backup and disaster recovery strategies based on their specific needs and compliance requirements.&lt;/p&gt;
&lt;h3&gt;Billing and Cost Allocation&lt;/h3&gt;
&lt;p&gt;You can use AWS Organizations to track individual tenant usage by providing cost allocation tags and consolidating billing across all tenant accounts.&lt;/p&gt;
&lt;h3&gt;Tenant Onboarding and Offboarding&lt;/h3&gt;
&lt;p&gt;You can set up automated processes for tenant onboarding and offboarding. When adding or removing tenants, you can automate the provisioning and de-provisioning of resources and permissions.&lt;/p&gt;
&lt;h3&gt;Cross-Tenant Functionality&lt;/h3&gt;
&lt;p&gt;You can set up a shared services or central admin account to manage functions that need to span across tenants, such as global authentication or reporting.&lt;/p&gt;
&lt;p&gt;This architecture offers a strong separation between tenants, making it ideal for scenarios requiring security and data isolation, such as SaaS applications or managed service providers. It allows tenants to control their resources and configurations while centralizing administrative tasks.&lt;/p&gt;
&lt;p&gt;When properly implemented, it offers a scalable, secure, and easily manageable multi-tenancy solution in the AWS cloud.&lt;/p&gt;
</content:encoded></item><item><title>Guide to AWS Certified SysOps Administrator – Associate</title><link>https://varsni.com/notes/aws-sysops</link><guid isPermaLink="true">https://varsni.com/notes/aws-sysops</guid><description>Becoming an AWS Certified SysOps Administrator – Associate is a significant step in showcasing your expertise in managing and operating systems on the AWS platform.</description><pubDate>Tue, 07 Nov 2023 09:04:41 GMT</pubDate><content:encoded>&lt;p&gt;Becoming an AWS Certified SysOps Administrator – Associate is a significant step in showcasing your expertise in managing and operating systems on the AWS platform. This guide will provide you with a roadmap to navigate through the preparation process successfully.&lt;/p&gt;
&lt;p&gt;First of all you will need about 6 months hands-on experience in AWS to get an idea on its services. Next take a look at the AWS whitepapers to see what is covered in the exams. To start with your learning you have lots of courses available in udemy. The course that I found helpful was the one from Stephane Maarek. Here are the links to the resources that I used :&lt;/p&gt;
&lt;h3&gt;Course and Practice Tests&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.udemy.com/course/ultimate-aws-certified-sysops-administrator-associate/&quot;&gt;Course: Ultimate AWS Certified SysOps Administrator Associate&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.udemy.com/course/practice-exams-aws-certified-sysops-administrator-associate/&quot;&gt;Practice Exams: AWS Certified SysOps Administrator Associate&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.examtopics.com&quot;&gt;Question Dumps: ExamTopics&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Embarking on the journey to become an AWS Certified SysOps Administrator – Associate requires a combination of hands-on experience, learning, and practice tests. The resources mentioned in this guide are meant to guide you through this process efficiently.&lt;/p&gt;
&lt;p&gt;Best of luck on your certification journey!! 🤞&lt;/p&gt;
</content:encoded></item></channel></rss>