07ff9ba193d5864d8328e33e51b12dc5408f20a7
4
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
865b5b4317 |
Close the /admin/features side-door that needed a container shell to flip AI spend
Board minutes #62 Decision 2 (server-26#64), due 2026-08-31. CTO draft #60 finding 1 and CISO draft #61 finding 3 reached this independently. GET/PUT /admin/features accepted only a Firebase admin token, so the unattended runbook had no headless path and SSHed into the c2-core container to write config/ai_features with the admin SDK. Moving a platform-wide AI cost switch required a full container shell, and set_flags() wrote no audit entry either way, so a flag flip was unattributable however it happened. - New agent_service_key (AGENT_SERVICE_KEY), deliberately separate from the Discord bot's service_key. Sharing one key would collapse two principals into a single unattributable identity in every log line, and the bot has no business flipping AI flags regardless. - require_agent_key_or_admin accepts the agent key or a Firebase admin, and rejects the Discord key. The "key is configured" guard is load-bearing: compare_digest("", "") is a match, so a deployment that never set the key would otherwise accept an empty credential. - set_flags() writes an audit_log entry with before/after values and the actor, wrapped so an audit failure cannot lose the flag write or 500 the route. - Cascade helper sets the global doc and every system carrying an ai_flags override in one call. A global False already beats everything, but a system False beats a global True, so turning AI *on* could half-apply and leave a radio system hot after shutoff. It scans for the override rather than hardcoding the two known system IDs, so a new system cannot silently defeat it. - cascade defaults to False. PUT /systems/{id}/ai-flags and the AiFlagsPanel toggle mean a per-system override is deliberate operator intent; cascading by default would erase it on any unrelated global flip. The runbook opts in. Issue items 5 and 6 (retiring the SSH path from drb-worksession.md) are NOT done here and the runbook is untouched. The credential does not exist in production yet, so the SSH path is still the only one that works; retiring it now would break the next unattended run. Owner activation is recorded on #64. Tests 273 -> 289. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
a2cd2c57ca |
Serve call audio through c2-core instead of GCS signed URLs
upload_audio() could only sign a URL when GCP_CREDENTIALS_PATH pointed at a service-account key file. The deployed VM runs on Application Default Credentials with no key file, so every upload silently took the fallback branch and returned a bare gs:// URI. That broke two things at once: * Browsers cannot fetch a gs:// URI, so no recording was ever playable. * _public_url_to_gcs_uri() only matched https://storage.googleapis.com/ and returned None for it, so `if gcs_uri:` in the upload path was always false and transcription never ran. Nothing was logged, which is why this looked like an OpenAI credits problem rather than a storage one. The fallback also interpolated the client-supplied filename instead of the call_id-derived safe name, so the URI did not even name the object written. Calls now store only the canonical gs:// location. A short-lived playback link is minted per read as an HMAC over (call_id, expiry) keyed by SERVICE_KEY, and audio is served from the private bucket by the new /media route. An <audio src> cannot carry an Authorization header, so the link has to be the credential; that router is therefore public with the check done inline, as enrollment.py already does. Signing GCS URLs from the VM would have needed a serviceAccountTokenCreator grant on its own service account — this avoids the IAM change entirely and keeps the bucket private. gcs_uri_for_call() reconstructs the object name from call_id, so recordings made before this fix are reachable again without a data migration. Frontend rows come straight from Firestore via onSnapshot and never see a server-minted field, so CallRow fetches the link lazily on expand. Also removes the last long-lived (1 year) signed URL and the log line that printed it. |
||
|
|
ee633cbe46 |
Secure the broker for public exposure: TLS and per-node credentials
Edge nodes are deployed to arbitrary locations by arbitrary people, so the
broker has to be reachable from the internet and secured on its own merits
rather than by a VPN.
Three defects made that impossible. The broker only had a plaintext 1883
listener; every node shared one drb-node password; and the ACL pattern used
%c, the client-supplied client id, so any holder of that shared password
could set client_id to another node and take over its namespace. The comment
claiming this cryptographically prevented cross-node access was wrong and is
gone.
Authentication now uses mosquitto 2.x's built-in dynamic-security plugin on
the stock eclipse-mosquitto image. c2-core administers it over the control
topic, creating each node's client on approval with username=<node_id> and
password=<its node_keys api_key>, attached to a role whose ACL is nodes/%u/#
against the authenticated username. One credential, one revocation point.
An HTTP-callback plugin was implemented first and rejected: that project is
archived upstream, which is not an acceptable dependency on an
internet-facing broker.
Because dynsec state is a second source of truth alongside Firestore,
approve/reissue/delete now write to the broker first and surface a 502
rather than drifting, and c2-core reconciles every approved node into dynsec
on startup.
Adds node self-enrollment (POST /nodes/enroll, GET /nodes/{id}/credentials)
so a new node can obtain its key over HTTPS without an operator handling
secrets by hand. Enrolling an already-approved node_id is refused on the
fleet token alone — otherwise a leaked token plus a guessable id would let
an attacker steal a live node's key before the real node asked for it.
Pickup secrets are stored hashed and returned once, and the endpoint is rate
limited per source IP.
Infrastructure: an 8883 TLS listener fed by Caddy's certificate via a
systemd path unit, a firewall rule for it, and Caddy now 404s /internal/*
so the api vhost cannot proxy internal routes.
Also fixes CORS, which allowed https://app.<domain> while the frontend is
served on the bare domain — every call from the portal would have failed —
and widens the vault gitignore to a glob, since ansible-vault leaves
backup siblings that the exact-name rule left committable.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
33700448bf |
add Terraform + Ansible infrastructure for GCP deployment
Provisions e2-micro VM (us-east1-b, free tier) with static IP, SSH and web firewall rules, Docker + Caddy startup script, and IAM bindings for Firestore and GCS access via ADC. Imports existing drb-calls bucket and c2-server Firestore database into state. Ansible roles handle first-time setup (swap, docker group) and all subsequent deploys via rsync + docker compose, with secrets managed via Ansible Vault. DNS stays on AWS Route 53. |