fix: scope GCP Leo database runtime
This commit is contained in:
parent
794635d17f
commit
ce90a79aac
12 changed files with 1867 additions and 138 deletions
361
deploy/sync-gcp-leoclean-runtime.sh
Executable file
361
deploy/sync-gcp-leoclean-runtime.sh
Executable file
|
|
@ -0,0 +1,361 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Deploy the source-controlled Cloud SQL bridge and non-secret service settings
|
||||||
|
# to the GCP parallel leoclean runtime. This does not create database users,
|
||||||
|
# read secret values, send Telegram messages, or promote Cloud SQL.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
PROJECT="${PROJECT:-teleo-501523}"
|
||||||
|
ZONE="${ZONE:-europe-west6-a}"
|
||||||
|
INSTANCE="${INSTANCE:-teleo-prod-1}"
|
||||||
|
SERVICE="${SERVICE:-leoclean-gcp-prod-parallel.service}"
|
||||||
|
MERGED_REF="${MERGED_REF:-origin/main}"
|
||||||
|
REMOTE_PROFILE_BIN="${REMOTE_PROFILE_BIN:-/home/teleo/.hermes/profiles/leoclean/bin}"
|
||||||
|
REMOTE_DROPIN_DIR="${REMOTE_DROPIN_DIR:-/etc/systemd/system/$SERVICE.d}"
|
||||||
|
REMOTE_CLOUDSDK_CONFIG="${REMOTE_CLOUDSDK_CONFIG:-/run/leoclean-gcp-prod-parallel}"
|
||||||
|
EXPECTED_VM_SERVICE_ACCOUNT="${EXPECTED_VM_SERVICE_ACCOUNT:-sa-teleo-prod-vm@teleo-501523.iam.gserviceaccount.com}"
|
||||||
|
|
||||||
|
WRAPPER_REL="hermes-agent/leoclean-bin/teleo-kb"
|
||||||
|
TOOL_REL="hermes-agent/leoclean-bin/cloudsql_memory_tool.py"
|
||||||
|
DROPIN_REL="systemd/leoclean-gcp-prod-parallel-cloudsql.conf"
|
||||||
|
DROPIN_NAME="10-cloudsql-memory.conf"
|
||||||
|
|
||||||
|
DRY_RUN=false
|
||||||
|
RESTART=false
|
||||||
|
VERIFY_ONLY=false
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'USAGE'
|
||||||
|
Usage: deploy/sync-gcp-leoclean-runtime.sh [--dry-run] [--restart] [--verify-only]
|
||||||
|
|
||||||
|
The database role and scoped Secret Manager secret must exist before --restart.
|
||||||
|
This script deploys only the bridge files and non-secret systemd drop-in.
|
||||||
|
USAGE
|
||||||
|
}
|
||||||
|
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--dry-run) DRY_RUN=true ;;
|
||||||
|
--restart) RESTART=true ;;
|
||||||
|
--verify-only) VERIFY_ONLY=true ;;
|
||||||
|
--help|-h)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unknown arg: $arg" >&2
|
||||||
|
usage >&2
|
||||||
|
exit 64
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
for path in "$WRAPPER_REL" "$TOOL_REL" "$DROPIN_REL"; do
|
||||||
|
if [ ! -f "$REPO_ROOT/$path" ]; then
|
||||||
|
echo "Required source file is missing: $path" >&2
|
||||||
|
exit 66
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if ! grep -Fxq 'UnsetEnvironment=PGPASSWORD' "$REPO_ROOT/$DROPIN_REL"; then
|
||||||
|
echo "Runtime drop-in must explicitly unset inherited PGPASSWORD." >&2
|
||||||
|
exit 65
|
||||||
|
fi
|
||||||
|
if ! grep -Fxq "Environment=CLOUDSDK_CONFIG=$REMOTE_CLOUDSDK_CONFIG" "$REPO_ROOT/$DROPIN_REL"; then
|
||||||
|
echo "Runtime drop-in CLOUDSDK_CONFIG does not match $REMOTE_CLOUDSDK_CONFIG." >&2
|
||||||
|
exit 65
|
||||||
|
fi
|
||||||
|
|
||||||
|
SOURCE_REVISION="$(git -C "$REPO_ROOT" rev-parse HEAD)"
|
||||||
|
if ! git -C "$REPO_ROOT" diff --quiet HEAD -- "$WRAPPER_REL" "$TOOL_REL" "$DROPIN_REL"; then
|
||||||
|
echo "Refusing to deploy runtime files that do not match HEAD." >&2
|
||||||
|
exit 65
|
||||||
|
fi
|
||||||
|
if [ -n "$(git -C "$REPO_ROOT" ls-files --others --exclude-standard -- "$WRAPPER_REL" "$TOOL_REL" "$DROPIN_REL")" ]; then
|
||||||
|
echo "Refusing to deploy untracked runtime files." >&2
|
||||||
|
exit 65
|
||||||
|
fi
|
||||||
|
if ! git -C "$REPO_ROOT" rev-parse --verify "$MERGED_REF^{commit}" >/dev/null 2>&1; then
|
||||||
|
echo "Merged reference is unavailable: $MERGED_REF" >&2
|
||||||
|
exit 69
|
||||||
|
fi
|
||||||
|
if ! git -C "$REPO_ROOT" merge-base --is-ancestor "$SOURCE_REVISION" "$MERGED_REF"; then
|
||||||
|
echo "Refusing to deploy unmerged revision $SOURCE_REVISION (not contained in $MERGED_REF)." >&2
|
||||||
|
exit 65
|
||||||
|
fi
|
||||||
|
WRAPPER_SHA="$(shasum -a 256 "$REPO_ROOT/$WRAPPER_REL" | awk '{print $1}')"
|
||||||
|
TOOL_SHA="$(shasum -a 256 "$REPO_ROOT/$TOOL_REL" | awk '{print $1}')"
|
||||||
|
DROPIN_SHA="$(shasum -a 256 "$REPO_ROOT/$DROPIN_REL" | awk '{print $1}')"
|
||||||
|
|
||||||
|
GCP_SSH=(
|
||||||
|
gcloud compute ssh "$INSTANCE"
|
||||||
|
--project="$PROJECT"
|
||||||
|
--zone="$ZONE"
|
||||||
|
--tunnel-through-iap
|
||||||
|
)
|
||||||
|
|
||||||
|
remote_helpers=$(cat <<'REMOTE'
|
||||||
|
assert_service_running() {
|
||||||
|
local active_state sub_state main_pid unit_user process_user
|
||||||
|
active_state="$(systemctl show "$SERVICE" --property=ActiveState --value)"
|
||||||
|
sub_state="$(systemctl show "$SERVICE" --property=SubState --value)"
|
||||||
|
if [ "$active_state" != "active" ] || [ "$sub_state" != "running" ]; then
|
||||||
|
echo "Service is not active/running: ActiveState=$active_state SubState=$sub_state" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
main_pid="$(systemctl show "$SERVICE" --property=MainPID --value)"
|
||||||
|
unit_user="$(systemctl show "$SERVICE" --property=User --value)"
|
||||||
|
case "$main_pid" in
|
||||||
|
''|0|*[!0-9]*)
|
||||||
|
echo "Service has no live MainPID: $main_pid" >&2
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
if [ "$unit_user" != "teleo" ]; then
|
||||||
|
echo "Service unit user is not teleo: $unit_user" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
process_user="$(ps -o user= -p "$main_pid" | awk '{print $1}')"
|
||||||
|
if [ "$process_user" != "teleo" ]; then
|
||||||
|
echo "Service process user is not teleo: $process_user" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_runtime_environment() {
|
||||||
|
local environment runtime_dir_state
|
||||||
|
environment="$(systemctl show "$SERVICE" --property=Environment --value)"
|
||||||
|
case " $environment " in
|
||||||
|
*" PGPASSWORD="*)
|
||||||
|
echo "Service environment still contains PGPASSWORD." >&2
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
case " $environment " in
|
||||||
|
*" CLOUDSDK_CONFIG=$REMOTE_CLOUDSDK_CONFIG "*) ;;
|
||||||
|
*)
|
||||||
|
echo "Service does not use the isolated CLOUDSDK_CONFIG." >&2
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
runtime_dir_state="$(stat -c '%U:%G:%a' "$REMOTE_CLOUDSDK_CONFIG")"
|
||||||
|
if [ "$runtime_dir_state" != "teleo:teleo:700" ]; then
|
||||||
|
echo "Isolated CLOUDSDK_CONFIG has unexpected ownership or mode: $runtime_dir_state" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_attached_service_account() {
|
||||||
|
local sdk_config="$1" metadata_email
|
||||||
|
sudo install -d -o teleo -g teleo -m 0700 "$sdk_config"
|
||||||
|
metadata_email="$(
|
||||||
|
sudo -n -u teleo env -u PGPASSWORD \
|
||||||
|
HOME=/home/teleo \
|
||||||
|
CLOUDSDK_CONFIG="$sdk_config" \
|
||||||
|
curl --fail --silent --show-error \
|
||||||
|
--header 'Metadata-Flavor: Google' \
|
||||||
|
'http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/email'
|
||||||
|
)"
|
||||||
|
if [ "$metadata_email" != "$EXPECTED_VM_SERVICE_ACCOUNT" ]; then
|
||||||
|
echo "Attached VM service account mismatch: $metadata_email" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# An empty, phase-local Cloud SDK config forces this token request to use
|
||||||
|
# the VM's attached identity instead of an operator's cached credentials.
|
||||||
|
sudo -n -u teleo env -u PGPASSWORD \
|
||||||
|
HOME=/home/teleo \
|
||||||
|
CLOUDSDK_CONFIG="$sdk_config" \
|
||||||
|
CLOUDSDK_CORE_PROJECT="$PROJECT" \
|
||||||
|
gcloud --quiet auth print-access-token >/dev/null
|
||||||
|
echo "ATTACHED_VM_SERVICE_ACCOUNT=$metadata_email"
|
||||||
|
}
|
||||||
|
|
||||||
|
run_scoped_status() {
|
||||||
|
local phase="$1" tool_path="$2" sdk_config="$3"
|
||||||
|
assert_attached_service_account "$sdk_config"
|
||||||
|
echo "SCOPED_STATUS phase=$phase user=teleo database=teleo_canonical"
|
||||||
|
sudo -n -u teleo env -u PGPASSWORD \
|
||||||
|
HOME=/home/teleo \
|
||||||
|
CLOUDSDK_CONFIG="$sdk_config" \
|
||||||
|
CLOUDSDK_CORE_PROJECT="$PROJECT" \
|
||||||
|
TELEO_GCP_PROJECT=teleo-501523 \
|
||||||
|
TELEO_CLOUDSQL_HOST=10.61.0.3 \
|
||||||
|
TELEO_CLOUDSQL_PORT=5432 \
|
||||||
|
TELEO_CLOUDSQL_DB=teleo_canonical \
|
||||||
|
TELEO_CANONICAL_CLOUDSQL_DB=teleo_canonical \
|
||||||
|
TELEO_CLOUDSQL_USER=leoclean_kb_runtime \
|
||||||
|
TELEO_CLOUDSQL_PASSWORD_SECRET=gcp-teleo-pgvector-standby-leoclean-kb-runtime-password \
|
||||||
|
TELEO_CLOUDSQL_ENABLE_AUDIT_FALLBACK=0 \
|
||||||
|
python3 "$tool_path" status --format json --redacted
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_deployed_files() {
|
||||||
|
local wrapper tool dropin
|
||||||
|
wrapper="$REMOTE_PROFILE_BIN/teleo-kb"
|
||||||
|
tool="$REMOTE_PROFILE_BIN/cloudsql_memory_tool.py"
|
||||||
|
dropin="$REMOTE_DROPIN_DIR/$DROPIN_NAME"
|
||||||
|
test "$(sha256sum "$wrapper" | awk '{print $1}')" = "$WRAPPER_SHA"
|
||||||
|
test "$(sha256sum "$tool" | awk '{print $1}')" = "$TOOL_SHA"
|
||||||
|
test "$(sudo sha256sum "$dropin" | awk '{print $1}')" = "$DROPIN_SHA"
|
||||||
|
sudo grep -Fx 'UnsetEnvironment=PGPASSWORD' "$dropin"
|
||||||
|
sudo grep -E '^Environment=(CLOUDSDK_CONFIG|TELEO_(KB_MODE|GCP_PROJECT|CLOUDSQL_|CANONICAL_CLOUDSQL_DB))' "$dropin"
|
||||||
|
}
|
||||||
|
REMOTE
|
||||||
|
)
|
||||||
|
|
||||||
|
printf -v verify_context \
|
||||||
|
'PROJECT=%q\nSERVICE=%q\nEXPECTED_VM_SERVICE_ACCOUNT=%q\nREMOTE_PROFILE_BIN=%q\nREMOTE_DROPIN_DIR=%q\nREMOTE_CLOUDSDK_CONFIG=%q\nDROPIN_NAME=%q\nWRAPPER_SHA=%q\nTOOL_SHA=%q\nDROPIN_SHA=%q\n' \
|
||||||
|
"$PROJECT" "$SERVICE" "$EXPECTED_VM_SERVICE_ACCOUNT" "$REMOTE_PROFILE_BIN" \
|
||||||
|
"$REMOTE_DROPIN_DIR" "$REMOTE_CLOUDSDK_CONFIG" "$DROPIN_NAME" \
|
||||||
|
"$WRAPPER_SHA" "$TOOL_SHA" "$DROPIN_SHA"
|
||||||
|
|
||||||
|
verify_body=$(cat <<'REMOTE'
|
||||||
|
set -euo pipefail
|
||||||
|
verification_tmp="$(mktemp -d /tmp/teleo-gcp-runtime-verify.XXXXXX)"
|
||||||
|
cleanup_verification() { sudo rm -rf -- "$verification_tmp"; }
|
||||||
|
trap cleanup_verification EXIT
|
||||||
|
assert_deployed_files
|
||||||
|
assert_service_running
|
||||||
|
assert_runtime_environment
|
||||||
|
run_scoped_status verify "$REMOTE_PROFILE_BIN/cloudsql_memory_tool.py" "$verification_tmp/gcloud"
|
||||||
|
systemctl show "$SERVICE" -p ActiveState -p SubState -p NRestarts -p MainPID -p User --no-pager
|
||||||
|
REMOTE
|
||||||
|
)
|
||||||
|
verify_command="${verify_context}${remote_helpers}"$'\n'"${verify_body}"
|
||||||
|
|
||||||
|
if $DRY_RUN; then
|
||||||
|
echo "DRY RUN: project=$PROJECT zone=$ZONE instance=$INSTANCE service=$SERVICE restart=$RESTART"
|
||||||
|
echo "DRY RUN: revision=$SOURCE_REVISION wrapper_sha256=$WRAPPER_SHA tool_sha256=$TOOL_SHA dropin_sha256=$DROPIN_SHA"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if $VERIFY_ONLY; then
|
||||||
|
"${GCP_SSH[@]}" --command="$verify_command"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! $RESTART; then
|
||||||
|
echo "Refusing to install runtime files without --restart; use --dry-run or --verify-only for read-only work." >&2
|
||||||
|
exit 64
|
||||||
|
fi
|
||||||
|
|
||||||
|
REMOTE_TMP="/tmp/teleo-gcp-leoclean-runtime-$(date -u +%Y%m%dT%H%M%SZ)-$$"
|
||||||
|
printf -v sync_context \
|
||||||
|
'PROJECT=%q\nSERVICE=%q\nEXPECTED_VM_SERVICE_ACCOUNT=%q\nREMOTE_PROFILE_BIN=%q\nREMOTE_DROPIN_DIR=%q\nREMOTE_CLOUDSDK_CONFIG=%q\nDROPIN_NAME=%q\nWRAPPER_SHA=%q\nTOOL_SHA=%q\nDROPIN_SHA=%q\nREMOTE_TMP=%q\nWRAPPER_REL=%q\nTOOL_REL=%q\nDROPIN_REL=%q\nSOURCE_REVISION=%q\n' \
|
||||||
|
"$PROJECT" "$SERVICE" "$EXPECTED_VM_SERVICE_ACCOUNT" "$REMOTE_PROFILE_BIN" \
|
||||||
|
"$REMOTE_DROPIN_DIR" "$REMOTE_CLOUDSDK_CONFIG" "$DROPIN_NAME" \
|
||||||
|
"$WRAPPER_SHA" "$TOOL_SHA" "$DROPIN_SHA" "$REMOTE_TMP" \
|
||||||
|
"$WRAPPER_REL" "$TOOL_REL" "$DROPIN_REL" "$SOURCE_REVISION"
|
||||||
|
|
||||||
|
sync_body=$(cat <<'REMOTE'
|
||||||
|
set -euo pipefail
|
||||||
|
remote_tmp="$REMOTE_TMP"
|
||||||
|
profile_bin="$REMOTE_PROFILE_BIN"
|
||||||
|
dropin_dir="$REMOTE_DROPIN_DIR"
|
||||||
|
backup_dir="$remote_tmp/backup"
|
||||||
|
dropin_stage="$dropin_dir/.${DROPIN_NAME}.livingip-new.$$"
|
||||||
|
mutation_started=false
|
||||||
|
|
||||||
|
backup_path() {
|
||||||
|
local target="$1" name="$2"
|
||||||
|
if sudo test -e "$target"; then
|
||||||
|
sudo cp -a -- "$target" "$backup_dir/$name"
|
||||||
|
touch "$backup_dir/$name.present"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
restore_path() {
|
||||||
|
local target="$1" name="$2" restore_rc=0 restore_tmp
|
||||||
|
if [ -f "$backup_dir/$name.present" ]; then
|
||||||
|
restore_tmp="${target}.livingip-rollback.$$"
|
||||||
|
sudo rm -f -- "$restore_tmp" || restore_rc=1
|
||||||
|
if sudo cp -a -- "$backup_dir/$name" "$restore_tmp"; then
|
||||||
|
sudo mv -f -- "$restore_tmp" "$target" || restore_rc=1
|
||||||
|
else
|
||||||
|
restore_rc=1
|
||||||
|
fi
|
||||||
|
sudo rm -f -- "$restore_tmp" || restore_rc=1
|
||||||
|
else
|
||||||
|
sudo rm -f -- "$target" || restore_rc=1
|
||||||
|
fi
|
||||||
|
return "$restore_rc"
|
||||||
|
}
|
||||||
|
|
||||||
|
rollback_runtime() {
|
||||||
|
local rollback_rc=0
|
||||||
|
echo "Deployment failed; restoring the complete previous runtime set." >&2
|
||||||
|
sudo systemctl stop "$SERVICE" || rollback_rc=1
|
||||||
|
restore_path "$profile_bin/teleo-kb" wrapper || rollback_rc=1
|
||||||
|
restore_path "$profile_bin/cloudsql_memory_tool.py" tool || rollback_rc=1
|
||||||
|
restore_path "$dropin_dir/$DROPIN_NAME" dropin || rollback_rc=1
|
||||||
|
restore_path "$profile_bin/.livingip-runtime-revision" revision || rollback_rc=1
|
||||||
|
sudo systemctl daemon-reload || rollback_rc=1
|
||||||
|
if [ "$rollback_rc" -eq 0 ]; then
|
||||||
|
sudo systemctl start "$SERVICE" || rollback_rc=1
|
||||||
|
assert_service_running || rollback_rc=1
|
||||||
|
fi
|
||||||
|
return "$rollback_rc"
|
||||||
|
}
|
||||||
|
|
||||||
|
on_exit() {
|
||||||
|
local rc="$1" rollback_rc=0
|
||||||
|
trap - EXIT INT TERM
|
||||||
|
if [ "$rc" -ne 0 ] && [ "$mutation_started" = true ]; then
|
||||||
|
set +e
|
||||||
|
rollback_runtime
|
||||||
|
rollback_rc=$?
|
||||||
|
set -e
|
||||||
|
if [ "$rollback_rc" -ne 0 ]; then
|
||||||
|
echo "CRITICAL: previous runtime restoration did not return the service to active/running." >&2
|
||||||
|
rc=1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
sudo rm -f -- "$dropin_stage" || true
|
||||||
|
sudo rm -rf -- "$remote_tmp" || true
|
||||||
|
exit "$rc"
|
||||||
|
}
|
||||||
|
|
||||||
|
trap 'on_exit $?' EXIT
|
||||||
|
trap 'exit 130' INT
|
||||||
|
trap 'exit 143' TERM
|
||||||
|
|
||||||
|
sudo rm -rf -- "$remote_tmp"
|
||||||
|
mkdir -p "$backup_dir"
|
||||||
|
tar -C "$remote_tmp" -xf -
|
||||||
|
|
||||||
|
assert_service_running
|
||||||
|
run_scoped_status pre "$remote_tmp/$TOOL_REL" "$remote_tmp/gcloud-pre"
|
||||||
|
|
||||||
|
backup_path "$profile_bin/teleo-kb" wrapper
|
||||||
|
backup_path "$profile_bin/cloudsql_memory_tool.py" tool
|
||||||
|
backup_path "$dropin_dir/$DROPIN_NAME" dropin
|
||||||
|
backup_path "$profile_bin/.livingip-runtime-revision" revision
|
||||||
|
|
||||||
|
mutation_started=true
|
||||||
|
sudo systemctl stop "$SERVICE"
|
||||||
|
sudo install -d -o teleo -g teleo -m 0755 "$profile_bin"
|
||||||
|
sudo install -o teleo -g teleo -m 0755 "$remote_tmp/$WRAPPER_REL" "$profile_bin/teleo-kb"
|
||||||
|
sudo install -o teleo -g teleo -m 0755 "$remote_tmp/$TOOL_REL" "$profile_bin/cloudsql_memory_tool.py"
|
||||||
|
sudo install -d -o root -g root -m 0755 "$dropin_dir"
|
||||||
|
sudo install -o root -g root -m 0644 "$remote_tmp/$DROPIN_REL" "$dropin_stage"
|
||||||
|
sudo mv -f -- "$dropin_stage" "$dropin_dir/$DROPIN_NAME"
|
||||||
|
printf '%s\n' "$SOURCE_REVISION" | sudo tee "$profile_bin/.livingip-runtime-revision" >/dev/null
|
||||||
|
sudo chown teleo:teleo "$profile_bin/.livingip-runtime-revision"
|
||||||
|
sudo chmod 0644 "$profile_bin/.livingip-runtime-revision"
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
sudo systemctl start "$SERVICE"
|
||||||
|
|
||||||
|
assert_deployed_files
|
||||||
|
assert_service_running
|
||||||
|
assert_runtime_environment
|
||||||
|
run_scoped_status post "$profile_bin/cloudsql_memory_tool.py" "$remote_tmp/gcloud-post"
|
||||||
|
systemctl show "$SERVICE" -p ActiveState -p SubState -p NRestarts -p MainPID -p User --no-pager
|
||||||
|
mutation_started=false
|
||||||
|
REMOTE
|
||||||
|
)
|
||||||
|
sync_command="${sync_context}${remote_helpers}"$'\n'"${sync_body}"
|
||||||
|
|
||||||
|
COPYFILE_DISABLE=1 tar --format=ustar -C "$REPO_ROOT" -cf - \
|
||||||
|
"$WRAPPER_REL" "$TOOL_REL" "$DROPIN_REL" | "${GCP_SSH[@]}" --command="$sync_command"
|
||||||
157
docs/gcp-leoclean-runtime-reconciliation.md
Normal file
157
docs/gcp-leoclean-runtime-reconciliation.md
Normal file
|
|
@ -0,0 +1,157 @@
|
||||||
|
# GCP Leo Runtime Reconciliation
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This runbook reconciles PR `#145` with the non-production GCP parallel Leo
|
||||||
|
service and removes its dependency on the PostgreSQL administrator credential.
|
||||||
|
It does not promote GCP, change the Telegram destination, copy newer VPS data,
|
||||||
|
or make `teleo_canonical` production-authoritative.
|
||||||
|
|
||||||
|
Target surfaces:
|
||||||
|
|
||||||
|
- project: `teleo-501523`;
|
||||||
|
- VM: `teleo-prod-1` in `europe-west6-a`;
|
||||||
|
- service: `leoclean-gcp-prod-parallel.service`;
|
||||||
|
- Cloud SQL database: `teleo_canonical` on private address `10.61.0.3`;
|
||||||
|
- runtime database role: `leoclean_kb_runtime`;
|
||||||
|
- staging-function owner: `leoclean_kb_stage_owner` (`NOLOGIN`);
|
||||||
|
- runtime secret: `gcp-teleo-pgvector-standby-leoclean-kb-runtime-password`.
|
||||||
|
|
||||||
|
Observed before reconciliation on 2026-07-14: the service was
|
||||||
|
`active/running`, but `/usr/local/bin/teleo-kb` matched unmerged PR `#145`, the
|
||||||
|
effective systemd environment selected database user `postgres` and the
|
||||||
|
administrator password secret, and a live `teleo-kb status` call exceeded a
|
||||||
|
20-second bound. Secret Manager access and private PostgreSQL reachability both
|
||||||
|
passed independently, isolating the timeout to the old helper path rather than
|
||||||
|
IAM or networking.
|
||||||
|
|
||||||
|
## Required End State
|
||||||
|
|
||||||
|
1. The live wrapper and Cloud SQL helper hashes match a merged repository
|
||||||
|
commit.
|
||||||
|
2. `TELEO_KB_MODE=cloudsql`; missing tools or credentials fail closed.
|
||||||
|
3. Canonical zero-hit searches do not consult `teleo_restore` unless an
|
||||||
|
operator explicitly opts in.
|
||||||
|
4. Leo can read the named canonical tables and stage only a
|
||||||
|
`pending_review` proposal through `kb_stage.stage_leoclean_proposal(...)`.
|
||||||
|
5. Leo cannot directly insert, update, or delete `public.*` or
|
||||||
|
`kb_stage.kb_proposals`, forge the proposer identity, `SET ROLE` into a
|
||||||
|
broader principal, or execute reviewer/apply functions.
|
||||||
|
6. The VM runtime identity can access only the scoped password secret needed by
|
||||||
|
this path, not the PostgreSQL administrator password secret.
|
||||||
|
7. Deployment proves the attached VM service account from the metadata server,
|
||||||
|
uses an empty phase-local Cloud SDK configuration, and rolls the complete
|
||||||
|
prior runtime set back if installation or post-restart verification fails.
|
||||||
|
|
||||||
|
## Safe Order Of Operations
|
||||||
|
|
||||||
|
### 1. Read-only access and IAM audit
|
||||||
|
|
||||||
|
Confirm the operator can read the project, use IAP/OS Login for the VM, inspect
|
||||||
|
Cloud SQL metadata, and inspect Secret Manager IAM. Identify the VM service
|
||||||
|
account and determine whether its secret access is project-wide or
|
||||||
|
secret-specific. Do not remove a project-wide binding until every legitimately
|
||||||
|
required runtime secret has an equivalent secret-level binding.
|
||||||
|
|
||||||
|
### 2. Merge reviewed repository code
|
||||||
|
|
||||||
|
Run CI on the completed PR `#145` repair before deployment. The deployment
|
||||||
|
source must be the resulting merged commit, not a working tree or unmerged
|
||||||
|
branch.
|
||||||
|
|
||||||
|
### 3. Create the scoped secret and grant only the VM runtime identity
|
||||||
|
|
||||||
|
Create the scoped secret without printing its value. Add one randomly generated
|
||||||
|
version, then grant `roles/secretmanager.secretAccessor` on that secret to the
|
||||||
|
VM runtime service account. Do not put `PGPASSWORD` in systemd, a repository,
|
||||||
|
an artifact, or a command transcript.
|
||||||
|
|
||||||
|
### 4. Provision the scoped PostgreSQL role once
|
||||||
|
|
||||||
|
Run `ops/gcp_leoclean_runtime_role.sql` as the Cloud SQL administrator from the
|
||||||
|
private VM path. Supply the new runtime password through
|
||||||
|
`TELEO_LEOCLEAN_DB_PASSWORD`; the SQL file does not contain it. The migration:
|
||||||
|
|
||||||
|
- creates or rotates `leoclean_kb_runtime`;
|
||||||
|
- creates a dedicated `NOLOGIN` function owner with no role memberships;
|
||||||
|
- removes stale table, column, sequence, function, and role-membership grants;
|
||||||
|
- grants exact canonical reads;
|
||||||
|
- creates a locked security-definer staging function that hard-codes both
|
||||||
|
`pending_review` and canonical proposer `leo`;
|
||||||
|
- grants no direct table writes;
|
||||||
|
- removes all access to the legacy `teleo_restore` schema;
|
||||||
|
- aborts if either scoped role owns or can reach anything outside the explicit
|
||||||
|
allowlist.
|
||||||
|
|
||||||
|
PostgreSQL grants `TEMP` to `PUBLIC` by default. The migration removes any
|
||||||
|
direct scoped `TEMP` grant and reports the remaining effective privilege, but
|
||||||
|
does not revoke `TEMP` from `PUBLIC`: PostgreSQL has no per-role deny, so that
|
||||||
|
would be a database-wide behavior change requiring a separate inventory of
|
||||||
|
`kb_apply`, reviewer, and operator use. The staging function remains protected
|
||||||
|
by a `pg_catalog, pg_temp` search path, schema-qualified relations, a fixed
|
||||||
|
`session_user`, and a tested temporary-object shadowing denial.
|
||||||
|
|
||||||
|
The administrator password is used only for this bounded bootstrap. Retain no
|
||||||
|
password output.
|
||||||
|
|
||||||
|
### 5. Preflight and deploy the merged runtime
|
||||||
|
|
||||||
|
From the exact merged checkout, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
deploy/sync-gcp-leoclean-runtime.sh --dry-run
|
||||||
|
deploy/sync-gcp-leoclean-runtime.sh --restart
|
||||||
|
```
|
||||||
|
|
||||||
|
The deploy script runs a scoped, redacted database status preflight before it
|
||||||
|
installs files or restarts the service. It refuses dirty or unmerged runtime
|
||||||
|
files and refuses a live install without `--restart`. After preflight it stops
|
||||||
|
the parallel service, atomically replaces the existing Cloud SQL systemd
|
||||||
|
drop-in, installs the reviewed wrapper and helper, records the Git revision,
|
||||||
|
starts the service, and verifies all three file hashes. A failure after mutation
|
||||||
|
restores the prior wrapper, helper, drop-in, and revision as one set before the
|
||||||
|
old service is restarted.
|
||||||
|
|
||||||
|
### 6. Verify positive and negative behavior
|
||||||
|
|
||||||
|
Retain a redacted receipt containing:
|
||||||
|
|
||||||
|
- merged Git commit and deployed file hashes;
|
||||||
|
- service `ActiveState`, `SubState`, `MainPID`, and `NRestarts`;
|
||||||
|
- metadata-server identity and an access token obtained with an empty Cloud SDK
|
||||||
|
configuration;
|
||||||
|
- `current_database()` and `current_user` showing
|
||||||
|
`teleo_canonical|leoclean_kb_runtime`;
|
||||||
|
- a real canonical status/search receipt;
|
||||||
|
- a transaction-rolled-back call to `stage_leoclean_proposal(...)` that returns
|
||||||
|
`pending_review`;
|
||||||
|
- denied direct insert into `kb_stage.kb_proposals`;
|
||||||
|
- denied canonical `public.*` write;
|
||||||
|
- denied reviewer/apply security-definer function;
|
||||||
|
- denied forged proposer identity and denied `SET ROLE` escalation;
|
||||||
|
- zero Telegram messages and zero committed canary rows.
|
||||||
|
|
||||||
|
### 7. Remove administrator-secret access
|
||||||
|
|
||||||
|
Only after the scoped service passes post-restart verification, remove the VM
|
||||||
|
runtime identity's access path to
|
||||||
|
`gcp-teleo-pgvector-standby-postgres-password`. Verify from the VM runtime
|
||||||
|
identity that the scoped secret is readable and the administrator secret is
|
||||||
|
denied. Never delete the administrator secret merely to enforce runtime least
|
||||||
|
privilege; backup/restore operators may still require it under a separate
|
||||||
|
identity.
|
||||||
|
|
||||||
|
## Stop Conditions
|
||||||
|
|
||||||
|
Stop without cutover if any of these are true:
|
||||||
|
|
||||||
|
- the deployed revision is not merged;
|
||||||
|
- the scoped status preflight fails;
|
||||||
|
- the role has direct proposal-table insert or canonical write permission;
|
||||||
|
- any approval/apply function is executable by the runtime role;
|
||||||
|
- removing broad Secret Manager access would break another required secret;
|
||||||
|
- GCP canonical rows are still stale relative to the chosen authority.
|
||||||
|
|
||||||
|
The last condition does not invalidate this runtime security repair. It means
|
||||||
|
GCP remains staging and data reconciliation stays a separate, explicitly
|
||||||
|
authorized slice.
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
"database_first_merge_commit": "08371bf2a0461536c112e1235a7229b52ed270cc",
|
"database_first_merge_commit": "08371bf2a0461536c112e1235a7229b52ed270cc",
|
||||||
"database_first_pull_request": 144,
|
"database_first_pull_request": 144,
|
||||||
"fail_closed_wrapper_commit": "8451c51",
|
"fail_closed_wrapper_commit": "8451c51",
|
||||||
|
"fail_closed_wrapper_merge_state_at_capture": "unmerged PR #145; manually installed on the GCP VM",
|
||||||
"repository": "living-ip/teleo-infrastructure"
|
"repository": "living-ip/teleo-infrastructure"
|
||||||
},
|
},
|
||||||
"deployment": {
|
"deployment": {
|
||||||
|
|
@ -55,7 +56,7 @@
|
||||||
"integration_regression": {
|
"integration_regression": {
|
||||||
"detected_live": true,
|
"detected_live": true,
|
||||||
"symptom": "The old auto-mode wrapper used a full receipted status call as a 30-second health probe; the stronger read exceeded that bound and the wrapper fell through to a different local tool.",
|
"symptom": "The old auto-mode wrapper used a full receipted status call as a 30-second health probe; the stronger read exceeded that bound and the wrapper fell through to a different local tool.",
|
||||||
"repair": "Supported GCP knowledge commands now route directly to Cloud SQL and fail closed on errors instead of changing databases.",
|
"repair": "The manually installed PR #145 wrapper routes supported GCP knowledge commands directly to Cloud SQL and propagates failures after selection. Missing prerequisites could still fall through before selection at capture time.",
|
||||||
"regression_tests": 2,
|
"regression_tests": 2,
|
||||||
"full_repository_tests": {
|
"full_repository_tests": {
|
||||||
"passed": 1240,
|
"passed": 1240,
|
||||||
|
|
@ -113,8 +114,10 @@
|
||||||
"post_restart_model_turn_run": false
|
"post_restart_model_turn_run": false
|
||||||
},
|
},
|
||||||
"claim_ceiling": {
|
"claim_ceiling": {
|
||||||
"proven": "Merged database-first helper and skill deployment, fail-closed Cloud SQL routing, controlled GCP service restart, and post-restart live status/search receipts.",
|
"proven": "Merged database-first helper and skill deployment, manually installed unmerged wrapper routing repair, controlled GCP service restart, and post-restart live status/search receipts.",
|
||||||
"not_proven": [
|
"not_proven": [
|
||||||
|
"repository/runtime parity for the wrapper",
|
||||||
|
"least-privilege Cloud SQL runtime credential",
|
||||||
"post-restart model reasoning turn",
|
"post-restart model reasoning turn",
|
||||||
"Telegram-visible delivery",
|
"Telegram-visible delivery",
|
||||||
"canonical proposal apply",
|
"canonical proposal apply",
|
||||||
|
|
|
||||||
|
|
@ -94,10 +94,11 @@ remaining target databases. The disabled rollback database still has zero
|
||||||
connections. The live GCP Leo service remained PID `148735`, active/running,
|
connections. The live GCP Leo service remained PID `148735`, active/running,
|
||||||
with `NRestarts=0` throughout restore, model replay, and cleanup.
|
with `NRestarts=0` throughout restore, model replay, and cleanup.
|
||||||
|
|
||||||
### 5. The merged GCP path survived deployment and restart
|
### 5. The merged database-first path and an unmerged wrapper repair survived restart
|
||||||
|
|
||||||
PR `#144` merged the database-first helper and skill. Their exact reviewed
|
PR `#144` merged the database-first helper and skill. Their exact reviewed
|
||||||
hashes were installed on `teleo-prod-1`, then
|
hashes were installed on `teleo-prod-1`. A separate wrapper repair from the
|
||||||
|
then-unmerged PR `#145` was also installed manually, then
|
||||||
`leoclean-gcp-prod-parallel.service` was intentionally restarted. It returned
|
`leoclean-gcp-prod-parallel.service` was intentionally restarted. It returned
|
||||||
active/running with a new PID (`304036`) and the deployed hashes remained in
|
active/running with a new PID (`304036`) and the deployed hashes remained in
|
||||||
place.
|
place.
|
||||||
|
|
@ -105,9 +106,12 @@ place.
|
||||||
The first post-restart search caught one real integration regression: the old
|
The first post-restart search caught one real integration regression: the old
|
||||||
wrapper used a full `status` read as a 30-second health probe. Stronger receipts
|
wrapper used a full `status` read as a 30-second health probe. Stronger receipts
|
||||||
made that probe exceed its bound, after which `auto` mode could silently select
|
made that probe exceed its bound, after which `auto` mode could silently select
|
||||||
a different local tool. The wrapper now routes every supported GCP knowledge
|
a different local tool. The manually deployed wrapper routes every supported
|
||||||
command directly to Cloud SQL and fails closed on errors. Two regression tests
|
GCP knowledge command directly to Cloud SQL and propagates errors after backend
|
||||||
cover direct routing and no fallback.
|
selection. At the time of this receipt, missing host prerequisites could still
|
||||||
|
cause an earlier fallback, the runtime still used the administrator database
|
||||||
|
credential, and the live wrapper did not match merged `main`. PR `#145`
|
||||||
|
therefore remained a reconciliation task rather than a production-ready result.
|
||||||
|
|
||||||
A corrected live search then found the expected sandbagging claim plus both
|
A corrected live search then found the expected sandbagging claim plus both
|
||||||
expected source rows and hashes from persistent `teleo_canonical`. A subsequent
|
expected source rows and hashes from persistent `teleo_canonical`. A subsequent
|
||||||
|
|
@ -140,7 +144,7 @@ rows as if they were source-code branches.
|
||||||
| `gcp-db-first-parity-current.json` | `0173ee6707016e8412e6dd4326d61f71b6ef862bbc5b819079d62680676729f2` | Schema, row, role, extension, and performance parity passed |
|
| `gcp-db-first-parity-current.json` | `0173ee6707016e8412e6dd4326d61f71b6ef862bbc5b819079d62680676729f2` | Schema, row, role, extension, and performance parity passed |
|
||||||
| `gcp-db-first-blind-claim-current.json` | `8a7cc3c1814eb385e858f12ef7579cd4524f37886c03fc6d9c61624b6aff2a52` | Real GCP Hermes reasoning passed with source-bound read receipts |
|
| `gcp-db-first-blind-claim-current.json` | `8a7cc3c1814eb385e858f12ef7579cd4524f37886c03fc6d9c61624b6aff2a52` | Real GCP Hermes reasoning passed with source-bound read receipts |
|
||||||
| `gcp-db-first-cleanup-current.json` | `cac7e34f45653fabb696d911b6ccc921d3f291bf8cbe05a429d757e717dcafb4` | Clone absent, rollback disabled, service unchanged |
|
| `gcp-db-first-cleanup-current.json` | `cac7e34f45653fabb696d911b6ccc921d3f291bf8cbe05a429d757e717dcafb4` | Clone absent, rollback disabled, service unchanged |
|
||||||
| `gcp-db-first-live-deploy-restart-current.json` | `d70fdbac859b8c98d557f4df900139a93e6009e12f7b80c9adbd69a5655b9df7` | Merged deploy, fail-closed routing, restart, and live status/search readback passed |
|
| `gcp-db-first-live-deploy-restart-current.json` | `78e9dde38d641bfa3d58b2b7d8605b37f2861605707eddc0c94ad5c9a70d080d` | Merged PR `#144` files plus an unmerged PR `#145` wrapper were installed; restart and live status/search readback passed |
|
||||||
|
|
||||||
## What This Unlocks
|
## What This Unlocks
|
||||||
|
|
||||||
|
|
@ -166,6 +170,9 @@ rows as if they were source-code branches.
|
||||||
6. A model reasoning turn after the controlled GCP restart. The post-restart
|
6. A model reasoning turn after the controlled GCP restart. The post-restart
|
||||||
proof in this report is the real service plus live `teleo-kb` status/search
|
proof in this report is the real service plus live `teleo-kb` status/search
|
||||||
path, not a new paid model response.
|
path, not a new paid model response.
|
||||||
|
7. Repository/runtime parity for the wrapper or a least-privilege Cloud SQL
|
||||||
|
credential. The captured service still used an unmerged wrapper and the
|
||||||
|
administrator database secret.
|
||||||
|
|
||||||
The next product-level proof is one natural Telegram challenge that reaches
|
The next product-level proof is one natural Telegram challenge that reaches
|
||||||
the same database-grounded reasoning, followed by one explicitly approved
|
the same database-grounded reasoning, followed by one explicitly approved
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,9 @@ STOPWORDS = {
|
||||||
|
|
||||||
DEFAULT_CLAIM_BASE_URL = "https://leo.livingip.xyz"
|
DEFAULT_CLAIM_BASE_URL = "https://leo.livingip.xyz"
|
||||||
RETRIEVAL_RECEIPT_SCHEMA = "livingip.teleoKbRetrievalReceipt.v1"
|
RETRIEVAL_RECEIPT_SCHEMA = "livingip.teleoKbRetrievalReceipt.v1"
|
||||||
|
RUNTIME_DB_USER = "leoclean_kb_runtime"
|
||||||
|
RUNTIME_PASSWORD_SECRET = "gcp-teleo-pgvector-standby-leoclean-kb-runtime-password"
|
||||||
|
CLONE_READ_ONLY_PGOPTIONS = "-c default_transaction_read_only=on"
|
||||||
RECEIPTED_READ_COMMANDS = frozenset(
|
RECEIPTED_READ_COMMANDS = frozenset(
|
||||||
{
|
{
|
||||||
"search",
|
"search",
|
||||||
|
|
@ -94,14 +97,20 @@ def parse_args() -> argparse.Namespace:
|
||||||
parser = argparse.ArgumentParser(description=__doc__)
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
parser.add_argument("--host", default=os.environ.get("TELEO_CLOUDSQL_HOST", "10.61.0.3"))
|
parser.add_argument("--host", default=os.environ.get("TELEO_CLOUDSQL_HOST", "10.61.0.3"))
|
||||||
parser.add_argument("--port", default=os.environ.get("TELEO_CLOUDSQL_PORT", "5432"))
|
parser.add_argument("--port", default=os.environ.get("TELEO_CLOUDSQL_PORT", "5432"))
|
||||||
parser.add_argument("--db", default=os.environ.get("TELEO_CLOUDSQL_DB", "teleo_kb"))
|
parser.add_argument("--db", default=os.environ.get("TELEO_CLOUDSQL_DB", "teleo_canonical"))
|
||||||
parser.add_argument("--canonical-db", default=os.environ.get("TELEO_CANONICAL_CLOUDSQL_DB", "teleo_canonical"))
|
parser.add_argument("--canonical-db", default=os.environ.get("TELEO_CANONICAL_CLOUDSQL_DB", "teleo_canonical"))
|
||||||
parser.add_argument("--user", default=os.environ.get("TELEO_CLOUDSQL_USER", "postgres"))
|
parser.add_argument("--user", default=os.environ.get("TELEO_CLOUDSQL_USER"))
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--password-secret",
|
"--password-secret",
|
||||||
default=os.environ.get("TELEO_CLOUDSQL_PASSWORD_SECRET", "gcp-teleo-pgvector-standby-postgres-password"),
|
default=os.environ.get("TELEO_CLOUDSQL_PASSWORD_SECRET"),
|
||||||
)
|
)
|
||||||
parser.add_argument("--project", default=os.environ.get("TELEO_GCP_PROJECT", "teleo-501523"))
|
parser.add_argument("--project", default=os.environ.get("TELEO_GCP_PROJECT", "teleo-501523"))
|
||||||
|
parser.add_argument(
|
||||||
|
"--credential-mode",
|
||||||
|
choices=["runtime", "clone-readonly"],
|
||||||
|
default=os.environ.get("TELEO_CLOUDSQL_CREDENTIAL_MODE", "runtime"),
|
||||||
|
help="Runtime mode requires the exact scoped Leo identity. Clone mode is limited to read-only teleo_clone_* tests.",
|
||||||
|
)
|
||||||
parser.add_argument("--format", choices=["markdown", "json"], default="markdown")
|
parser.add_argument("--format", choices=["markdown", "json"], default="markdown")
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--include-excerpts",
|
"--include-excerpts",
|
||||||
|
|
@ -185,7 +194,7 @@ def parse_args() -> argparse.Namespace:
|
||||||
propose.add_argument("--proposed", required=True, help="Proposed replacement or correction.")
|
propose.add_argument("--proposed", required=True, help="Proposed replacement or correction.")
|
||||||
propose.add_argument("--evidence", action="append", default=[], help="Evidence/source pointer. Can be repeated.")
|
propose.add_argument("--evidence", action="append", default=[], help="Evidence/source pointer. Can be repeated.")
|
||||||
propose.add_argument("--implication", action="append", default=[], help="Downstream implication. Can be repeated.")
|
propose.add_argument("--implication", action="append", default=[], help="Downstream implication. Can be repeated.")
|
||||||
propose.add_argument("--proposed-by", default="leo")
|
propose.add_argument("--proposed-by", choices=["leo"], default="leo")
|
||||||
propose.add_argument("--originator", default="", help="Human or agent who originated the correction.")
|
propose.add_argument("--originator", default="", help="Human or agent who originated the correction.")
|
||||||
propose.add_argument("--channel", default="telegram")
|
propose.add_argument("--channel", default="telegram")
|
||||||
propose.add_argument("--source-ref", required=True)
|
propose.add_argument("--source-ref", required=True)
|
||||||
|
|
@ -196,7 +205,7 @@ def parse_args() -> argparse.Namespace:
|
||||||
propose_edge.add_argument("from_claim")
|
propose_edge.add_argument("from_claim")
|
||||||
propose_edge.add_argument("edge_type")
|
propose_edge.add_argument("edge_type")
|
||||||
propose_edge.add_argument("to_claim")
|
propose_edge.add_argument("to_claim")
|
||||||
propose_edge.add_argument("--proposed-by", default="leo")
|
propose_edge.add_argument("--proposed-by", choices=["leo"], default="leo")
|
||||||
propose_edge.add_argument("--channel", default="telegram")
|
propose_edge.add_argument("--channel", default="telegram")
|
||||||
propose_edge.add_argument("--source-ref", required=True)
|
propose_edge.add_argument("--source-ref", required=True)
|
||||||
propose_edge.add_argument("--rationale", required=True)
|
propose_edge.add_argument("--rationale", required=True)
|
||||||
|
|
@ -228,6 +237,34 @@ def parse_args() -> argparse.Namespace:
|
||||||
return parser.parse_args()
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def validate_runtime_connection(args: argparse.Namespace) -> None:
|
||||||
|
user = str(args.user or "")
|
||||||
|
password_secret = str(args.password_secret or "")
|
||||||
|
mode = str(getattr(args, "credential_mode", "runtime"))
|
||||||
|
if mode == "runtime":
|
||||||
|
if user != RUNTIME_DB_USER:
|
||||||
|
raise SystemExit(f"Leo runtime requires database user {RUNTIME_DB_USER}.")
|
||||||
|
if password_secret != RUNTIME_PASSWORD_SECRET:
|
||||||
|
raise SystemExit(f"Leo runtime requires scoped password secret {RUNTIME_PASSWORD_SECRET}.")
|
||||||
|
if "PGPASSWORD" in os.environ:
|
||||||
|
raise SystemExit("Refusing inherited PGPASSWORD in Leo runtime mode.")
|
||||||
|
return
|
||||||
|
|
||||||
|
if mode != "clone-readonly":
|
||||||
|
raise SystemExit(f"Unsupported Cloud SQL credential mode: {mode}.")
|
||||||
|
canonical_db = str(args.canonical_db or "")
|
||||||
|
audit_db = str(args.db or "")
|
||||||
|
pgoptions = os.environ.get("PGOPTIONS", "")
|
||||||
|
if not re.fullmatch(r"teleo_clone_[a-z0-9_]+", canonical_db) or audit_db != canonical_db:
|
||||||
|
raise SystemExit("Clone credential mode is limited to one teleo_clone_* database.")
|
||||||
|
if args.command not in RECEIPTED_READ_COMMANDS:
|
||||||
|
raise SystemExit("Clone credential mode permits read commands only.")
|
||||||
|
if pgoptions != CLONE_READ_ONLY_PGOPTIONS:
|
||||||
|
raise SystemExit("Clone credential mode requires server-enforced default_transaction_read_only=on.")
|
||||||
|
if not user or (not password_secret and not os.environ.get("PGPASSWORD")):
|
||||||
|
raise SystemExit("Clone credential mode requires an explicit user and credential source.")
|
||||||
|
|
||||||
|
|
||||||
def terms_for(query: str) -> list[str]:
|
def terms_for(query: str) -> list[str]:
|
||||||
terms: list[str] = []
|
terms: list[str] = []
|
||||||
for token in re.findall(r"[A-Za-z0-9][A-Za-z0-9.+#/-]*", query.lower()):
|
for token in re.findall(r"[A-Za-z0-9][A-Za-z0-9.+#/-]*", query.lower()):
|
||||||
|
|
@ -269,8 +306,11 @@ def sql_array(values: list[str], cast: str = "text") -> str:
|
||||||
|
|
||||||
|
|
||||||
def password(args: argparse.Namespace) -> str:
|
def password(args: argparse.Namespace) -> str:
|
||||||
if os.environ.get("PGPASSWORD"):
|
validate_runtime_connection(args)
|
||||||
|
if args.credential_mode == "clone-readonly" and os.environ.get("PGPASSWORD"):
|
||||||
return os.environ["PGPASSWORD"]
|
return os.environ["PGPASSWORD"]
|
||||||
|
if not args.password_secret:
|
||||||
|
raise SystemExit("Scoped Cloud SQL password secret is not configured.")
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
[
|
[
|
||||||
"gcloud",
|
"gcloud",
|
||||||
|
|
@ -287,15 +327,32 @@ def password(args: argparse.Namespace) -> str:
|
||||||
)
|
)
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
raise SystemExit(f"Secret access failed: {result.stderr.strip()}")
|
raise SystemExit(f"Secret access failed: {result.stderr.strip()}")
|
||||||
return result.stdout.strip()
|
credential = result.stdout.strip()
|
||||||
|
if not credential:
|
||||||
|
raise SystemExit("Scoped Cloud SQL password secret resolved to an empty value.")
|
||||||
|
return credential
|
||||||
|
|
||||||
|
|
||||||
def run_psql(args: argparse.Namespace, sql: str, db: str | None = None) -> str:
|
def run_psql(args: argparse.Namespace, sql: str, db: str | None = None) -> str:
|
||||||
env = os.environ.copy()
|
env = os.environ.copy()
|
||||||
env["PGPASSWORD"] = password(args)
|
for key in tuple(env):
|
||||||
conn = f"host={args.host} port={args.port} dbname={db or args.db} user={args.user} sslmode=require"
|
if key.startswith("PG"):
|
||||||
|
env.pop(key, None)
|
||||||
|
env.update(
|
||||||
|
{
|
||||||
|
"PGHOST": str(args.host),
|
||||||
|
"PGPORT": str(args.port),
|
||||||
|
"PGDATABASE": str(db or args.db),
|
||||||
|
"PGUSER": str(args.user),
|
||||||
|
"PGSSLMODE": "require",
|
||||||
|
"PGCONNECT_TIMEOUT": "8",
|
||||||
|
"PGPASSWORD": password(args),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if args.credential_mode == "clone-readonly":
|
||||||
|
env["PGOPTIONS"] = CLONE_READ_ONLY_PGOPTIONS
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
["psql", conn, "-At", "-q", "-v", "ON_ERROR_STOP=1"],
|
["psql", "-X", "-At", "-q", "-v", "ON_ERROR_STOP=1"],
|
||||||
text=True,
|
text=True,
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
input=sql,
|
input=sql,
|
||||||
|
|
@ -868,6 +925,11 @@ def query_rows(args: argparse.Namespace, query: str, limit: int) -> dict[str, An
|
||||||
canonical = query_canonical_rows(args, query, limit)
|
canonical = query_canonical_rows(args, query, limit)
|
||||||
if canonical.get("hit_count_total", 0) or canonical.get("hits"):
|
if canonical.get("hit_count_total", 0) or canonical.get("hits"):
|
||||||
return canonical
|
return canonical
|
||||||
|
if os.environ.get("TELEO_CLOUDSQL_ENABLE_AUDIT_FALLBACK", "") != "1":
|
||||||
|
canonical["canonical_fallback_reason"] = (
|
||||||
|
"no matching canonical public/persona/strategy/belief rows; audit fallback disabled"
|
||||||
|
)
|
||||||
|
return canonical
|
||||||
if not audit_restore_available(args):
|
if not audit_restore_available(args):
|
||||||
canonical["canonical_fallback_reason"] = (
|
canonical["canonical_fallback_reason"] = (
|
||||||
"no matching canonical public/persona/strategy/belief rows; teleo_restore audit fallback unavailable"
|
"no matching canonical public/persona/strategy/belief rows; teleo_restore audit fallback unavailable"
|
||||||
|
|
@ -1054,57 +1116,37 @@ def propose_core_change(args: argparse.Namespace) -> dict[str, Any]:
|
||||||
with params as (
|
with params as (
|
||||||
select
|
select
|
||||||
{sql_literal(args.proposal_type)}::text as proposal_type,
|
{sql_literal(args.proposal_type)}::text as proposal_type,
|
||||||
nullif({sql_literal(args.proposed_by)}, '') as proposed_by_handle,
|
|
||||||
nullif({sql_literal(args.channel)}, '') as channel,
|
nullif({sql_literal(args.channel)}, '') as channel,
|
||||||
nullif({sql_literal(args.source_ref)}, '') as source_ref,
|
nullif({sql_literal(args.source_ref)}, '') as source_ref,
|
||||||
{sql_literal(args.rationale)} as rationale,
|
{sql_literal(args.rationale)} as rationale,
|
||||||
{sql_literal(payload_json)}::jsonb as payload
|
{sql_literal(payload_json)}::jsonb as payload
|
||||||
), proposer as (
|
), proposer as (
|
||||||
select a.id, a.handle
|
select kb_stage.stage_leoclean_proposal(
|
||||||
from public.agents a, params p
|
|
||||||
where a.handle = p.proposed_by_handle
|
|
||||||
limit 1
|
|
||||||
), inserted as (
|
|
||||||
insert into kb_stage.kb_proposals (
|
|
||||||
proposal_type,
|
|
||||||
status,
|
|
||||||
proposed_by_handle,
|
|
||||||
proposed_by_agent_id,
|
|
||||||
channel,
|
|
||||||
source_ref,
|
|
||||||
rationale,
|
|
||||||
payload
|
|
||||||
)
|
|
||||||
select
|
|
||||||
p.proposal_type,
|
p.proposal_type,
|
||||||
'pending_review',
|
p.channel,
|
||||||
p.proposed_by_handle,
|
|
||||||
proposer.id,
|
|
||||||
coalesce(p.channel, 'telegram'),
|
|
||||||
p.source_ref,
|
p.source_ref,
|
||||||
p.rationale,
|
p.rationale,
|
||||||
p.payload
|
p.payload
|
||||||
|
) as proposal
|
||||||
from params p
|
from params p
|
||||||
left join proposer on true
|
|
||||||
returning *
|
|
||||||
)
|
)
|
||||||
select jsonb_build_object(
|
select jsonb_build_object(
|
||||||
'artifact', 'teleo_cloudsql_kb_core_change_proposal',
|
'artifact', 'teleo_cloudsql_kb_core_change_proposal',
|
||||||
'backend', {sql_literal(canonical_backend(args))},
|
'backend', {sql_literal(canonical_backend(args))},
|
||||||
'id', id::text,
|
'id', proposal->>'id',
|
||||||
'proposal_type', proposal_type,
|
'proposal_type', proposal->>'proposal_type',
|
||||||
'status', status,
|
'status', proposal->>'status',
|
||||||
'proposed_by_handle', proposed_by_handle,
|
'proposed_by_handle', proposal->>'proposed_by_handle',
|
||||||
'proposed_by_agent_id', proposed_by_agent_id::text,
|
'proposed_by_agent_id', proposal->>'proposed_by_agent_id',
|
||||||
'channel', channel,
|
'channel', proposal->>'channel',
|
||||||
'source_ref', source_ref,
|
'source_ref', proposal->>'source_ref',
|
||||||
'rationale', rationale,
|
'rationale', proposal->>'rationale',
|
||||||
'payload', payload,
|
'payload', proposal->'payload',
|
||||||
'created_at', created_at::text,
|
'created_at', proposal->>'created_at',
|
||||||
'canonical_apply_done', false,
|
'canonical_apply_done', false,
|
||||||
'runtime_memory_write_done', false
|
'runtime_memory_write_done', false
|
||||||
)::text
|
)::text
|
||||||
from inserted;
|
from proposer;
|
||||||
"""
|
"""
|
||||||
rows = psql_json_lines(args, sql, db=args.canonical_db)
|
rows = psql_json_lines(args, sql, db=args.canonical_db)
|
||||||
if not rows:
|
if not rows:
|
||||||
|
|
@ -1119,7 +1161,6 @@ with params as (
|
||||||
{sql_literal(args.from_claim)}::uuid as from_claim,
|
{sql_literal(args.from_claim)}::uuid as from_claim,
|
||||||
{sql_literal(args.to_claim)}::uuid as to_claim,
|
{sql_literal(args.to_claim)}::uuid as to_claim,
|
||||||
{sql_literal(args.edge_type)}::edge_type as edge_type,
|
{sql_literal(args.edge_type)}::edge_type as edge_type,
|
||||||
nullif({sql_literal(args.proposed_by)}, '') as proposed_by_handle,
|
|
||||||
nullif({sql_literal(args.channel)}, '') as channel,
|
nullif({sql_literal(args.channel)}, '') as channel,
|
||||||
nullif({sql_literal(args.source_ref)}, '') as source_ref,
|
nullif({sql_literal(args.source_ref)}, '') as source_ref,
|
||||||
{sql_literal(args.rationale)} as rationale
|
{sql_literal(args.rationale)} as rationale
|
||||||
|
|
@ -1131,11 +1172,6 @@ with params as (
|
||||||
select c.id, c.text
|
select c.id, c.text
|
||||||
from public.claims c, params p
|
from public.claims c, params p
|
||||||
where c.id = p.to_claim
|
where c.id = p.to_claim
|
||||||
), proposer as (
|
|
||||||
select a.id, a.handle
|
|
||||||
from public.agents a, params p
|
|
||||||
where a.handle = p.proposed_by_handle
|
|
||||||
limit 1
|
|
||||||
), existing_edge as (
|
), existing_edge as (
|
||||||
select e.id
|
select e.id
|
||||||
from public.claim_edges e, params p
|
from public.claim_edges e, params p
|
||||||
|
|
@ -1143,23 +1179,10 @@ with params as (
|
||||||
and e.to_claim = p.to_claim
|
and e.to_claim = p.to_claim
|
||||||
and e.edge_type = p.edge_type
|
and e.edge_type = p.edge_type
|
||||||
limit 1
|
limit 1
|
||||||
), inserted as (
|
), staged as (
|
||||||
insert into kb_stage.kb_proposals (
|
select kb_stage.stage_leoclean_proposal(
|
||||||
proposal_type,
|
|
||||||
status,
|
|
||||||
proposed_by_handle,
|
|
||||||
proposed_by_agent_id,
|
|
||||||
channel,
|
|
||||||
source_ref,
|
|
||||||
rationale,
|
|
||||||
payload
|
|
||||||
)
|
|
||||||
select
|
|
||||||
'add_edge',
|
'add_edge',
|
||||||
'pending_review',
|
p.channel,
|
||||||
p.proposed_by_handle,
|
|
||||||
proposer.id,
|
|
||||||
coalesce(p.channel, 'telegram'),
|
|
||||||
p.source_ref,
|
p.source_ref,
|
||||||
p.rationale,
|
p.rationale,
|
||||||
jsonb_build_object(
|
jsonb_build_object(
|
||||||
|
|
@ -1175,29 +1198,28 @@ with params as (
|
||||||
'edge_type', p.edge_type::text
|
'edge_type', p.edge_type::text
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
) as proposal
|
||||||
from params p
|
from params p
|
||||||
join from_row on true
|
join from_row on true
|
||||||
join to_row on true
|
join to_row on true
|
||||||
left join proposer on true
|
|
||||||
returning *
|
|
||||||
)
|
)
|
||||||
select jsonb_build_object(
|
select jsonb_build_object(
|
||||||
'artifact', 'teleo_cloudsql_kb_edge_proposal',
|
'artifact', 'teleo_cloudsql_kb_edge_proposal',
|
||||||
'backend', {sql_literal(canonical_backend(args))},
|
'backend', {sql_literal(canonical_backend(args))},
|
||||||
'id', id::text,
|
'id', proposal->>'id',
|
||||||
'proposal_type', proposal_type,
|
'proposal_type', proposal->>'proposal_type',
|
||||||
'status', status,
|
'status', proposal->>'status',
|
||||||
'proposed_by_handle', proposed_by_handle,
|
'proposed_by_handle', proposal->>'proposed_by_handle',
|
||||||
'proposed_by_agent_id', proposed_by_agent_id::text,
|
'proposed_by_agent_id', proposal->>'proposed_by_agent_id',
|
||||||
'channel', channel,
|
'channel', proposal->>'channel',
|
||||||
'source_ref', source_ref,
|
'source_ref', proposal->>'source_ref',
|
||||||
'rationale', rationale,
|
'rationale', proposal->>'rationale',
|
||||||
'payload', payload,
|
'payload', proposal->'payload',
|
||||||
'created_at', created_at::text,
|
'created_at', proposal->>'created_at',
|
||||||
'canonical_apply_done', false,
|
'canonical_apply_done', false,
|
||||||
'runtime_memory_write_done', false
|
'runtime_memory_write_done', false
|
||||||
)::text
|
)::text
|
||||||
from inserted;
|
from staged;
|
||||||
"""
|
"""
|
||||||
rows = psql_json_lines(args, sql, db=args.canonical_db)
|
rows = psql_json_lines(args, sql, db=args.canonical_db)
|
||||||
if not rows:
|
if not rows:
|
||||||
|
|
@ -1365,6 +1387,8 @@ select jsonb_build_object(
|
||||||
|
|
||||||
|
|
||||||
def audit_restore_available(args: argparse.Namespace) -> bool:
|
def audit_restore_available(args: argparse.Namespace) -> bool:
|
||||||
|
if os.environ.get("TELEO_CLOUDSQL_ENABLE_AUDIT_FALLBACK", "") != "1":
|
||||||
|
return False
|
||||||
sql = """
|
sql = """
|
||||||
select bool_and(to_regclass(table_name) is not null)
|
select bool_and(to_regclass(table_name) is not null)
|
||||||
from (values
|
from (values
|
||||||
|
|
@ -1675,6 +1699,7 @@ def emit_markdown(value: dict[str, Any]) -> None:
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
args = parse_args()
|
args = parse_args()
|
||||||
|
validate_runtime_connection(args)
|
||||||
read_loaders = {
|
read_loaders = {
|
||||||
"search": lambda: query_rows(args, args.query, args.limit),
|
"search": lambda: query_rows(args, args.query, args.limit),
|
||||||
"context": lambda: query_rows(args, args.query, args.limit),
|
"context": lambda: query_rows(args, args.query, args.limit),
|
||||||
|
|
|
||||||
|
|
@ -23,14 +23,25 @@ cloudsql_args() {
|
||||||
}
|
}
|
||||||
|
|
||||||
run_cloudsql() {
|
run_cloudsql() {
|
||||||
if cloudsql_supported; then
|
if ! cloudsql_supported; then
|
||||||
|
echo "teleo-kb: command '$COMMAND' is not supported by the Cloud SQL backend" >&2
|
||||||
|
exit 64
|
||||||
|
fi
|
||||||
|
if [ ! -x "$CLOUDSQL_TOOL" ]; then
|
||||||
|
echo "teleo-kb: Cloud SQL tool is missing or not executable: $CLOUDSQL_TOOL" >&2
|
||||||
|
exit 69
|
||||||
|
fi
|
||||||
|
for dependency in python3 psql gcloud; do
|
||||||
|
if ! command -v "$dependency" >/dev/null 2>&1; then
|
||||||
|
echo "teleo-kb: required Cloud SQL dependency is unavailable: $dependency" >&2
|
||||||
|
exit 69
|
||||||
|
fi
|
||||||
|
done
|
||||||
extra_args=()
|
extra_args=()
|
||||||
while IFS= read -r arg; do
|
while IFS= read -r arg; do
|
||||||
[ -n "$arg" ] && extra_args+=("$arg")
|
[ -n "$arg" ] && extra_args+=("$arg")
|
||||||
done < <(cloudsql_args)
|
done < <(cloudsql_args)
|
||||||
exec python3 "$CLOUDSQL_TOOL" "$@" "${extra_args[@]}"
|
exec python3 "$CLOUDSQL_TOOL" "$@" "${extra_args[@]}"
|
||||||
fi
|
|
||||||
exec python3 "$KB_TOOL" "$@"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case "$MODE" in
|
case "$MODE" in
|
||||||
|
|
@ -46,8 +57,8 @@ case "$MODE" in
|
||||||
auto)
|
auto)
|
||||||
# Canonical commands must fail closed on Cloud SQL errors. Falling through
|
# Canonical commands must fail closed on Cloud SQL errors. Falling through
|
||||||
# to a different local database would make source-of-truth behavior depend
|
# to a different local database would make source-of-truth behavior depend
|
||||||
# on health-check latency.
|
# on health-check latency or host package state.
|
||||||
if [ -x "$CLOUDSQL_TOOL" ] && command -v psql >/dev/null 2>&1 && command -v gcloud >/dev/null 2>&1 && cloudsql_supported; then
|
if cloudsql_supported; then
|
||||||
run_cloudsql "$@"
|
run_cloudsql "$@"
|
||||||
fi
|
fi
|
||||||
if command -v docker >/dev/null 2>&1; then
|
if command -v docker >/dev/null 2>&1; then
|
||||||
|
|
|
||||||
847
ops/gcp_leoclean_runtime_role.sql
Normal file
847
ops/gcp_leoclean_runtime_role.sql
Normal file
|
|
@ -0,0 +1,847 @@
|
||||||
|
\set ON_ERROR_STOP on
|
||||||
|
\getenv runtime_password TELEO_LEOCLEAN_DB_PASSWORD
|
||||||
|
|
||||||
|
-- One-time GCP staging provisioning for Leo's Cloud SQL identity. The password
|
||||||
|
-- is supplied only through the process environment and is never stored here.
|
||||||
|
--
|
||||||
|
-- The login can read the canonical allowlist and can stage a pending proposal
|
||||||
|
-- through one narrowly-scoped SECURITY DEFINER function. It cannot inherit or
|
||||||
|
-- SET ROLE into another principal, forge the proposer identity, write directly
|
||||||
|
-- to canonical/staging tables, or inspect the legacy teleo_restore audit schema.
|
||||||
|
select 'create role leoclean_kb_runtime login nosuperuser nocreatedb nocreaterole noinherit noreplication nobypassrls connection limit 8'
|
||||||
|
where not exists (select 1 from pg_catalog.pg_roles where rolname = 'leoclean_kb_runtime')
|
||||||
|
\gexec
|
||||||
|
|
||||||
|
select 'create role leoclean_kb_stage_owner nologin nosuperuser nocreatedb nocreaterole noinherit noreplication nobypassrls connection limit -1'
|
||||||
|
where not exists (select 1 from pg_catalog.pg_roles where rolname = 'leoclean_kb_stage_owner')
|
||||||
|
\gexec
|
||||||
|
|
||||||
|
select format('alter role leoclean_kb_runtime password %L', :'runtime_password')
|
||||||
|
\gexec
|
||||||
|
|
||||||
|
alter role leoclean_kb_runtime
|
||||||
|
with login nosuperuser nocreatedb nocreaterole noinherit noreplication nobypassrls connection limit 8;
|
||||||
|
alter role leoclean_kb_stage_owner
|
||||||
|
with nologin nosuperuser nocreatedb nocreaterole noinherit noreplication nobypassrls connection limit -1
|
||||||
|
password null;
|
||||||
|
|
||||||
|
alter role leoclean_kb_runtime reset all;
|
||||||
|
alter role leoclean_kb_stage_owner reset all;
|
||||||
|
|
||||||
|
-- NOINHERIT does not prevent SET ROLE. Remove every membership edge in either
|
||||||
|
-- direction so neither scoped principal can reach, or be reached through, a
|
||||||
|
-- broader role left behind by an earlier deployment.
|
||||||
|
select format('revoke %I from %I', granted_role.rolname, member_role.rolname)
|
||||||
|
from pg_catalog.pg_auth_members membership
|
||||||
|
join pg_catalog.pg_roles granted_role on granted_role.oid = membership.roleid
|
||||||
|
join pg_catalog.pg_roles member_role on member_role.oid = membership.member
|
||||||
|
where granted_role.rolname in ('leoclean_kb_runtime', 'leoclean_kb_stage_owner')
|
||||||
|
or member_role.rolname in ('leoclean_kb_runtime', 'leoclean_kb_stage_owner')
|
||||||
|
\gexec
|
||||||
|
|
||||||
|
revoke all privileges on database teleo_canonical from leoclean_kb_runtime;
|
||||||
|
revoke all privileges on database teleo_canonical from leoclean_kb_stage_owner;
|
||||||
|
revoke all privileges on database teleo_kb from leoclean_kb_runtime;
|
||||||
|
revoke all privileges on database teleo_kb from leoclean_kb_stage_owner;
|
||||||
|
grant connect on database teleo_canonical to leoclean_kb_runtime;
|
||||||
|
|
||||||
|
-- PostgreSQL normally grants TEMP to PUBLIC. A role-specific REVOKE cannot
|
||||||
|
-- override that inherited grant, while REVOKE TEMP FROM PUBLIC would be a
|
||||||
|
-- database-wide change that could break kb_apply/review or operator workflows.
|
||||||
|
-- Leave PUBLIC TEMP policy unchanged here and surface it in the final receipt.
|
||||||
|
-- TEMP cannot shadow this SECURITY DEFINER boundary: pg_catalog is first,
|
||||||
|
-- pg_temp is explicitly last, and every relation reference is schema-qualified.
|
||||||
|
-- Database CREATE, direct TEMP grants, and all unexpected direct database ACLs
|
||||||
|
-- are rejected below. A database-wide TEMP removal requires a separate role-use
|
||||||
|
-- inventory and explicit grants to every workload that still needs it.
|
||||||
|
|
||||||
|
alter role leoclean_kb_runtime in database teleo_canonical reset all;
|
||||||
|
alter role leoclean_kb_stage_owner in database teleo_canonical reset all;
|
||||||
|
alter role leoclean_kb_runtime in database teleo_kb reset all;
|
||||||
|
alter role leoclean_kb_stage_owner in database teleo_kb reset all;
|
||||||
|
|
||||||
|
alter role leoclean_kb_runtime in database teleo_canonical
|
||||||
|
set search_path = pg_catalog, public, kb_stage;
|
||||||
|
alter role leoclean_kb_runtime in database teleo_canonical
|
||||||
|
set statement_timeout = '15s';
|
||||||
|
alter role leoclean_kb_runtime in database teleo_canonical
|
||||||
|
set lock_timeout = '2s';
|
||||||
|
|
||||||
|
\connect teleo_canonical
|
||||||
|
|
||||||
|
begin;
|
||||||
|
|
||||||
|
-- The deploy principal needs temporary ownership authority to replace an
|
||||||
|
-- existing function on idempotent reruns. This membership is transaction-local
|
||||||
|
-- and is revoked before verification/commit.
|
||||||
|
select format('grant leoclean_kb_stage_owner to %I', current_user)
|
||||||
|
where current_user <> 'leoclean_kb_stage_owner'
|
||||||
|
\gexec
|
||||||
|
|
||||||
|
revoke all on schema public, kb_stage
|
||||||
|
from leoclean_kb_runtime, leoclean_kb_stage_owner;
|
||||||
|
revoke all privileges on all tables in schema public, kb_stage
|
||||||
|
from leoclean_kb_runtime, leoclean_kb_stage_owner;
|
||||||
|
revoke all privileges on all sequences in schema public, kb_stage
|
||||||
|
from leoclean_kb_runtime, leoclean_kb_stage_owner;
|
||||||
|
revoke all privileges on all functions in schema public, kb_stage
|
||||||
|
from leoclean_kb_runtime, leoclean_kb_stage_owner;
|
||||||
|
revoke all privileges on all procedures in schema public, kb_stage
|
||||||
|
from leoclean_kb_runtime, leoclean_kb_stage_owner;
|
||||||
|
|
||||||
|
-- Table-level REVOKE does not clear column ACLs. Clear every column ACL for both
|
||||||
|
-- principals before rebuilding the exact allowlist below.
|
||||||
|
select format(
|
||||||
|
'revoke all privileges (%s) on table %I.%I from %I',
|
||||||
|
pg_catalog.string_agg(pg_catalog.quote_ident(attribute.attname), ', ' order by attribute.attnum),
|
||||||
|
namespace.nspname,
|
||||||
|
relation.relname,
|
||||||
|
target.rolname
|
||||||
|
)
|
||||||
|
from pg_catalog.pg_class relation
|
||||||
|
join pg_catalog.pg_namespace namespace on namespace.oid = relation.relnamespace
|
||||||
|
join pg_catalog.pg_attribute attribute on attribute.attrelid = relation.oid
|
||||||
|
cross join (
|
||||||
|
values ('leoclean_kb_runtime'), ('leoclean_kb_stage_owner')
|
||||||
|
) as target(rolname)
|
||||||
|
where namespace.nspname in ('public', 'kb_stage')
|
||||||
|
and relation.relkind in ('r', 'p', 'v', 'm', 'f')
|
||||||
|
and attribute.attnum > 0
|
||||||
|
and not attribute.attisdropped
|
||||||
|
group by namespace.nspname, relation.relname, target.rolname
|
||||||
|
\gexec
|
||||||
|
|
||||||
|
grant usage on schema public, kb_stage to leoclean_kb_runtime;
|
||||||
|
|
||||||
|
grant select on
|
||||||
|
public.agents,
|
||||||
|
public.claims,
|
||||||
|
public.claim_evidence,
|
||||||
|
public.claim_edges,
|
||||||
|
public.sources,
|
||||||
|
public.personas,
|
||||||
|
public.strategies,
|
||||||
|
public.beliefs,
|
||||||
|
public.blindspots,
|
||||||
|
public.agent_roles,
|
||||||
|
public.peer_models,
|
||||||
|
public.behavioral_rules,
|
||||||
|
public.contributor_rules,
|
||||||
|
public.reasoning_tools,
|
||||||
|
public.governance_gates,
|
||||||
|
kb_stage.kb_proposals
|
||||||
|
to leoclean_kb_runtime;
|
||||||
|
|
||||||
|
-- The function owner is a dedicated NOLOGIN role. Grant only the columns the
|
||||||
|
-- function reads/inserts. CREATE is temporary and is revoked immediately after
|
||||||
|
-- ownership is assigned.
|
||||||
|
grant usage, create on schema kb_stage to leoclean_kb_stage_owner;
|
||||||
|
grant usage on schema public to leoclean_kb_stage_owner;
|
||||||
|
grant select (id, handle) on public.agents to leoclean_kb_stage_owner;
|
||||||
|
grant select on kb_stage.kb_proposals to leoclean_kb_stage_owner;
|
||||||
|
grant insert (
|
||||||
|
proposal_type,
|
||||||
|
status,
|
||||||
|
proposed_by_handle,
|
||||||
|
proposed_by_agent_id,
|
||||||
|
channel,
|
||||||
|
source_ref,
|
||||||
|
rationale,
|
||||||
|
payload
|
||||||
|
) on kb_stage.kb_proposals to leoclean_kb_stage_owner;
|
||||||
|
|
||||||
|
-- Remove the prior caller-supplied-identity overload before installing the
|
||||||
|
-- session-bound form. A runtime caller can only stage as canonical agent Leo.
|
||||||
|
drop function if exists kb_stage.stage_leoclean_proposal(text, text, text, text, text, jsonb);
|
||||||
|
|
||||||
|
create or replace function kb_stage.stage_leoclean_proposal(
|
||||||
|
p_proposal_type text,
|
||||||
|
p_channel text,
|
||||||
|
p_source_ref text,
|
||||||
|
p_rationale text,
|
||||||
|
p_payload jsonb
|
||||||
|
) returns jsonb
|
||||||
|
language plpgsql
|
||||||
|
security definer
|
||||||
|
set search_path = pg_catalog, pg_temp
|
||||||
|
as $function$
|
||||||
|
declare
|
||||||
|
staged kb_stage.kb_proposals%rowtype;
|
||||||
|
proposer_id uuid;
|
||||||
|
begin
|
||||||
|
if session_user <> 'leoclean_kb_runtime' then
|
||||||
|
raise exception 'stage_leoclean_proposal is restricted to leoclean_kb_runtime'
|
||||||
|
using errcode = '42501';
|
||||||
|
end if;
|
||||||
|
if p_proposal_type not in ('revise_claim', 'revise_strategy', 'add_edge') then
|
||||||
|
raise exception 'unsupported Leo proposal type: %', p_proposal_type using errcode = '22023';
|
||||||
|
end if;
|
||||||
|
if nullif(btrim(p_source_ref), '') is null then
|
||||||
|
raise exception 'source_ref is required' using errcode = '22023';
|
||||||
|
end if;
|
||||||
|
if nullif(btrim(p_rationale), '') is null then
|
||||||
|
raise exception 'rationale is required' using errcode = '22023';
|
||||||
|
end if;
|
||||||
|
if p_payload is null or jsonb_typeof(p_payload) <> 'object' then
|
||||||
|
raise exception 'proposal payload must be a JSON object' using errcode = '22023';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
select agent.id
|
||||||
|
into proposer_id
|
||||||
|
from public.agents as agent
|
||||||
|
where agent.handle = 'leo';
|
||||||
|
|
||||||
|
if proposer_id is null then
|
||||||
|
raise exception 'canonical Leo agent row is required' using errcode = '23503';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
insert into kb_stage.kb_proposals (
|
||||||
|
proposal_type,
|
||||||
|
status,
|
||||||
|
proposed_by_handle,
|
||||||
|
proposed_by_agent_id,
|
||||||
|
channel,
|
||||||
|
source_ref,
|
||||||
|
rationale,
|
||||||
|
payload
|
||||||
|
) values (
|
||||||
|
p_proposal_type,
|
||||||
|
'pending_review',
|
||||||
|
'leo',
|
||||||
|
proposer_id,
|
||||||
|
coalesce(nullif(p_channel, ''), 'telegram'),
|
||||||
|
p_source_ref,
|
||||||
|
p_rationale,
|
||||||
|
p_payload
|
||||||
|
)
|
||||||
|
returning * into staged;
|
||||||
|
|
||||||
|
return to_jsonb(staged);
|
||||||
|
end
|
||||||
|
$function$;
|
||||||
|
|
||||||
|
alter function kb_stage.stage_leoclean_proposal(text, text, text, text, jsonb)
|
||||||
|
owner to leoclean_kb_stage_owner;
|
||||||
|
revoke create on schema kb_stage from leoclean_kb_stage_owner;
|
||||||
|
|
||||||
|
-- Normalize explicit ACL entries left by CREATE OR REPLACE or an earlier grant.
|
||||||
|
revoke all on function kb_stage.stage_leoclean_proposal(text, text, text, text, jsonb)
|
||||||
|
from public;
|
||||||
|
select format(
|
||||||
|
'revoke all on function kb_stage.stage_leoclean_proposal(text, text, text, text, jsonb) from %I',
|
||||||
|
grantee.rolname
|
||||||
|
)
|
||||||
|
from pg_catalog.pg_proc function_row
|
||||||
|
cross join lateral pg_catalog.aclexplode(
|
||||||
|
coalesce(
|
||||||
|
function_row.proacl,
|
||||||
|
pg_catalog.acldefault('f', function_row.proowner)
|
||||||
|
)
|
||||||
|
) as acl
|
||||||
|
join pg_catalog.pg_roles grantee on grantee.oid = acl.grantee
|
||||||
|
where function_row.oid = 'kb_stage.stage_leoclean_proposal(text,text,text,text,jsonb)'::pg_catalog.regprocedure
|
||||||
|
and grantee.rolname <> 'leoclean_kb_stage_owner'
|
||||||
|
\gexec
|
||||||
|
|
||||||
|
grant execute on function kb_stage.stage_leoclean_proposal(text, text, text, text, jsonb)
|
||||||
|
to leoclean_kb_runtime;
|
||||||
|
|
||||||
|
select format('revoke leoclean_kb_stage_owner from %I', current_user)
|
||||||
|
where current_user <> 'leoclean_kb_stage_owner'
|
||||||
|
\gexec
|
||||||
|
|
||||||
|
do $verification$
|
||||||
|
declare
|
||||||
|
unexpected text;
|
||||||
|
runtime_oid oid := 'leoclean_kb_runtime'::pg_catalog.regrole::oid;
|
||||||
|
owner_oid oid := 'leoclean_kb_stage_owner'::pg_catalog.regrole::oid;
|
||||||
|
stage_function oid := 'kb_stage.stage_leoclean_proposal(text,text,text,text,jsonb)'::pg_catalog.regprocedure::oid;
|
||||||
|
begin
|
||||||
|
if exists (
|
||||||
|
select 1
|
||||||
|
from pg_catalog.pg_roles role_row
|
||||||
|
where role_row.rolname = 'leoclean_kb_runtime'
|
||||||
|
and (not role_row.rolcanlogin
|
||||||
|
or role_row.rolsuper
|
||||||
|
or role_row.rolcreatedb
|
||||||
|
or role_row.rolcreaterole
|
||||||
|
or role_row.rolinherit
|
||||||
|
or role_row.rolreplication
|
||||||
|
or role_row.rolbypassrls
|
||||||
|
or role_row.rolconnlimit <> 8)
|
||||||
|
) then
|
||||||
|
raise exception 'leoclean_kb_runtime has unsafe role attributes';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
if exists (
|
||||||
|
select 1
|
||||||
|
from pg_catalog.pg_roles role_row
|
||||||
|
where role_row.rolname = 'leoclean_kb_stage_owner'
|
||||||
|
and (role_row.rolcanlogin
|
||||||
|
or role_row.rolsuper
|
||||||
|
or role_row.rolcreatedb
|
||||||
|
or role_row.rolcreaterole
|
||||||
|
or role_row.rolinherit
|
||||||
|
or role_row.rolreplication
|
||||||
|
or role_row.rolbypassrls
|
||||||
|
or role_row.rolconnlimit <> -1)
|
||||||
|
) then
|
||||||
|
raise exception 'leoclean_kb_stage_owner has unsafe role attributes';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
select pg_catalog.string_agg(
|
||||||
|
pg_catalog.format('%I->%I', granted_role.rolname, member_role.rolname),
|
||||||
|
', '
|
||||||
|
order by granted_role.rolname, member_role.rolname
|
||||||
|
)
|
||||||
|
into unexpected
|
||||||
|
from pg_catalog.pg_auth_members membership
|
||||||
|
join pg_catalog.pg_roles granted_role on granted_role.oid = membership.roleid
|
||||||
|
join pg_catalog.pg_roles member_role on member_role.oid = membership.member
|
||||||
|
where membership.roleid in (runtime_oid, owner_oid)
|
||||||
|
or membership.member in (runtime_oid, owner_oid);
|
||||||
|
if unexpected is not null then
|
||||||
|
raise exception 'scoped Leo roles retain role memberships: %', unexpected;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
select pg_catalog.string_agg(
|
||||||
|
pg_catalog.format(
|
||||||
|
'%I:%s:%s:grantable=%s',
|
||||||
|
database_row.datname,
|
||||||
|
grantee.rolname,
|
||||||
|
acl.privilege_type,
|
||||||
|
acl.is_grantable
|
||||||
|
),
|
||||||
|
', '
|
||||||
|
order by database_row.datname, grantee.rolname, acl.privilege_type
|
||||||
|
)
|
||||||
|
into unexpected
|
||||||
|
from pg_catalog.pg_database database_row
|
||||||
|
cross join lateral pg_catalog.aclexplode(
|
||||||
|
coalesce(
|
||||||
|
database_row.datacl,
|
||||||
|
pg_catalog.acldefault('d', database_row.datdba)
|
||||||
|
)
|
||||||
|
) as acl
|
||||||
|
join pg_catalog.pg_roles grantee on grantee.oid = acl.grantee
|
||||||
|
where database_row.datname in ('teleo_canonical', 'teleo_kb')
|
||||||
|
and grantee.rolname in ('leoclean_kb_runtime', 'leoclean_kb_stage_owner')
|
||||||
|
and not (
|
||||||
|
database_row.datname = 'teleo_canonical'
|
||||||
|
and grantee.rolname = 'leoclean_kb_runtime'
|
||||||
|
and acl.privilege_type = 'CONNECT'
|
||||||
|
and not acl.is_grantable
|
||||||
|
);
|
||||||
|
if unexpected is not null then
|
||||||
|
raise exception 'scoped Leo roles retain unexpected direct database ACLs: %', unexpected;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
if has_database_privilege('leoclean_kb_runtime', 'teleo_canonical', 'CREATE')
|
||||||
|
or has_database_privilege('leoclean_kb_runtime', 'teleo_kb', 'CREATE')
|
||||||
|
or has_database_privilege('leoclean_kb_stage_owner', 'teleo_canonical', 'CREATE')
|
||||||
|
or has_database_privilege('leoclean_kb_stage_owner', 'teleo_kb', 'CREATE') then
|
||||||
|
raise exception 'a scoped Leo role unexpectedly has database CREATE';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
select pg_catalog.string_agg(
|
||||||
|
pg_catalog.format('%s:%s:%s', dependency.classid::pg_catalog.regclass, dependency.objid, dependency.objsubid),
|
||||||
|
', '
|
||||||
|
)
|
||||||
|
into unexpected
|
||||||
|
from pg_catalog.pg_shdepend dependency
|
||||||
|
where dependency.refclassid = 'pg_catalog.pg_authid'::pg_catalog.regclass
|
||||||
|
and dependency.refobjid = runtime_oid
|
||||||
|
and dependency.deptype = 'o';
|
||||||
|
if unexpected is not null then
|
||||||
|
raise exception 'leoclean_kb_runtime unexpectedly owns database objects: %', unexpected;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
select pg_catalog.string_agg(
|
||||||
|
pg_catalog.format('%s:%s:%s', dependency.classid::pg_catalog.regclass, dependency.objid, dependency.objsubid),
|
||||||
|
', '
|
||||||
|
)
|
||||||
|
into unexpected
|
||||||
|
from pg_catalog.pg_shdepend dependency
|
||||||
|
where dependency.refclassid = 'pg_catalog.pg_authid'::pg_catalog.regclass
|
||||||
|
and dependency.refobjid = owner_oid
|
||||||
|
and dependency.deptype = 'o'
|
||||||
|
and not (
|
||||||
|
dependency.dbid = (select database_row.oid from pg_catalog.pg_database database_row where database_row.datname = current_database())
|
||||||
|
and
|
||||||
|
dependency.classid = 'pg_catalog.pg_proc'::pg_catalog.regclass
|
||||||
|
and dependency.objid = stage_function
|
||||||
|
and dependency.objsubid = 0
|
||||||
|
);
|
||||||
|
if unexpected is not null then
|
||||||
|
raise exception 'leoclean_kb_stage_owner owns unexpected database objects: %', unexpected;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
if has_schema_privilege('leoclean_kb_runtime', 'public', 'CREATE')
|
||||||
|
or has_schema_privilege('leoclean_kb_runtime', 'kb_stage', 'CREATE')
|
||||||
|
or has_schema_privilege('leoclean_kb_stage_owner', 'public', 'CREATE')
|
||||||
|
or has_schema_privilege('leoclean_kb_stage_owner', 'kb_stage', 'CREATE') then
|
||||||
|
raise exception 'a scoped Leo role unexpectedly has schema CREATE';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
select pg_catalog.string_agg(
|
||||||
|
pg_catalog.format('%I.%I:%s', namespace.nspname, relation.relname, privilege.name),
|
||||||
|
', '
|
||||||
|
order by namespace.nspname, relation.relname, privilege.name
|
||||||
|
)
|
||||||
|
into unexpected
|
||||||
|
from pg_catalog.pg_class relation
|
||||||
|
join pg_catalog.pg_namespace namespace on namespace.oid = relation.relnamespace
|
||||||
|
cross join (values ('INSERT'), ('UPDATE'), ('DELETE'), ('TRUNCATE'), ('REFERENCES'), ('TRIGGER')) privilege(name)
|
||||||
|
where namespace.nspname in ('public', 'kb_stage')
|
||||||
|
and relation.relkind in ('r', 'p', 'v', 'm', 'f')
|
||||||
|
and has_table_privilege('leoclean_kb_runtime', relation.oid, privilege.name);
|
||||||
|
if unexpected is not null then
|
||||||
|
raise exception 'leoclean_kb_runtime unexpectedly has table-level DML: %', unexpected;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
select pg_catalog.string_agg(
|
||||||
|
pg_catalog.format('%I.%I.%I:%s', namespace.nspname, relation.relname, attribute.attname, privilege.name),
|
||||||
|
', '
|
||||||
|
order by namespace.nspname, relation.relname, attribute.attnum, privilege.name
|
||||||
|
)
|
||||||
|
into unexpected
|
||||||
|
from pg_catalog.pg_class relation
|
||||||
|
join pg_catalog.pg_namespace namespace on namespace.oid = relation.relnamespace
|
||||||
|
join pg_catalog.pg_attribute attribute on attribute.attrelid = relation.oid
|
||||||
|
cross join (values ('INSERT'), ('UPDATE'), ('REFERENCES')) privilege(name)
|
||||||
|
where namespace.nspname in ('public', 'kb_stage')
|
||||||
|
and relation.relkind in ('r', 'p', 'v', 'm', 'f')
|
||||||
|
and attribute.attnum > 0
|
||||||
|
and not attribute.attisdropped
|
||||||
|
and has_column_privilege('leoclean_kb_runtime', relation.oid, attribute.attnum, privilege.name);
|
||||||
|
if unexpected is not null then
|
||||||
|
raise exception 'leoclean_kb_runtime unexpectedly has column-level DML: %', unexpected;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
select pg_catalog.string_agg(
|
||||||
|
pg_catalog.format('%I.%I', namespace.nspname, relation.relname),
|
||||||
|
', '
|
||||||
|
order by namespace.nspname, relation.relname
|
||||||
|
)
|
||||||
|
into unexpected
|
||||||
|
from pg_catalog.pg_class relation
|
||||||
|
join pg_catalog.pg_namespace namespace on namespace.oid = relation.relnamespace
|
||||||
|
where namespace.nspname in ('public', 'kb_stage')
|
||||||
|
and relation.relkind in ('r', 'p', 'v', 'm', 'f')
|
||||||
|
and has_table_privilege('leoclean_kb_runtime', relation.oid, 'SELECT')
|
||||||
|
and (namespace.nspname, relation.relname) not in (
|
||||||
|
('public', 'agents'),
|
||||||
|
('public', 'claims'),
|
||||||
|
('public', 'claim_evidence'),
|
||||||
|
('public', 'claim_edges'),
|
||||||
|
('public', 'sources'),
|
||||||
|
('public', 'personas'),
|
||||||
|
('public', 'strategies'),
|
||||||
|
('public', 'beliefs'),
|
||||||
|
('public', 'blindspots'),
|
||||||
|
('public', 'agent_roles'),
|
||||||
|
('public', 'peer_models'),
|
||||||
|
('public', 'behavioral_rules'),
|
||||||
|
('public', 'contributor_rules'),
|
||||||
|
('public', 'reasoning_tools'),
|
||||||
|
('public', 'governance_gates'),
|
||||||
|
('kb_stage', 'kb_proposals')
|
||||||
|
);
|
||||||
|
if unexpected is not null then
|
||||||
|
raise exception 'leoclean_kb_runtime can SELECT outside its table allowlist: %', unexpected;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
select pg_catalog.string_agg(
|
||||||
|
pg_catalog.format('%I.%I.%I', namespace.nspname, relation.relname, attribute.attname),
|
||||||
|
', '
|
||||||
|
order by namespace.nspname, relation.relname, attribute.attnum
|
||||||
|
)
|
||||||
|
into unexpected
|
||||||
|
from pg_catalog.pg_class relation
|
||||||
|
join pg_catalog.pg_namespace namespace on namespace.oid = relation.relnamespace
|
||||||
|
join pg_catalog.pg_attribute attribute on attribute.attrelid = relation.oid
|
||||||
|
where namespace.nspname in ('public', 'kb_stage')
|
||||||
|
and relation.relkind in ('r', 'p', 'v', 'm', 'f')
|
||||||
|
and attribute.attnum > 0
|
||||||
|
and not attribute.attisdropped
|
||||||
|
and has_column_privilege('leoclean_kb_runtime', relation.oid, attribute.attnum, 'SELECT')
|
||||||
|
and (namespace.nspname, relation.relname) not in (
|
||||||
|
('public', 'agents'),
|
||||||
|
('public', 'claims'),
|
||||||
|
('public', 'claim_evidence'),
|
||||||
|
('public', 'claim_edges'),
|
||||||
|
('public', 'sources'),
|
||||||
|
('public', 'personas'),
|
||||||
|
('public', 'strategies'),
|
||||||
|
('public', 'beliefs'),
|
||||||
|
('public', 'blindspots'),
|
||||||
|
('public', 'agent_roles'),
|
||||||
|
('public', 'peer_models'),
|
||||||
|
('public', 'behavioral_rules'),
|
||||||
|
('public', 'contributor_rules'),
|
||||||
|
('public', 'reasoning_tools'),
|
||||||
|
('public', 'governance_gates'),
|
||||||
|
('kb_stage', 'kb_proposals')
|
||||||
|
);
|
||||||
|
if unexpected is not null then
|
||||||
|
raise exception 'leoclean_kb_runtime has column SELECT outside its allowlist: %', unexpected;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
if has_table_privilege('leoclean_kb_stage_owner', 'public.agents', 'SELECT')
|
||||||
|
or not has_column_privilege('leoclean_kb_stage_owner', 'public.agents', 'id', 'SELECT')
|
||||||
|
or not has_column_privilege('leoclean_kb_stage_owner', 'public.agents', 'handle', 'SELECT')
|
||||||
|
or not has_table_privilege('leoclean_kb_stage_owner', 'kb_stage.kb_proposals', 'SELECT')
|
||||||
|
or has_table_privilege('leoclean_kb_stage_owner', 'kb_stage.kb_proposals', 'INSERT') then
|
||||||
|
raise exception 'leoclean_kb_stage_owner does not have the exact read/insert grant shape';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
select pg_catalog.string_agg(
|
||||||
|
pg_catalog.format('%I.%I.%I', namespace.nspname, relation.relname, attribute.attname),
|
||||||
|
', '
|
||||||
|
order by namespace.nspname, relation.relname, attribute.attnum
|
||||||
|
)
|
||||||
|
into unexpected
|
||||||
|
from pg_catalog.pg_class relation
|
||||||
|
join pg_catalog.pg_namespace namespace on namespace.oid = relation.relnamespace
|
||||||
|
join pg_catalog.pg_attribute attribute on attribute.attrelid = relation.oid
|
||||||
|
where namespace.nspname in ('public', 'kb_stage')
|
||||||
|
and relation.relkind in ('r', 'p', 'v', 'm', 'f')
|
||||||
|
and attribute.attnum > 0
|
||||||
|
and not attribute.attisdropped
|
||||||
|
and has_column_privilege('leoclean_kb_stage_owner', relation.oid, attribute.attnum, 'INSERT')
|
||||||
|
and not (
|
||||||
|
namespace.nspname = 'kb_stage'
|
||||||
|
and relation.relname = 'kb_proposals'
|
||||||
|
and attribute.attname in (
|
||||||
|
'proposal_type',
|
||||||
|
'status',
|
||||||
|
'proposed_by_handle',
|
||||||
|
'proposed_by_agent_id',
|
||||||
|
'channel',
|
||||||
|
'source_ref',
|
||||||
|
'rationale',
|
||||||
|
'payload'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
if unexpected is not null then
|
||||||
|
raise exception 'leoclean_kb_stage_owner can INSERT unexpected columns: %', unexpected;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
select pg_catalog.string_agg(
|
||||||
|
pg_catalog.format('%I.%I:%s', namespace.nspname, relation.relname, privilege.name),
|
||||||
|
', '
|
||||||
|
order by namespace.nspname, relation.relname, privilege.name
|
||||||
|
)
|
||||||
|
into unexpected
|
||||||
|
from pg_catalog.pg_class relation
|
||||||
|
join pg_catalog.pg_namespace namespace on namespace.oid = relation.relnamespace
|
||||||
|
cross join (values ('INSERT'), ('UPDATE'), ('DELETE'), ('TRUNCATE'), ('REFERENCES'), ('TRIGGER')) privilege(name)
|
||||||
|
where namespace.nspname in ('public', 'kb_stage')
|
||||||
|
and relation.relkind in ('r', 'p', 'v', 'm', 'f')
|
||||||
|
and has_table_privilege('leoclean_kb_stage_owner', relation.oid, privilege.name);
|
||||||
|
if unexpected is not null then
|
||||||
|
raise exception 'leoclean_kb_stage_owner unexpectedly has table-level DML: %', unexpected;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
select pg_catalog.string_agg(
|
||||||
|
pg_catalog.format('%I.%I.%I:%s', namespace.nspname, relation.relname, attribute.attname, privilege.name),
|
||||||
|
', '
|
||||||
|
order by namespace.nspname, relation.relname, attribute.attnum, privilege.name
|
||||||
|
)
|
||||||
|
into unexpected
|
||||||
|
from pg_catalog.pg_class relation
|
||||||
|
join pg_catalog.pg_namespace namespace on namespace.oid = relation.relnamespace
|
||||||
|
join pg_catalog.pg_attribute attribute on attribute.attrelid = relation.oid
|
||||||
|
cross join (values ('UPDATE'), ('REFERENCES')) privilege(name)
|
||||||
|
where namespace.nspname in ('public', 'kb_stage')
|
||||||
|
and relation.relkind in ('r', 'p', 'v', 'm', 'f')
|
||||||
|
and attribute.attnum > 0
|
||||||
|
and not attribute.attisdropped
|
||||||
|
and has_column_privilege('leoclean_kb_stage_owner', relation.oid, attribute.attnum, privilege.name);
|
||||||
|
if unexpected is not null then
|
||||||
|
raise exception 'leoclean_kb_stage_owner unexpectedly has column-level DML: %', unexpected;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
select pg_catalog.string_agg(
|
||||||
|
pg_catalog.format('%I.%I.%I', namespace.nspname, relation.relname, attribute.attname),
|
||||||
|
', '
|
||||||
|
order by namespace.nspname, relation.relname, attribute.attnum
|
||||||
|
)
|
||||||
|
into unexpected
|
||||||
|
from pg_catalog.pg_class relation
|
||||||
|
join pg_catalog.pg_namespace namespace on namespace.oid = relation.relnamespace
|
||||||
|
join pg_catalog.pg_attribute attribute on attribute.attrelid = relation.oid
|
||||||
|
where namespace.nspname in ('public', 'kb_stage')
|
||||||
|
and relation.relkind in ('r', 'p', 'v', 'm', 'f')
|
||||||
|
and attribute.attnum > 0
|
||||||
|
and not attribute.attisdropped
|
||||||
|
and has_column_privilege('leoclean_kb_stage_owner', relation.oid, attribute.attnum, 'SELECT')
|
||||||
|
and not (
|
||||||
|
(namespace.nspname = 'kb_stage' and relation.relname = 'kb_proposals')
|
||||||
|
or (
|
||||||
|
namespace.nspname = 'public'
|
||||||
|
and relation.relname = 'agents'
|
||||||
|
and attribute.attname in ('id', 'handle')
|
||||||
|
)
|
||||||
|
);
|
||||||
|
if unexpected is not null then
|
||||||
|
raise exception 'leoclean_kb_stage_owner can SELECT unexpected columns: %', unexpected;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
select pg_catalog.string_agg(
|
||||||
|
pg_catalog.format('%I.%I:%s:%s', namespace.nspname, relation.relname, scoped_role.name, privilege.name),
|
||||||
|
', '
|
||||||
|
order by namespace.nspname, relation.relname, scoped_role.name, privilege.name
|
||||||
|
)
|
||||||
|
into unexpected
|
||||||
|
from pg_catalog.pg_class relation
|
||||||
|
join pg_catalog.pg_namespace namespace on namespace.oid = relation.relnamespace
|
||||||
|
cross join (values ('leoclean_kb_runtime'), ('leoclean_kb_stage_owner')) scoped_role(name)
|
||||||
|
cross join (values ('USAGE'), ('SELECT'), ('UPDATE')) privilege(name)
|
||||||
|
where namespace.nspname in ('public', 'kb_stage')
|
||||||
|
and relation.relkind = 'S'
|
||||||
|
and has_sequence_privilege(scoped_role.name, relation.oid, privilege.name);
|
||||||
|
if unexpected is not null then
|
||||||
|
raise exception 'a scoped Leo role unexpectedly has sequence privileges: %', unexpected;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
if exists (
|
||||||
|
select 1
|
||||||
|
from pg_catalog.pg_attribute attribute
|
||||||
|
where attribute.attrelid = 'kb_stage.kb_proposals'::pg_catalog.regclass
|
||||||
|
and attribute.attname in (
|
||||||
|
'proposal_type',
|
||||||
|
'status',
|
||||||
|
'proposed_by_handle',
|
||||||
|
'proposed_by_agent_id',
|
||||||
|
'channel',
|
||||||
|
'source_ref',
|
||||||
|
'rationale',
|
||||||
|
'payload'
|
||||||
|
)
|
||||||
|
and not has_column_privilege(
|
||||||
|
'leoclean_kb_stage_owner',
|
||||||
|
attribute.attrelid,
|
||||||
|
attribute.attnum,
|
||||||
|
'INSERT'
|
||||||
|
)
|
||||||
|
) then
|
||||||
|
raise exception 'leoclean_kb_stage_owner is missing a required INSERT column grant';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
select pg_catalog.string_agg(
|
||||||
|
pg_catalog.format('%I.%I(%s)', namespace.nspname, function_row.proname, pg_catalog.pg_get_function_identity_arguments(function_row.oid)),
|
||||||
|
', '
|
||||||
|
)
|
||||||
|
into unexpected
|
||||||
|
from pg_catalog.pg_proc function_row
|
||||||
|
join pg_catalog.pg_namespace namespace on namespace.oid = function_row.pronamespace
|
||||||
|
where namespace.nspname in ('public', 'kb_stage')
|
||||||
|
and function_row.prosecdef
|
||||||
|
and function_row.oid <> stage_function
|
||||||
|
and has_function_privilege('leoclean_kb_runtime', function_row.oid, 'EXECUTE');
|
||||||
|
if unexpected is not null then
|
||||||
|
raise exception 'leoclean_kb_runtime can execute unexpected SECURITY DEFINER functions: %', unexpected;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
if not exists (
|
||||||
|
select 1
|
||||||
|
from pg_catalog.pg_proc function_row
|
||||||
|
where function_row.oid = stage_function
|
||||||
|
and function_row.prosecdef
|
||||||
|
and function_row.proowner = owner_oid
|
||||||
|
and function_row.proconfig = array['search_path=pg_catalog, pg_temp']::text[]
|
||||||
|
) then
|
||||||
|
raise exception 'stage_leoclean_proposal owner/security/search_path contract is invalid';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
select pg_catalog.string_agg(
|
||||||
|
pg_catalog.format(
|
||||||
|
'grantee=%s privilege=%s grantable=%s',
|
||||||
|
acl.grantee,
|
||||||
|
acl.privilege_type,
|
||||||
|
acl.is_grantable
|
||||||
|
),
|
||||||
|
', '
|
||||||
|
)
|
||||||
|
into unexpected
|
||||||
|
from pg_catalog.pg_proc function_row
|
||||||
|
cross join lateral pg_catalog.aclexplode(
|
||||||
|
coalesce(
|
||||||
|
function_row.proacl,
|
||||||
|
pg_catalog.acldefault('f', function_row.proowner)
|
||||||
|
)
|
||||||
|
) as acl
|
||||||
|
where function_row.oid = stage_function
|
||||||
|
and not (
|
||||||
|
acl.grantee in (runtime_oid, owner_oid)
|
||||||
|
and acl.privilege_type = 'EXECUTE'
|
||||||
|
and (acl.grantee = owner_oid or not acl.is_grantable)
|
||||||
|
);
|
||||||
|
if unexpected is not null then
|
||||||
|
raise exception 'stage_leoclean_proposal has unexpected ACL entries: %', unexpected;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
if not has_function_privilege(
|
||||||
|
'leoclean_kb_runtime',
|
||||||
|
'kb_stage.stage_leoclean_proposal(text,text,text,text,jsonb)',
|
||||||
|
'EXECUTE'
|
||||||
|
) then
|
||||||
|
raise exception 'leoclean_kb_runtime cannot execute stage_leoclean_proposal';
|
||||||
|
end if;
|
||||||
|
end
|
||||||
|
$verification$;
|
||||||
|
|
||||||
|
commit;
|
||||||
|
|
||||||
|
-- The legacy restore database is not an audit fallback for the scoped runtime.
|
||||||
|
-- Strip direct ACLs from both scoped roles and fail if PUBLIC/group grants still
|
||||||
|
-- make any teleo_restore relation reachable.
|
||||||
|
\connect teleo_kb
|
||||||
|
|
||||||
|
begin;
|
||||||
|
|
||||||
|
select format('revoke all on schema %I from leoclean_kb_runtime, leoclean_kb_stage_owner', namespace.nspname)
|
||||||
|
from pg_catalog.pg_namespace namespace
|
||||||
|
where namespace.nspname = 'teleo_restore'
|
||||||
|
\gexec
|
||||||
|
|
||||||
|
select format('revoke all privileges on all tables in schema %I from leoclean_kb_runtime, leoclean_kb_stage_owner', namespace.nspname)
|
||||||
|
from pg_catalog.pg_namespace namespace
|
||||||
|
where namespace.nspname = 'teleo_restore'
|
||||||
|
\gexec
|
||||||
|
|
||||||
|
select format('revoke all privileges on all sequences in schema %I from leoclean_kb_runtime, leoclean_kb_stage_owner', namespace.nspname)
|
||||||
|
from pg_catalog.pg_namespace namespace
|
||||||
|
where namespace.nspname = 'teleo_restore'
|
||||||
|
\gexec
|
||||||
|
|
||||||
|
select format('revoke all privileges on all functions in schema %I from leoclean_kb_runtime, leoclean_kb_stage_owner', namespace.nspname)
|
||||||
|
from pg_catalog.pg_namespace namespace
|
||||||
|
where namespace.nspname = 'teleo_restore'
|
||||||
|
\gexec
|
||||||
|
|
||||||
|
select format('revoke all privileges on all procedures in schema %I from leoclean_kb_runtime, leoclean_kb_stage_owner', namespace.nspname)
|
||||||
|
from pg_catalog.pg_namespace namespace
|
||||||
|
where namespace.nspname = 'teleo_restore'
|
||||||
|
\gexec
|
||||||
|
|
||||||
|
select format(
|
||||||
|
'revoke all privileges (%s) on table %I.%I from %I',
|
||||||
|
pg_catalog.string_agg(pg_catalog.quote_ident(attribute.attname), ', ' order by attribute.attnum),
|
||||||
|
namespace.nspname,
|
||||||
|
relation.relname,
|
||||||
|
target.rolname
|
||||||
|
)
|
||||||
|
from pg_catalog.pg_class relation
|
||||||
|
join pg_catalog.pg_namespace namespace on namespace.oid = relation.relnamespace
|
||||||
|
join pg_catalog.pg_attribute attribute on attribute.attrelid = relation.oid
|
||||||
|
cross join (
|
||||||
|
values ('leoclean_kb_runtime'), ('leoclean_kb_stage_owner')
|
||||||
|
) as target(rolname)
|
||||||
|
where namespace.nspname = 'teleo_restore'
|
||||||
|
and relation.relkind in ('r', 'p', 'v', 'm', 'f')
|
||||||
|
and attribute.attnum > 0
|
||||||
|
and not attribute.attisdropped
|
||||||
|
group by namespace.nspname, relation.relname, target.rolname
|
||||||
|
\gexec
|
||||||
|
|
||||||
|
do $audit_verification$
|
||||||
|
declare
|
||||||
|
unexpected text;
|
||||||
|
scoped_role text;
|
||||||
|
begin
|
||||||
|
foreach scoped_role in array array['leoclean_kb_runtime', 'leoclean_kb_stage_owner'] loop
|
||||||
|
if to_regnamespace('teleo_restore') is not null
|
||||||
|
and (has_schema_privilege(scoped_role, 'teleo_restore', 'USAGE')
|
||||||
|
or has_schema_privilege(scoped_role, 'teleo_restore', 'CREATE')) then
|
||||||
|
raise exception '% unexpectedly has teleo_restore schema access', scoped_role;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
select pg_catalog.string_agg(
|
||||||
|
pg_catalog.format('%I.%I:%s', namespace.nspname, relation.relname, privilege.name),
|
||||||
|
', '
|
||||||
|
order by relation.relname, privilege.name
|
||||||
|
)
|
||||||
|
into unexpected
|
||||||
|
from pg_catalog.pg_class relation
|
||||||
|
join pg_catalog.pg_namespace namespace on namespace.oid = relation.relnamespace
|
||||||
|
cross join (
|
||||||
|
values ('SELECT'), ('INSERT'), ('UPDATE'), ('DELETE'), ('TRUNCATE'), ('REFERENCES'), ('TRIGGER')
|
||||||
|
) privilege(name)
|
||||||
|
where namespace.nspname = 'teleo_restore'
|
||||||
|
and relation.relkind in ('r', 'p', 'v', 'm', 'f')
|
||||||
|
and has_table_privilege(scoped_role, relation.oid, privilege.name);
|
||||||
|
if unexpected is not null then
|
||||||
|
raise exception '% unexpectedly has teleo_restore table access: %', scoped_role, unexpected;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
select pg_catalog.string_agg(
|
||||||
|
pg_catalog.format('%I.%I.%I:%s', namespace.nspname, relation.relname, attribute.attname, privilege.name),
|
||||||
|
', '
|
||||||
|
order by relation.relname, attribute.attnum, privilege.name
|
||||||
|
)
|
||||||
|
into unexpected
|
||||||
|
from pg_catalog.pg_class relation
|
||||||
|
join pg_catalog.pg_namespace namespace on namespace.oid = relation.relnamespace
|
||||||
|
join pg_catalog.pg_attribute attribute on attribute.attrelid = relation.oid
|
||||||
|
cross join (values ('SELECT'), ('INSERT'), ('UPDATE'), ('REFERENCES')) privilege(name)
|
||||||
|
where namespace.nspname = 'teleo_restore'
|
||||||
|
and relation.relkind in ('r', 'p', 'v', 'm', 'f')
|
||||||
|
and attribute.attnum > 0
|
||||||
|
and not attribute.attisdropped
|
||||||
|
and has_column_privilege(scoped_role, relation.oid, attribute.attnum, privilege.name);
|
||||||
|
if unexpected is not null then
|
||||||
|
raise exception '% unexpectedly has teleo_restore column access: %', scoped_role, unexpected;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
select pg_catalog.string_agg(
|
||||||
|
pg_catalog.format('%I.%I:%s', namespace.nspname, relation.relname, privilege.name),
|
||||||
|
', '
|
||||||
|
order by relation.relname, privilege.name
|
||||||
|
)
|
||||||
|
into unexpected
|
||||||
|
from pg_catalog.pg_class relation
|
||||||
|
join pg_catalog.pg_namespace namespace on namespace.oid = relation.relnamespace
|
||||||
|
cross join (values ('USAGE'), ('SELECT'), ('UPDATE')) privilege(name)
|
||||||
|
where namespace.nspname = 'teleo_restore'
|
||||||
|
and relation.relkind = 'S'
|
||||||
|
and has_sequence_privilege(scoped_role, relation.oid, privilege.name);
|
||||||
|
if unexpected is not null then
|
||||||
|
raise exception '% unexpectedly has teleo_restore sequence access: %', scoped_role, unexpected;
|
||||||
|
end if;
|
||||||
|
end loop;
|
||||||
|
end
|
||||||
|
$audit_verification$;
|
||||||
|
|
||||||
|
commit;
|
||||||
|
|
||||||
|
\connect teleo_canonical
|
||||||
|
|
||||||
|
select jsonb_build_object(
|
||||||
|
'role', rolname,
|
||||||
|
'superuser', rolsuper,
|
||||||
|
'create_db', rolcreatedb,
|
||||||
|
'create_role', rolcreaterole,
|
||||||
|
'inherit', rolinherit,
|
||||||
|
'replication', rolreplication,
|
||||||
|
'bypass_rls', rolbypassrls,
|
||||||
|
'has_role_memberships', exists (
|
||||||
|
select 1
|
||||||
|
from pg_catalog.pg_auth_members membership
|
||||||
|
where membership.roleid = pg_catalog.pg_roles.oid
|
||||||
|
or membership.member = pg_catalog.pg_roles.oid
|
||||||
|
),
|
||||||
|
'can_select_claims', has_table_privilege(rolname, 'public.claims', 'SELECT'),
|
||||||
|
'can_select_proposals', has_table_privilege(rolname, 'kb_stage.kb_proposals', 'SELECT'),
|
||||||
|
'can_insert_proposals_directly', has_table_privilege(rolname, 'kb_stage.kb_proposals', 'INSERT'),
|
||||||
|
'can_create_database_objects', has_database_privilege(rolname, 'teleo_canonical', 'CREATE'),
|
||||||
|
'can_create_temp_objects', has_database_privilege(rolname, 'teleo_canonical', 'TEMP'),
|
||||||
|
'can_stage_proposal', has_function_privilege(
|
||||||
|
rolname,
|
||||||
|
'kb_stage.stage_leoclean_proposal(text,text,text,text,jsonb)',
|
||||||
|
'EXECUTE'
|
||||||
|
)
|
||||||
|
)::text
|
||||||
|
from pg_catalog.pg_roles
|
||||||
|
where rolname = 'leoclean_kb_runtime';
|
||||||
|
|
@ -47,7 +47,7 @@ DEFAULT_HOST = "10.61.0.3"
|
||||||
DEFAULT_PROJECT = "teleo-501523"
|
DEFAULT_PROJECT = "teleo-501523"
|
||||||
DEFAULT_SECRET = "gcp-teleo-pgvector-standby-postgres-password"
|
DEFAULT_SECRET = "gcp-teleo-pgvector-standby-postgres-password"
|
||||||
DEFAULT_MANIFEST_SQL = HERE.parent / "ops" / "postgres_parity_manifest.sql"
|
DEFAULT_MANIFEST_SQL = HERE.parent / "ops" / "postgres_parity_manifest.sql"
|
||||||
REVIEWED_CLOUDSQL_TOOL_SHA256 = "7d2074a0fc5f0d48fbf8f7905a72ead8f2696c86a041fa43e078fff5500baa51"
|
REVIEWED_CLOUDSQL_TOOL_SHA256 = "2617bb501456758131ff41a6c77b3c462e21c28c4348da319b60defb051c7c87"
|
||||||
REVIEWED_MANIFEST_SQL_SHA256 = "8b8cdc25d54fdd8de05eb38c6e4423d2836953eb6012d4545f5c9c71b5f0150a"
|
REVIEWED_MANIFEST_SQL_SHA256 = "8b8cdc25d54fdd8de05eb38c6e4423d2836953eb6012d4545f5c9c71b5f0150a"
|
||||||
COUNT_READBACK_RE = re.compile(
|
COUNT_READBACK_RE = re.compile(
|
||||||
r"DB readback:\s*claims:\s*`?(?P<claims>\d+)`?;\s*"
|
r"DB readback:\s*claims:\s*`?(?P<claims>\d+)`?;\s*"
|
||||||
|
|
@ -416,6 +416,8 @@ $PYTHON "$CLOUDSQL_TOOL" \
|
||||||
--db "$TARGET_DATABASE" \
|
--db "$TARGET_DATABASE" \
|
||||||
--canonical-db "$TARGET_DATABASE" \
|
--canonical-db "$TARGET_DATABASE" \
|
||||||
--project "$PROJECT" \
|
--project "$PROJECT" \
|
||||||
|
--credential-mode clone-readonly \
|
||||||
|
--user postgres \
|
||||||
--password-secret "$PASSWORD_SECRET" \
|
--password-secret "$PASSWORD_SECRET" \
|
||||||
"$@"
|
"$@"
|
||||||
STATUS=$?
|
STATUS=$?
|
||||||
|
|
|
||||||
14
systemd/leoclean-gcp-prod-parallel-cloudsql.conf
Normal file
14
systemd/leoclean-gcp-prod-parallel-cloudsql.conf
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
[Service]
|
||||||
|
UnsetEnvironment=PGPASSWORD
|
||||||
|
RuntimeDirectory=leoclean-gcp-prod-parallel
|
||||||
|
RuntimeDirectoryMode=0700
|
||||||
|
Environment=CLOUDSDK_CONFIG=/run/leoclean-gcp-prod-parallel
|
||||||
|
Environment=TELEO_KB_MODE=cloudsql
|
||||||
|
Environment=TELEO_GCP_PROJECT=teleo-501523
|
||||||
|
Environment=TELEO_CLOUDSQL_HOST=10.61.0.3
|
||||||
|
Environment=TELEO_CLOUDSQL_PORT=5432
|
||||||
|
Environment=TELEO_CLOUDSQL_DB=teleo_canonical
|
||||||
|
Environment=TELEO_CANONICAL_CLOUDSQL_DB=teleo_canonical
|
||||||
|
Environment=TELEO_CLOUDSQL_USER=leoclean_kb_runtime
|
||||||
|
Environment=TELEO_CLOUDSQL_PASSWORD_SECRET=gcp-teleo-pgvector-standby-leoclean-kb-runtime-password
|
||||||
|
Environment=TELEO_CLOUDSQL_ENABLE_AUDIT_FALLBACK=0
|
||||||
|
|
@ -115,6 +115,8 @@ def test_wrapper_binds_private_read_only_database_and_records_calls(tmp_path: Pa
|
||||||
assert "dbname=$TARGET_DATABASE" in wrapper
|
assert "dbname=$TARGET_DATABASE" in wrapper
|
||||||
assert '--db "$TARGET_DATABASE"' in wrapper
|
assert '--db "$TARGET_DATABASE"' in wrapper
|
||||||
assert '--canonical-db "$TARGET_DATABASE"' in wrapper
|
assert '--canonical-db "$TARGET_DATABASE"' in wrapper
|
||||||
|
assert "--credential-mode clone-readonly" in wrapper
|
||||||
|
assert "--user postgres" in wrapper
|
||||||
assert "sslmode=require" in wrapper
|
assert "sslmode=require" in wrapper
|
||||||
assert "begin transaction read only" in wrapper
|
assert "begin transaction read only" in wrapper
|
||||||
assert 'export PGOPTIONS="-c default_transaction_read_only=on"' in wrapper
|
assert 'export PGOPTIONS="-c default_transaction_read_only=on"' in wrapper
|
||||||
|
|
@ -306,6 +308,8 @@ def test_generated_wrapper_executes_only_clone_bound_default_read_only_tool(tmp_
|
||||||
"--canonical-db",
|
"--canonical-db",
|
||||||
"teleo_clone_test",
|
"teleo_clone_test",
|
||||||
]
|
]
|
||||||
|
assert tool_receipt["argv"][tool_receipt["argv"].index("--credential-mode") + 1] == "clone-readonly"
|
||||||
|
assert tool_receipt["argv"][tool_receipt["argv"].index("--user") + 1] == "postgres"
|
||||||
events = [json.loads(line) for line in log.read_text().splitlines()]
|
events = [json.loads(line) for line in log.read_text().splitlines()]
|
||||||
assert [event["phase"] for event in events] == ["start", "end"]
|
assert [event["phase"] for event in events] == ["start", "end"]
|
||||||
assert events[0]["database_identity"]["default_transaction_read_only"] == "on"
|
assert events[0]["database_identity"]["default_transaction_read_only"] == "on"
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import io
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
|
|
@ -23,6 +24,7 @@ from scripts.working_leo_open_ended_benchmark import (
|
||||||
ROOT = Path(__file__).resolve().parents[1]
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
BRIDGE_DIR = ROOT / "hermes-agent" / "leoclean-bin"
|
BRIDGE_DIR = ROOT / "hermes-agent" / "leoclean-bin"
|
||||||
DB_CONTEXT_PLUGIN = ROOT / "hermes-agent" / "leoclean-plugins" / "vps" / "leo-db-context" / "__init__.py"
|
DB_CONTEXT_PLUGIN = ROOT / "hermes-agent" / "leoclean-plugins" / "vps" / "leo-db-context" / "__init__.py"
|
||||||
|
GCP_RUNTIME_ROLE_SQL = ROOT / "ops" / "gcp_leoclean_runtime_role.sql"
|
||||||
|
|
||||||
|
|
||||||
def test_leoclean_bridge_files_are_present_and_parseable() -> None:
|
def test_leoclean_bridge_files_are_present_and_parseable() -> None:
|
||||||
|
|
@ -67,7 +69,7 @@ def test_cloudsql_wrapper_supports_expected_review_gated_commands() -> None:
|
||||||
assert "canonical apply" in cloudsql_text.lower()
|
assert "canonical apply" in cloudsql_text.lower()
|
||||||
|
|
||||||
|
|
||||||
def _install_wrapper_fixture(tmp_path: Path) -> tuple[Path, dict[str, str]]:
|
def _install_wrapper_fixture(tmp_path: Path, *, missing: frozenset[str] = frozenset()) -> tuple[Path, dict[str, str]]:
|
||||||
profile = tmp_path / "profile"
|
profile = tmp_path / "profile"
|
||||||
bin_dir = profile / "bin"
|
bin_dir = profile / "bin"
|
||||||
bin_dir.mkdir(parents=True)
|
bin_dir.mkdir(parents=True)
|
||||||
|
|
@ -75,6 +77,7 @@ def _install_wrapper_fixture(tmp_path: Path) -> tuple[Path, dict[str, str]]:
|
||||||
wrapper.write_bytes((BRIDGE_DIR / "teleo-kb").read_bytes())
|
wrapper.write_bytes((BRIDGE_DIR / "teleo-kb").read_bytes())
|
||||||
wrapper.chmod(0o700)
|
wrapper.chmod(0o700)
|
||||||
cloudsql = bin_dir / "cloudsql_memory_tool.py"
|
cloudsql = bin_dir / "cloudsql_memory_tool.py"
|
||||||
|
if "cloudsql-tool" not in missing:
|
||||||
cloudsql.write_text(
|
cloudsql.write_text(
|
||||||
"import json, sys\nprint(json.dumps({'backend': 'cloudsql', 'argv': sys.argv[1:]}))\n",
|
"import json, sys\nprint(json.dumps({'backend': 'cloudsql', 'argv': sys.argv[1:]}))\n",
|
||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
|
|
@ -88,7 +91,13 @@ def _install_wrapper_fixture(tmp_path: Path) -> tuple[Path, dict[str, str]]:
|
||||||
local.chmod(0o700)
|
local.chmod(0o700)
|
||||||
fake_bin = tmp_path / "fake-bin"
|
fake_bin = tmp_path / "fake-bin"
|
||||||
fake_bin.mkdir()
|
fake_bin.mkdir()
|
||||||
|
for name in ("bash", "python3"):
|
||||||
|
target = shutil.which(name)
|
||||||
|
assert target
|
||||||
|
(fake_bin / name).symlink_to(target)
|
||||||
for name in ("psql", "gcloud"):
|
for name in ("psql", "gcloud"):
|
||||||
|
if name in missing:
|
||||||
|
continue
|
||||||
executable = fake_bin / name
|
executable = fake_bin / name
|
||||||
executable.write_text("#!/usr/bin/env bash\nexit 0\n", encoding="utf-8")
|
executable.write_text("#!/usr/bin/env bash\nexit 0\n", encoding="utf-8")
|
||||||
executable.chmod(0o700)
|
executable.chmod(0o700)
|
||||||
|
|
@ -96,7 +105,7 @@ def _install_wrapper_fixture(tmp_path: Path) -> tuple[Path, dict[str, str]]:
|
||||||
**os.environ,
|
**os.environ,
|
||||||
"HERMES_HOME": str(profile),
|
"HERMES_HOME": str(profile),
|
||||||
"TELEO_KB_MODE": "auto",
|
"TELEO_KB_MODE": "auto",
|
||||||
"PATH": f"{fake_bin}:{os.environ['PATH']}",
|
"PATH": str(fake_bin),
|
||||||
}
|
}
|
||||||
return wrapper, env
|
return wrapper, env
|
||||||
|
|
||||||
|
|
@ -137,6 +146,40 @@ def test_cloudsql_wrapper_auto_mode_never_falls_back_after_supported_command_fai
|
||||||
assert "local" not in completed.stdout
|
assert "local" not in completed.stdout
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("missing", ["cloudsql-tool", "psql", "gcloud"])
|
||||||
|
def test_cloudsql_wrapper_auto_mode_fails_closed_when_prerequisite_is_missing(tmp_path: Path, missing: str) -> None:
|
||||||
|
wrapper, env = _install_wrapper_fixture(tmp_path, missing=frozenset({missing}))
|
||||||
|
|
||||||
|
completed = subprocess.run(
|
||||||
|
[str(wrapper), "search", "canonical only", "--format", "json"],
|
||||||
|
text=True,
|
||||||
|
capture_output=True,
|
||||||
|
check=False,
|
||||||
|
env=env,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert completed.returncode == 69
|
||||||
|
assert "local" not in completed.stdout
|
||||||
|
assert "Cloud SQL" in completed.stderr
|
||||||
|
|
||||||
|
|
||||||
|
def test_cloudsql_wrapper_explicit_mode_rejects_unsupported_commands_without_fallback(tmp_path: Path) -> None:
|
||||||
|
wrapper, env = _install_wrapper_fixture(tmp_path)
|
||||||
|
env["TELEO_KB_MODE"] = "cloudsql"
|
||||||
|
|
||||||
|
completed = subprocess.run(
|
||||||
|
[str(wrapper), "prepare-source", "example.md"],
|
||||||
|
text=True,
|
||||||
|
capture_output=True,
|
||||||
|
check=False,
|
||||||
|
env=env,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert completed.returncode == 64
|
||||||
|
assert "local" not in completed.stdout
|
||||||
|
assert "not supported by the Cloud SQL backend" in completed.stderr
|
||||||
|
|
||||||
|
|
||||||
def test_bridge_edge_proposals_stage_strict_apply_payloads() -> None:
|
def test_bridge_edge_proposals_stage_strict_apply_payloads() -> None:
|
||||||
cloudsql_text = (BRIDGE_DIR / "cloudsql_memory_tool.py").read_text()
|
cloudsql_text = (BRIDGE_DIR / "cloudsql_memory_tool.py").read_text()
|
||||||
vps_text = (BRIDGE_DIR / "kb_tool.py").read_text()
|
vps_text = (BRIDGE_DIR / "kb_tool.py").read_text()
|
||||||
|
|
@ -148,6 +191,9 @@ def test_bridge_edge_proposals_stage_strict_apply_payloads() -> None:
|
||||||
assert "'to_claim', to_row.id::text" in text
|
assert "'to_claim', to_row.id::text" in text
|
||||||
assert "'edge_type', p.edge_type::text" in text
|
assert "'edge_type', p.edge_type::text" in text
|
||||||
|
|
||||||
|
assert "kb_stage.stage_leoclean_proposal(" in cloudsql_text
|
||||||
|
assert "insert into kb_stage.kb_proposals" not in cloudsql_text.lower()
|
||||||
|
|
||||||
|
|
||||||
def test_bridge_source_does_not_commit_raw_secret_values() -> None:
|
def test_bridge_source_does_not_commit_raw_secret_values() -> None:
|
||||||
combined = "\n".join(path.read_text(errors="replace") for path in BRIDGE_DIR.iterdir() if path.is_file())
|
combined = "\n".join(path.read_text(errors="replace") for path in BRIDGE_DIR.iterdir() if path.is_file())
|
||||||
|
|
@ -163,6 +209,206 @@ def test_bridge_source_does_not_commit_raw_secret_values() -> None:
|
||||||
assert not re.search(pattern, combined), pattern
|
assert not re.search(pattern, combined), pattern
|
||||||
|
|
||||||
|
|
||||||
|
def test_cloudsql_runtime_requires_the_exact_scoped_identity(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
module = _load_module(BRIDGE_DIR / "cloudsql_memory_tool.py")
|
||||||
|
monkeypatch.delenv("PGPASSWORD", raising=False)
|
||||||
|
|
||||||
|
with pytest.raises(SystemExit, match="requires database user leoclean_kb_runtime"):
|
||||||
|
module.validate_runtime_connection(SimpleNamespace(credential_mode="runtime", user=None, password_secret=None))
|
||||||
|
with pytest.raises(SystemExit, match="requires database user leoclean_kb_runtime"):
|
||||||
|
module.validate_runtime_connection(
|
||||||
|
SimpleNamespace(
|
||||||
|
credential_mode="runtime",
|
||||||
|
user="postgres",
|
||||||
|
password_secret=module.RUNTIME_PASSWORD_SECRET,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
with pytest.raises(SystemExit, match="requires database user leoclean_kb_runtime"):
|
||||||
|
module.validate_runtime_connection(
|
||||||
|
SimpleNamespace(
|
||||||
|
credential_mode="runtime",
|
||||||
|
user=f" {module.RUNTIME_DB_USER}",
|
||||||
|
password_secret=module.RUNTIME_PASSWORD_SECRET,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
with pytest.raises(SystemExit, match="requires scoped password secret"):
|
||||||
|
module.validate_runtime_connection(
|
||||||
|
SimpleNamespace(
|
||||||
|
credential_mode="runtime",
|
||||||
|
user=module.RUNTIME_DB_USER,
|
||||||
|
password_secret="gcp-teleo-pgvector-standby-postgres-password",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
with pytest.raises(SystemExit, match="requires scoped password secret"):
|
||||||
|
module.validate_runtime_connection(
|
||||||
|
SimpleNamespace(credential_mode="runtime", user=module.RUNTIME_DB_USER, password_secret=None)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_cloudsql_runtime_accepts_only_scoped_credentials_and_rejects_inherited_password(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
module = _load_module(BRIDGE_DIR / "cloudsql_memory_tool.py")
|
||||||
|
args = SimpleNamespace(
|
||||||
|
credential_mode="runtime",
|
||||||
|
user=module.RUNTIME_DB_USER,
|
||||||
|
password_secret=module.RUNTIME_PASSWORD_SECRET,
|
||||||
|
)
|
||||||
|
monkeypatch.delenv("PGPASSWORD", raising=False)
|
||||||
|
|
||||||
|
module.validate_runtime_connection(args)
|
||||||
|
|
||||||
|
monkeypatch.setenv("PGPASSWORD", "")
|
||||||
|
with pytest.raises(SystemExit, match="Refusing inherited PGPASSWORD"):
|
||||||
|
module.validate_runtime_connection(args)
|
||||||
|
|
||||||
|
|
||||||
|
def test_cloudsql_clone_mode_is_read_only_and_bound_to_one_disposable_database(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
module = _load_module(BRIDGE_DIR / "cloudsql_memory_tool.py")
|
||||||
|
args = SimpleNamespace(
|
||||||
|
credential_mode="clone-readonly",
|
||||||
|
user="postgres",
|
||||||
|
password_secret=None,
|
||||||
|
canonical_db="teleo_clone_canary_123",
|
||||||
|
db="teleo_clone_canary_123",
|
||||||
|
command="status",
|
||||||
|
)
|
||||||
|
monkeypatch.setenv("PGPASSWORD", "clone-only-password")
|
||||||
|
monkeypatch.setenv("PGOPTIONS", module.CLONE_READ_ONLY_PGOPTIONS)
|
||||||
|
|
||||||
|
module.validate_runtime_connection(args)
|
||||||
|
assert module.password(args) == "clone-only-password"
|
||||||
|
|
||||||
|
args.command = "propose-core-change"
|
||||||
|
with pytest.raises(SystemExit, match="read commands only"):
|
||||||
|
module.validate_runtime_connection(args)
|
||||||
|
args.command = "status"
|
||||||
|
args.canonical_db = "teleo_canonical"
|
||||||
|
with pytest.raises(SystemExit, match=r"teleo_clone_\*"):
|
||||||
|
module.validate_runtime_connection(args)
|
||||||
|
|
||||||
|
|
||||||
|
def test_cloudsql_psql_uses_only_explicit_pg_environment_fields(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
module = _load_module(BRIDGE_DIR / "cloudsql_memory_tool.py")
|
||||||
|
args = SimpleNamespace(
|
||||||
|
credential_mode="runtime",
|
||||||
|
user=module.RUNTIME_DB_USER,
|
||||||
|
password_secret=module.RUNTIME_PASSWORD_SECRET,
|
||||||
|
host="10.61.0.3",
|
||||||
|
port="5432",
|
||||||
|
db="teleo_canonical",
|
||||||
|
canonical_db="teleo_canonical",
|
||||||
|
project="teleo-501523",
|
||||||
|
command="status",
|
||||||
|
)
|
||||||
|
monkeypatch.delenv("PGPASSWORD", raising=False)
|
||||||
|
monkeypatch.setenv("PGHOST", "attacker.invalid")
|
||||||
|
monkeypatch.setenv("PGSERVICE", "untrusted-service")
|
||||||
|
monkeypatch.setenv("PGPASSFILE", "/tmp/untrusted-pgpass")
|
||||||
|
monkeypatch.setenv("PGOPTIONS", "-c search_path=attacker")
|
||||||
|
captured: dict[str, object] = {}
|
||||||
|
|
||||||
|
def fake_run(command, **kwargs):
|
||||||
|
if command[0] == "gcloud":
|
||||||
|
return SimpleNamespace(returncode=0, stdout="scoped-runtime-password\n", stderr="")
|
||||||
|
captured["command"] = command
|
||||||
|
captured["kwargs"] = kwargs
|
||||||
|
return SimpleNamespace(returncode=0, stdout="ok\n", stderr="")
|
||||||
|
|
||||||
|
monkeypatch.setattr(module.subprocess, "run", fake_run)
|
||||||
|
|
||||||
|
assert module.run_psql(args, "select 1;") == "ok\n"
|
||||||
|
assert captured["command"] == ["psql", "-X", "-At", "-q", "-v", "ON_ERROR_STOP=1"]
|
||||||
|
kwargs = captured["kwargs"]
|
||||||
|
assert isinstance(kwargs, dict)
|
||||||
|
env = kwargs["env"]
|
||||||
|
assert env["PGHOST"] == "10.61.0.3"
|
||||||
|
assert env["PGPORT"] == "5432"
|
||||||
|
assert env["PGDATABASE"] == "teleo_canonical"
|
||||||
|
assert env["PGUSER"] == module.RUNTIME_DB_USER
|
||||||
|
assert env["PGSSLMODE"] == "require"
|
||||||
|
assert env["PGCONNECT_TIMEOUT"] == "8"
|
||||||
|
assert env["PGPASSWORD"] == "scoped-runtime-password"
|
||||||
|
assert "PGOPTIONS" not in env
|
||||||
|
assert "PGSERVICE" not in env
|
||||||
|
assert "PGPASSFILE" not in env
|
||||||
|
|
||||||
|
|
||||||
|
def test_cloudsql_proposal_calls_leave_leo_identity_to_the_database(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
module = _load_module(BRIDGE_DIR / "cloudsql_memory_tool.py")
|
||||||
|
captured: list[str] = []
|
||||||
|
|
||||||
|
def fake_psql_json_lines(_args, sql, db=None):
|
||||||
|
captured.append(sql)
|
||||||
|
return [{"id": "proposal-id", "database": db}]
|
||||||
|
|
||||||
|
monkeypatch.setattr(module, "psql_json_lines", fake_psql_json_lines)
|
||||||
|
core_args = SimpleNamespace(
|
||||||
|
proposal_type="revise_claim",
|
||||||
|
target_kind="claim",
|
||||||
|
target_ref="claim-id",
|
||||||
|
current="old",
|
||||||
|
proposed="new",
|
||||||
|
evidence=[],
|
||||||
|
implication=[],
|
||||||
|
originator="",
|
||||||
|
channel="telegram",
|
||||||
|
source_ref="telegram:1",
|
||||||
|
rationale="Grounded correction",
|
||||||
|
canonical_db="teleo_canonical",
|
||||||
|
)
|
||||||
|
edge_args = SimpleNamespace(
|
||||||
|
from_claim="11111111-1111-4111-8111-111111111111",
|
||||||
|
to_claim="22222222-2222-4222-8222-222222222222",
|
||||||
|
edge_type="supports",
|
||||||
|
channel="telegram",
|
||||||
|
source_ref="telegram:2",
|
||||||
|
rationale="Grounded relation",
|
||||||
|
canonical_db="teleo_canonical",
|
||||||
|
)
|
||||||
|
|
||||||
|
module.propose_core_change(core_args)
|
||||||
|
module.propose_edge(edge_args)
|
||||||
|
|
||||||
|
assert "p.proposed_by_handle" not in captured[0]
|
||||||
|
assert "as proposed_by_handle" not in captured[0]
|
||||||
|
assert "p.proposed_by_handle" not in captured[1]
|
||||||
|
assert "as proposed_by_handle" not in captured[1]
|
||||||
|
assert "stage_leoclean_proposal(\n p.proposal_type,\n p.channel," in captured[0]
|
||||||
|
assert "stage_leoclean_proposal(\n 'add_edge',\n p.channel," in captured[1]
|
||||||
|
|
||||||
|
|
||||||
|
def test_gcp_runtime_role_uses_one_narrow_staging_function() -> None:
|
||||||
|
sql = GCP_RUNTIME_ROLE_SQL.read_text()
|
||||||
|
|
||||||
|
assert "security definer" in sql.lower()
|
||||||
|
assert "stage_leoclean_proposal" in sql
|
||||||
|
assert "'pending_review'" in sql
|
||||||
|
assert "revoke all privileges on all tables in schema public, kb_stage" in sql
|
||||||
|
assert "can_insert_proposals_directly" in sql
|
||||||
|
assert "can_create_database_objects" in sql
|
||||||
|
assert "can_create_temp_objects" in sql
|
||||||
|
assert "unexpectedly has database CREATE" in sql
|
||||||
|
assert "REVOKE TEMP FROM PUBLIC would be a" in sql
|
||||||
|
assert "can execute unexpected SECURITY DEFINER functions" in sql
|
||||||
|
assert "revoke all privileges (%s) on table %I.%I from %I" in sql
|
||||||
|
assert re.search(
|
||||||
|
r"grant\s+insert\s*\([^)]*proposal_type[^)]*payload[^)]*\)\s+"
|
||||||
|
r"on\s+kb_stage\.kb_proposals\s+to\s+leoclean_kb_stage_owner",
|
||||||
|
sql,
|
||||||
|
re.IGNORECASE | re.DOTALL,
|
||||||
|
)
|
||||||
|
assert not re.search(
|
||||||
|
r"grant\s+insert(?:\s*\([^)]*\))?\s+on\s+[^;]+\s+to\s+leoclean_kb_runtime",
|
||||||
|
sql,
|
||||||
|
re.IGNORECASE | re.DOTALL,
|
||||||
|
)
|
||||||
|
assert "grant update" not in sql.lower()
|
||||||
|
assert "grant delete" not in sql.lower()
|
||||||
|
|
||||||
|
|
||||||
def _load_module(path: Path):
|
def _load_module(path: Path):
|
||||||
spec = importlib.util.spec_from_file_location(path.stem, path)
|
spec = importlib.util.spec_from_file_location(path.stem, path)
|
||||||
assert spec and spec.loader
|
assert spec and spec.loader
|
||||||
|
|
@ -323,11 +569,16 @@ def test_cloudsql_status_works_without_optional_audit_fallback(monkeypatch) -> N
|
||||||
assert all("teleo_restore.response_audit" not in sql or "to_regclass" in sql for sql, _db in calls)
|
assert all("teleo_restore.response_audit" not in sql or "to_regclass" in sql for sql, _db in calls)
|
||||||
|
|
||||||
|
|
||||||
def test_cloudsql_zero_hit_search_stays_canonical_when_audit_is_unavailable(monkeypatch) -> None:
|
def test_cloudsql_zero_hit_search_stays_canonical_when_audit_is_disabled(monkeypatch) -> None:
|
||||||
module = _load_module(BRIDGE_DIR / "cloudsql_memory_tool.py")
|
module = _load_module(BRIDGE_DIR / "cloudsql_memory_tool.py")
|
||||||
canonical = {"artifact": "teleo_cloudsql_canonical_kb_context", "hit_count_total": 0, "hits": []}
|
canonical = {"artifact": "teleo_cloudsql_canonical_kb_context", "hit_count_total": 0, "hits": []}
|
||||||
monkeypatch.setattr(module, "query_canonical_rows", lambda *_args: canonical.copy())
|
monkeypatch.setattr(module, "query_canonical_rows", lambda *_args: canonical.copy())
|
||||||
monkeypatch.setattr(module, "audit_restore_available", lambda _args: False)
|
monkeypatch.delenv("TELEO_CLOUDSQL_ENABLE_AUDIT_FALLBACK", raising=False)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
module,
|
||||||
|
"audit_restore_available",
|
||||||
|
lambda _args: (_ for _ in ()).throw(AssertionError("audit availability must not be probed")),
|
||||||
|
)
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
module,
|
module,
|
||||||
"query_audit_rows",
|
"query_audit_rows",
|
||||||
|
|
@ -337,7 +588,22 @@ def test_cloudsql_zero_hit_search_stays_canonical_when_audit_is_unavailable(monk
|
||||||
result = module.query_rows(SimpleNamespace(), "unseen query", 8)
|
result = module.query_rows(SimpleNamespace(), "unseen query", 8)
|
||||||
|
|
||||||
assert result["hits"] == []
|
assert result["hits"] == []
|
||||||
assert "audit fallback unavailable" in result["canonical_fallback_reason"]
|
assert "audit fallback disabled" in result["canonical_fallback_reason"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_cloudsql_zero_hit_search_uses_audit_only_when_explicitly_enabled(monkeypatch) -> None:
|
||||||
|
module = _load_module(BRIDGE_DIR / "cloudsql_memory_tool.py")
|
||||||
|
canonical = {"artifact": "teleo_cloudsql_canonical_kb_context", "hit_count_total": 0, "hits": []}
|
||||||
|
audit = {"artifact": "teleo_cloudsql_memory_context", "hits": [{"row_id": "audit-1"}]}
|
||||||
|
monkeypatch.setenv("TELEO_CLOUDSQL_ENABLE_AUDIT_FALLBACK", "1")
|
||||||
|
monkeypatch.setattr(module, "query_canonical_rows", lambda *_args: canonical.copy())
|
||||||
|
monkeypatch.setattr(module, "audit_restore_available", lambda _args: True)
|
||||||
|
monkeypatch.setattr(module, "query_audit_rows", lambda *_args: audit.copy())
|
||||||
|
|
||||||
|
result = module.query_rows(SimpleNamespace(), "unseen query", 8)
|
||||||
|
|
||||||
|
assert result["hits"] == [{"row_id": "audit-1"}]
|
||||||
|
assert result["canonical_fallback_reason"] == "no matching canonical public/persona/strategy/belief rows"
|
||||||
|
|
||||||
|
|
||||||
def test_vps_bridge_verifies_relative_source_artifact_against_canonical_hash(tmp_path: Path) -> None:
|
def test_vps_bridge_verifies_relative_source_artifact_against_canonical_hash(tmp_path: Path) -> None:
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ def test_auto_deploy_prefers_github_remote_when_present():
|
||||||
auto_deploy = (REPO_ROOT / "deploy" / "auto-deploy.sh").read_text()
|
auto_deploy = (REPO_ROOT / "deploy" / "auto-deploy.sh").read_text()
|
||||||
|
|
||||||
assert 'DEPLOY_REMOTE="${TELEO_DEPLOY_REMOTE:-}"' in auto_deploy
|
assert 'DEPLOY_REMOTE="${TELEO_DEPLOY_REMOTE:-}"' in auto_deploy
|
||||||
assert 'remote get-url github' in auto_deploy
|
assert "remote get-url github" in auto_deploy
|
||||||
assert 'git fetch "$DEPLOY_REMOTE" main' in auto_deploy
|
assert 'git fetch "$DEPLOY_REMOTE" main' in auto_deploy
|
||||||
assert 'git rev-parse "$DEPLOY_REMOTE/main"' in auto_deploy
|
assert 'git rev-parse "$DEPLOY_REMOTE/main"' in auto_deploy
|
||||||
assert 'git merge --ff-only "$DEPLOY_REMOTE/main"' in auto_deploy
|
assert 'git merge --ff-only "$DEPLOY_REMOTE/main"' in auto_deploy
|
||||||
|
|
@ -50,7 +50,7 @@ def test_auto_deploy_prefers_github_remote_when_present():
|
||||||
def test_auto_deploy_executable_safety_net_stays_inside_synced_dirs():
|
def test_auto_deploy_executable_safety_net_stays_inside_synced_dirs():
|
||||||
auto_deploy = (REPO_ROOT / "deploy" / "auto-deploy.sh").read_text()
|
auto_deploy = (REPO_ROOT / "deploy" / "auto-deploy.sh").read_text()
|
||||||
|
|
||||||
assert "for dir in \"$PIPELINE_DIR\" \"$TELEGRAM_DIR\" \"$DIAGNOSTICS_DIR\" \"$AGENT_STATE_DIR\"" in auto_deploy
|
assert 'for dir in "$PIPELINE_DIR" "$TELEGRAM_DIR" "$DIAGNOSTICS_DIR" "$AGENT_STATE_DIR"' in auto_deploy
|
||||||
assert "find /opt/teleo-eval -maxdepth 3 -name '*.sh'" not in auto_deploy
|
assert "find /opt/teleo-eval -maxdepth 3 -name '*.sh'" not in auto_deploy
|
||||||
assert "backups" in auto_deploy
|
assert "backups" in auto_deploy
|
||||||
|
|
||||||
|
|
@ -61,7 +61,7 @@ def test_agent_healthcheck_timer_runs_both_live_telegram_agents():
|
||||||
|
|
||||||
assert "/opt/teleo-eval/telegram/agent_healthcheck.py" in service
|
assert "/opt/teleo-eval/telegram/agent_healthcheck.py" in service
|
||||||
assert "--agents leo leo-wallet-test" in service
|
assert "--agents leo leo-wallet-test" in service
|
||||||
assert "--since \"20 min ago\"" in service
|
assert '--since "20 min ago"' in service
|
||||||
assert "OnUnitActiveSec=5min" in timer
|
assert "OnUnitActiveSec=5min" in timer
|
||||||
assert "WantedBy=timers.target" in timer
|
assert "WantedBy=timers.target" in timer
|
||||||
|
|
||||||
|
|
@ -74,7 +74,7 @@ def test_deploy_scripts_install_systemd_units_and_enable_agent_healthcheck():
|
||||||
assert 'sudo install -m 0644 "$unit" "$SYSTEMD_DIR/$(basename "$unit")"' in auto_deploy
|
assert 'sudo install -m 0644 "$unit" "$SYSTEMD_DIR/$(basename "$unit")"' in auto_deploy
|
||||||
assert "sudo systemctl daemon-reload" in auto_deploy
|
assert "sudo systemctl daemon-reload" in auto_deploy
|
||||||
assert "sudo systemctl enable --now teleo-agent-healthcheck.timer" in auto_deploy
|
assert "sudo systemctl enable --now teleo-agent-healthcheck.timer" in auto_deploy
|
||||||
assert "git diff --name-only \"$OLD_SHA\" \"$NEW_SHA\" -- systemd/teleo-agent@.service" in auto_deploy
|
assert 'git diff --name-only "$OLD_SHA" "$NEW_SHA" -- systemd/teleo-agent@.service' in auto_deploy
|
||||||
|
|
||||||
assert 'VPS_SYSTEMD="/etc/systemd/system"' in manual_deploy
|
assert 'VPS_SYSTEMD="/etc/systemd/system"' in manual_deploy
|
||||||
assert "teleo-agent-healthcheck.timer" in manual_deploy
|
assert "teleo-agent-healthcheck.timer" in manual_deploy
|
||||||
|
|
@ -100,16 +100,16 @@ def test_deploy_scripts_sync_leoclean_skills_and_restart_gateway_for_runtime_cha
|
||||||
assert 'LEOCLEAN_BIN_DIR="/home/teleo/.hermes/profiles/leoclean/bin"' in auto_deploy
|
assert 'LEOCLEAN_BIN_DIR="/home/teleo/.hermes/profiles/leoclean/bin"' in auto_deploy
|
||||||
assert 'LEOCLEAN_SKILLS_DIR="/home/teleo/.hermes/profiles/leoclean/skills"' in auto_deploy
|
assert 'LEOCLEAN_SKILLS_DIR="/home/teleo/.hermes/profiles/leoclean/skills"' in auto_deploy
|
||||||
assert 'LEOCLEAN_PLUGINS_DIR="/home/teleo/.hermes/profiles/leoclean/plugins"' in auto_deploy
|
assert 'LEOCLEAN_PLUGINS_DIR="/home/teleo/.hermes/profiles/leoclean/plugins"' in auto_deploy
|
||||||
assert 'hermes-agent/leoclean-bin/' in auto_deploy
|
assert "hermes-agent/leoclean-bin/" in auto_deploy
|
||||||
assert 'hermes-agent/leoclean-skills/vps/' in auto_deploy
|
assert "hermes-agent/leoclean-skills/vps/" in auto_deploy
|
||||||
assert 'hermes-agent/leoclean-plugins/vps/' in auto_deploy
|
assert "hermes-agent/leoclean-plugins/vps/" in auto_deploy
|
||||||
assert 'HERMES_PATCH_DIR="/home/teleo/.hermes/teleo-runtime-patches"' in auto_deploy
|
assert 'HERMES_PATCH_DIR="/home/teleo/.hermes/teleo-runtime-patches"' in auto_deploy
|
||||||
assert '"status": "installed_now"' in auto_deploy
|
assert '"status": "installed_now"' in auto_deploy
|
||||||
assert 'Hermes response-transform drift repaired' in auto_deploy
|
assert "Hermes response-transform drift repaired" in auto_deploy
|
||||||
assert 'deploy/leoclean-gateway-restart-required.sh' in auto_deploy
|
assert "deploy/leoclean-gateway-restart-required.sh" in auto_deploy
|
||||||
assert 'TELEO_AUTO_DEPLOY_REEXECED' in auto_deploy
|
assert "TELEO_AUTO_DEPLOY_REEXECED" in auto_deploy
|
||||||
assert 'exec bash "$DEPLOY_CHECKOUT/deploy/auto-deploy.sh"' in auto_deploy
|
assert 'exec bash "$DEPLOY_CHECKOUT/deploy/auto-deploy.sh"' in auto_deploy
|
||||||
assert 'add_restart_if_unit_active leoclean-gateway' in auto_deploy
|
assert "add_restart_if_unit_active leoclean-gateway" in auto_deploy
|
||||||
assert 'sudo systemctl restart "$svc"' in auto_deploy
|
assert 'sudo systemctl restart "$svc"' in auto_deploy
|
||||||
assert "sudo systemctl restart $RESTART" not in auto_deploy
|
assert "sudo systemctl restart $RESTART" not in auto_deploy
|
||||||
assert "NOPASSWD: /usr/bin/systemctl restart leoclean-gateway" in sudoers
|
assert "NOPASSWD: /usr/bin/systemctl restart leoclean-gateway" in sudoers
|
||||||
|
|
@ -117,11 +117,11 @@ def test_deploy_scripts_sync_leoclean_skills_and_restart_gateway_for_runtime_cha
|
||||||
assert 'VPS_LEOCLEAN_BIN="/home/teleo/.hermes/profiles/leoclean/bin"' in manual_deploy
|
assert 'VPS_LEOCLEAN_BIN="/home/teleo/.hermes/profiles/leoclean/bin"' in manual_deploy
|
||||||
assert 'VPS_LEOCLEAN_SKILLS="/home/teleo/.hermes/profiles/leoclean/skills"' in manual_deploy
|
assert 'VPS_LEOCLEAN_SKILLS="/home/teleo/.hermes/profiles/leoclean/skills"' in manual_deploy
|
||||||
assert 'VPS_LEOCLEAN_PLUGINS="/home/teleo/.hermes/profiles/leoclean/plugins"' in manual_deploy
|
assert 'VPS_LEOCLEAN_PLUGINS="/home/teleo/.hermes/profiles/leoclean/plugins"' in manual_deploy
|
||||||
assert 'hermes-agent/leoclean-bin/' in manual_deploy
|
assert "hermes-agent/leoclean-bin/" in manual_deploy
|
||||||
assert 'hermes-agent/leoclean-skills/vps/' in manual_deploy
|
assert "hermes-agent/leoclean-skills/vps/" in manual_deploy
|
||||||
assert 'hermes-agent/leoclean-plugins/vps/' in manual_deploy
|
assert "hermes-agent/leoclean-plugins/vps/" in manual_deploy
|
||||||
assert 'VPS_HERMES_PATCHES="/home/teleo/.hermes/teleo-runtime-patches"' in manual_deploy
|
assert 'VPS_HERMES_PATCHES="/home/teleo/.hermes/teleo-runtime-patches"' in manual_deploy
|
||||||
assert 'systemctl is-active --quiet leoclean-gateway.service' in manual_deploy
|
assert "systemctl is-active --quiet leoclean-gateway.service" in manual_deploy
|
||||||
|
|
||||||
|
|
||||||
def test_deploy_scripts_syntax_check_leoclean_database_runtime_before_restart():
|
def test_deploy_scripts_syntax_check_leoclean_database_runtime_before_restart():
|
||||||
|
|
@ -135,32 +135,22 @@ def test_deploy_scripts_syntax_check_leoclean_database_runtime_before_restart():
|
||||||
|
|
||||||
|
|
||||||
def test_auto_deploy_hot_syncs_leoclean_markdown_without_gateway_restart():
|
def test_auto_deploy_hot_syncs_leoclean_markdown_without_gateway_restart():
|
||||||
assert not _leoclean_gateway_restart_required(
|
assert not _leoclean_gateway_restart_required("hermes-agent/leoclean-skills/vps/teleo-kb-bridge/SKILL.md")
|
||||||
"hermes-agent/leoclean-skills/vps/teleo-kb-bridge/SKILL.md"
|
assert not _leoclean_gateway_restart_required("hermes-agent/leoclean-skills/vps/live-leo-telegram/SKILL.md")
|
||||||
)
|
|
||||||
assert not _leoclean_gateway_restart_required(
|
|
||||||
"hermes-agent/leoclean-skills/vps/live-leo-telegram/SKILL.md"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_auto_deploy_restarts_gateway_for_leoclean_runtime_changes():
|
def test_auto_deploy_restarts_gateway_for_leoclean_runtime_changes():
|
||||||
assert _leoclean_gateway_restart_required("hermes-agent/leoclean-bin/kb_tool.py")
|
assert _leoclean_gateway_restart_required("hermes-agent/leoclean-bin/kb_tool.py")
|
||||||
assert _leoclean_gateway_restart_required(
|
assert _leoclean_gateway_restart_required("hermes-agent/leoclean-skills/vps/teleo-kb-bridge/runtime_helper.py")
|
||||||
"hermes-agent/leoclean-skills/vps/teleo-kb-bridge/runtime_helper.py"
|
assert _leoclean_gateway_restart_required("hermes-agent/leoclean-plugins/vps/leo-db-context/__init__.py")
|
||||||
)
|
assert _leoclean_gateway_restart_required("hermes-agent/patches/apply_response_transform_hook.py")
|
||||||
assert _leoclean_gateway_restart_required(
|
|
||||||
"hermes-agent/leoclean-plugins/vps/leo-db-context/__init__.py"
|
|
||||||
)
|
|
||||||
assert _leoclean_gateway_restart_required(
|
|
||||||
"hermes-agent/patches/apply_response_transform_hook.py"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_auto_deploy_sudoers_installer_is_root_only_and_visudo_checked():
|
def test_auto_deploy_sudoers_installer_is_root_only_and_visudo_checked():
|
||||||
installer = (REPO_ROOT / "deploy" / "install-auto-deploy-sudoers.sh").read_text()
|
installer = (REPO_ROOT / "deploy" / "install-auto-deploy-sudoers.sh").read_text()
|
||||||
|
|
||||||
assert 'TARGET="/etc/sudoers.d/teleo-auto-deploy"' in installer
|
assert 'TARGET="/etc/sudoers.d/teleo-auto-deploy"' in installer
|
||||||
assert 'id -u' in installer
|
assert "id -u" in installer
|
||||||
assert 'install -m 0440 "$SOURCE" "$TARGET"' in installer
|
assert 'install -m 0440 "$SOURCE" "$TARGET"' in installer
|
||||||
assert 'visudo -cf "$TARGET"' in installer
|
assert 'visudo -cf "$TARGET"' in installer
|
||||||
|
|
||||||
|
|
@ -183,3 +173,45 @@ def test_gcp_leoclean_skill_sync_targets_parallel_runtime_without_secrets():
|
||||||
assert "sudo rsync" not in script
|
assert "sudo rsync" not in script
|
||||||
assert "TELEGRAM_BOT_TOKEN" not in script
|
assert "TELEGRAM_BOT_TOKEN" not in script
|
||||||
assert "CLOUDSQL_PASSWORD" not in script
|
assert "CLOUDSQL_PASSWORD" not in script
|
||||||
|
|
||||||
|
|
||||||
|
def test_gcp_runtime_sync_is_source_bound_and_scoped():
|
||||||
|
script = (REPO_ROOT / "deploy" / "sync-gcp-leoclean-runtime.sh").read_text()
|
||||||
|
dropin = (REPO_ROOT / "systemd" / "leoclean-gcp-prod-parallel-cloudsql.conf").read_text()
|
||||||
|
|
||||||
|
assert 'git -C "$REPO_ROOT" rev-parse HEAD' in script
|
||||||
|
assert 'git -C "$REPO_ROOT" diff --quiet HEAD' in script
|
||||||
|
assert "merge-base --is-ancestor" in script
|
||||||
|
assert "Refusing to deploy unmerged revision" in script
|
||||||
|
assert "Refusing to install runtime files without --restart" in script
|
||||||
|
assert 'DROPIN_NAME="10-cloudsql-memory.conf"' in script
|
||||||
|
assert "20-canonical-kb.conf" not in script
|
||||||
|
assert "sha256sum" in script
|
||||||
|
assert "--restart" in script
|
||||||
|
assert 'python3 "$tool_path" status --format json --redacted' in script
|
||||||
|
assert "sa-teleo-prod-vm@teleo-501523.iam.gserviceaccount.com" in script
|
||||||
|
assert "metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/email" in script
|
||||||
|
assert "env -u PGPASSWORD" in script
|
||||||
|
assert 'run_scoped_status pre "$remote_tmp/$TOOL_REL"' in script
|
||||||
|
assert 'run_scoped_status post "$profile_bin/cloudsql_memory_tool.py"' in script
|
||||||
|
assert "backup_path" in script
|
||||||
|
assert "restore_path" in script
|
||||||
|
assert "rollback_runtime" in script
|
||||||
|
assert "trap 'on_exit $?' EXIT" in script
|
||||||
|
assert 'backup_path "$dropin_dir/$DROPIN_NAME" dropin' in script
|
||||||
|
assert 'dropin_stage="$dropin_dir/.${DROPIN_NAME}.livingip-new.$$"' in script
|
||||||
|
assert 'sudo mv -f -- "$dropin_stage" "$dropin_dir/$DROPIN_NAME"' in script
|
||||||
|
assert 'active_state" != "active"' in script
|
||||||
|
assert 'sub_state" != "running"' in script
|
||||||
|
assert 'sudo systemctl stop "$SERVICE"' in script
|
||||||
|
assert 'sudo systemctl start "$SERVICE"' in script
|
||||||
|
assert "UnsetEnvironment=PGPASSWORD" in dropin
|
||||||
|
assert "RuntimeDirectory=leoclean-gcp-prod-parallel" in dropin
|
||||||
|
assert "RuntimeDirectoryMode=0700" in dropin
|
||||||
|
assert "Environment=CLOUDSDK_CONFIG=/run/leoclean-gcp-prod-parallel" in dropin
|
||||||
|
assert "TELEO_KB_MODE=cloudsql" in dropin
|
||||||
|
assert "TELEO_CLOUDSQL_USER=leoclean_kb_runtime" in dropin
|
||||||
|
assert "gcp-teleo-pgvector-standby-leoclean-kb-runtime-password" in dropin
|
||||||
|
assert "TELEO_CLOUDSQL_ENABLE_AUDIT_FALLBACK=0" in dropin
|
||||||
|
assert not any(line.startswith("Environment=PGPASSWORD") for line in dropin.splitlines())
|
||||||
|
assert "gcp-teleo-pgvector-standby-postgres-password" not in dropin
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue