14 views
Your coding agent has twelve tools. One of them — shell_exec — starts returning weird side effects after a model upgrade. You need that tool gone in under a minute, not after a Lambda redeploy, alias traffic shift, and a nervous CloudFormation change set. Environment variables and SSM Parameter Store “eventually” work; AppConfig feature flags give you versioned, validated, polled config with explicit fail-closed semantics when the service is unreachable. That is the difference between a kill switch and a hope.
⚡ TL;DR: Put tool allowlists, maxDepth, and model IDs in AppConfig (feature flags or hosted freeform JSON). Poll on a short interval from the agent runtime; fail closed when AppConfig cannot be fetched. Pair with depth limits from Lambda Recursive Loop Protection, tool contracts in AI Coding Agent Tool Schemas, and blast-radius tags in IAM Condition Keys for Agent Runtimes. Never bake kill switches into a redeploy-only path.
AppConfig Feature Flags: production guidance
Lambda environment variables are deploy-time. ChangingTOOLS_ENABLED=shell_exec:false means a new version, alias update, and cold starts. For multi-tenant coding agents you also need per-tenant overrides: disable deploy_prod for tenant A without touching tenant B. AppConfig supports:
- Feature flags (boolean / multivariate) with targeting rules
- Hosted configuration (freeform JSON/YAML) for richer tool policy documents
- Deployment strategies (canary / linear) so a bad flag does not flash-crash every agent at once
- Validators (JSON Schema / Lambda) so a typo cannot empty the allowlist
typescript
✅ Treat runtime policy as data. The function code should ask AppConfig, not ship the policy in the zip.
// ❌ Kill switch buried in Lambda env — requires redeploy
const ENABLED = (process.env.ENABLED_TOOLS ?? "")
.split(",")
.filter(Boolean);
export function isToolEnabled(name: string): boolean {
return ENABLED.includes(name); // stale until next deploy
}
Feature flags vs freeform hosted config
Use feature flags when the decision is a small set of toggles:tool.shell_exec, tool.git_push, agent.maxDepth, model.id. Use hosted freeform when you need a structured policy:
json
Flags are easier to flip in the console; freeform is easier to review in PRs and validate with JSON Schema. Many teams run both: flags for emergency kills, freeform for the resting policy.
{
"tools": {
"shell_exec": { "enabled": false, "reason": "incident-2026-09-19" },
"read_file": { "enabled": true },
"apply_patch": { "enabled": true, "maxBytes": 65536 }
},
"agent": { "maxDepth": 2, "modelId": "anthropic.claude-sonnet-4-20250514-v1:0" },
"failClosed": true
}
typescript
// ✅ AppConfig agent extension / GetLatestConfiguration pattern
import {
AppConfigDataClient,
StartConfigurationSessionCommand,
GetLatestConfigurationCommand,
} from "@aws-sdk/client-appconfigdata";
const client = new AppConfigDataClient({});
let token: string | undefined;
let cached: AgentPolicy | null = null;
let fetchedAt = 0;
const POLL_MS = 30_000; // ✅ short for kill switches; not 5 minutes
type AgentPolicy = {
tools: Record<string, { enabled: boolean; maxBytes?: number }>;
agent: { maxDepth: number; modelId: string };
failClosed: boolean;
};
async function refreshPolicy(): Promise<AgentPolicy | null> {
if (!token) {
const start = await client.send(
new StartConfigurationSessionCommand({
ApplicationIdentifier: process.env.APPCONFIG_APP!,
EnvironmentIdentifier: process.env.APPCONFIG_ENV!,
ConfigurationProfileIdentifier: process.env.APPCONFIG_PROFILE!,
RequiredMinimumPollIntervalInSeconds: 15,
})
);
token = start.InitialConfigurationToken!;
}
const out = await client.send(
new GetLatestConfigurationCommand({ ConfigurationToken: token })
);
token = out.NextPollConfigurationToken ?? token;
if (out.Configuration && out.Configuration.byteLength > 0) {
cached = JSON.parse(Buffer.from(out.Configuration).toString("utf8"));
fetchedAt = Date.now();
}
return cached;
}
export async function getPolicy(): Promise<AgentPolicy> {
const stale = Date.now() - fetchedAt > POLL_MS;
if (!cached || stale) {
try {
const p = await refreshPolicy();
if (p) return p;
} catch (err) {
// fall through to fail-closed
console.error("appconfig_unreachable", err);
}
}
if (cached) return cached;
// ❌ Do NOT default to "all tools on" when AppConfig is down
return {
tools: {},
agent: { maxDepth: 0, modelId: process.env.SAFE_MODEL_ID! },
failClosed: true,
};
}
Poll interval and Lambda lifecycle
For request-path agents (API Gateway / Function URL), poll on a module-level cache with a 15–45s TTL. Do not call AppConfig on every tool invoke — that adds latency and throttling risk. The AWS AppConfig Lambda extension is often cleaner: it exposeshttp://localhost:2772 and handles session tokens for you.
typescript
Wire the policy into the tool router before the model sees the catalog. If // ✅ Prefer the Lambda extension HTTP endpoint when available
export async function getPolicyViaExtension(): Promise<AgentPolicy> {
const app = process.env.AWS_APPCONFIG_APPLICATION!;
const env = process.env.AWS_APPCONFIG_ENVIRONMENT!;
const profile = process.env.AWS_APPCONFIG_CONFIGURATION_PROFILE!;
const url =
`http://localhost:2772/applications/${app}/environments/${env}` +
`/configurations/${profile}`;
try {
const res = await fetch(url, { signal: AbortSignal.timeout(800) });
if (!res.ok) throw new Error(`appconfig_http_${res.status}`);
return (await res.json()) as AgentPolicy;
} catch {
// fail closed — empty allowlist
return {
tools: {},
agent: { maxDepth: 0, modelId: process.env.SAFE_MODEL_ID! },
failClosed: true,
};
}
}
shell_exec is disabled, omit it from toolConfig entirely — do not list it and then refuse. Models retry refused tools; missing tools change the plan.
Fail-closed when AppConfig is unreachable
This is the part most tutorials skip. Three failure modes:- Cold start + extension not ready — first invoke times out talking to localhost:2772
- Throttling / regional incident — GetLatestConfiguration errors
- Empty configuration — deploy wiped the profile
typescript
Contrast with fail-open: a network blip re-enables // ✅ Tool gate that fails closed
export async function assertToolAllowed(
toolName: string,
depth: number
): Promise<{ ok: true } | { ok: false; error: string }> {
const policy = await getPolicy();
if (policy.failClosed && Object.keys(policy.tools).length === 0) {
return { ok: false, error: "agent_policy_unavailable" };
}
const t = policy.tools[toolName];
if (!t?.enabled) {
return { ok: false, error: "tool_disabled_by_flag" };
}
if (depth > policy.agent.maxDepth) {
return { ok: false, error: "maxDepth_flag_exceeded" };
}
return { ok: true };
}
deploy_prod for ten minutes. For coding agents with write tools, that is unacceptable. Pair fail-closed with a CloudWatch alarm on AgentPolicyUnavailable (see CloudWatch EMF for LLM Cost for the EMF counter pattern).
Targeting rules and incident playbooks
AppConfig feature flags support targeting by user ID, time window, and custom attributes. MaptenantId / agentRole into the retrieval context so you can disable git_push for a single noisy tenant without a global outage. Document the playbook:
- Flip
tool.shell_exec→ off (or push freeform withenabled: false) - Wait one poll interval (≤ 45s) — verify EMF
tool_disabled_by_flag - Fix root cause; re-enable with a canary deployment strategy
- Never “fix” by redeploying an older Lambda that still has the tool hardcoded on
appconfig:StartConfigurationSession + appconfig:GetLatestConfiguration on the specific application ARN — not *. Combine with the tag conditions from the IAM condition-keys post so only runtimes tagged AgentRuntime=true can read the profile.
Checklist
- [ ] Tool allowlist / maxDepth / modelId live in AppConfig, not only Lambda env
- [ ] Choose flags (toggles) vs freeform JSON (policy doc) deliberately — or both
- [ ] Poll ≤ 45s for kill-switch freshness; use Lambda extension when possible
- [ ] Fail closed on unreachable / empty config (empty tool map, maxDepth 0)
- [ ] Omit disabled tools from Bedrock
toolConfig— do not advertise then refuse - [ ] Alarm on policy-unavailable and on emergency flag flips
- [ ] IAM scoped to the AppConfig application; no
appconfig:*on* - [ ] Incident runbook: flip flag → wait poll → verify → canary re-enable
shell_exec storm ends with a flag flip, not a Friday deploy.
Related: Lambda Warm Pools: Low-Latency Backends for Coding Agent Tools; Human-in-the-Loop Gates: Dual Control for Prod-Touching Agent Tools; Day 54: Specialist Tools per Agent.
Watch: 100 Days of AI on YouTube
Short videos from CheatCoders, one AI engineering topic per day.
Most viewed
- Python Decorators Explained: From Simple Wrappers to Production Patterns
- Claude Code vs Cursor vs GitHub Copilot: Choose by Workflow, and Set Up One Repo That Works With All Three
- AI Agent Frameworks in 2025: LangGraph vs CrewAI vs AutoGen vs Raw API
- Python String Methods: Every str Method With Real Production Examples
- REST API Design Best Practices: The Patterns That Make APIs a Joy to Use
Newly added
- n8n vs AI Agent: I Gave Both the Same Job (Local, Ollama)
- Local AI Video Generator: Build Your Own App (Free, Python, No GPU)
- AI Wrote Raw Binary Instead of Code. Here’s What Happened (WebAssembly)
- I Built a Browser That Only Runs Encrypted Binary (Ep 1)
- RAG From Scratch: Chunking, Embeddings, Reranking and Evaluation (Real Numbers)
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.