Ir al contenido

One host

Esta página aún no está disponible en tu idioma.

cloudd speaks plain HTTP. It does not terminate TLS. Put it on loopback and let a front (Caddy or nginx) own the certificate, the public port, and the HTTP/2 or HTTP/3 side; the front proxies to 127.0.0.1:8080. That is the whole recommended shape for one host:

internet ──443/tls──▶ Caddy or nginx ──http──▶ cloudd 127.0.0.1:8080
│ spawns one `hyphae serve`
└─ per project on 127.0.0.1:20000+

Tenant engines never leave loopback either. Nothing in this repository listens for TLS; if a certificate is in play, the front holds it.

Caddy obtains and renews the certificate itself (ACME). Replace the host name; keep the upstream on loopback.

cloud.example.com {
encode zstd gzip
reverse_proxy 127.0.0.1:8080 {
# Function invokes may run up to CLOUDD_FN_TIMEOUT_MS; SSE streams
# stay open until CLOUDD_REALTIME_IDLE_SECS. Do not buffer them.
flush_interval -1
}
}

Caddy sets X-Forwarded-For and X-Forwarded-Proto on the way in. cloudd ignores both unless told to trust that front (see CLOUDD_TRUST_PROXY below), because anyone who can reach the daemon directly could otherwise forge them.

The Studio, when CLOUDD_STUDIO_DIR points at a built studio/dist, is served by cloudd itself at /studio/ on the same origin (/ redirects there), so the front needs no extra route and no CORS: https://cloud.example.com/studio/.

The Studio bundle is tied to one deployment by three optional build-time variables (make studio passes the environment through; the bundle must never read them at runtime):

Variable Default Effect
VITE_SIGNUP_MODE open invite shows the invite-code field on the login page up front. On any build the field appears as soon as a signup is refused with 403 authorization_denied, so an open bundle still works against an invite daemon.
VITE_DOCS_BASE /docs Prefix of every “Learn more” and “Docs” link: a path on this origin or an absolute URL (then links open in a new tab).
VITE_SITE_ORIGIN unset https:// origin of the public site; when set, the login page links to <origin>/cloud/access/ for requesting an invite.

The docs site has the same split: make docs builds for /docs/ on the daemon’s origin; make docs-public builds for https://hyphae.dev/cloud/docs/ (links to the Studio become absolute, light theme by default). Both come from one content tree; the post-build checker fails on any leftover /docs/ link.

The equivalent server block, with the certificate paths you manage (certbot or otherwise):

server {
listen 443 ssl http2;
server_name cloud.example.com;
ssl_certificate /etc/letsencrypt/live/cloud.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/cloud.example.com/privkey.pem;
client_max_body_size 40m; # ≥ CLOUDD_STORAGE_MAX_BYTES, ≥ CLOUDD_PROXY_BODY_LIMIT
location / {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffering off; # SSE and long function invokes
proxy_read_timeout 330s; # > CLOUDD_REALTIME_IDLE_SECS
}
}

Loopback is the default and needs no ceremony beyond the secrets decision:

Terminal window
CLOUDD_SECRETS_KEY=$(cat /etc/cloudd/secrets.key) \
CLOUDD_DATA_ROOT=/var/lib/cloudd \
CLOUDD_STUDIO_DIR=/opt/hyphae-cloud/studio/dist \
cloudd

cloudd refuses to start, naming the variable, when:

  • CLOUDD_BIND is a non-loopback address (0.0.0.0, ::, a LAN address) and either CLOUDD_PUBLIC=1 is missing or CLOUDD_SECRETS_KEY is empty. Both are required together. Loopback never needs CLOUDD_PUBLIC. Binding publicly is for the case where the TLS front runs on another machine or in another network namespace (a container); it is still not TLS, and it still expects a front.
  • CLOUDD_TRUST_PROXY=1 is set and CLOUDD_PROXY_NETS is empty. Trusting forwarded headers from everyone is the same as trusting no one.
  • neither CLOUDD_SECRETS_KEY nor CLOUDD_ALLOW_EPHEMERAL_SECRETS=1 is set (function secrets at rest need an explicit decision).
  • CLOUDD_SIGNUP_MODE=invite without CLOUDD_INVITE_CODES_FILE, or with a file that is missing, empty, or readable by others (mode bits 0o077). An open signup on a public host is a decision, not a default that happened by omission.

