Escher
An AI interface for running a cloud estate. Ask about cost, security or reliability, and get an answer you can check and act on.
- Role
- Product designer. Built the prototype end to end, and now the frontend of the desktop app.
- Company
- Tessell
- Platforms
- Web prototype, Electron desktop build, Tauri app
- Team
- Two product designers, 2026
Are any S3 buckets public?
local estate 0/41 · writes locked
A copy of the estate, on your device
The Escher server sends a scan. The app on your device runs it against AWS with read-only calls, using keys that never leave the device, and keeps what it finds.
- Account
- prod
- Calls
- Read-only, 12 at a time
- Keys
- In the device’s keychain
- Copied
- 41 S3 buckets
One question in
You ask in plain English. Only the prompt goes up to the Escher server.
Are any of our S3 buckets actually public right now?
The server writes a query
The server sends a query back down, and the app runs it on the local estate. The answer comes from your data, not from the model’s memory.
query_resources
- 1type: s3-bucket
- 2public_access: true
- →41 rows checked, 1 match
- sellers-exports
An answer you can check
The row goes up, and the server turns it into interface. Every value on it points back to the estate.
- Public
- 1 of 41 buckets
- Bucket
- sellers-exports
- Grants
- s3:GetObject to Principal: "*"
- Objects
- ~12,400
- Source
- 1AWS Config, bucket ACLs
Fixes, sized by blast radius
Ask Escher to lock it down and the server sends back options. Each one shows what else it would touch. The write line stays locked.
Delete the bucket1 bucket, 2 services
The export job and analytics break. ~12,400 objects are lost.Block public access for the account41 buckets, 1 CDN
cdn-origin may stop serving the CDN.Make sellers-exports private1 bucket
Every read came from internal roles. Presigned links keep working.Recommended
You approve, then it runs
Turning the key opens the write line. The app runs each step on AWS with your keys and reads it back before the next one.
- Policy savedrollback point
- "Principal": "*" removedread back
- Block Public Access onread back
2 writes · rollback: 1 call
The local copy catches up
The app reads the bucket back from AWS and updates the local estate. Ask again, and the answer is none.
- Read back from AWSprivate
- Anonymous GET403 Forbidden
- Local estate0 of 41 public
A worked example from the prototype’s demo estate. The Escher server writes every script and every answer, the app on your device runs them, and AWS keys stay on the device.
The problem
Companies were forecast to spend $723 billion on public cloud in 2025, and nearly a third of it is wasted. When something breaks, the answer is spread across a dozen consoles, runbooks and people’s heads. AI can answer fast, but no engineer will put an answer they can’t check on call.
Public cloud spend forecast for 2025
GartnerOf cloud spend estimated to be wasted
Flexera, 2026Of organisations call managing cloud spend their top challenge
Flexera, 2025Of outages cost $100K or more
Uptime Institute, 2022Scattered
Cost, security and performance data live in separate tools, each with its own console.
Untrusted
A confident paragraph from a model isn’t evidence. Without sources, it can’t be acted on.
Manual
Fixes are still written by hand in Python, Bash and Terraform, one incident at a time.
The data to fix all of this already exists. Nothing connects it, checks it and acts on it safely.
Who it’s for
A staff-level SRE who owns production reliability and writes the runbooks today. Their test is simple: would I put this on call?
The same estate answers different questions for seven roles.
Owns production reliability. Asks about scaling, capacity, incident root cause and latency.
Connects the first accounts and gets the team started.
IAM hygiene, open security groups, encryption and patching.
Drift, network layout, runbook automation and tickets.
Rightsizing, reserved coverage, orphaned resources and cost spikes.
Many accounts, scanned and read as one estate.
Gaps against SOC 2, HIPAA, ISO 27001 and PCI DSS.
Each role has its own test journeys in the prompt matrix: 70 in all, four or five turns each.
The model
Every cloudops task is one of five steps: Design, Develop, Detect, Debug, Deploy. Building something new and fixing something broken both end at Deploy.
An alert arrives with context
The signal comes already linked to the resources, owners and history around it.
Ask anything
Plain-language questions, answered from the live estate.
Escher proposes the work
It watches for drift and suggests fixes before anyone asks.
Principles
Grounded
Every claim cites live estate data. Missing data is said plainly, never filled in.
Safe
Fixes are plans you can read like a diff, with approval, dry run and rollback.
Earned trust
Automation moves from shadow to live on evidence. One switch pulls it all back.
Sketches
Ideas from stakeholders became HTML sketches people could click, long before they reached the prototype. There are 24 of them, from the knowledge base to what happens when the model goes down.






Ask
Type a question and the answer comes back as interface: numbers, tables and cards built for that question, each one citing its source. Reports run in two passes. The first finds the facts and the second writes them up, so the writing can’t claim more than the data shows.
Pass 1Chat, read-only tools
- 1
query_resourcestype: s3-bucket41 buckets in prod - 2
analyze_postureservice: s31 public: sellers-exports - 3
aws_clis3api get-bucket-policys3:GetObject to Principal "*" - 4
query_blast_radiussellers-exportsRead by 2 services open_report"S3 exposure review"Captured, never run
Pass 2Report, no tools
S3 exposure reviewWarning
prod · 41 buckets
sellers-exports lets anyone read its objects. Two services read from it, so make it private rather than delete it.
Hover a number to see the call it came from.
Generative UI
The model answers in OpenUI Lang, a small language for interface. The app renders it with Escher’s own components as it streams, so an answer is something you can check, compare and act on, not a paragraph to read.
SROur AWS bill jumped 31% this month and finance is asking. What’s going on?
Your AWS bill rose about 31% compared with last month. I looked at Cost Explorer grouped by service over the last 90 days and found three main drivers.
First, a new EKS node group called prod-pool was created 12 days ago in us-east-1. It runs eight m6i.2xlarge instances and adds roughly $4,218 per month. Second, a one-year reserved instance renewal of $11,400 was posted on May 1. This is an annual charge rather than an ongoing increase. Third, data transfer out rose about six times between May 14 and 16, which is most likely an S3 backfill, although I can’t see the job itself.
If you exclude the one-time charges, the recurring increase is about $4.2k per month. You may want to consider right-sizing the prod-pool node group and setting up cost anomaly alerts so you catch this earlier next time.
End of answer
Fetched Cost Explorer, compared with a 3-month baseline · 1.8s
Most of the 31% isn’t real growth. One line costs more every month; the other two are one-offs.
prod-pool is the only recurring increase
A new EKS node group, prod-pool, adds $4,218 a month.
| Driver | Kind | This month |
|---|---|---|
| prod-pool node group | Recurring | $4,218 |
| Reserved-instance renewal | Annual | $11,400 |
| S3 data transfer | One-off | 6× for 3 days |
The renewal is an annual charge. The transfer spike looks like an S3 backfill, so I’m treating it as a one-off.
The honest increase is about $4.2k a month. I’d right-size prod-pool first.
- The answer comes lastThe point, a recurring $4.2k, is in the third paragraph.
- No sourcesNothing says which figure came from where, or which ones are guesses.
- Nothing to act onThe next step is a suggestion you would have to type back.
- The answer comes firstOne or two sentences, then the evidence.
- The heading names the factNot “Findings” or “Cost”, but the thing you need to know.
- One component for one pointA table, because the point is a comparison.
- Every claim shows its sourceAnd how sure it is: evidence, inference or assumption.
- Next steps are buttonsTwo or three specific moves, one click each.
Both answers use the same tool results, from the prototype’s cost story. The plain one is how a text-only assistant would put them.
Built while it streams
The first line names the parts of the answer, so the layout exists before any part is written. Each part renders the moment its line parses. A part that never parses shows a local error instead of breaking the thread.
OpenUI Lang · streaming
root = Stack([lead, head, drivers, take, next])lead = TextContent("Most of the 31% isn’t real growth.")head = TextContent("## prod-pool is the only recurring increase")drivers = Table([driver, kind, amount])driver = Col("Driver", ["prod-pool", "RI renewal", "S3 transfer"])kind = Col("Kind", ["Recurring", "Annual", "One-off"])amount = Col("This month", ["$4,218", "$11,400", "6× for 3 days"])take = TextContent("The honest increase is about $4.2k a month.")next = FollowUpBlock([f1, f2])f1 = FollowUpItem("Right-size prod-pool")f2 = FollowUpItem("Set a cost anomaly alert")
0 of 11 statements · 0 waiting
Most of the 31% isn’t real growth.
prod-pool is the only recurring increase
The honest increase is about $4.2k a month.
One question, one form
The component follows the question: a ranking gets a table, a trend gets a chart, three or more numbers of one kind get metric cards. A count or a yes/no stays a sentence. The model can only use what the library defines, the OpenUI base set plus thirteen of Escher’s own.
“What are we spending this month?”
Metric“Which service costs the most?”
BarChartHover a bar“What are our most expensive resources?”
| orders-db | us-east-1 | $4,210 |
| warehouse | us-west-2 | $3,880 |
| etl-jobs | us-east-1 | $1,290 |
| cdn-logs | eu-west-1 | $412 |
DataGridSort a column“How is this bucket exposed?”
HtmlFigure“What would shorter log retention save?”
WidgetDrag“Why did spend go up?”
Spend rose 8% this month, mostly from RDS.
CiteClick the marker“Lock down the public bucket.”
1 of 2
Which environment?
QuestionnairePick one“Review S3 exposure across prod.”
ReportChoosing the chart
The first version picked a chart from the shape of the data, which produced charts where every bar was 1. Now the form follows the reader’s question, and charts that carry no information are blocked in code.
Every bar is 1. The chart answers nothing.
One number and the two facts that matter.
Answers you can check
Every load-bearing claim carries a numbered citation that says where it came from and how sure it is. Records like findings never pass through the model at all: it writes the framing, the tool supplies the rows.
1EvidenceObserved in a named source.
3InferenceA conclusion drawn from data.
4AssumptionTaken as true, not yet confirmed.
Modelbuild_findings tool
2 findings · 1 critical internet exposureheadline
The bucket is the one to act on today; the Lambda health cluster is worth watching, not fixing.lead
Anyone on the internet can read every object in it.note
Watch, don’t fix: all three share one health signal.note
The model can’t type a severity, a resource or a count. It writes the framing, and the rows arrive from the tool through React context.
Escher’s own charts
Band-scale bars whose labels are measured in the browser, so a long name is trimmed instead of running out of the chart.
Figures and widgets
A figure is sanitised and may load nothing from outside. A widget runs its script in a sandboxed frame with no access to the page.
Guards in code
No chart of zeros, no invented capitals in legends, and a section heading always stays with its figure.
Plan and execute
Start from a finding. Escher lays out ways to fix it, asks only what it can’t look up, and writes a plan with its blast radius up front. You review the change like a diff, dry-run it and approve it, and fresh reads prove it landed.
Nothing sent yet
Sent with the question
findingufnd_xysj
rules3-public-read
resourcesellers-exports
evidence3 items
scopeprodfrozen for the session
Estate graph
exports-cdnreadssellers-exportspublic URL
batchreadssellers-exportsIAM role batch-runner
Asking from a finding sends the record with the question, so the answer starts from it.
| Option | Resources | Undo | Track record |
|---|---|---|---|
| Private behind the CDN | 2 | yes | playbook · 34 runs, 0 failed |
| Account-wide block | 41 | yes | none |
| Delete the bucket | 3 | no | none |
Blast radius is a walk of the estate graph, not a guess by the model. The model writes the options; the numbers come from the tool.
ask_userthe turn waits for answers
cdnShould exports-cdn keep serving these files?Yes, through origin access control
whenWhen should it run?Now
notifyWho should hear about it?#seller-platform
Looked up, not asked
Who reads the bucketestate graph
How batch reads itIAM role
Has this fix worked before34 playbook runs
create_plancaptured, then written in its own pass
"steps": [{ "n": 1, "source": "playbook", "id": "snapshot-policy" },{ "n": 2, "source": "generated", "id": "create-oac" },{ "n": 3, "source": "generated", "id": "allow-cdn-read" },{ "n": 4, "source": "playbook", "id": "enable-pab", "gate": "ask" },{ "n": 5, "source": "playbook", "id": "strip-wildcard" }],"rollback": "restore:step-1"
steps53 playbook, 2 new
gatestep 4asks before it runs
order3 before 4exports-cdn never loses access
rollbackstep 1 snapshot
blast2 resourcesfrom the estate graph
01-snapshot-policy.pyplaybook v1, unchanged
02-create-oac.pynew · review
03-allow-cdn-read.pynew · review
04-enable-pab.pyplaybook v1, unchanged
05-strip-wildcard-grant.pyplaybook v1, unchanged
dry runsimulated against live state
reads4policy, ACL, access block, distribution
writes0
changes2 resources
errors0
- 10:42:03
run E-1 approved by Sam Rivera - 10:42:03
step 1 snapshot → s3://escher-artifacts/P-1/before.json - 10:42:04
step 2 create-origin-access-control → oac-7f3a - 10:42:07
step 3 put-bucket-policy, +1 statement - 10:42:08
step 4 waiting for confirmation - 10:42:11
step 4 confirmed by Sam Rivera · put-public-access-block - 10:42:14
step 5 put-bucket-policy, −1 statement
The model has no path to the write endpoint. Each step is its own request, sent after a person approves it.
| Check | Before | After |
|---|---|---|
| Access block flags on | 0 of 4 | 4 of 4 |
| Anonymous GET | 200 | 403 |
| exports-cdn GET | 200 | 200 |
| Public buckets in prod | 1 | 0 |
ufnd_xysj resolved, 4 reads attachedE-1
Posted to #seller-platform
Verification runs its own reads. It doesn’t trust a step’s exit code.
Design mode
Operators write playbooks by talking them through instead of scripting them. Imported runbooks become playbooks too, and each one keeps its success rate and evidence.
Agents
Agents start as drafts, run in shadow until their results earn trust, then go live one playbook at a time. One switch drops every agent back to propose-only.
Strictest rule wins
Three settings decide what an agent may do: its own mode, the grant on each playbook, and one org-wide break-glass lever. The strictest of the three always wins.
Design system
One token file builds CSS variables, a Tailwind theme and typed tokens, and every app reads from it. Change a colour once and every app picks it up.
Ink#1D1C1B
Paper#F8F8F7
Stone#EAEAE7
Escher#FA504A
Action#0265DC
Success#007C0F
Warning#E8C600
Danger#D31510
Geist. Interface and reading text.
Geist Mono. Code, IDs and logs.
Semantic tokens: colour, space, radius, type, shadow
Source file, built into CSS, Tailwind and TypeScript
Changes needed in each app when a token changes
Architecture
Three layers. The browser composes each turn and renders the answer. A thin API layer talks to models, the estate database and AWS. Every claim in an answer traces back to a row or a command output.
One turn
A question becomes a short loop of tool calls, then an answer built from what the tools returned.
Nothing sent yet
POST/api/chat
modelclaude-opus-5-5
systemchat rules + gen-UI grammarcache hit
scopeprodfrozen for the turn
roundsup to 3
Tools 14
query_estatequery_resourcesquery_findingsscan_findingsanalyze_posturebuild_findingssearch_knowledgequery_reportsaws_cliopen_reportcreate_plancreate_changesetrun_executionask_userruns, returns datacaptured, never runpropose_change run_steps not sent in Ask
modelquery_resourcestype: "s3"41 bucketsanalyze_posturetype: "s3"1 flaggedaws_cliget-bucket-policyPrincipal "*"aws_cliget-public-access-block4 flags falseaws_clicloudfront list-distributionsexports-cdnEach aws_cli call runs as an argument list, with no shell:["aws", "s3api", "get-bucket-policy", "--bucket", "sellers-exports", "--profile", "prod"]
root = Stack([lead, stats, take, next])lead = TextContent("Yes, one: sellers-exports. Its policy lets…")stats = Stack([m1, m2, m3], "row")m1 = Metric("Public buckets", "1 of 41", null, null, null, "in prod")m2 = Metric("Public-access block", "Off", null, null, "bad")m3 = Metric("Served by", "exports-cdn")take = TextContent("Make it private and let exports-cdn…")next = FollowUpBlock([FollowUpItem("Write it up as a report"), …])
0of 8 statements
The first line names four parts, so the layout is on screen before any of them is written. Each part fills in when its line parses.
Pass 1chat
tools14
calls5 run
open_reportcaptured, never run
Pass 2report
toolsnone
context5 tool results
writesS3 exposure review
Pass 2 has nothing it can call, so the report can only use what pass 1 found.
scan_findingsrun by the app, no model call
{"fingerprint": "s3-public-read:sellers-exports","ruleSlug": "s3-public-read","resourceId": "sellers-exports","severity": "critical","count": 1}
RowsFrom the scan. The model can’t type a severity, a resource or a count.
Headline and notesFrom the model, keyed to each row’s fingerprint.
Saved to the findings storeufnd_xysj
Safety in code, not in the prompt
A role named read-only can still carry write permissions, so the checks live in the API, not in IAM or the system prompt.
Reads
- Commands run as an argument list with no shell, so
;, pipes and$()mean nothing. - Only allowlisted read verbs run.
- The profile must exist on the machine, or the call is refused.
- Every call has a timeout and an output cap.
Writes
- The model can only propose a change. Its tool loop has no path to the write endpoint.
- Each step runs only after a person clicks it.
- The first step records the current state, so rollback restores it.
- Model IDs are allowlisted, and the deploy sits behind a signed password cookie.
API routes
Database migrations
Tool rounds per turn, at most
Tools the model can call
- React, ViteThe frontend, shared by the web prototype, the desktop build and the Tauri app.
- Anthropic, OpenRouterNative Messages API for prompt caching, with failover to another model when one is down.
- Postgres, pgvectorThe estate, findings, reports and alerts, plus embeddings for semantic search.
- pnpm, TurborepoOne monorepo for the apps, the design system and the estate store.
- VercelEdge functions, password-gated deploys, share links in Vercel Blob.
- Tauri, ElectronThe shipping desktop app, and the build that runs the prototype on real accounts.
Data model
The estate is thirteen Postgres tables. resources holds current state. Everything around it keeps the history: what changed, what each resource looked like before a scan, how resources connect, and an embedding of each one for search.
Scans
Estate
History and search
Work
resources Current state of every resource, one row per profile, account and ARN. A deleted resource stays, marked deleted.
Primary keyLinks to another tableChoose a table to trace it.
All or nothing
A scan is classified in one transaction, so a half-finished scan can never read as a complete one.
Diff by hash
Each resource has a content hash. Unchanged resources are left alone, and a deleted resource that comes back counts as new.
Encrypted
Sensitive payloads use AES-256-GCM with a fresh IV each time, so matching resources never produce matching ciphertext.
Scanning
Every resource type in every region is its own read-only call. They all go into one queue that runs twelve at a time, so the limit is AWS throttling, not the order of the work.
A call that fails is recorded and retried with backoff. A scan that couldn’t look never reports a clean estate.
AWS services in the registry
Calls in flight at once
Between automatic rescans on the desktop build
Model layer
Every route speaks Anthropic’s Messages API, because prompt caching only exists there. Claude models go straight to Anthropic. Other models go through OpenRouter’s Anthropic-format endpoint, so the request and the stream parser stay the same.
Only model IDs on an allowlist reach a provider. If a model has no key configured, the user gets a sentence explaining why, not a dead stream.
Token optimisation
Every request carried about 11,000 tokens before the conversation even started: a 9,000-token system prompt and 2,200 tokens of tool schemas. A report turn made five to eight model calls, and each one re-read the whole conversation.
I measured it first, then cut it without changing a single answer.
138k billed input, in token-equivalents
An example report turn of eight calls. Bottom to top: tool schemas, system prompt, conversation so far, the new turn. Prices are the published cache multipliers.
Tokens of fixed prefix on every request
Model calls in one report turn
Price of a cached read
Sources in the token-efficiency survey
Three tiers
Every idea was sorted by how much it could change an answer. Only the first tier ships without an evaluation.
Ship it
- Cache the conversation prefix, not just the system prompt
- Keep the cached prefix stable across modes
- Remove dead and duplicate prompt text
Spot-check and canary
- Clear stale tool results from long loops
- Compact output formats
- A sane output ceiling, with an alert if it clips
Evaluation gate first
- Lower reasoning effort on easy turns
- Summarise long history
- Route easy turns to a cheaper model
Measured
The thinking panel breaks every turn down by model call, with cache reads and writes shown apart.
Honest cost
Cost is shown cache-discounted, so the number on screen matches the bill.
Lossless first
Prompt caching was the biggest win, and the provider guarantees it leaves output unchanged.
Tools
The model works through 52 tools in four groups. Control tools are the important ones: the app captures them and shows them to a person, and they never run on their own.
Read
The estate, AWS, the knowledge base, Slack and Linear.
- query_estate
- query_resources
- search_estate
- query_service_topology
- query_blast_radius
- query_findings
- query_reports
- query_playbooks
- search_knowledge
- analyze_posture
- aws_cli
- aws_api_batch
- scan_repo
- fetch_url
- linear_tickets
- slack_channels
- slack_users
Record
Turn tool results into findings, reports and plans.
- build_findings
- scan_findings
- create_plan
- open_report
- open_estate_topology
Control
Proposals a person accepts or rejects.
- propose_write_plan
- revise_plan
- skip_execution_step
- ask_user
- request_review
- propose_slack_post
- propose_contract
- propose_readiness
- propose_task_outline
- offer_execution_session
- offer_playbook_session
- suggest_replies
Author
Build a playbook step by step in Design mode.
- add_step
- add_tool
- add_guardrail
- add_parameter
- add_risk
- add_capability
- add_knowledge
- set_trigger
- set_gate
- set_approval
- set_blast_radius
- set_context
- set_scenario
- set_language
- wire_step
- test_step
- run_end_to_end
- publish_playbook
Execution
The model can propose a change but has no way to run one. Each step runs only after a person confirms it. The first step records the current state, each step is re-read after it runs, and a step can stop the run or ask before going on.
Evaluation
Answers are graded by six plain functions, not by another model. They check that every number came from a tool result, that knowledge answers cite a source, and that the interface code parses. They run in CI with no API key.
Tool results
query_resources → {"type": "ebs-snapshot", "count": 5152, "byAccount": {"prod": 3904, "data": 1248}}search_knowledge → {"count": 2, "results": ["Snapshot retention", …]}
Answer, in OpenUI Lang
root = Stack([head, kpi, grid, cite, more])head = TextContent("Prod holds 4,812 of 5,152 snapshots")kpi = Metric("EBS snapshots", "5,152")grid = Table([Col("Account", accts), Col("Snapshots", counts)])cite = Cite(1, "evidence", "Snapshot retention runbook")more = FollowUpBlock(["Which are older than 90 days?"])
- groundingEvery number is in a tool result
- citationKnowledge answers cite a source
- parseThe interface code parses
- shapeNo empty or one-column parts
- absenceEmpty results aren’t read as “none”
- reachableEvery part is on screen
numeric claim "4,812" does not appear in this turn’s tool results
The model rounded from memory. 4,812 is nowhere in what the tools returned.
Deterministic graders
Test files
Roles in the prompt matrix
Desktop app
The web prototype runs on a made-up estate. The Electron build runs the same app against real AWS accounts, with the database, credentials and scans kept on the laptop.
Real accounts
It scans every resource type in every region of a connected account, rescans every six hours and keeps the history.
Local first
The database runs inside the app and keys live in the OS keychain. Only the prompt goes out to the model.
Native
System notifications, Slack and Linear sign-in through escher:// links, and updates that install themselves.
Demos and design
A made-up estate anyone can explore in a browser.
Real accounts
The same app on a laptop, connected to real AWS.
The product
The shipping desktop app, using this frontend code.
How we worked
One repo, with one set of rules for people and AI agents: an agent roster, issue-first changes and a decision log that stops settled debates from reopening. Vision became sketches, approved sketches became the prototype, and the prototype’s code moves into the app.
Commits since May 2026
Of them mine
Decisions logged
Sketches
Outcome
Tessell uses the prototype to demo Escher to potential customers and investors. Its frontend code moves straight into the Tauri app, so the prototype works as a spec as well as a demo.
What I learned
A bigger role
I started as a product designer making a prototype. Now I build the frontend of the desktop app as well.
A different process
Design happens in working code instead of static mockups. The sketch, the prototype and the shipped frontend are the same material.
A faster pace
Ideas go from sketch to something people can click in days, not weeks.