Coding agents: the agent host¶
The coding-agent half of the demo is one EC2 instance playing a small engineering team. It runs the Claude apps gateway, its Postgres, a PDC agent, a host Alloy and one Claude Code container per developer, all under docker compose. agent-host/README.md covers the boot sequence and the files in detail; this page is the operator's view.
What runs where¶
| Container | Role |
|---|---|
gateway | Claude apps gateway on 443 inside the compose network, hostname touchline-gateway.internal, TLS from a private CA Terraform generates. Published on the host at 127.0.0.1:8443 only. |
postgres | the gateway's store: sessions, spend counters, caps, admin audit |
pdc-agent | outbound tunnel so Grafana can query Postgres (datasource touchline-gateway-spend) |
alloy | ships container logs and host metrics to Grafana Cloud |
spend-caps | one-shot: sets the caps from spend_caps through the gateway admin API |
dev-<name> | one per developer: Claude Code, the login bot, the traffic loop, local stdio MCP servers |
claude-install | one-shot: downloads the pinned Claude Code release (claude_code_version) and verifies its signature and checksum |
Nothing listens on the VPC. You reach the host with SSM Session Manager.
id=$(tofu -chdir=examples/complete output -raw agent_host_instance_id)
aws ssm start-session --target "$id"
cd /etc/agent-host && sudo docker compose ps
Sign-in¶
Claude Code has no non-interactive way to sign in to a gateway, so each developer container runs a login bot at start. It drives the real Claude Code terminal UI in tmux, checks the gateway certificate against the private CA, opens the device-verification page in headless Chromium, and signs in on the Cognito hosted UI with the developer's generated password. Credentials persist on a per-developer volume, so restarts keep the session.
Gateway sessions last 12 hours. Cognito issues no refresh token for this flow, so the traffic loop signs a developer in again when a session lapses.
The bot is tested against the pinned claude_code_version. If you change that version and the bot stops working, use the manual fallback and report the version.
Manual fallback: just login-developers¶
This opens an SSM session on the host and runs agent-host-login-links, which prints each developer's verification link and waits until the sign-ins complete. The links point at https://touchline-gateway.internal/device?..., and after the Cognito sign-in the browser is sent back to https://touchline-gateway.internal/oauth/callback on port 443. To open them from your laptop, run just login-tunnel in a second terminal. It:
- forwards local port 443 (not 8443) to the gateway over SSM (
aws ssm start-session ... --parameters portNumber=8443,localPortNumber=443); binding a port below 1024 may needsudoon Linux; - prints the SPKI pin of the gateway's leaf certificate (from the
gateway_cert_pemoutput) and opens a throwaway Chrome or Chromium profile (--user-data-dirin a new temporary directory) started with--ignore-certificate-errors-spki-list=<pin>and--host-resolver-rules="MAP touchline-gateway.internal 127.0.0.1". That profile accepts only the gateway's own key and resolves only the gateway's name to the tunnel. Nothing is added to/etc/hostsor to any system or browser trust store. If no Chrome or Chromium is found, the recipe prints the full command to start one yourself; - closes the browser and deletes the profile when you stop the tunnel with Ctrl-C.
Do not add the demo's private CA (gateway_ca_pem) to your system or browser trust store: the Terraform provider cannot name-constrain it, so a trusted copy could vouch for any hostname, and its private key sits in Terraform state. If you did trust it on an earlier version of these instructions, remove it (macOS: Keychain Access, delete the "touchline demo private CA" certificate; Linux: remove the file you added under /usr/local/share/ca-certificates/ and run update-ca-certificates), and remove any touchline-gateway.internal line from /etc/hosts.
Then open each link in that browser and sign in as the developer named in it. Usernames are in tofu output developers, passwords in tofu output -json developer_passwords.
You can also sign one developer in from inside the host: sudo docker compose exec dev-alex-morgan dev-login --force.
Reaching the gateway: just gateway-tunnel¶
Then save the CA for curl (a file you pass to --cacert, never a trust store entry): tofu -chdir=examples/complete output -raw gateway_ca_pem > ca.pem, and:
curl --cacert ca.pem --resolve touchline-gateway.internal:8443:127.0.0.1 \
https://touchline-gateway.internal:8443/readyz
curl --cacert ca.pem --resolve touchline-gateway.internal:8443:127.0.0.1 \
-H "x-api-key: $(tofu -chdir=examples/complete output -raw gateway_admin_read_key)" \
https://touchline-gateway.internal:8443/v1/organizations/spend_limits/effective
The read key shows caps and spend. The write key stays on the host (/etc/agent-host/secrets/gateway/gateway_admin_write_key).
Developers and teams¶
Defaults: five developers in three teams.
| Developer | Team |
|---|---|
| alex.morgan | newsroom |
| priya.shah | newsroom |
| sam.okafor | trading |
| jordan.lee | trading |
| casey.nguyen | platform |
Each team is a Cognito group, a gateway policy (managed settings by group) and a set of Bedrock application inference profiles, one per model. Developer telemetry carries the team as user.groups, and the gateway also stamps team.name in the resource attributes.
To change them, set the module inputs and apply:
teams = ["newsroom", "trading", "platform", "data"]
developers = [
{ name = "alex.morgan", team = "newsroom" },
{ name = "robin.patel", team = "data" },
# ...
]
nameis a stable key (letters, digits, dots). Email is<name>@<developer_email_domain>.- Every developer's team must be in
teams(a precondition checks this). - Keep
newsroom,tradingandplatforminteams. The in-app agents are assigned to those three teams and call their inference profiles. - The trading team is limited to Haiku at the gateway through
team_model_allowlist(default{ trading = ["haiku"] }). A team left out ofteam_model_allowlistgets everygateway_modelsentry. - The developer list, teams, spend caps and
team_model_allowlistlive in the agent-host secret'sconfigkey, not in the host's user data. An apply that changes them updates the secret; the host re-reads it within 5 minutes and recreates only the containers whose settings changed, with no host replacement and no interruption to the Postgres spend ledger. The login bot signs in any developer whose container was recreated.
Spend caps¶
The gateway enforces caps in USD per period (daily, weekly, monthly), per organization, per group (team) and per developer. The organization cap is a per-seat default, and the most restrictive cap that applies wins. A developer over a cap gets HTTP 429 with the message "touchline demo spend cap reached".
Defaults:
spend_caps = {
organization = { daily = 20 } # every developer
groups = { trading = { daily = 5 } } # trading developers
users = { "casey.nguyen" = { daily = 2 } }
}
Omitting a key keeps its default; set it to {} to remove that level. The spend-caps container applies them at every boot. Spend and caps appear on the gateway dashboard's Spend store tab and the Claude Code dashboard's Gateway spend tab, and the gateway spend alert fires at 5% of the organization daily cap.
Gateway spend is the gateway's own estimate from token counts, a circuit breaker rather than a Bedrock invoice. Each unattended session also has a hard --max-budget-usd 0.30.
Traffic¶
With traffic_enabled = true, each developer runs a scripted session every developer_session_interval_minutes (20 by default, jittered): usually a prompt written for their team, sometimes a general coding task, and about one in five a PII probe that the preflight deny guard should block. Prompts are in agent-host/prompts. Models are weighted towards Haiku within the team's allowed models. Each session writes one JSON result line to the container log (event=dev_session).
Run one session by hand:
sudo docker compose exec dev-priya-shah dev-session # random prompt
sudo docker compose exec dev-priya-shah dev-session /opt/agent-host/prompts/21-best-price-table.txt
What the gateway pushes¶
Per team, through managed settings: Claude Code's OpenTelemetry exporters (the gateway forwards client telemetry to the Grafana Cloud OTLP gateway with telemetry.forward_to), content logging switches (from content_capture), resource attributes scoping everything to service.namespace=touchline, the Agent Observability plugin with its tags and its agent name (claude-code/touchline, which the guards match on), MCP servers, and the model list. The developer containers carry only the three managed settings needed to find and sign in to the gateway.