dunkbot Request invite
Engineering brief · dunkbot on AWS

A computer for every Bot, watchable, interruptible, and fenced by exact-action approvals.

Named AI Bots that chat, run routines and use connectors, each on a persistent cloud computer. Built on Bedrock AgentCore with one AppSync API, a transactional outbox into EventBridge, and Terraform (aws + awscc) for the whole stack.

us-east-1 · one AWS account · private beta, invite only · web client live at app.dunkbot.ai

  • 105–120msGraphQL sendMessage ack, p95, in-regionS18 · n=300 × 4
  • 103–126msAppSync Events publish to receive, p95S6 · in-region
  • 510msOutbox commit to bus, p95; target re-baselined to ≤600 msS17
  • 238msTenant STS credentials, p95, over 142 isolation checksS16
  • 0opsBot operations during a 20 s human hold on the screenEXIT3 · live on dev
01 · Architecture

Seven layers, five flows, one account.

Select a component for what it does, why it was chosen, what was rejected and what was measured. Pick a flow and step through it.

dunkbot system map on AWSSeven bands from top to bottom: clients, edge and API, orchestration on Lambda, the AgentCore brain, AgentCore tools, data and events, and channels. Each component links to its row in the stack table below; the five flows are listed as text under the map.01 · CLIENTS02 · EDGE & API03 · ORCHESTRATION (LAMBDA)04 · BRAINAGENTCORE05 · AGENTCORE TOOLSAGENTCORE06 · DATA & EVENTS07 · CHANNELSWeb app → CloudFront · S3 · WAF: loads the appWeb app → Cognito (invite-gated): signs inWeb app → AppSync GraphQL: queries and mutationsAppSync Events (AG-UI) → Web app: AG-UI eventsDesktop app (Tauri) → AppSync GraphQL: same API (planned)Mobile app (Expo) → AppSync GraphQL: same API (planned)AgentCore Browser → Web app: DCV live viewAppSync GraphQL → DynamoDB single table: one transactionAppSync GraphQL → Computer control: openScreen, takeoverAPI Gateway HTTP (/hooks, /stripe) → DynamoDB single table: webhook transactionDynamoDB single table → Relay (DynamoDB Streams): DynamoDB StreamsRelay (DynamoDB Streams) → SQS FIFO + Dispatcher: SQS FIFORelay (DynamoDB Streams) → EventBridge EventsV2 + subscribers: outbox eventsSQS FIFO + Dispatcher → AgentCore Runtime: InvokeAgentRuntimeDurable runs → AgentCore Runtime: one brain turn per stepinbound-router → Durable runs: run.requestedEventBridge Scheduler → Durable runs: routine firesAgentCore Runtime → Computer control: ComputerOps + screenGenAgentCore Runtime → AppSync Events (AG-UI): publishes chunksAgentCore Runtime → Bedrock models: model callsAgentCore Runtime → AgentCore Memory: reads memoryComputer control → AgentCore Browser: starts the screenAgentCore Runtime → AgentCore Browser: browser actionsAgentCore Runtime → Code Interpreter: terminalAgentCore Runtime → AgentCore Gateway + Policy (Cedar): MCP with run tokenAgentCore Gateway + Policy (Cedar) → AgentCore Identity (3LO): 3LO consentAgentCore Gateway + Policy (Cedar) → Connector targets: target callCode Interpreter → S3 + KMS: workspaceEventBridge EventsV2 + subscribers → AgentCore Memory: memory-ingestEventBridge EventsV2 + subscribers → inbound-router: inbound.receivedSES email → DynamoDB single table: receipt transactionEnd User Messaging SMS → DynamoDB single table: receipt transactionWeb appReact 19 · ViteDesktopTauriMobileExpoCloudFrontS3 · WAF · OACCognitoinvite-gatedAppSync GraphQLone client APIAppSync EventsAG-UI streamAPI Gateway/hooks · /stripeRelayStreams →DispatcherSQS FIFO · leaseComputer controlscreenGen fenceinbound-routermatches routinesDurable runsLambda durableSchedulerEventBridgeGuardrailsPII · inboundMemoryepisodic · factsRuntimeStrands TS · V1Bedrock modelsSonnet 4.6, NovaEvals · OTelCI gate · tracesLinux desktopInstancesCode Interpreterno IAM roleBrowser1920×1200 · DCVIdentity 3LOtoken vaultGateway · CedarMCP · run tokenConnectors6 targets on devDynamoDBsingle tableS3 + KMS5 buckets·3 keysEventBridgeEventsV2 · awsccSTS tagsper attemptSecrets Managerwrite-onlySES emailout: log modeSMSsimulator onlyVoiceAmazon Connect
  • SHIPPED shown live on dev, or deployed by Terraform on main
  • IN PROGRESS merged or built on a branch, not yet shown live
  • PLANNED design only
  • Teal band: an AgentCore service

