Isolation, and the one trade-off
Every boundary below has a named mechanism rather than a policy statement, and one of them has a documented cost. A platform that publishes its own trade-offs is trusted differently from one that omits a weakness its own source code comments on — and the source is public either way.
The six boundaries
| Boundary | Mechanism |
|---|---|
| Job ↔ job, same node | Fresh Playwright subprocess, fresh Browser, fresh BrowserContext per request, torn down in try/finally. Not a cleared context — a new process. The lifecycle → |
| Session ↔ session | A dedicated OS thread per session owning its own browser process; the registry is keyed by session id. Why the thread affinity is load-bearing → |
| Node ↔ node | Separate EC2 instances, per-node security group, and a per-node API key that is never reused |
| Untrusted JavaScript | JS__Expression__Allowlist — deny by default, exact match. Below. |
| Network | Browser traffic routed through the mitmproxy sidecar; “isolation is Docker network”; the sidecar's :8080 is unreachable from outside the instance |
| Cookies | SET_COOKIE applies to the per-request context and is discarded with it — “stateless — the context is fresh per request… no session persistence” |
The strongest of these is the node boundary, because it is the one that does not depend on the process model being correct: two workloads on two nodes are two EC2 instances with two security groups and two keys. Everything above that line is a software boundary; the node boundary is an AWS one.
Deny by default — and the one place it is switched off
JS__Expression__Allowlist is deny by default with exact matching. An agent calling evaluate gets rejected until an operator has explicitly populated the allowlist with the expressions it may run. Nothing is permitted by omission, and there is no pattern syntax to get wrong.
There is one bypass, allow_all, and it is set in exactly one place: the screenshot surface. The stated ground is that “each call is an isolated session” — which is true, given the per-request teardown above.
It is probably the right call, and it should still be a written decision rather than a flag. It is the single place a deny-by-default control is turned off in the whole platform, and the argument for it depends entirely on the per-request isolation continuing to hold. If that ever changes — a pooled browser, a reused context, a performance optimisation — this bypass changes meaning silently. It is on the loose-ends list.
The trade-off: a per-node API key in an EC2 tag
EC2__Platform.create_node mints a per-node key, writes it to SSM — the correct store — and then also writes it to an EC2 tag:
if node_info.instance_id: # tag so dashboard can read key without SSM
EC2__Launch__Helper().add_tags(region, str(node_info.instance_id),
[{'Key': 'sg-compute:host-api-key', 'Value': api_key}])
And the codebase already documents the consequence, in a schema comment:
“Visible to anyone with ec2:DescribeInstances.”
The facts, stated fairly
- No live key is exposed in the repository. This is a design trade-off, not a leak.
- The key is also in SSM, which is where it belongs. The tag is a dashboard convenience, with a comment saying exactly that.
- The blast radius is bounded: the key is per-node and never reused, and nodes self-terminate on a default one-hour timer.
- A reader who can exploit it already holds
ec2:DescribeInstancesin the account — a meaningful IAM position, though a very commonly granted one.
The mitigation, named
Read the key from SSM in the dashboard and drop the tag, accepting one extra API call per node on a page that is not latency-sensitive. That is the whole fix.
Whether to take it is Q6, published unresolved: the trade is one API call against exposure to a widely-granted IAM permission. A stated decision either way is fine. Silence is not — which is why this section exists rather than not.
This is published as an architecture note, not a vulnerability disclosure, and deliberately carries no exploitation path. The behaviour is visible in public source, the code comments on it itself, and omitting it would cost more credibility than the trade-off does.
What was checked, and is clean
Stated because it was verified rather than assumed, and because a security page that only lists problems is as unbalanced as one that lists none.
| Checked | Result |
|---|---|
.env files with real values | none — three .env.example only; .gitignore covers the rest |
| AWS access keys | none — every match is the AWS public documentation example or a synthetic fixture |
| Private keys | none — all synthetic, or generated at test runtime |
| GitHub / Slack / OpenAI / Anthropic tokens | none — zero matches |
| Public IP addresses in code | none |
| CI credentials | all from ${{ secrets.* }} |
What this site will not publish
The platform documented here is live infrastructure, so the editorial rule is shapes, not addresses: <stack-name>.sg-compute.<zone> is documentation; a live FQDN is a target.
An AWS account id, ten internal hostnames, five named live stack FQDNs and sixteen real EC2 instance ids with three AMI ids were stripped from the source material before any of it was published here. Two of the source documents were the redaction list itself, and are published redacted under their own rule.
The list is encoded in the release gate, so it cannot regress. admin/build/validate.js fails the build if any file in the tree contains the account id, any of the hostnames, a stack-slug FQDN, an EC2 or AMI id shape, or a vault-key-shaped string. No tag, no publish. How the tripwire works, including its one deliberate exception →