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_pull_request": 144,
|
||||
"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"
|
||||
},
|
||||
"deployment": {
|
||||
|
|
@ -55,7 +56,7 @@
|
|||
"integration_regression": {
|
||||
"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.",
|
||||
"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,
|
||||
"full_repository_tests": {
|
||||
"passed": 1240,
|
||||
|
|
@ -113,8 +114,10 @@
|
|||
"post_restart_model_turn_run": false
|
||||
},
|
||||
"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": [
|
||||
"repository/runtime parity for the wrapper",
|
||||
"least-privilege Cloud SQL runtime credential",
|
||||
"post-restart model reasoning turn",
|
||||
"Telegram-visible delivery",
|
||||
"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,
|
||||
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
|
||||
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
|
||||
active/running with a new PID (`304036`) and the deployed hashes remained in
|
||||
place.
|
||||
|
|
@ -105,9 +106,12 @@ place.
|
|||
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
|
||||
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
|
||||
command directly to Cloud SQL and fails closed on errors. Two regression tests
|
||||
cover direct routing and no fallback.
|
||||
a different local tool. The manually deployed wrapper routes every supported
|
||||
GCP knowledge command directly to Cloud SQL and propagates errors after backend
|
||||
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
|
||||
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-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-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
|
||||
|
||||
|
|
@ -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
|
||||
proof in this report is the real service plus live `teleo-kb` status/search
|
||||
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 same database-grounded reasoning, followed by one explicitly approved
|
||||
|
|
|
|||
|
|
@ -74,6 +74,9 @@ STOPWORDS = {
|
|||
|
||||
DEFAULT_CLAIM_BASE_URL = "https://leo.livingip.xyz"
|
||||
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(
|
||||
{
|
||||
"search",
|
||||
|
|
@ -94,14 +97,20 @@ def parse_args() -> argparse.Namespace:
|
|||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
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("--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("--user", default=os.environ.get("TELEO_CLOUDSQL_USER", "postgres"))
|
||||
parser.add_argument("--user", default=os.environ.get("TELEO_CLOUDSQL_USER"))
|
||||
parser.add_argument(
|
||||
"--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(
|
||||
"--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(
|
||||
"--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("--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("--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("--channel", default="telegram")
|
||||
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("edge_type")
|
||||
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("--source-ref", required=True)
|
||||
propose_edge.add_argument("--rationale", required=True)
|
||||
|
|
@ -228,6 +237,34 @@ def parse_args() -> argparse.Namespace:
|
|||
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]:
|
||||
terms: list[str] = []
|
||||
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:
|
||||
if os.environ.get("PGPASSWORD"):
|
||||
validate_runtime_connection(args)
|
||||
if args.credential_mode == "clone-readonly" and os.environ.get("PGPASSWORD"):
|
||||
return os.environ["PGPASSWORD"]
|
||||
if not args.password_secret:
|
||||
raise SystemExit("Scoped Cloud SQL password secret is not configured.")
|
||||
result = subprocess.run(
|
||||
[
|
||||
"gcloud",
|
||||
|
|
@ -287,15 +327,32 @@ def password(args: argparse.Namespace) -> str:
|
|||
)
|
||||
if result.returncode != 0:
|
||||
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:
|
||||
env = os.environ.copy()
|
||||
env["PGPASSWORD"] = password(args)
|
||||
conn = f"host={args.host} port={args.port} dbname={db or args.db} user={args.user} sslmode=require"
|
||||
for key in tuple(env):
|
||||
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(
|
||||
["psql", conn, "-At", "-q", "-v", "ON_ERROR_STOP=1"],
|
||||
["psql", "-X", "-At", "-q", "-v", "ON_ERROR_STOP=1"],
|
||||
text=True,
|
||||
capture_output=True,
|
||||
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)
|
||||
if canonical.get("hit_count_total", 0) or canonical.get("hits"):
|
||||
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):
|
||||
canonical["canonical_fallback_reason"] = (
|
||||
"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 (
|
||||
select
|
||||
{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.source_ref)}, '') as source_ref,
|
||||
{sql_literal(args.rationale)} as rationale,
|
||||
{sql_literal(payload_json)}::jsonb as payload
|
||||
), proposer as (
|
||||
select a.id, a.handle
|
||||
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
|
||||
select kb_stage.stage_leoclean_proposal(
|
||||
p.proposal_type,
|
||||
'pending_review',
|
||||
p.proposed_by_handle,
|
||||
proposer.id,
|
||||
coalesce(p.channel, 'telegram'),
|
||||
p.channel,
|
||||
p.source_ref,
|
||||
p.rationale,
|
||||
p.payload
|
||||
) as proposal
|
||||
from params p
|
||||
left join proposer on true
|
||||
returning *
|
||||
)
|
||||
select jsonb_build_object(
|
||||
'artifact', 'teleo_cloudsql_kb_core_change_proposal',
|
||||
'backend', {sql_literal(canonical_backend(args))},
|
||||
'id', id::text,
|
||||
'proposal_type', proposal_type,
|
||||
'status', status,
|
||||
'proposed_by_handle', proposed_by_handle,
|
||||
'proposed_by_agent_id', proposed_by_agent_id::text,
|
||||
'channel', channel,
|
||||
'source_ref', source_ref,
|
||||
'rationale', rationale,
|
||||
'payload', payload,
|
||||
'created_at', created_at::text,
|
||||
'id', proposal->>'id',
|
||||
'proposal_type', proposal->>'proposal_type',
|
||||
'status', proposal->>'status',
|
||||
'proposed_by_handle', proposal->>'proposed_by_handle',
|
||||
'proposed_by_agent_id', proposal->>'proposed_by_agent_id',
|
||||
'channel', proposal->>'channel',
|
||||
'source_ref', proposal->>'source_ref',
|
||||
'rationale', proposal->>'rationale',
|
||||
'payload', proposal->'payload',
|
||||
'created_at', proposal->>'created_at',
|
||||
'canonical_apply_done', false,
|
||||
'runtime_memory_write_done', false
|
||||
)::text
|
||||
from inserted;
|
||||
from proposer;
|
||||
"""
|
||||
rows = psql_json_lines(args, sql, db=args.canonical_db)
|
||||
if not rows:
|
||||
|
|
@ -1119,7 +1161,6 @@ with params as (
|
|||
{sql_literal(args.from_claim)}::uuid as from_claim,
|
||||
{sql_literal(args.to_claim)}::uuid as to_claim,
|
||||
{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.source_ref)}, '') as source_ref,
|
||||
{sql_literal(args.rationale)} as rationale
|
||||
|
|
@ -1131,11 +1172,6 @@ with params as (
|
|||
select c.id, c.text
|
||||
from public.claims c, params p
|
||||
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 (
|
||||
select e.id
|
||||
from public.claim_edges e, params p
|
||||
|
|
@ -1143,23 +1179,10 @@ with params as (
|
|||
and e.to_claim = p.to_claim
|
||||
and e.edge_type = p.edge_type
|
||||
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
|
||||
), staged as (
|
||||
select kb_stage.stage_leoclean_proposal(
|
||||
'add_edge',
|
||||
'pending_review',
|
||||
p.proposed_by_handle,
|
||||
proposer.id,
|
||||
coalesce(p.channel, 'telegram'),
|
||||
p.channel,
|
||||
p.source_ref,
|
||||
p.rationale,
|
||||
jsonb_build_object(
|
||||
|
|
@ -1175,29 +1198,28 @@ with params as (
|
|||
'edge_type', p.edge_type::text
|
||||
)
|
||||
)
|
||||
) as proposal
|
||||
from params p
|
||||
join from_row on true
|
||||
join to_row on true
|
||||
left join proposer on true
|
||||
returning *
|
||||
)
|
||||
select jsonb_build_object(
|
||||
'artifact', 'teleo_cloudsql_kb_edge_proposal',
|
||||
'backend', {sql_literal(canonical_backend(args))},
|
||||
'id', id::text,
|
||||
'proposal_type', proposal_type,
|
||||
'status', status,
|
||||
'proposed_by_handle', proposed_by_handle,
|
||||
'proposed_by_agent_id', proposed_by_agent_id::text,
|
||||
'channel', channel,
|
||||
'source_ref', source_ref,
|
||||
'rationale', rationale,
|
||||
'payload', payload,
|
||||
'created_at', created_at::text,
|
||||
'id', proposal->>'id',
|
||||
'proposal_type', proposal->>'proposal_type',
|
||||
'status', proposal->>'status',
|
||||
'proposed_by_handle', proposal->>'proposed_by_handle',
|
||||
'proposed_by_agent_id', proposal->>'proposed_by_agent_id',
|
||||
'channel', proposal->>'channel',
|
||||
'source_ref', proposal->>'source_ref',
|
||||
'rationale', proposal->>'rationale',
|
||||
'payload', proposal->'payload',
|
||||
'created_at', proposal->>'created_at',
|
||||
'canonical_apply_done', false,
|
||||
'runtime_memory_write_done', false
|
||||
)::text
|
||||
from inserted;
|
||||
from staged;
|
||||
"""
|
||||
rows = psql_json_lines(args, sql, db=args.canonical_db)
|
||||
if not rows:
|
||||
|
|
@ -1365,6 +1387,8 @@ select jsonb_build_object(
|
|||
|
||||
|
||||
def audit_restore_available(args: argparse.Namespace) -> bool:
|
||||
if os.environ.get("TELEO_CLOUDSQL_ENABLE_AUDIT_FALLBACK", "") != "1":
|
||||
return False
|
||||
sql = """
|
||||
select bool_and(to_regclass(table_name) is not null)
|
||||
from (values
|
||||
|
|
@ -1675,6 +1699,7 @@ def emit_markdown(value: dict[str, Any]) -> None:
|
|||
|
||||
def main() -> None:
|
||||
args = parse_args()
|
||||
validate_runtime_connection(args)
|
||||
read_loaders = {
|
||||
"search": 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() {
|
||||
if cloudsql_supported; then
|
||||
extra_args=()
|
||||
while IFS= read -r arg; do
|
||||
[ -n "$arg" ] && extra_args+=("$arg")
|
||||
done < <(cloudsql_args)
|
||||
exec python3 "$CLOUDSQL_TOOL" "$@" "${extra_args[@]}"
|
||||
if ! cloudsql_supported; then
|
||||
echo "teleo-kb: command '$COMMAND' is not supported by the Cloud SQL backend" >&2
|
||||
exit 64
|
||||
fi
|
||||
exec python3 "$KB_TOOL" "$@"
|
||||
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=()
|
||||
while IFS= read -r arg; do
|
||||
[ -n "$arg" ] && extra_args+=("$arg")
|
||||
done < <(cloudsql_args)
|
||||
exec python3 "$CLOUDSQL_TOOL" "$@" "${extra_args[@]}"
|
||||
}
|
||||
|
||||
case "$MODE" in
|
||||
|
|
@ -46,8 +57,8 @@ case "$MODE" in
|
|||
auto)
|
||||
# Canonical commands must fail closed on Cloud SQL errors. Falling through
|
||||
# to a different local database would make source-of-truth behavior depend
|
||||
# on health-check latency.
|
||||
if [ -x "$CLOUDSQL_TOOL" ] && command -v psql >/dev/null 2>&1 && command -v gcloud >/dev/null 2>&1 && cloudsql_supported; then
|
||||
# on health-check latency or host package state.
|
||||
if cloudsql_supported; then
|
||||
run_cloudsql "$@"
|
||||
fi
|
||||
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_SECRET = "gcp-teleo-pgvector-standby-postgres-password"
|
||||
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"
|
||||
COUNT_READBACK_RE = re.compile(
|
||||
r"DB readback:\s*claims:\s*`?(?P<claims>\d+)`?;\s*"
|
||||
|
|
@ -416,6 +416,8 @@ $PYTHON "$CLOUDSQL_TOOL" \
|
|||
--db "$TARGET_DATABASE" \
|
||||
--canonical-db "$TARGET_DATABASE" \
|
||||
--project "$PROJECT" \
|
||||
--credential-mode clone-readonly \
|
||||
--user postgres \
|
||||
--password-secret "$PASSWORD_SECRET" \
|
||||
"$@"
|
||||
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 '--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 "begin transaction read only" 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",
|
||||
"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()]
|
||||
assert [event["phase"] for event in events] == ["start", "end"]
|
||||
assert events[0]["database_identity"]["default_transaction_read_only"] == "on"
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import io
|
|||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
|
@ -23,6 +24,7 @@ from scripts.working_leo_open_ended_benchmark import (
|
|||
ROOT = Path(__file__).resolve().parents[1]
|
||||
BRIDGE_DIR = ROOT / "hermes-agent" / "leoclean-bin"
|
||||
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:
|
||||
|
|
@ -67,7 +69,7 @@ def test_cloudsql_wrapper_supports_expected_review_gated_commands() -> None:
|
|||
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"
|
||||
bin_dir = profile / "bin"
|
||||
bin_dir.mkdir(parents=True)
|
||||
|
|
@ -75,11 +77,12 @@ def _install_wrapper_fixture(tmp_path: Path) -> tuple[Path, dict[str, str]]:
|
|||
wrapper.write_bytes((BRIDGE_DIR / "teleo-kb").read_bytes())
|
||||
wrapper.chmod(0o700)
|
||||
cloudsql = bin_dir / "cloudsql_memory_tool.py"
|
||||
cloudsql.write_text(
|
||||
"import json, sys\nprint(json.dumps({'backend': 'cloudsql', 'argv': sys.argv[1:]}))\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
cloudsql.chmod(0o700)
|
||||
if "cloudsql-tool" not in missing:
|
||||
cloudsql.write_text(
|
||||
"import json, sys\nprint(json.dumps({'backend': 'cloudsql', 'argv': sys.argv[1:]}))\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
cloudsql.chmod(0o700)
|
||||
local = bin_dir / "kb_tool.py"
|
||||
local.write_text(
|
||||
"import json, sys\nprint(json.dumps({'backend': 'local', 'argv': sys.argv[1:]}))\n",
|
||||
|
|
@ -88,7 +91,13 @@ def _install_wrapper_fixture(tmp_path: Path) -> tuple[Path, dict[str, str]]:
|
|||
local.chmod(0o700)
|
||||
fake_bin = tmp_path / "fake-bin"
|
||||
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"):
|
||||
if name in missing:
|
||||
continue
|
||||
executable = fake_bin / name
|
||||
executable.write_text("#!/usr/bin/env bash\nexit 0\n", encoding="utf-8")
|
||||
executable.chmod(0o700)
|
||||
|
|
@ -96,7 +105,7 @@ def _install_wrapper_fixture(tmp_path: Path) -> tuple[Path, dict[str, str]]:
|
|||
**os.environ,
|
||||
"HERMES_HOME": str(profile),
|
||||
"TELEO_KB_MODE": "auto",
|
||||
"PATH": f"{fake_bin}:{os.environ['PATH']}",
|
||||
"PATH": str(fake_bin),
|
||||
}
|
||||
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
|
||||
|
||||
|
||||
@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:
|
||||
cloudsql_text = (BRIDGE_DIR / "cloudsql_memory_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 "'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:
|
||||
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
|
||||
|
||||
|
||||
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):
|
||||
spec = importlib.util.spec_from_file_location(path.stem, path)
|
||||
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)
|
||||
|
||||
|
||||
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")
|
||||
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, "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(
|
||||
module,
|
||||
"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)
|
||||
|
||||
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:
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ def test_auto_deploy_prefers_github_remote_when_present():
|
|||
auto_deploy = (REPO_ROOT / "deploy" / "auto-deploy.sh").read_text()
|
||||
|
||||
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 rev-parse "$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():
|
||||
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 "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 "--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 "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 systemctl daemon-reload" 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 "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_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 'hermes-agent/leoclean-bin/' 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-bin/" in auto_deploy
|
||||
assert "hermes-agent/leoclean-skills/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 '"status": "installed_now"' in auto_deploy
|
||||
assert 'Hermes response-transform drift repaired' in auto_deploy
|
||||
assert 'deploy/leoclean-gateway-restart-required.sh' in auto_deploy
|
||||
assert 'TELEO_AUTO_DEPLOY_REEXECED' in auto_deploy
|
||||
assert "Hermes response-transform drift repaired" in auto_deploy
|
||||
assert "deploy/leoclean-gateway-restart-required.sh" in auto_deploy
|
||||
assert "TELEO_AUTO_DEPLOY_REEXECED" 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 $RESTART" not in auto_deploy
|
||||
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_SKILLS="/home/teleo/.hermes/profiles/leoclean/skills"' 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-skills/vps/' in manual_deploy
|
||||
assert 'hermes-agent/leoclean-plugins/vps/' in manual_deploy
|
||||
assert "hermes-agent/leoclean-bin/" in manual_deploy
|
||||
assert "hermes-agent/leoclean-skills/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 '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():
|
||||
|
|
@ -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():
|
||||
assert not _leoclean_gateway_restart_required(
|
||||
"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/teleo-kb-bridge/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():
|
||||
assert _leoclean_gateway_restart_required("hermes-agent/leoclean-bin/kb_tool.py")
|
||||
assert _leoclean_gateway_restart_required(
|
||||
"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-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")
|
||||
|
||||
|
||||
def test_auto_deploy_sudoers_installer_is_root_only_and_visudo_checked():
|
||||
installer = (REPO_ROOT / "deploy" / "install-auto-deploy-sudoers.sh").read_text()
|
||||
|
||||
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 '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 "TELEGRAM_BOT_TOKEN" 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