The five flows, step by step.

Each line is what to say at that step; the number and its source follow.

Chat

7 steps
  1. The web app subscribes to Events, backfills what it missed, then calls sendMessage.
  2. One DynamoDB transaction writes the message, message.posted and the turn.requested outbox item, then acks. ack p95 105-120 ms S18
  3. Streams feed the relay, which puts the turn on SQS FIFO, grouped by turnRouteKey.
  4. The dispatcher writes TRN# and LEASE# (gen+1), mints STS credentials and a run token, and calls InvokeAgentRuntime. STS mint p95 238 ms S16
  5. The brain streams from Bedrock, checkpoints, and extends its lease as it goes. model first token p50 902 ms (Sonnet 4.6) S2
  6. Text is coalesced every 100 ms and published to Events. publish to receive p95 103-126 ms S6
  7. A closing transaction records the reply and usage; the bus fans out to ledger, audit, memory-ingest and notify. commit to bus p95 510 ms S17

No end-to-end first-token figure is recorded on dev.

Takeover

6 steps
  1. openScreen starts or joins one durable wake episode; the Browser starts with the saved sign-ins. 3 screens up in 8.3 s EXIT3
  2. liveTicket returns a 300 s presigned DCV URL. DCV allows one viewer; a second is refused. first frame 3.4 s S4
  3. Every ComputerOp carries screenGen; computer-control admits only the current generation. +33 ms p95 warm hop S3
  4. Take control bumps screenGen. Old ops get STALE_SCREEN_GEN and the turn ends paused_by_user. 0 Bot ops during a 20 s hold EXIT3
  5. “I'm done” bumps screenGen again and saves the sign-ins to the Browser profile. screenGen 1 → 2 → 3 EXIT3
  6. The resume turn starts with a fresh screenshot. barrier held in 6 trials, ~690 ops S4

Live-view first frame: 3.4 s in S4; about 2 s in EXIT3 with the real DCV driver on localhost, not the signed-in app.

Approval

6 steps
  1. The brain checkpoints the tool_use with an actionId; policy says ask.
  2. The broker writes APR# bound to actionId + argsHash, and the turn ends in an interrupt.
  3. The approval card reaches the chat: Allow once, Always allow or Deny.
  4. decideApproval makes a conditional write, requested → approved, and queues the resume.
  5. The resumed brain calls the tool.
  6. The enforcement point consumes the approval only on an exact match; otherwise ARGS_MISMATCH, TOOL_MISMATCH or TARGET_MISMATCH. changed args, tool, target each refused live EXIT3

A background approval expires at 10 min (measured 10 min 0.1 s, EXIT4).

Inbound webhook

5 steps
  1. POST /hooks/{id} verifies the Standard Webhooks HMAC and timestamp. bad signature 401, stale timestamp 401 EXIT5
  2. One transaction (dedupe claim, receipt, inbound.received) answers 202. reused webhook-id: 202 duplicate, nothing fired EXIT5
  3. The relay puts inbound.received on the bus.
  4. inbound-router applies the sender rule, the spend check and the cap (max 25 per event). 30 matched routines: 25 started, 5 reported EXIT5
  5. A durable run starts; the input reaches the brain only inside a tainted <untrusted_inbound> block, so sends and writes ask.

Email and SMS follow the same path; their exit checks used synthetic MIME and simulator numbers.

Connector

5 steps
  1. The dispatcher's broker issues a run token: ES256, KMS-signed, 15 min.
  2. The Gateway checks the JWT against our JWKS, then the REQUEST interceptor classes the tool.
  3. Cedar evaluates the dis_* forbids and the temporal approve_* policy.
  4. No token for this user yields a URL elicitation (-32042), shown as a Connect card. Connect card in 11 s, live EXIT5
  5. The target call runs; the RESPONSE interceptor scrubs credentials and marks the result untrusted. read p50 416 ms, p95 513 ms S13

Gateway numbers are from the S13 spike stack, laptop to us-east-1.

02 · Hard problems

Five hard problems and how we solved them.

Each one: what made it hard, what we weighed, what we picked and what we measured. Spike numbers (S) come from isolated tests; exit numbers (EXIT) come from the live dev stack.

P1

Give every Bot a computer without running a fleet of desktops

Problem
Bots need a screen, a terminal and files that persist. A full VM per Bot is slow to wake (a cold wake measured p95 74 s) and costly to keep.
Options
  • Full Linux desktop on Runtime Instances
  • EC2 + DCV
  • Lambda microVMs, E2B
  • Browser + Code Interpreter + S3
Decision
Composite first. AgentCore Browser is the screen, Code Interpreter the terminal, S3 the workspace. The desktop stays a conditional later phase, an explicit parity gap with Grok.
Evidence
3 screens in 8.3 s; a lost Browser session recovered in 6.9 s; a 250 MB file in 4.96 s, hash matched; terminal start p95 2.7 s. The planned desktop's spike: 60 fps, input p95 109 ms, but cold wake p95 74 s.EXIT3 · S5 · S10 · S9
P2

Let a human watch and take over without the agent stepping on them

Problem
While you sign in or type a 2FA code, a model with a stale plan must not click. In-flight actions and late results are the danger.
Options
  • Cooperative pause flag
  • Kill the session and restart
  • Generation fence on every op
Decision
screenGen barrier. Taking control bumps a generation; computer-control refuses older ops with STALE_SCREEN_GEN. The turn ends paused_by_user; on release the resume turn starts with a fresh screenshot. The live view is DCV in a Watching mode.
Evidence
0 ops reached the session during a 20 s human hold, screenGen 1 → 2 → 3. Spike: the barrier held in 6 trials, ~690 ops, no late effect. First frame 3.4 s in the spike; about 2 s in EXIT3 with the real DCV driver on localhost, not the signed-in app.EXIT3 row 2 · S4
P3

Make “ask before anything goes out” a property of the system, not the prompt

Problem
A prompt-injected model will happily call send, pay or delete. Rules inside the prompt can be talked out of.
Options
  • Prompt rules
  • An LLM judge only
  • Exact-action approval + Cedar at the Gateway
Decision
Approval bound to the exact action. An APR# record keys on actionId + argsHash + target. The enforcement point consumes it once, or refuses. Cedar forbids send and pay without a recorded approval. Auto Review sees the action, never the untrusted transcript.
Evidence
Changed args, tool, target or document each refused live (ARGS_MISMATCH and kin). A background approval expired at 10 min 0.1 s. A seeded injection was blocked end to end; the delete did not run. Gateway read p50 416 ms, p95 513 ms on the spike stack.EXIT3 row 3 · EXIT4 · S13
P4

Isolate tenants so a bug becomes an AccessDenied, not a leak

Problem
One DynamoDB table and five S3 buckets serve every tenant, and the outbox lives in that same table. One wrong key in a repository must not expose another tenant's data.
Options
  • Repository convention only
  • Per-tenant KMS keys (~$10k/month at 10k tenants)
  • Per-attempt STS session tags
Decision
Defence in depth. TenantContext is the only data path, keys carry the tenant, an evil-twin CI suite checks it, and the dispatcher mints per-attempt credentials tagged with tenant and attempt, so IAM enforces the partition. S3 objects sit under a t/{tenant}/ prefix; three KMS keys, with Bucket Keys.
Evidence
142 checks against raw SDK clients, none unexpected. AssumeRole p50 173 ms, p95 238 ms. Known limit: IAM fences partitions, not sort keys, so a gen condition is the fence inside a partition.S16 · ARCH
P5

Deliver every event once in effect, through crashes and reconnects

Problem
A message must be answered, cancelled or visibly failed. Writing to the database and then publishing to a bus loses events when the process dies between the two.
Options
  • Handlers call PutEvents
  • Step Functions for runs
  • Transactional outbox + durable functions
Decision
Outbox, idempotent consumers. State and event commit together; Streams relay to EventBridge; subscribers default to unordered with aggregateVersion checks. Runs are Lambda durable functions. Clients backfill after a reconnect.
Evidence
Commit to bus p95 510 ms. A FIFO burst of 200: p95 8.4 s against 1.2 s unordered. 9 crashes, one outcome per action. 41% of events missed the live socket and were recovered only by backfill.S17 · S19 · S6
More choices

Four more choices, with what we turned down.

Strands TypeScript on AgentCore Runtime runs the brain

Turned down
The Claude Agent SDK (a CLI subprocess per session); AgentCore Harness; Lambda; a fully hand-rolled loop.
Why
Strands hooks plus a custom Anthropic-SDK provider cost almost nothing (first token p50 −4%, p95 +5%), and our own ~1.5k-line loop stays behind the same contract as a fallback. Strands' own checkpoints re-sample the model on resume, so ours are hook-written. It runs on Runtime V1: S1, the V2 spike, was not run.
S21 · ARCH D6, D7

Lambda durable functions for routine runs, not Step Functions

Turned down
Step Functions Standard: a second language and a 25,000-event history cap.
Why
One language end to end. 9 injected crashes each produced one outcome per action. The 30,000-operation cap is terminal, so episodes hand over at 10,000 ops or 24 h.
S19 · ARCH D22

