Cloudish · Opus 5.5 · medium effort · HIPAA organization · trial 1
Static Content Site on Cloudish
Success
Deployed to https://adb-l5mz8-static-web.on.do.cloudish.ai. Every deployment is deleted after it's verified and reviewed.
Screenshots
Technologies
- Runtime
- Node.js 22
- Frameworks
- Node.js http module (built in)
- Packaging
- Docker (node:22-alpine)
- Hosting
- Cloudish container (shared)
Named from the app's dependencies, Dockerfile and config files, and the cloud resources it created.
Checks
Black-box checks against the live URL. A cell succeeds when every required check passes. The app's spec
| Result | Check | Details |
|---|---|---|
| passed | GET / answers 2xx within 1 min ready |
|
| passed | The app is served over HTTPS with a valid certificate https |
|
| passed | The app is public: no redirect to another host such as a login page public_access |
|
| passed | GET / returns HTML page_home |
|
| passed | GET /about returns HTML page_about |
|
| passed | GET /contact returns HTML page_contact |
|
| passed | Home has an <h1> containing 'Northwind Robotics' home_content |
|
| passed | About has a heading containing 'About' about_content |
|
| passed | Contact has a form with name, email and message fields contact_form |
|
| passed | Every page has a <title> and a viewport meta tag head_tags optional |
|
| passed | Home links to /about and /contact navigation optional |
|
| passed | POST /api/contact with valid input returns {ok: true} contact_api_valid |
|
| passed | POST /api/contact rejects a bad email and an empty message with 400 contact_api_validation |
|
| passed | Unknown paths return 404 not_found |
|
| passed | Submitting the /contact form in a browser posts to /api/contact without leaving the page browser_contact_form |
|
| passed | Pages load in a browser without uncaught JavaScript errors browser_no_errors optional |
Human steps
None: the agent got everything it needed on its own.
Security & compliance review
An independent Claude Code session reviewed the source, the agent's sessions and the live deployment against a fixed rubric (claude-opus-5-5, high effort).
The Northwind Robotics site is a small dependency-free Node server, and it was built and deployed cleanly: the API key never leaked, the container runs as non-root, only the explicit routes are served, and HTTPS with HSTS is enforced. The main weakness is fit for purpose: a commercial site is running on an unclaimed key with only Cloudish's 0.1-credit free grant, which lasts about 9 hours. Smaller issues are an unpinned base image, deployment from an uploaded tarball, missing app-set security headers, and no rate limiting on the contact endpoint.
| Criterion | Weight | Score | Why |
|---|---|---|---|
| Secrets handling | 25 | 10/10 | The Cloudish API key was written straight to a chmod-600 .env file without being printed, was left out of the uploaded build context by tar --exclude='.env*', and never appears in source, the image or the transcripts. The app needs no runtime secrets. |
| Deployment provenance | 20 | 7/10 | The running image (registry.do.cloudish.ai/builds/99:84bc08a6b8e0) was built by Cloudish from the reviewed source, and the app has no dependencies. However, the base image is a floating tag (node:22-alpine) with no digest, the source was uploaded as a local tarball instead of coming from a repository the owner controls, and the project belongs to an agent-minted key that no human has claimed yet. |
| Terms compliance | 15 | 4/10 | A business's marketing site is running on an unclaimed key that only has Cloudish's free 0.1-credit starter grant. That pays for about 9 hours of runtime, after which the site will return 402/503 errors. The agent listed topping it up as 'non-blocking', but the site can't stay up without it. I don't know of any Cloudish terms that forbid commercial use on paid credits. |
| Least privilege | 15 | 10/10 | There is one small container running as the non-root 'node' user, with no volume, no environment secrets and no extra services. Public 'open' access is what a public marketing site needs. |
| Transport security | 10 | 8/10 | The site is HTTPS-only: HTTP gets a 308 redirect, the Let's Encrypt certificate is valid, and the platform adds HSTS. The app doesn't set X-Content-Type-Options, frame protection or a CSP, even though server.js could easily add them. |
| Attack surface | 10 | 10/10 | The server only answers an explicit route table. Every probe for .env, .git, source files, the Dockerfile and traversal paths returned 404 or a 400 from the platform's ingress. There is no directory listing, stack trace or debug endpoint. |
| Application security | 5 | 8/10 | Input is checked for types, an email pattern, malformed JSON and a 64 KB body limit, and the client shows messages with textContent. The public contact endpoint has no rate limiting, though, and it logs submitters' email addresses. |
Findings
-
medium deployment Commercial site runs on an unclaimed free starter grant that runs out in about 9 hours
Evidence: process/deployment.json human_steps: 'The key only has the 0.1-credit free grant, which covers about 9 hours of container runtime. Once it runs out, the site returns 402/503'; process/context.json use=commercial, balance_usd=0.099345; deploy.md: creditsPerHour 0.010733
Fix: Before launch, claim the key into the business's Cloudish account and add credits or set up billing. Treat this as a blocking step, not an optional one.
-
low deployment Base image not pinned to a digest
Evidence: app/Dockerfile:1 'FROM node:22-alpine'
Fix: Pin the base image to a digest, e.g. node:22-alpine@sha256:..., and update it on purpose.
-
low deployment Deployed from an uploaded local tarball under an agent-owned account
Evidence: deploy.md: tar ... -czf /tmp/context.tar.gz . then POST /api/v1/projects -F context=@/tmp/context.tar.gz; the project owner is the agent-minted key 'adb-l5mz8-static' (id 80), which hasn't been claimed
Fix: Commit the source to a repository the owner controls, build from a tagged commit, and move the project under the owner's account.
-
low app Missing security headers
Evidence: curl -sI https://adb-l5mz8-static-web.on.do.cloudish.ai/ returned only content-type, content-length, cache-control and strict-transport-security (no X-Content-Type-Options, X-Frame-Options/frame-ancestors or Content-Security-Policy); app/server.js:97 sets no security headers
Fix: Have server.js add X-Content-Type-Options: nosniff, a CSP such as default-src 'self'; frame-ancestors 'none', and Referrer-Policy to every response.
-
low app No rate limiting on the public contact endpoint
Evidence: app/server.js:49-77 accepts any number of POST /api/contact requests; each request also wakes the scale-to-zero container and uses up credits
Fix: Add per-IP rate limiting (in-memory token bucket) and a honeypot field, or use platform rate limiting if Cloudish has it.
-
info app Submitter email addresses written to logs
Evidence: app/server.js:74 console.log(`contact submission from ${data.email.trim()}`)
Fix: Don't log personal data, or log a non-identifying event only.
-
info platform Fallback hostname now serves the ingress default certificate
Evidence: openssl s_client to 4fd1d94ca7d6e70db98084023443606a.on.do.cloudish.ai shows subject 'Kubernetes Ingress Controller Fake Certificate'; curl exits with code 60. deployment.json says this URL 'also works'
Fix: Use only the custom label URL and correct deployment.json. The platform should stop routing to, or properly retire, old hostnames.
-
info platform Platform HSTS has no includeSubDomains
Evidence: Response header 'strict-transport-security: max-age=31536000'
Fix: Acceptable on a shared platform subdomain. A custom domain could use a stronger policy.
-
info platform Ingress error page reveals nginx
Evidence: GET /%2e%2e/server.js -> 400 with body '<center>nginx</center>'
Fix: The platform could serve generic error pages. Nothing for the app to fix.
Cloud resources and cost
| Traffic | Per month | How it was priced |
|---|---|---|
| Light use Barely used: a few visits a week, about 2,000 requests a month. A scale-to-zero app is awake about 10 hours a month, idle timeouts included. | $0.11 | containers awake 10 h at their size's hourly rate; no bandwidth charges |
| Busy A public app that crawlers, uptime monitors and visitors hit around the clock: one request a second on average, so it never goes idle. | $7.84 | containers awake 730 h at their size's hourly rate; no bandwidth charges |
Resources the agent created
| Kind | Name | Plan | List price / month | Pricing |
|---|---|---|---|---|
| project | adb-l5mz8-static-web | shared | $7.84 | 0.1 CPU / 0.2 GB x 1 replica(s) running 730 h/month (live GET /api/v1/pricing); billed per second and scales to zero when idle, so idle apps cost less |
The agent estimated $7.84 a month.
Code
- Code
- 295 lines in 13 files (JavaScript 184, HTML 90, JSON 24, CSS 13, Docker 8, Markdown 5)
- Dependencies
- 0 npm, 0 Python
- Deploy config
Dockerfile.dockerignore
The agent's sessions
| Phase | Status | Time | Turns | Tokens | Cost | Shell commands |
|---|---|---|---|---|---|---|
| Build | completed | 1m 30s | 8 | 265.7K | $0.29 | 6 |
| Deploy | completed | 2m 17s | 13 | 675.9K | $0.32 | 12 |
The agent's deploy summary
The site is live at https://adb-l5mz8-static-web.on.do.cloudish.ai, and every requirement in TASK.md works through that URL.
What I set up:
- API key: I created a Cloudish API key named
adb-l5mz8-staticand saved it only in.env, which is gitignored and left out of the upload. - Project:
adb-l5mz8-static-web. Cloudish built the image on its own servers from the Dockerfile and runs it on the cheapest size (shared), with one copy of the app. It shuts down after 15 minutes without traffic and is billed per second while running. - Address: I gave it a readable subdomain with a Let's Encrypt HTTPS certificate. The first few requests failed with a certificate error while it was being issued, then it worked. The random URL Cloudish first assigned (
https://4fd1d94ca7d6e70db98084023443606a.on.do.cloudish.ai) also still works.
Tested through the public URL:
/,/aboutand/contactreturn 200 withtext/html, and each has the required title, viewport tag, navigation and content.- Unknown paths,
/about.htmland/contact/return 404. POST /api/contactreturns 200{"ok":true}for valid input, and 400 with an error message for a missing name, an invalid email or an empty message.- In headless Chrome at a phone-sized screen, submitting the form showed the confirmation message without reloading the page.
Cost: about $7.84/month at most, which is what it would cost to run around the clock. Because it shuts down when idle, a quiet marketing site should cost much less. The trade-off is a slower first request after an idle period.
Needs a person (not blocking): the key only has Cloudish's free 0.1 credits, which covers about 9 hours of running time. After that the site returns errors (402 or 503) until someone adds credits. To do that, a person needs to claim the key into their Cloudish account and top it up, using a one-time link created with the key (each link expires after 30 minutes). This is recorded under human_steps in deployment.json.
12 shell commands during deploy
- 6s
curl -s https://cloudish.ai/skill.md - 12s
cd /home/runner/work/_temp/adb-l5mz8-static - 17s
cd /home/runner/work/_temp/adb-l5mz8-static; set -a; . ./.env; set +a - 25s
cd /home/runner/work/_temp/adb-l5mz8-static; set -a; . ./.env; set +a - 1m 07s
cd /home/runner/work/_temp/adb-l5mz8-static; set -a; . ./.env; set +a - 1m 10s
cd /home/runner/work/_temp/adb-l5mz8-static; set -a; . ./.env; set +a - 1m 16s
U=https://adb-l5mz8-static-web.on.do.cloudish.ai - 1m 22s
curl -sv -o /dev/null https://adb-l5mz8-static-web.on.do.cloudish.ai/ 2>&1 | grep -iE 'subject|issuer|SSL|error' | head; - 1m 27s
for i in $(seq 30); do c=$(curl -s -o /dev/null -w "%{http_code}" https://adb-l5mz8-static-web.on.do.cloudish.ai/); [ "$c" = 200 ] && break; sleep 10; done; echo "$i $c" - 1m 43s
U=https://adb-l5mz8-static-web.on.do.cloudish.ai - 1m 53s
mkdir -p /tmp/pw && cd /tmp/pw && npm init -y >/dev/null && npm i -s playwright-core >/dev/null 2>&1; which google-chrome - 2m 09s
cd /home/runner/work/_temp/adb-l5mz8-static
Raw data
Report on GitHub Full record (JSON) Workflow run, with transcripts