Generate a secrets key once and keep it outside the data root: openssl rand -hex 32. Rotating it orphans existing function secrets.

Run it under a supervisor (systemd Type=simple, KillSignal=SIGTERM, TimeoutStopSec=10). On SIGTERM the daemon stops accepting, ends every SSE stream, kills every in-flight function worker’s process group, puts the tenant engines to sleep, and exits within a 5 s bound even if a listener or a sleeping function is still attached (tested in crates/cloudd/tests/process.rs).

Both routes are unauthenticated, JSON, and never carry a project name, a key, or a secret. Point the supervisor at the first and the load balancer at the second.

Route Answer Meaning
GET /healthz 200 {"ok":true} The process is up and answering HTTP.
GET /readyz 200 {"ok":true,"store":true,"engine":"ok (3.0.0)","projects":N} The control store reads and, if any project exists, the pinned hyphae binary is found. engine is unchecked while there are no projects.
GET /readyz 503 {"ok":false,…,"engine":"unavailable"} Store unreadable or engine missing/wrong version. The reason is in the daemon log, not in the body.

The full table is in the README; these are the ones that matter in front of a terminator.

Variable Default Meaning
CLOUDD_BIND 127.0.0.1:8080 Listener. Loopback needs nothing else.
CLOUDD_STUDIO_DIR / CLOUDD_DOCS_DIR unset Built Studio (/studio/) and docs site (/docs/) to serve from the same origin. install.sh puts them under /usr/local/share/hyphae-cloud/.
CLOUDD_SIGNUP_MODE open invite makes POST /v0/signup require a code from CLOUDD_INVITE_CODES_FILE (one per line, 8–128 bytes, optional label; 0600, owned by the daemon user). Missing/unknown code → 403 authorization_denied, same body either way, before any account lookup. The daemon refuses to start in invite mode without a readable, private, non-empty file. Codes are hashed on load, compared over the whole list, never logged or receipted (a receipt records the entry’s index and label). Rotation: edit the file, systemctl restart hyphae-cloud.
CLOUDD_PUBLIC unset 1 permits a non-loopback CLOUDD_BIND, and only together with a non-empty CLOUDD_SECRETS_KEY. Does not add TLS.
CLOUDD_SECRETS_KEY unset 64 hex chars (or exactly 32 raw bytes) for function secrets at rest. Mandatory for a public bind; otherwise mandatory unless the next variable says so.
CLOUDD_ALLOW_EPHEMERAL_SECRETS unset 1 derives a development key from a salt file under the data root. Loopback and development only; ignored as a substitute for CLOUDD_SECRETS_KEY when binding publicly.
CLOUDD_TRUST_PROXY unset 1 honours X-Forwarded-For / X-Forwarded-Proto, but only on connections whose peer address is inside CLOUDD_PROXY_NETS. Off, the headers are ignored and the TCP peer is the client.
CLOUDD_PROXY_NETS empty Comma-separated CIDRs of the front(s), e.g. 127.0.0.1/32 or 10.0.0.0/24,fd00::/64. Required when CLOUDD_TRUST_PROXY=1.
CLOUDD_FN_TIMEOUT_MS 5000 Wall-clock cap per function invocation; the worker’s process group is killed and the caller gets 504. Set the front’s read timeout above it.
CLOUDD_STORAGE_MAX_BYTES 33554432 Cap on one stored object (32 MiB). Set the front’s body limit at or above it, and above CLOUDD_PROXY_BODY_LIMIT (8 MiB) for /v2.

The client address and scheme resolved under these rules appear in the access log (cloudd::access) as client= and scheme=, with forwarded=true when a trusted front supplied them. No route changes its answer based on them in this phase; they exist so the log is truthful.

The Cloud’s own authority — as opposed to tenant data, which is Native’s — is three things under CLOUDD_DATA_ROOT:

File What it is
control.json Accounts (password hashes), sessions, projects, keys (verifiers), snapshot catalog, blob catalog, function metadata, encrypted function secrets, receipts.
secrets.salt Only with CLOUDD_ALLOW_EPHEMERAL_SECRETS=1: the salt the dev secrets key is derived from. Without it, secrets in the store cannot be opened.
functions/<project>/<name>/index.js Function sources (CLOUDD_FUNCTIONS_ROOT). The Cloud-owned runners in functions/_runtime/ are regenerated at start and not backed up.