Nova for tests, Claude for brain work

Turned down
Running test suites, evals and load tests on Claude.
Why
A local nightly of 336 runs cost $2.16 on Nova 2 Lite, and the PR gate $0.39. A CI check fails if a test names a Claude model outside an allowlist. The trade-off: Nova is weaker on the injection and computer-use cases.
EXIT3 · EXIT5 · ADR 0002

AppSync Events for streaming, not GraphQL subscriptions

Turned down
GraphQL subscriptions; API Gateway WebSocket; streaming direct from the Runtime (CORS undocumented).
Why
Tenant checks run in the OnSubscribe and OnPublish handlers, publish to receive is p95 103-126 ms in-region, and reconnect safety comes from a backfill query.
S6 · ARCH D8
03 · Stack

What runs where.

The map as a table, plus what it does not draw. Terraform for the stack (aws + awscc): 40 Lambda resource blocks (several for_each, so more deploy) and 50 IAM role blocks, all permission-bounded. Select a name to see it on the map.

Every component on the map, grouped by layer, then platform pieces and in-flight work that the map does not draw.
ComponentRole in dunkbotStatusMeasured
01 · Clients
Web appThe only client today, at app.dunkbot.ai: chat, approval and Connect cards, and the live view of a Bot's screen with Take control.
Why, and what we turned down

Why. Web first: nothing to install, and the live view runs in the browser over DCV.

Rejected. Native clients first. Tauri desktop and Expo mobile wait on the same GraphQL API.

SHIPPEDVite 8.3.5, React 19.3.1. Open item: DCV's LZ4 decoder worker still violates Trusted Types; frames still render. EXIT3
Desktop app (Tauri)A desktop client, plus local execution on the user's own machine (Phase 9).
Why, and what we turned down

Why. Same API as the web client, so it is a client project, not a backend one.

PLANNEDNot started: apps/ holds only web and site. PLAN §3
Mobile app (Expo)A mobile client (Phase 10). Push would go through SNS, since Pinpoint ends 2026-10-30.
Why, and what we turned down

Why. Same API as the web client.

PLANNEDNot started. PLAN §3
02 · Edge & API
CloudFront · S3 · WAFServes the web app from S3 through CloudFront with origin access control and a CSP that requires Trusted Types. WAF runs the AWS managed Common and SQLi rule sets plus a per-IP rate limit.
Why, and what we turned down

Why. Static hosting with nothing to patch, and the CSP is set at the edge.

SHIPPEDWeb WAF is about $8/month idle; the GraphQL API has its own WAF (KnownBadInputs + IP rate) at about $7/month. TF
Cognito (invite-gated)Sign-in for the web app: ESSENTIALS tier, MFA off, SRP auth and a hosted domain with code + PKCE. A PreSignUp trigger admits only a valid single-use invite bound to the email; post-confirmation creates the tenant, user and trial.
Why, and what we turned down

Why. Native JWT auth for AppSync, and managed identity for a small team.

SHIPPEDPasskeys and Google sign-in are PLANNED (design only): nothing in Terraform or the web code configures them. TF
AppSync GraphQLThe single client API. Cognito JWT auth; Lambda resolvers run every call through TenantContext. Mutations are DB, EXTERNAL or CAPABILITY class and are acked with an operationId.
Why, and what we turned down

Why. One typed API for every client. No data-source resolvers, because they bypass TenantContext; AppSync has no persisted operations, so a CI-generated allowlist is enforced in resolvers.

Rejected. REST on API Gateway.

SHIPPEDsendMessage ack p95 105-120 ms in-region (n=300 × 4); idempotent replay p95 77-90 ms; resolver cold start about 1.1 s. S18
AppSync Events (AG-UI)Streams AG-UI 1.0 events (reply text, screen state, cards) to clients. OnSubscribe and OnPublish handlers check the tenant.
Why, and what we turned down

Why. Purpose-built pub/sub with per-channel auth, so no GraphQL subscriptions.

Rejected. API Gateway WebSocket; streaming direct from the Runtime (CORS undocumented).

SHIPPEDPublish to receive p95 103-126 ms in-region. No loss across 100 reconnects, but only through backfill: 41% of events missed the socket. S6
API Gateway HTTP (/hooks, /stripe)The door for third-party callers only: POST /hooks/{hookId} (Standard Webhooks, at hooks.dunkbot.ai) and POST /stripe.
Why, and what we turned down

Why. Webhook senders cannot hold a Cognito JWT, so they get a separate, narrow door.

