Files
infrastructure/servers/gcp/sandbox/README.md
Will Anderson 40c6a31784 add sandbox.neurontechnologies.ai - internal-only experimentation env
Locked down to email_domain == neurontechnologies.ai via Cloudflare
Access. Dedicated SA with zero prod IAM. Isolated Supabase project
(not a schema in prod). No outbound email path. /api/share and /said
return 410 Gone in sandbox so the public-artifact surface stays
strictly in prod.

Stub deploy ready. Real soul build pipeline lands separately.
2026-05-02 12:53:16 -05:00

169 lines
7.9 KiB
Markdown

# sandbox.neurontechnologies.ai
Internal-only experimentation environment. The real soul (with safety stripped)
runs here when the build pipeline lands. Sandbox is intentionally separate
from stage so the stage path stays clean for normal QA work.
The terraform itself lives at `servers/gcp/sandbox.tf` (single file, mirrors
the existing flat-file layout under `servers/gcp/`). This directory holds
documentation only.
## What is sandbox
| Layer | Where |
|------------------|-------------------------------------------------------------|
| Domain | `sandbox.neurontechnologies.ai` |
| Cloud Run | `neuron-sandbox-us` (us-central1, single region) |
| Image registry | `us-central1-docker.pkg.dev/neuron-785695/neuron-sandbox/` |
| Service account | `neuron-sandbox@neuron-785695.iam.gserviceaccount.com` |
| Access control | Cloudflare Zero Trust Access, single rule |
| Identity rule | `email_domain == neurontechnologies.ai` |
| Supabase | Separate project (NOT a schema in prod's project) |
| Secret Manager | `sandbox-supabase-{url,anon-key,service-key}` |
## How to get in
1. Sign in with your `@neurontechnologies.ai` Google account at
`https://sandbox.neurontechnologies.ai/`. Cloudflare Access intercepts
the request, redirects you to the Google IdP, and only allows
`email_domain == neurontechnologies.ai` through.
2. After auth, you land on the running stub. With the stub deployed, the
root response is:
```json
{"env":"sandbox","status":"ready","soul":"not_loaded"}
```
3. There are no service tokens, no mTLS certs, no bypass paths. The single
policy is the entire access surface.
Verify the lockdown from a shell at any time:
```bash
curl -sI https://sandbox.neurontechnologies.ai/ | head -2
# HTTP/2 302
# location: https://neuralplatform.cloudflareaccess.com/cdn-cgi/access/login/sandbox.neurontechnologies.ai?...
```
If the first line is anything other than 302 to the cloudflareaccess domain,
something has broken. Treat it as a P0.
## What's NOT in sandbox
- **No outbound email.** `RESEND_API_KEY` is intentionally not mounted on
`neuron-sandbox-us`. Defense-in-depth against an experimental soul
accidentally emailing real users.
- **No public share endpoints.** `/api/share`, `/said`, and `/share/<id>`
return `410 Gone` with `{"error":"not_available_in_sandbox"}`. Public
artifact distribution stays in prod.
- **No prod data.** Sandbox uses an isolated Supabase project. Nothing in
the prod Supabase is reachable from the sandbox SA.
- **No prod IAM.** The `neuron-sandbox` SA has zero project-level role
bindings. Its only grants are `roles/secretmanager.secretAccessor` on the
three sandbox-specific secrets. Verify with:
```bash
# As neuron-sandbox SA, all of these MUST return PERMISSION_DENIED:
gcloud secrets versions access latest --secret=stripe-secret-key
gcloud secrets versions access latest --secret=anthropic-api-key
gcloud secrets versions access latest --secret=supabase-service-key
gcloud run services describe marketing-prod-us --region=us-central1
```
- **No prod LLM credentials.** Sandbox does NOT mount `anthropic-api-key`.
When the real soul lands, it will need its own sandbox-only LLM key
(cheaper key, lower rate limits) provisioned and granted to the sandbox SA.
## Provision the isolated Supabase project (one-time, manual)
There is no Supabase Management API token in `~/Secrets/api-keys/` or in
GCP Secret Manager today, so the project creation is a manual step.
1. Sign in to https://supabase.com/dashboard with the Neuron Technologies
org owner account.
2. New project. Name: `neuron-sandbox`. Region: `us-east-1` (closest to
us-central1 Cloud Run latency-wise; single-region experimental env, so
geo doesn't matter much).
3. After provisioning, capture from Project Settings -> API:
- Project URL: `https://<ref>.supabase.co`
- `anon` public key
- `service_role` secret key
4. Push them into Secret Manager (this overwrites the placeholder versions
that terraform created):
```bash
echo -n 'https://<ref>.supabase.co' | gcloud secrets versions add \
sandbox-supabase-url --data-file=- --project=neuron-785695
echo -n '<anon-key>' | gcloud secrets versions add \
sandbox-supabase-anon-key --data-file=- --project=neuron-785695
echo -n '<service-key>' | gcloud secrets versions add \
sandbox-supabase-service-key --data-file=- --project=neuron-785695
```
5. Force a new sandbox revision so it picks up the secrets:
```bash
gcloud run services update neuron-sandbox-us --region=us-central1 \
--project=neuron-785695 \
--image=us-central1-docker.pkg.dev/neuron-785695/neuron-sandbox/sandbox:initial
```
6. Do NOT seed any tables in this project yet. Real soul might want a
different schema than the demo soul. Schema lands when real soul lands.
When a Supabase Management API token does become available, automate this
by writing a small script in `scripts/` that calls
`POST https://api.supabase.com/v1/projects` and immediately writes the
returned values back to Secret Manager. Track that as a follow-up.
## Deploy a new build (manual, until CI is wired)
```bash
cd ~/Development/neuron-technologies/products/sandbox-stub # or real soul source
bash ./build.sh # cross-compile + push to AR
gcloud run services update neuron-sandbox-us \
--region=us-central1 \
--project=neuron-785695 \
--image=us-central1-docker.pkg.dev/neuron-785695/neuron-sandbox/sandbox:<tag>
```
The terraform `google_cloud_run_v2_service.neuron_sandbox` has
`lifecycle.ignore_changes = [template[0].containers[0].image]` so CI
rollouts won't be undone by the next `terraform apply`.
CI wiring lands as part of the real-soul-build follow-up backlog item
(see `mcp__neuron__planWork` for the tracked item).
## Disaster recovery
Sandbox is disposable. If state gets weird, nuke it and rebuild:
```bash
cd ~/Development/infrastructure/servers/gcp
terraform destroy \
-target=cloudflare_zero_trust_access_policy.sandbox_only_neurontech \
-target=cloudflare_zero_trust_access_application.sandbox \
-target=cloudflare_record.sandbox \
-target=google_cloud_run_domain_mapping.sandbox \
-target=google_cloud_run_v2_service_iam_member.neuron_sandbox_public \
-target=google_cloud_run_v2_service.neuron_sandbox \
-target=google_secret_manager_secret_iam_member.sandbox_url_accessor \
-target=google_secret_manager_secret_iam_member.sandbox_anon_accessor \
-target=google_secret_manager_secret_iam_member.sandbox_service_accessor \
-target=google_secret_manager_secret_version.sandbox_supabase_url_placeholder \
-target=google_secret_manager_secret_version.sandbox_supabase_anon_key_placeholder \
-target=google_secret_manager_secret_version.sandbox_supabase_service_key_placeholder \
-target=google_secret_manager_secret.sandbox_supabase_url \
-target=google_secret_manager_secret.sandbox_supabase_anon_key \
-target=google_secret_manager_secret.sandbox_supabase_service_key \
-target=google_service_account.neuron_sandbox \
-target=google_artifact_registry_repository.sandbox
# Then re-apply:
terraform apply
```
If the Supabase project was provisioned, also delete it from the Supabase
dashboard and re-create with a new name. The Secret Manager secrets carry
forward as placeholders; rewrite them with the new project's values.
## Files
| File | What |
|-------------------------------------------------------|-------------------------------------|
| `servers/gcp/sandbox.tf` | All sandbox terraform |
| `servers/gcp/sandbox/README.md` | This document |
| `~/Development/neuron-technologies/products/sandbox-stub/` | Stub source code (Go, 4 routes) |