cloudd control-backup copies exactly those into a new directory and writes a manifest; cloudd control-restore verifies the manifest and puts them back. Both are subcommands of the daemon binary and read the same environment (CLOUDD_DATA_ROOT, CLOUDD_FUNCTIONS_ROOT). There is deliberately no HTTP route for either: restoring the authority file under a running daemon is not a thing this daemon does, and an operator token would be one more secret to rotate.

Terminal window
# Any time; the daemon may be running (the store is written by rename, so
# the copy is a whole file). Prints the manifest.
CLOUDD_DATA_ROOT=/var/lib/cloudd cloudd control-backup --out /var/backups/cloudd
# → /var/backups/cloudd/20260915T120301Z-8f3a1c2e/{MANIFEST.json,control.json,secrets.salt,functions/…}

MANIFEST.json carries id, created_at, cloudd_version, project_count, and sha256 per copied file. That is Cloud metadata, a checksum over the copied bytes; it is not a Native proof and says nothing about tenant data.

When to back up: before upgrading cloudd, before rotating CLOUDD_SECRETS_KEY, after creating projects or keys you cannot afford to re-issue, and on a schedule (a cron line calling the command above is the whole scheduler). Keep the backup directory off the host; the files are 0600, and control.json contains password hashes and key verifiers — treat it as sensitive even though no plaintext secret or key is in it.

What is not included:

  • Tenant volumes (projects/<id>/native). Those are Native’s; use POST /v0/projects/{id}/snapshots (hyphae backup create) per project. A control backup restored without its tenant directories yields projects whose engines have no data.
  • Blob bytes (CLOUDD_BLOB_ROOT, or the S3 bucket). The catalog is in the store; the payloads are not. Copy the blob root (or the bucket) separately, ideally right after the control backup so catalog and bytes agree.
  • cloudd.lock, logs, replaced data directories, and anything under snapshots/.

Restore is destructive (to the control files; never to tenants):

Terminal window
systemctl stop cloudd # tenants are put to sleep on the way down
CLOUDD_DATA_ROOT=/var/lib/cloudd cloudd control-restore --from /var/backups/cloudd/20260915T120301Z-8f3a1c2e
systemctl start cloudd

Restore refuses, touching nothing, when a daemon holds the data root’s cloudd.lock (so no tenant can be awake), when any file’s SHA-256 differs from the manifest, or when the manifest is missing. On success the current control.json, secrets.salt, and functions tree are moved aside with a .pre-restore-<stamp> suffix, not deleted; delete them yourself once the restored daemon looks right. Every project comes back asleep. Sessions in the backup are restored too, so a token issued after the backup stops working and one issued before it works again until it expires.

Keys and salts. Function secrets are encrypted under CLOUDD_SECRETS_KEY (or the salt-derived dev key). A backup taken under one key is only useful with that key: restoring onto a host with a different CLOUDD_SECRETS_KEY brings the secret names back but every invoke that needs one fails with a clear error until the owner sets the value again. Rotate the key by taking a backup, changing the variable, and re-setting each secret. There is no operator token to rotate: the only credentials in play are the host’s file permissions on the data root.

scripts/backup-all.sh ties the pieces above into one operator kit for a stopped data root: the control backup, one Native backup per tenant that has a data directory, a copy of the filesystem blob root, and a KIT.json saying what was and was not captured. It is a bundle of things taken one after another — not a Native snapshot of the host and not point-in-time across tenants. Each tenant’s NATIVE_BACKUP.json is Native’s statement about that tenant; KIT.json is Cloud metadata.

Terminal window
systemctl stop cloudd # SIGTERM: tenants are put to sleep by the daemon, never by the script
scripts/backup-all.sh \
--data-root /var/lib/cloudd \
--out /var/backups/cloudd/kit-$(date -u +%Y%m%dT%H%M%SZ) \
--cloudd-bin /usr/local/bin/cloudd \
--hyphae-bin ~/.cache/hyphae/3.0.0/bin/hyphae \
--blob-root /var/lib/cloudd/blobs
systemctl start cloudd