SHIPPEDBad signature 401, stale timestamp 401; a reused webhook-id returns 202 duplicate and fires nothing. EXIT5
03 · Orchestration (Lambda)
Relay (DynamoDB Streams)Two relay Lambdas read DynamoDB Streams: the command relay feeds the SQS FIFO turn queue, the event relay puts outbox events on the EventBridge bus.
Why, and what we turned down

Why. A transactional outbox: state and event commit together, so there is no dual write.

Rejected. Handlers calling PutEvents after their own write.

SHIPPEDCommit to bus p95 510 ms, 530 ms to an unordered subscriber. The first target was ≤300 ms; it was re-baselined to ≤600 ms. S17
SQS FIFO + DispatcherTakes chat turns off an SQS FIFO queue grouped by turnRouteKey, writes the TRN# receipt and a LEASE# (gen+1), mints per-attempt tenant STS credentials and a run token, then calls InvokeAgentRuntime. It also brokers approvals and holds.
Why, and what we turned down

Why. Chat needs the lowest latency and steering. Stale attempts lose authority through gen fencing, so every acknowledged message is answered, cancelled or visibly failed.

Rejected. Running chat on the durable-function loop.

SHIPPEDOwner DM answered in 6.9 s while a routine was running. No end-to-end first-token figure is recorded on dev. EXIT4
Computer controlStarts or joins a Bot's screen in one durable wake episode, issues the 300 s live-view ticket, and admits only ComputerOps from the current screenGen.
Why, and what we turned down

Why. Takeover needs a hard barrier, not a polite request.

SHIPPED+33 ms p95 warm hop. The barrier held over 6 trials and about 690 ops with no late effect. S3, S4
inbound-routerAn EventBridge subscriber that matches the tenant's routines, applies each routine's sender rule (DMARC for mail, a number list for SMS), the spend check and the daily cap, then commits run.requested.
Why, and what we turned down

Why. One place decides what an inbound message may start; an empty sender list means nobody.

SHIPPED30 matched routines: 25 started, 5 reported (max 25 per event). An off-allowlist sender and a DMARC fail fire nothing. EXIT5
Durable runsRoutine runs, long tasks and approval waits as TypeScript Lambda durable functions; each step is one brain turn of up to 8 h.
Why, and what we turned down

Why. Two loops: the business loop is durable code in one language, the brain loop is separate.

Rejected. Step Functions Standard: a second language and a 25,000-event history cap.

SHIPPED9 injected crashes, one outcome per action (about 2.3 s each). The 30,000-operation cap is terminal, so episodes hand over at 10,000 ops or 24 h. S19
EventBridge SchedulerPer-routine schedules are created at run time in the routines group. Fixed schedules: a 1-min receipt sweeper, a 10-min session reaper, a daily profile reaper, 5-min and 03:15 billing jobs, 15-min usage reconciliation and a 06:00 absence job.
Why, and what we turned down

Why. Managed schedules, so nothing runs between fires.

SHIPPED10 consecutive fires started 4.3-6.7 s after nominal; 9 completed and 1 was skipped as OVERLAP by policy. EXIT4
04 · Brain
Bedrock GuardrailsTwo guardrails: pii-mask anonymises PII (names excepted) before every Memory write; inbound screens connector results for prompt attacks.
Why, and what we turned down

Why. Memory stays useful without storing PII.

SHIPPEDMask mode works standalone and on /anthropic input; output-side masking is untested. S2
AgentCore MemoryOne Memory per env with episodic (with reflections), semantic and user-preference strategies, namespaced per tenant and Bot with record expiry. memory-ingest is the only writer; the brain and resolvers read.
Why, and what we turned down

Why. A single writer plus PII masking keeps memory auditable.

Rejected. Strands' Memory session manager for writes.

SHIPPEDA routine's outcome became an episodic record 16 min after the run and was recalled in a new conversation. The first attempt failed on a missing IngestData permission. EXIT4
AgentCore RuntimeHosts the Strands TypeScript brain in an arm64 microVM, one session per chat route or run step, invoked only by the dispatcher with SigV4. Two endpoints, stable and canary (Runtime version 29).
Why, and what we turned down

Why. Strands with hooks and a custom Anthropic-SDK model provider. Checkpoints are hook-written, because Strands' own re-sample the model on resume. Our ~1.5k-line loop stays as a fallback behind the same contract.

Rejected. Lambda; the Claude Agent SDK (a CLI subprocess per session); Harness, kept as an experiment.

SHIPPEDStrands vs the direct SDK: first token p50 902 → 868 ms, p95 1,200 → 1,260 ms. Canary: internal tenants, then a sticky canaryPct; ≥48 h and ≥200 scored turns per step; an alarm sets canaryPct to 0. S21, ARCH

