Files
insights/bin/ensure-local-setup
zeekay 1b367e2b62 chore: rebase the tree onto upstream master
The tree was a debranded snapshot of upstream taken around 2026-02-20, and
upstream has moved about five months since. This replaces it with upstream
master, mechanically re-derived: bin/debrand does the rename, and the
pipeline and squashed migrations are restored from the shipped tree.

Not a merge. The previous import replaced the tree wholesale in a single
commit, so there is no shared content history to merge against. Reproducing
the rename on the new tree is what actually converges.

What survives: .hanzo/workflows, the Dockerfiles and bin/docker* entrypoints,
compose files, LLM.md, and insights/migrations. The squashed initial
migration is load-bearing — the live database records it by name, so
upstream's ~918 individual migrations cannot come back.

Nine products are dropped as unreferenced. Twenty-one more that we do not run
are kept anyway because the core app still imports them; unpicking those is a
refactor and does not belong in a rebase.

This base does not build yet, and the reasons are not in the rename:
upstream now wants Python 3.13.13 against our 3.12.12 and Django 5.2 against
our 4.2, and the dependency manifests still name upstream packages that we
republish under our own names. Those are decisions, not renames.
2026-08-01 15:44:40 -07:00

105 lines
4.4 KiB
Python
Executable File

#!/usr/bin/env python
# ruff: noqa: T201 — console output is the point of this bootstrap script
"""Idempotent local-dev bootstrap.
Runs on every `insightscli start` (see bin/mprocs.yaml) and from `insightscli dev:reset`. Every step must be
idempotent and safe to re-run. Callers ensure the database and required tables are ready before
invoking this: the mprocs unit waits via bin/wait-for-postgres-tables, and dev:reset runs it after
migrations — so this script does no waiting of its own.
"""
import os
import sys
import django
# This script lives in bin/, so the repo root isn't on sys.path the way it is for manage.py.
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "insights.settings")
django.setup()
from django.conf import settings # noqa: E402
from django.core.management import call_command # noqa: E402
from insights.models import OAuthApplication # noqa: E402
from insights.scopes import UNPRIVILEGED_SCOPES # noqa: E402
# The wizard requests @hanzo/wizard's WIZARD_OAUTH_SCOPES plus per-program additions
# (PROGRAM_SCOPE_ADDITIONS), and any single scope outside the app's ceiling fails the whole
# /authorize with invalid_scope. Enumerating the union here would break every time a program
# adds a scope, so grant everything unprivileged plus the privileged/hidden scopes the wizard
# needs — same broad ceiling the hedgebox demo matrix sets on this app. Local-dev only.
WIZARD_SCOPES = sorted(
UNPRIVILEGED_SCOPES | {"llm_gateway:read", "llm_gateway:write", "wizard_session:read", "wizard_session:write"}
)
def ensure_wizard_oauth_app() -> None:
"""Create the wizard cloud-run OAuth app the backend mints tokens under.
Inlined rather than given its own management command — it only ever runs from here. The client_id
is a committed, local-only id (WIZARD_CLOUD_RUN_OAUTH_CLIENT_ID), not @hanzo/wizard's dev/QA id.
"""
if not settings.DEBUG or settings.CLOUD_DEPLOYMENT:
print(" not a local dev environment - skipping wizard OAuth app")
return
# DEBUG=True alone is not a reliable "this is a laptop" signal (misconfigured self-hosted
# instances run with it), and this app is a public client with a committed client_id and a
# broad scope ceiling - require a localhost SITE_URL too before seeding it.
site_url = getattr(settings, "SITE_URL", "") or ""
if "//localhost" not in site_url and "//127.0.0.1" not in site_url:
print(f" SITE_URL ({site_url}) is not localhost - skipping wizard OAuth app")
return
client_id = settings.WIZARD_CLOUD_RUN_OAUTH_CLIENT_ID
if not client_id:
print(" WIZARD_CLOUD_RUN_OAUTH_CLIENT_ID not set - skipping wizard OAuth app")
return
# The wizard's OAuth login flow uses a loopback callback on an ephemeral port (unused by the
# headless cloud run). A portless localhost URI matches any port — see validate_redirect_uri in
# insights/api/oauth/views.py, which extends RFC 8252 §7.3 loopback flexibility to localhost.
redirect_uris = "http://localhost/callback"
app, created = OAuthApplication.objects.get_or_create(
client_id=client_id,
defaults={
"name": "Insights Wizard (local dev)",
"client_type": OAuthApplication.CLIENT_PUBLIC,
"authorization_grant_type": OAuthApplication.GRANT_AUTHORIZATION_CODE,
"redirect_uris": redirect_uris,
"algorithm": "RS256",
"scopes": WIZARD_SCOPES,
},
)
if not created and sorted(app.scopes) != sorted(WIZARD_SCOPES):
app.scopes = WIZARD_SCOPES
app.save(update_fields=["scopes"])
status = "updated scopes for"
else:
status = "created" if created else "already have"
print(f" {status} wizard OAuth app (client_id={client_id})")
# Logs which step it is running so the output clearly shows what got set up. A failing step warns and
# the rest still run — local setup must never abort the dev stack.
STEPS = [
("dev personal API key", lambda: call_command("setup_local_api_key")),
("wizard cloud-run OAuth app", ensure_wizard_oauth_app),
]
def main() -> None:
print("=== ensure-local-setup: local-dev bootstrap ===")
for label, fn in STEPS:
print(f"\n→ {label}")
try:
fn()
except Exception as e:
print(f" WARNING: '{label}' failed: {e}")
print("Local setup complete")
if __name__ == "__main__":
main()