What it does, in order:

  1. Takes the same flock on <data_root>/cloudd.lock that the daemon holds. If a daemon is running it exits 1 and says to send SIGTERM; it never signals anything itself.
  2. cloudd control-backup --out OUT/control (reads CLOUDD_DATA_ROOT, set by the script from --data-root; CLOUDD_FUNCTIONS_ROOT is honoured if you export it).
  3. For every project id in the store (cloudd control-projects): if <data_root>/projects/<id>/native exists, hyphae backup create --data-dir <that> --out OUT/tenants/<id> with the pinned 3.0.0 binary (a binary reporting any other engine_version makes the script refuse). No engine → every such tenant is listed under tenants_skipped with skipped_engine and the kit is still written. Projects never woken have no directory and are listed with no_data_dir.
  4. cp -a of --blob-root into OUT/blobs/ — filesystem backend only. With CLOUDD_BLOB_BACKEND=s3 in the environment the kit records "blobs": "external" and copies nothing: the bucket is backed up with the bucket’s own tooling, out of band. No --blob-root"absent" (with a note on stderr if <data_root>/blobs exists).
  5. OUT/KIT.json: created_at, control_backup_id, engine, projects, tenants_backed_up, tenants_skipped, blobs (copied|external|absent), and sha256 of the kit-level manifests (the control MANIFEST.json and each tenant report).

OUT must be a fresh, empty directory per kit. Move the kit off the host; it contains everything the control backup contains (see above) plus tenant data.

Restore order — written, not automated. There is no kit restore script; a half-automated restore would be worse than none. Do it in this order, with cloudd stopped throughout:

  1. Control first. CLOUDD_DATA_ROOT=/var/lib/cloudd cloudd control-restore --from OUT/control/<control_backup_id> — refuses if a daemon holds the lock or a digest differs; moves the current control files aside.
  2. Each tenant. For every id in tenants_backed_up: hyphae restore --backup OUT/tenants/<id> --data-dir /var/lib/cloudd/projects/<id>/native.restored then move the current native aside and rename native.restored to native. (This is exactly what POST /v0/projects/{id}/restore does for a Cloud snapshot; the kit’s tenant backups are the same Native format, so they can also be restored through that route once the daemon is up, one project at a time.) Tenants in tenants_skipped keep whatever directory is on disk.
  3. Blobs last. cp -a OUT/blobs/. /var/lib/cloudd/blobs/ if "blobs": "copied"; for external, restore the bucket with the bucket’s tooling. The catalog came back in step 1, so any object whose bytes are missing will 404 on download until this step is done.
  4. Start cloudd. Every project comes back asleep; wake them from the Studio, hcloud, or the API.

deploy/aws/ is the first cloud deploy and it changes nothing about the process model: one EC2 instance, cloudd on 127.0.0.1:8080, one hyphae serve per project on loopback, the data root on one EBS volume. TLS is terminated at CloudFront (tls_at = cloudfront, the default): the distribution owns the certificate (ACM, us-east-1), speaks HTTP/2+3 to viewers, caches nothing, forwards every viewer header, and reaches Caddy on the instance’s :80 over plain HTTP — the security group admits only CloudFront’s origin-facing ranges there. tls_at = caddy is the no-CloudFront shape where Caddy holds the certificate and 80/443 are open. Terraform (validated with OpenTofu, never applied from this repository), a cloud-init user-data.sh, a systemd unit, a Caddyfile, and a release tarball from make dist. Full detail and every flag: deploy/aws/README.md. The click-path:

  1. Build the tarball on Debian 12’s glibc: make dist-bookworm (or make dist on such a box), then make dist-check.
  2. Provision: cd deploy/aws/terraform && cp terraform.tfvars.example terraform.tfvars (region, name, domain), terraform init && terraform apply. Creates the security group (80/443), the blobs and artifacts buckets, a role that may read/write only that blobs bucket, read only the tarball, and read only one SSM parameter, an encrypted gp3 volume with prevent_destroy, the Debian 12 instance (IMDSv2), and an Elastic IP. The volume is attached as /dev/sdf and mounted by user data at /var/lib/hyphae-cloud.
  3. Secrets key: aws ssm put-parameter --name "$(terraform output -raw ssm_parameter)" --type SecureString --value "$(openssl rand -hex 32)". User data reads it into /etc/hyphae-cloud/secrets.env (0600); it is never in the AMI, the tarball, or Terraform state. Without it cloudd refuses to start, by design.
  4. Tarball: aws s3 cp dist/hyphae-cloud-0.1.0-linux-x86_64.tar.gz s3://$(terraform output -raw artifacts_bucket)/. User data downloads it, checks SHA256SUMS, runs install.sh. (Or scp it and run sudo ./install.sh yourself.)
  5. DNS: an A record for domainterraform output -raw public_ip. Caddy obtains the certificate once the name resolves.
  6. Check: curl https://$DOMAIN/healthz{"ok":true}; curl https://$DOMAIN/readyz; https://$DOMAIN/studio/ in a browser.