Runtime V1: S1, the V2 spike, was not run. The ≤4 s warm / ≤6 s cold first-token SLO is a V2 design target, not a measurement.

Bedrock modelsModel calls through global inference profiles. The dev brain runs Claude Sonnet 4.6; Haiku 4.5 and Sonnet 4.6 are the Terraform defaults for review and the group router. Exit runs used Amazon Nova by rule.
Why, and what we turned down

Why. Global profiles at Anthropic list price. The Anthropic SDK with a SigV4 custom fetch was chosen over bedrock-sdk on first token (942 ms vs 1,854 ms).

Rejected. bedrock-sdk.

SHIPPEDFirst token p50 902 ms, p95 1,200 ms on Sonnet 4.6, with occasional 3-5 s stalls. S2

Claude 5.5 is the design. It was not enabled on the account and its quotas were 0, so no 5.5 number is measured.

Evaluations + ObservabilityAn offline PR gate (deterministic checks first, then a GoalSuccessRate judge) and online evals on 100% of canary and 1% of stable turns. The brain exports OTel to CloudWatch Transaction Search; 16 metric alarms page through SNS.
Why, and what we turned down

Why. Authorization invariants stay deterministic tests, never judge-gated.

SHIPPEDPR gate on Nova: deterministic 46/60, final 39/60, $0.39. EXIT5

Online evals are deployed, but no online-eval score is recorded yet.

05 · AgentCore tools
Runtime Instances desktopA full Linux desktop with stop/resume and a disk (Phase 7, conditional).
Why, and what we turned down

Why. Closes the parity gap with Grok's computer.

PLANNEDS10 passed: 60 fps, input p95 109 ms. S9 and S11 failed as written: cold wake p95 74 s, and restic needs DeleteObject. Needs three OKs from Ish. S9, S10, S11
Code InterpreterThe Bot's terminal. PUBLIC network mode and no execution role, so untrusted code cannot reach AWS credentials. User-level pip and npm only.
Why, and what we turned down

Why. Isolation by construction: there is no role to steal.

SHIPPEDStart p95 2.7 s; 250 MiB in 4.7 s via presigned GET; stopTask kills the process tree in about 0.7 s. S5
AgentCore BrowserOne AgentCore Browser session per Bot screen, with saved sign-in profiles. The user watches over DCV and can take control.
Why, and what we turned down

Why. A composite computer first (Browser, Code Interpreter, S3), not a full desktop: an explicit parity gap with Grok.

Rejected. Runtime Instances desktop, EC2 + DCV, Lambda MicroVMs, E2B.

SHIPPED3 screens up in 8.3 s; a forced session loss recovered in 6.9 s; 5 of 5 sites stayed signed in. EXIT3, S3
AgentCore Identity (3LO)Per-user OAuth consent and a token vault, with Google and GitHub credential providers. Vault keys are the usr_ id, so two users never share a token.SHIPPEDConnect card live in 11 s. Real Gmail, Calendar and GitHub reads worked for Ish's user; a test account's consent is still open. There is no API to delete a vault token. EXIT5, S13
AgentCore Gateway + Policy (Cedar)One Gateway per env with CUSTOM_JWT inbound (our run token), REQUEST and RESPONSE interceptors, and Cedar policies over the token's claims. The temporal approve_* policy forbids send or pay without a recorded approval.
Why, and what we turned down

Why. Connectors are bought, not hand-built, and policy sits outside the model.

Rejected. Hand-built per-app tools.

SHIPPEDRead p50 416 ms, p95 513 ms; approve + send p50 850 ms, p95 2,094 ms (spike stack). Found: the interceptor runs before Cedar, so a refused call may already have spent its approval; it fails closed. S13

The #21 prf Cedar policy fix is merged; its deploy to dev is not confirmed.

Connector targetsGateway targets on dev: Gmail and Calendar (OpenAPI), GitHub (remote MCP), one Composio proxy (hackernews and gmail toolkits), and our approvals and dkbtest Lambdas.
Why, and what we turned down

Why. Breadth without writing each integration; the long tail goes through one Composio target.

SHIPPEDComposio: 77 tools through one proxy. Live reads proven for Ish's Gmail, Calendar and GitHub. S23, S13
06 · Data & events
DynamoDB single tableOne on-demand table with 2 GSIs, TTL, PITR, KMS and Streams. Outbox items live in the same table and commit in the same transaction as the state change. TenantContext is the only data path; in the evil-twin CI suite a foreign id returns NOT_FOUND.
Why, and what we turned down

Why. A transactional outbox: no dual write, and the stream is the truth.

Rejected. A separate outbox table (drawn in the design doc, not built); handlers calling PutEvents.

