[cleo] claude config + new packages
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
---
|
||||
name: prod-db
|
||||
description: >-
|
||||
Look up data in Cleo production read replicas (Postgres, IAM auth, read-only).
|
||||
Use when the user asks to look up, find, or check something in prod / the prod
|
||||
DB, e.g. a user id, family id, guide id, or other record IDs for a given
|
||||
user. Returns IDs and metadata only, never PII/PHI.
|
||||
user-invocable: true
|
||||
argument-hint: '[question about prod data]'
|
||||
---
|
||||
# prod-db
|
||||
|
||||
Query prod read replicas through one script. Never call `psql` or `aws rds` directly.
|
||||
|
||||
```sh
|
||||
~/.config/claude/skills/prod-db/prod-query.sh <alias> "<select sql>"
|
||||
~/.config/claude/skills/prod-db/prod-query.sh <alias> --tables
|
||||
~/.config/claude/skills/prod-db/prod-query.sh <alias> --columns <table>
|
||||
```
|
||||
|
||||
The script connects to the `cluster-ro` endpoints as `reader`, forces read-only sessions with a 30s statement timeout, and caps output at 200 rows. If it reports no SSO session, ask the user to run `! aws sso login --profile prod-mono-reader`.
|
||||
|
||||
## PII/PHI rules (org policy: never put PII/PHI into Claude)
|
||||
|
||||
- SELECT only IDs, foreign keys, UUIDs, timestamps, statuses, flags and counts.
|
||||
- Never select names, emails, phones, DOB, addresses, notes, message bodies, health data, or Kratos `traits`. Never `SELECT *`.
|
||||
- A value the user gives you (an email) may appear only in a `WHERE` clause. Do not repeat it in your output; refer to "the user".
|
||||
- The script blocks `*`, non-SELECT statements, and result columns whose names look sensitive (it prints only the offending column names). Aliasing a sensitive column to dodge this is forbidden. If the user needs a PII field, tell them to look it up in Postico.
|
||||
- Report results as IDs only.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Pick the alias. Run `--tables` / `--columns <table>` (schema only) before writing SQL for an unfamiliar table.
|
||||
2. Write a narrow SELECT with explicit columns and a `LIMIT`.
|
||||
3. Summarize the IDs. Don't paste raw output beyond what's needed.
|
||||
|
||||
## Aliases
|
||||
|
||||
backend (backenddb), guides, kratos, userflow (userpreferencesdb), scheduling (schedulingdb), messaging (messagingdb), eligibility (eligibilitydb), entitlements (entitlementsdb), benefits (benefitsdb), assessments (assessmentsdb), engagement (engagementdb), tagging (taggingdb), supportplan (supportplandb), integrations, content (thegoodcontentdb), retool.
|
||||
|
||||
## Schema notes
|
||||
|
||||
**backend** (Django app `lucy_web`, `public` schema):
|
||||
- `auth_user(id, email)` is the user.
|
||||
- `lucy_web_family(id, employee_user_id, partner_user_id)` links a user to a family, as employee or partner.
|
||||
- `lucy_web_childfamilypackage(id, family_id, child_id, guide_id, guide_assigned_on)` links a family to its guide. The current guide is the row with the highest `id` for the family (see `Family.lucy_guide` in `services/cleo-backend/lucy_web/models/family.py`).
|
||||
- `lucy_web_lucyguide(id, uuid, user_id, is_active)` is the guide. `user_id` is the guide's own `auth_user` row. Do not select the guide's name or email.
|
||||
- Caregiver users are linked through the caregiver childfamilypackage models, which this query does not cover.
|
||||
|
||||
**guides**: tables store a numeric `guide_id` equal to `lucy_web_lucyguide.id`. It does not own the user-to-guide assignment.
|
||||
|
||||
**kratos**: `identities(id uuid, traits jsonb)`. The Kratos id is not known to equal backend `auth_user.id`. Bridge on email, inside `WHERE` only. Unverified for this deployment.
|
||||
|
||||
## Example: user id and guide id for an email
|
||||
|
||||
```sql
|
||||
select u.id as user_id, f.id as family_id,
|
||||
g.id as guide_id, g.uuid as guide_uuid, cfp.guide_assigned_on
|
||||
from auth_user u
|
||||
join lucy_web_family f on f.employee_user_id = u.id or f.partner_user_id = u.id
|
||||
join lucy_web_childfamilypackage cfp on cfp.family_id = f.id
|
||||
join lucy_web_lucyguide g on g.id = cfp.guide_id
|
||||
where lower(u.email) = lower('<email>')
|
||||
order by cfp.id desc
|
||||
limit 5
|
||||
```
|
||||
|
||||
Run it with alias `backend`.
|
||||
@@ -0,0 +1,16 @@
|
||||
backend prod-backend-aurora-cluster.cluster-ro-czxwiektwmk0.us-east-1.rds.amazonaws.com backenddb
|
||||
guides prod-guides.cluster-ro-czxwiektwmk0.us-east-1.rds.amazonaws.com guides
|
||||
kratos prod-orykratos.cluster-ro-czxwiektwmk0.us-east-1.rds.amazonaws.com kratos
|
||||
userflow prod-userflow.cluster-ro-czxwiektwmk0.us-east-1.rds.amazonaws.com userpreferencesdb
|
||||
scheduling prod-scheduling.cluster-ro-czxwiektwmk0.us-east-1.rds.amazonaws.com schedulingdb
|
||||
messaging prod-messaging.cluster-ro-czxwiektwmk0.us-east-1.rds.amazonaws.com messagingdb
|
||||
eligibility prod-eligibility.cluster-ro-czxwiektwmk0.us-east-1.rds.amazonaws.com eligibilitydb
|
||||
entitlements prod-entitlements.cluster-ro-czxwiektwmk0.us-east-1.rds.amazonaws.com entitlementsdb
|
||||
benefits prod-benefits.cluster-ro-czxwiektwmk0.us-east-1.rds.amazonaws.com benefitsdb
|
||||
assessments prod-assessments.cluster-ro-czxwiektwmk0.us-east-1.rds.amazonaws.com assessmentsdb
|
||||
engagement prod-engagement.cluster-ro-czxwiektwmk0.us-east-1.rds.amazonaws.com engagementdb
|
||||
tagging prod-tagging.cluster-ro-czxwiektwmk0.us-east-1.rds.amazonaws.com taggingdb
|
||||
supportplan prod-supportplan.cluster-ro-czxwiektwmk0.us-east-1.rds.amazonaws.com supportplandb
|
||||
integrations prod-platformintegrations.cluster-ro-czxwiektwmk0.us-east-1.rds.amazonaws.com integrations
|
||||
content prod-content.cluster-ro-czxwiektwmk0.us-east-1.rds.amazonaws.com thegoodcontentdb
|
||||
retool prod-retoolpub.cluster-ro-czxwiektwmk0.us-east-1.rds.amazonaws.com retool
|
||||
|
Executable
+86
@@ -0,0 +1,86 @@
|
||||
#!/usr/bin/env bash
|
||||
# Read-only lookups against prod read replicas (IAM auth). IDs/metadata only.
|
||||
#
|
||||
# prod-query.sh <alias> "<select sql>"
|
||||
# prod-query.sh <alias> --tables
|
||||
# prod-query.sh <alias> --columns <table>
|
||||
set -euo pipefail
|
||||
|
||||
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROFILE="prod-mono-reader"
|
||||
REGION="us-east-1"
|
||||
DB_USER="reader"
|
||||
MAX_ROWS=200
|
||||
DENY_COLS='email|name|phone|birth|dob|address|street|zip|postal|ssn|traits|note|body|content|message|text|diagnos|medical|password|token|secret'
|
||||
|
||||
die() { echo "prod-query: $*" >&2; exit 1; }
|
||||
|
||||
[[ $# -ge 2 ]] || die "usage: prod-query.sh <alias> \"<select sql>\" | --tables | --columns <table>"
|
||||
|
||||
alias_name="$1"; shift
|
||||
row="$(awk -F'\t' -v a="$alias_name" '$1 == a { print; exit }' "$DIR/databases.tsv")"
|
||||
[[ -n "$row" ]] || die "unknown alias '$alias_name'. Known: $(cut -f1 "$DIR/databases.tsv" | paste -sd' ' -)"
|
||||
host="$(cut -f2 <<<"$row")"
|
||||
database="$(cut -f3 <<<"$row")"
|
||||
|
||||
internal=0
|
||||
case "$1" in
|
||||
--tables)
|
||||
internal=1
|
||||
sql="select table_schema, table_name from information_schema.tables where table_schema not in ('pg_catalog','information_schema') order by 1, 2"
|
||||
;;
|
||||
--columns)
|
||||
[[ $# -eq 2 ]] || die "--columns needs a table name"
|
||||
[[ "$2" =~ ^[A-Za-z0-9_.]+$ ]] || die "invalid table name"
|
||||
internal=1
|
||||
sql="select column_name, data_type from information_schema.columns where table_name = '${2##*.}' order by ordinal_position"
|
||||
;;
|
||||
*)
|
||||
sql="$1"
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ $internal -eq 0 ]]; then
|
||||
sql="$(sed -E 's/[[:space:]]*;[[:space:]]*$//' <<<"$sql")"
|
||||
lc="$(tr '[:upper:]' '[:lower:]' <<<"$sql" | tr '\n' ' ')"
|
||||
|
||||
[[ "$lc" =~ ^[[:space:]]*(select|with)[[:space:]] ]] || die "only a single SELECT/WITH statement is allowed"
|
||||
[[ "$sql" != *";"* ]] || die "multiple statements are not allowed"
|
||||
[[ ! "$lc" =~ (^|[^a-z_])(insert|update|delete|drop|alter|create|grant|revoke|truncate|copy|call|do|set|reset|lock|vacuum|pg_read_file|pg_ls_dir|dblink|lo_import|lo_export)([^a-z_]|$) ]] \
|
||||
|| die "statement contains a disallowed keyword"
|
||||
|
||||
stripped="$(sed -E 's/count\([[:space:]]*\*[[:space:]]*\)//g' <<<"$lc")"
|
||||
[[ ! "$stripped" =~ (select|,)[[:space:]]*(distinct[[:space:]]+)?([a-z_0-9]+\.)?\* ]] \
|
||||
|| die "'*' in the select list is not allowed; list explicit ID/metadata columns (use --columns <table>)"
|
||||
fi
|
||||
|
||||
aws sts get-caller-identity --profile "$PROFILE" >/dev/null 2>&1 \
|
||||
|| die "no valid AWS SSO session. Run: ! aws sso login --profile $PROFILE"
|
||||
|
||||
PGPASSWORD="$(aws --profile "$PROFILE" rds generate-db-auth-token \
|
||||
--hostname "$host" --port 5432 --region "$REGION" --username "$DB_USER")"
|
||||
export PGPASSWORD
|
||||
export PGHOST="$host" PGPORT=5432 PGDATABASE="$database" PGUSER="$DB_USER" PGSSLMODE=require
|
||||
export PGOPTIONS="-c default_transaction_read_only=on -c statement_timeout=30000"
|
||||
|
||||
out="$(mktemp)"
|
||||
trap 'rm -f "$out"' EXIT
|
||||
psql -X --csv -v ON_ERROR_STOP=1 -c "$sql" >"$out"
|
||||
|
||||
header="$(head -n 1 "$out")"
|
||||
if [[ $internal -eq 0 ]]; then
|
||||
bad="$(tr ',' '\n' <<<"$header" | tr -d '"' | grep -Ei "$DENY_COLS" || true)"
|
||||
if [[ -n "$bad" ]]; then
|
||||
echo "prod-query: blocked, output columns look like PII/PHI (rows not shown):" >&2
|
||||
echo "$bad" >&2
|
||||
echo "Select IDs/metadata only; use these fields only inside WHERE." >&2
|
||||
exit 2
|
||||
fi
|
||||
fi
|
||||
|
||||
total=$(( $(wc -l <"$out") - 1 ))
|
||||
head -n $(( MAX_ROWS + 1 )) "$out" \
|
||||
| sed -E 's/[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/<redacted-email>/g'
|
||||
if (( total > MAX_ROWS )); then
|
||||
echo "prod-query: output truncated to $MAX_ROWS of $total rows" >&2
|
||||
fi
|
||||
Reference in New Issue
Block a user