With domain empty, Caddy serves plain HTTP on :80 so you can curl /healthzthat is a smoke test, not a real deploy: no TLS, and a Studio session token would cross the wire in clear. Set the domain before creating any account you care about.

On this host blobs go to S3 over HTTPS: CLOUDD_BLOB_BACKEND=s3, CLOUDD_S3_ENDPOINT=https://s3.<region>.amazonaws.com, CLOUDD_S3_BUCKET=<blobs bucket> (all written by user data from Terraform outputs), signed with SigV4. Credentials: cloudd tries CLOUDD_S3_ACCESS_KEY/SECRET_KEY, then AWS_* environment, then ~/.aws/credentials, then IMDSv2 (session token signed), and refuses to start with none. Here the first one is used — a dedicated IAM user’s key stored in SSM and written to secrets.env at boot — because user data blocks the instance metadata service for the cloudd uid so that project functions, which run as that uid with this host’s network, cannot obtain the instance role’s credentials. That deny stays; the static key is the consequence, not an oversight. Rotation and teardown: deploy/aws/README.md.

The single-host stack is deployed and reachable at https://infra-hyphae.run through CloudFront. What exists, all created by deploy/aws/terraform (tls_at = cloudfront, region us-east-1, name hyphae-cloud):

Thing Id / name
Public URL https://infra-hyphae.run (www. and plain http:// 301 to it)
CloudFront distribution E3CUZB5Q3YAHV6 (dic7uue0olw38.cloudfront.net), TLS 1.2+, HTTP/2+3, CachingDisabled, AllViewer, all methods
WAF web ACL (CLOUDFRONT scope, us-east-1) hyphae-cloud-edge — rate rule 500 req / 5 min / IP → block; AWSManagedRulesCommonRuleSet in count (its body-size and injection heuristics would reject legitimate function sources and object uploads; read its counts before ever switching to block)
ACM certificate (us-east-1) …:certificate/11f52c51-47d4-4d95-8592-3857311263fe, infra-hyphae.run + www, ISSUED via Route 53 validation
Route 53 zone Z04465543PQWN7YWAXS3X: A/AAAA aliases for apex and www → CloudFront, two ACM validation CNAMEs
EC2 instance i-0a33fa6493251c190 (Debian 12, t3.small, IMDSv2), Elastic IP 3.223.25.205 (eipalloc-0a2d9d41da440136f)
Security group sg-0cd43f1950900e655: ingress only tcp/80 from prefix list pl-3b927c52 (com.amazonaws.global.cloudfront.origin-facing); no 22, no 443, no 0.0.0.0/0
EBS data volume vol-0871cdb05e3e36872, 50 GiB gp3, encrypted, prevent_destroy, mounted at /var/lib/hyphae-cloud
S3 blobs hyphae-cloud-blobs-456859598621 (SSE-S3, public access blocked); artifacts hyphae-cloud-artifacts-456859598621 (tarball + SHA256SUMS)
IAM role/profile hyphae-cloud-instance (tarball read, SSM prefix read, SSM agent core, blobs policy); user hyphae-cloud-blobs (blobs policy only; one access key, stored in SSM)
SSM parameters /hyphae-cloud/hyphae-cloud/{secrets_key,s3_access_key,s3_secret_key} (SecureString)