SHIPPEDCommit to bus p95 510 ms through Streams and the relay. S17
S3 + KMSFive buckets in dev (workspace, uploads, web, inbound mail, connector schemas), SSE-KMS with Bucket Keys. Tenants are separated by the t/{tenant}/ prefix plus tenant STS. Three KMS keys: the data key, an ES256 run-token key and an HMAC key.
Why, and what we turned down

Why. Bucket Keys keep KMS cost flat as objects grow.

Rejected. Per-tenant KMS keys: about $10k/month at 10k tenants.

SHIPPEDBucket Keys cut KMS requests about 92% (46 vs 604 for 300 PUT + 300 GET). KMS Sign 758/s at concurrency 100, 0 throttles. S16, S13
EventBridge EventsV2 + subscribersThe enhanced bus (EventsV2) with idempotent subscribers, each with a DLQ: ledger, audit, usage, notify, memory-ingest, purge, inbound-router and more.
Why, and what we turned down

Why. Fan-out without producers knowing consumers. Subscribers default to unordered with aggregateVersion checks.

Rejected. SQS FIFO per subscriber (kept as a fallback, not triggered).

SHIPPEDA 200-event FIFO burst took p95 8.4 s against 1.2 s unordered. EventsV2 has no GA label and no native Terraform resource, so it uses awscc. S17
STS session tagsThe dispatcher mints AssumeRole credentials per attempt, tagged with tenant, attempt, Bot, conversation, turn, run and channel. IAM reads the tags in DynamoDB leading keys and S3 paths.
Why, and what we turned down

Why. A repository bug becomes AccessDenied, not a leak.

Rejected. A write broker: not needed.

SHIPPEDAssumeRole p50 173 ms, p95 238 ms; 142 checks against raw SDK clients, none unexpected. IAM fences partitions, not sort keys, so the gen condition is the fence inside one. S16
Secrets ManagerUser secrets at dunkbot/{env}/t/{tenant}/{secret}, bound to tools and never shells. The model never sees a value; it types one with type_secret.SHIPPEDA revoked secret cannot be typed (NOT_ACTIVE, live); exec output is redacted. A putSecret canary was absent from CloudWatch. EXIT3, EXIT5
07 · Channels
SES emailA receipt rule on mail.dunkbot.ai stores mail in S3 and calls the email adapter, with a sender allowlist and DMARC required. Outbound email.send is a hard ask and sends once per approval.
Why, and what we turned down

Why. Email is a trigger and a channel in one.

SHIPPEDThe receipt path is deployed and active, but the exit checks used synthetic MIME; no real inbound mail is on record. Outbound runs in log mode: SES production access was denied. EXIT5
End User Messaging SMSA number per Bot through End User Messaging. Inbound goes through SNS to sms-inbound and the router; STOP, HELP and START are handled; sms.send is a hard ask.SHIPPEDSimulator numbers only, at most 3 per account. A text from a Bot number started a routine that completed on Nova. Real numbers wait on the 10DLC campaign. EXIT5
Amazon Connect voiceCalls through Amazon Connect with a Nova Sonic voice agent (Phase 5d).
Why, and what we turned down

Why. End User Messaging voice is one-way text-to-audio, and one number cannot carry both SMS and Connect voice.

Rejected. Vonage or Twilio streaming, kept as the fallback.

PLANNEDDesign and ADR 0005 only. Gated on spike S25, which has no result. ADR5
Not drawn · platform and in-flight work
Route 53 + ACMHosted zone for dunkbot.ai, the ACM certificate and the app, hooks and mail records.SHIPPEDTF
ECRThe repository for the brain image.SHIPPEDTF
SSM parameterscanaryPct, the version catalog, the Gateway URL, and memory and review config.SHIPPEDTF
AWS Budgetsdunkbot-monthly at $200 (alerts at 50, 80 and 100% plus forecast) and dunkbot-models at $20 for Bedrock and Marketplace. A per-user hard spend cap is enforced by ledger holds.SHIPPEDTF
GitHub OIDC + permission boundaryPlan-only and dev-apply roles. Merge to main applies a guarded plan that refuses deletes or replaces of DynamoDB, KMS, Cognito, S3 and ECR. Every role carries dunkbot-workload-boundary.SHIPPED50 IAM role blocks, all permission-bounded; 40 Lambda resource blocks (several for_each, so more deploy). TF
SNSThree topics: inbound SMS, ops alarms and billing paging.SHIPPEDTF
Lambda Function URLsThe OIDC/JWKS endpoint for run tokens and the Composio proxy.SHIPPEDTF
activity module (Glass box)Activity tab, costs, budgets and a no-progress stop. Built on phase5c/glass; PR #23 is open.IN PROGRESSbranch
sends module (Rehearse, Undo send)Rehearse runs, Undo send and workspace snapshots. Built on a branch; PR #23 is open.IN PROGRESSbranch
loops module (open loops)Open loops with stall alerts. Built on phase5d/build, not merged.IN PROGRESSbranch
SMS opt-inA consent box and 6-digit code with a verified-recipient allowlist, which the 10DLC campaign needs. Built on phase5c/glass.IN PROGRESSbranch
Payments / intent networkBots asking other people's Bots, with paid answers in testnet USDC on Base Sepolia through an A2A exchange. Mainnet needs Ish's explicit OK.PLANNEDADR4
Notion, Linear, Atlassian and Slack targetsOfficial MCP targets in the design; not in the deployed catalog.PLANNEDARCH
04 · Demo

