Playground
The playground runs every Halos component on Docker with a seeded policy repo: the console and self-service portal behind an OIDC login, halo-proxy, halo-shadow, mock model upstreams and the evidence plane (collector, ClickHouse, Grafana). Nothing talks to a real model provider or cloud account. It is dev only: the IdP signs in anyone, and every key and token is generated at start and deleted on teardown.
Start it
Section titled “Start it”With halo installed (Docker and git on the machine):
halo quickstart # clones a Halos checkout into ~/.cache/halos/src if needed, then runs make demohalo quickstart down # later: stop it and delete its volumesIn the browser: . The dev container builds
bin/ and puts it on PATH; run make demo in its terminal and open the forwarded Halos console port (UNVERIFIED: CI builds the dev container with the devcontainer CLI, but make demo has not been run in a real Codespace).
From a checkout (Docker with Compose v2.20+, Go to build bin/):
make demo # make demo-down removes itA one-shot seed container copies examples/acme-corp plus a small overlay (deploy/compose/demo/policy: mock upstreams, the shadow experiment running at 100%; halos.yaml is patched to the mock IdP issuer), generates throwaway keys, publishes a signed release per ring to a local registry:2 and compiles policy.json. When everything is healthy it prints:
| What | URL |
|---|---|
| Console + portal | http://localhost:18080 (Sign in, pick a demo user) |
| Mock IdP (OIDC) | http://localhost:18081/.well-known/openid-configuration |
| halo-proxy (model API) | http://localhost:18088/v1/messages |
| halo-shadow metrics | http://localhost:18090/metrics |
| Grafana | http://localhost:13000 (admin / halo-dev-only) |
| Demo user | Groups | Lands in |
|---|---|---|
alice@acme.com |
ai-platform |
ring0-harness-team, admin |
bob@acme.com |
eng |
ring2-early (claude-cli A/B, sonnet shadow) |
mallory@acme.com |
eng |
ring1-canary (opus-5-5 canary) |
dave@acme.com |
eng |
ring3-ga |
The seeded repo is examples/acme-corp, a full-mode repo with every document written out so each feature has something to show. Your own repo would usually start as one file: halo init writes a simple-mode halos.yaml, and halo explain --policy-dir examples/simple shows what the examples/simple file expands to.
The seeded repo has three running experiments, the opus-5-5-upgrade rollout active at step canary-5, and three toggles, including sonnet-next-route (ring0’s sonnet goes to the next model).
Try this
Section titled “Try this”-
Sign in. Open the console, click Sign in and choose
alice@acme.com. The mock IdP runs a real authorization-code + PKCE flow, so the console sees exactly what it would from Okta or Entra. The same flow with curl:GET /auth/login -> 302 http://localhost:18081/authorize?...&code_challenge_method=S256GET /authorize&user=alice -> 302 http://localhost:18080/auth/callback?code=...&state=...GET /auth/callback -> 302 http://localhost:18080/GET /api/v1/me alice@acme.com ['ai-platform'] admin=True ring=ring0-harness-teamGET /api/v1/experiments claude-cli-2.1.3xx-ab running, opus-5-5-canary running, sonnet-next-shadow runningGET /api/v1/rollouts opus-5-5-upgrade active canary-5 (plus two drafts) -
Call the model API as three users. Each token comes from the mock IdP; a spoofed
x-halo-ring: ring3-gaheader is ignored because the cohort comes from the verified token.Terminal window TOK=$(curl -s 'localhost:18081/token?user=bob@acme.com&groups=eng')curl -s localhost:18088/v1/messages -H "Authorization: Bearer $TOK" \-H 'content-type: application/json' -H 'anthropic-version: 2023-06-01' \-H 'x-halo-ring: ring3-ga' \-d '{"model":"sonnet","max_tokens":5,"messages":[{"role":"user","content":"hi"}]}'alice sonnet: served-by=anthropic model=claude-sonnet-5-5 x-halo-ring:ring0-harness-teambob sonnet: served-by=orch model=us.anthropic.claude-sonnet-4-5-v1:0 x-halo-experiment:claude-cli-2.1.3xx-ab x-halo-ring:ring2-early x-halo-variant:controlmallory opus: served-by=orch model=us.anthropic.claude-opus-4-1-v1:0 x-halo-experiment:opus-5-5-canary x-halo-ring:ring1-canary x-halo-variant:controlno token: {"error":{"type":"unauthorized","message":"invalid or missing credentials"}}Alice’s
sonnetgoes to the next model because thesonnet-next-routetoggle targets ring0. -
Kill the toggle. On the console’s Toggles page (as alice), kill
sonnet-next-route(orhalo toggle killwith her session cookie). halo-proxy polls the signed kill list every 2s here, so alice’s next request falls back to the default route with no release and no policy PR:POST /api/v1/toggles/sonnet-next-route/kill -> 200alice sonnet: served-by=orch model=us.anthropic.claude-sonnet-4-5-v1:0POST /api/v1/toggles/sonnet-next-route/unkill -> 200alice sonnet: served-by=anthropic model=claude-sonnet-5-5 -
Watch the shadow. Only requests that carry a session id are mirrored, and only first turns unless the experiment sets
turns: all(Claude Code sendsx-claude-code-session-id, Codexsession_id), so add that header to Bob’s call. The mirror goes to the candidate route; users only ever see the primary reply.Terminal window curl -s localhost:18088/v1/messages -H "Authorization: Bearer $TOK" -H 'x-claude-code-session-id: demo-1' \-H 'content-type: application/json' -H 'anthropic-version: 2023-06-01' \-d '{"model":"sonnet","max_tokens":5,"messages":[{"role":"user","content":"hi"}]}'$ curl -s localhost:18090/metricshalo_shadow_mirrored_total 1halo_shadow_spend_usd 0.001092halo_shadow_budget_usd 5 -
See the evidence. halo-proxy exports
halo.gateway.*metrics to the collector every 5s. Grafana’s Halos: fleet and experiments dashboard reads them from ClickHouse. -
Manage a feature toggle. Open Toggles in the console, pick
github-mcp, and try Who gets it? with a user and groups. Kill needs a reason and reacheshalo-proxyat its next poll (every 2 s in the playground). Propose change (for example, ramp the rule to 25%) validates the edit and opens a “PR”. In the playground that PR is only a local branch: the stack has no GitHub, sohalo-serverpusheshalos/toggle-...to a bare repo on thepolicy-repovolume and a stubghprints where it landed. Nothing merges, and the served policy does not change. Inspect it withdocker run --rm -v halos-demo_policy-repo:/repo --entrypoint git halos-demo/halo-server -C /repo/remote.git branch.
Tear down
Section titled “Tear down”make demo-down # stops every container and deletes the volumes (keys, releases, data)Troubleshooting
Section titled “Troubleshooting”- A port is taken. Every host port binds to
127.0.0.1and is overridable:HALO_DEMO_CONSOLE_PORT,HALO_DEMO_IDP_PORT,HALO_DEMO_PROXY_PORT,HALO_DEMO_SHADOW_METRICS_PORT, and for the evidence planeHALO_OBS_GRAFANA_PORT,HALO_OBS_CH_HTTP_PORT,HALO_OBS_OTLP_*_PORT. Pass the same values tomake demo-down. - A service never got healthy. The script names it;
docker compose -f deploy/compose/docker-compose.demo.yml logs <service>shows why.seedfailing usually means the registry was not reachable. - Login loops back to the IdP. The console’s redirect URI and the IdP’s browser URL are derived from the ports above (
HALO_DEMO_CONSOLE_URL,HALO_DEMO_IDP_URLoverride them). In a Codespace the script uses the forwardedhttps://<codespace>-<port>.app.github.devURLs. - Start over.
make demo-down && make demoreseeds from scratch.