Smoke results, all through CloudFront: /healthz 200, /readyz 200 (engine: ok (3.0.0)), /studio/ 200; hcloud signup → project prj_221db12762ce352b → wakeup (Native 3.0.0 running on the instance) → key → /v2/capabilities decoded → head live probe → sleep → snapshot snap_71a9c80623b286c4 (hyphae backup create on the instance); storage put of a 31-byte object → aws s3api head-object on hyphae-cloud-blobs-…/prj_221db12762ce352b/assets/docs/hello.txt (SSE AES256) → get round-trips identical; function deploy + invoke → 200; Studio browser smoke 16/16 (including SSE through CloudFront). On the instance (via SSM Run Command, no SSH): S3 credentials resolved source="static (CLOUDD_S3_ACCESS_KEY)", blob backend ready backend="s3", functions engine ready runtime="node", the nftables IMDS deny present and effective (curl as cloudd to 169.254.169.254 refused), secrets.env 0600, no secret string in the user-data log. From a non-CloudFront address, http://3.223.25.205/healthz times out (security group).

Two things went wrong and were fixed during the deploy, both now in the repo: the Debian 12 AMI ships no SSM agent (user data installs the official package) and its Node is 18 (user data installs Node 22 LTS from nodejs.org, checksum-verified; cloudd now refuses a node older than 20 at detection time instead of failing every invoke). The instance was replaced once to pick those up.

Operating it. Shell: aws ssm start-session --target i-0a33fa6493251c190. Logs: journalctl -u hyphae-cloud -u caddy. Upgrade: upload the new tarball, then on the box systemctl stop hyphae-cloud, unpack, ./install.sh, systemctl start hyphae-cloud. Backups: the kit (hyphae-cloud-backup-all) with the daemon stopped; blob bytes are in the bucket ("blobs": "external"). The Terraform state is local to deploy/aws/terraform/ on the operator machine and holds no secrets; keep it. The AWS CLI on that machine uses aws login session credentials, which the provider cannot read — run tofu as eval "$(aws configure export-credentials --format env)" && tofu ….

2026-09-15, signup closed. The daemon runs with CLOUDD_SIGNUP_MODE=invite (codes from SSM …/invite_codes, installed by user data as /etc/hyphae-cloud/invite-codes, cloudd:cloudd 0600). The Studio bundle on the box is built with VITE_SIGNUP_MODE=invite, so the login page asks for the code up front; hcloud signup --invite-code and the JS SDK’s auth.signup({ invite_code }) are the other two doors. Upgrades go through SSM Run Command (AWS-RunShellScript, sh — no pipefail) with the exact stop → install.sh → start sequence above; the browser smoke runs against production with SMOKE_INVITE_CODE read from SSM into the environment.

Teardown (not run; AWS_DESTROY=1 is the gate):

Terminal window
cd deploy/aws/terraform
eval "$(aws configure export-credentials --format env)"
aws iam list-access-keys --user-name hyphae-cloud-blobs --query 'AccessKeyMetadata[].AccessKeyId' --output text \
| xargs -n1 aws iam delete-access-key --user-name hyphae-cloud-blobs --access-key-id
aws s3 rm s3://hyphae-cloud-blobs-456859598621 --recursive
aws s3 rm s3://hyphae-cloud-artifacts-456859598621 --recursive
# The data volume is the only copy of every tenant: remove `prevent_destroy` in main.tf on purpose, then
tofu destroy
aws ssm delete-parameters --region us-east-1 --names /hyphae-cloud/hyphae-cloud/secrets_key /hyphae-cloud/hyphae-cloud/s3_access_key /hyphae-cloud/hyphae-cloud/s3_secret_key
  • Not TLS. cloudd has no certificate, no ACME client, no https:// listener. Remove the front and you have plain HTTP.
  • Not multi-host realtime. The SSE fanout is in-process; two daemons do not share subscribers or events. One host, one daemon — on AWS too: one instance, one volume, no multi-AZ, no failover.
  • Not an HA control store. The control store is one file under CLOUDD_DATA_ROOT written by one process; control-backup and the kit copy it, nothing replicates it. There is no failover, for the control plane or for tenants (CLAUDE.md, invariant 6).
  • Compose is unverified as Compose. deploy/compose/docker-compose.yml describes the same image and environment, now with CLOUDD_PUBLIC=1 (the bind is 0.0.0.0 inside the container namespace, published on host loopback) and a mandatory CLOUDD_SECRETS_KEY; it has never been executed with docker compose on the development machine, and the image was not rebuilt for this phase. Treat it as documentation until a run is recorded in the README.
  • No OAuth, no KMS, no sandbox. Function secrets are encrypted with the process key above, functions are trusted project code (docs/product/claims.md), and accounts are password sessions.