What works today, and what to type to show it.

SHIPPED means shown live on dev. IN PROGRESS is merged or built but not yet shown live. The prompts are suggestions to type into a Bot.

Bot with a computer

SHIPPED

A browser screen, a terminal and a workspace; watch it live over DCV.

Demo this

Open news.ycombinator.com on your screen and tell me the top story about AWS.

Open the live view next to the chat. The spike measured the first frame at 3.4 s.

BrowserComputer controlCode Interpreter

Takeover and resume

SHIPPED

Press Take control, act, then “I'm done”. The Bot resumes from a fresh screenshot.

Demo this

Go to github.com/login and wait for me to sign in, then open my notifications.

Take control, sign in, release. Point out the paused_by_user turn.

Computer controlBrowserFlow: Take control

Approvals

SHIPPED

Allow once, Always allow or Deny, bound to the exact action. Auto Review on send, pay, delete and share.

Demo this

Email me a two-line summary of today's open GitHub notifications.

Outbound mail runs in log mode (SES production access was denied); the card and the single-use consume are the point.

Flow: ApprovalGateway · CedarSES email

Connectors

SHIPPED

Gateway + Identity 3LO + Cedar. A Connect card in 11 s; Gmail, Calendar and GitHub reads live on Ish's account.

Demo this

What is on my calendar tomorrow, and which meeting needs prep?

Use Ish's own account; a second test account's consent is still open.

Flow: ConnectorIdentity 3LOConnectors

Routines

SHIPPED

Schedule, webhook, email or SMS triggers start durable runs, up to 50 per Bot, at least 5 minutes apart.

Demo this

Every weekday at 8am, check my GitHub notifications and DM me anything that needs a reply.

Fires started 4.3-6.7 s after nominal; 9 of 10 completed and 1 was skipped as OVERLAP.

SchedulerDurable runsFlow: Inbound webhook

Teach by demonstration

SHIPPED

Record 45 s of the browser and get a draft skill with screenshots. No skill file exists until you save the draft.

Demo this

Watch me file an expense once, then draft a skill so you can do it next time.

Browser only. The 10-min cap is unit-tested, not shown live.

BrowserS3 + KMS

Group chats

SHIPPED

2 to 6 Bots in one chat. A floor router wakes at most 2 per message, at $0.0059 per message measured on Nova.

Demo this

Which of you should take the weekly report, and who reviews it?

In a group, approval cards go to that Bot's DM. Handoffs stop at 4 hops.

RuntimeBedrock models

Rules, brief-and-watch, Glass box, Rehearse

IN PROGRESS

Per-Bot rule tiers compiled to Cedar claims and the morning brief are merged; the brief's deploy to dev is not confirmed and neither has a recorded live run. Glass box, Rehearse, Undo send and SMS opt-in are built on a branch (PR #23, open).

Walk through

Brief me every morning at 7 on my calendar and PRs, and only ping me if something is urgent.

Show the design, not a live run.

Gateway · Cedaractivity module

Voice, full desktop, desktop and mobile apps, intent network

PLANNED

Voice calls on Amazon Connect, the Runtime Instances desktop, the Tauri and Expo clients and the paid intent network are designed, not built.

Say this

Designed and written up (ADRs 0004 and 0005), not built. Ask me what gates each one.

Do not demo these.

VoiceLinux desktopDesktop

FAQ

Questions people ask.

Why not Step Functions?

A second language and a 25,000-event history cap. Durable functions keep the whole run loop in TypeScript; Step Functions stays the untriggered fallback.

How do you test cheaply?

Nova for every test workflow, enforced in CI. A local nightly of 336 runs cost $2.16; the PR gate costs $0.39.

What broke?

A main deploy once destroyed unmerged resources in dev, and approval events stalled 10 minutes until they moved to non-Bot outbox groups. Both were fixed and re-shown.