diff --git a/.dockerignore b/.dockerignore index cbe0513..4a09224 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,12 +1,16 @@ -.git -.gitignore -.DS_Store .env .env.* -.venv -__pycache__ -*.py[cod] +.git +.github +.DS_Store +.coverage +.mypy_cache .pytest_cache .ruff_cache -.coverage +.venv +**/__pycache__ +*.py[cod] htmlcov +infra +output +web diff --git a/.env.example b/.env.example index 12e3d34..80b6402 100644 --- a/.env.example +++ b/.env.example @@ -6,6 +6,9 @@ API_PORT=8000 WEB_PORT=3000 CHANGEOPS_API_BASE_URL=http://api:8000 DATABASE_URL=postgresql+psycopg://changeops:changeops@localhost:5432/changeops +# Deployed ECS uses separate DB_HOST, DB_PORT, DB_NAME, DB_USERNAME, DB_PASSWORD, and +# DB_SSLMODE=require fields instead of DATABASE_URL. +CHANGEOPS_IDENTITY_MODE=demo EXTRACTION_MODEL_PROVIDER=openai EXTRACTION_MODEL=gpt-5.6-luna EXTRACTION_MODEL_TIMEOUT_SECONDS=120 diff --git a/.github/workflows/deploy-production.yml b/.github/workflows/deploy-production.yml new file mode 100644 index 0000000..724dd4a --- /dev/null +++ b/.github/workflows/deploy-production.yml @@ -0,0 +1,372 @@ +name: Deploy production + +on: + workflow_dispatch: + inputs: + release_mode: + description: Prepare images and task revisions while off, or deploy to an enabled window + required: true + default: prepare + type: choice + options: + - prepare + - deploy + +permissions: + contents: read + id-token: write + +concurrency: + group: changeops-production-release + cancel-in-progress: false + +env: + AWS_REGION: ${{ vars.AWS_REGION }} + APPLICATION_URL: ${{ vars.APPLICATION_URL }} + ECS_CLUSTER: changeops-demo-cluster + ECS_SERVICE: changeops-demo-service + APPLICATION_TASK_FAMILY: changeops-demo-application + MIGRATION_TASK_FAMILY: changeops-demo-migration + BOOTSTRAP_TASK_FAMILY: changeops-demo-bootstrap + API_REPOSITORY: changeops-demo-api + WEB_REPOSITORY: changeops-demo-web + RELEASE_MODE: ${{ inputs.release_mode }} + +jobs: + deploy: + name: migrate, deploy, and verify + if: github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + timeout-minutes: 45 + environment: production + + steps: + - name: Check out reviewed main revision + uses: actions/checkout@v6 + + - name: Validate protected deployment configuration + env: + AWS_DEPLOY_ROLE_ARN: ${{ vars.AWS_DEPLOY_ROLE_ARN }} + run: | + set -euo pipefail + test -n "${AWS_DEPLOY_ROLE_ARN}" + test -n "${AWS_REGION}" + test "${GITHUB_REF}" = "refs/heads/main" + if test "${RELEASE_MODE}" = "deploy"; then + case "${APPLICATION_URL}" in + https://*) ;; + *) echo "Deploy mode requires an HTTPS APPLICATION_URL." >&2; exit 1 ;; + esac + fi + + - name: Obtain short-lived AWS credentials + uses: aws-actions/configure-aws-credentials@v6.1.2 + with: + role-to-assume: ${{ vars.AWS_DEPLOY_ROLE_ARN }} + aws-region: ${{ env.AWS_REGION }} + + - name: Enforce the reviewed release phase + run: | + set -euo pipefail + desired_count="$( + aws ecs describe-services \ + --cluster "${ECS_CLUSTER}" \ + --services "${ECS_SERVICE}" \ + --query 'services[0].desiredCount' \ + --output text + )" + case "${RELEASE_MODE}" in + prepare) + test "${desired_count}" = "0" || { + echo "Prepare mode is permitted only while the demo service is off." >&2 + exit 1 + } + ;; + deploy) + test "${desired_count}" = "1" || { + echo "Deploy mode requires the separately approved demo window." >&2 + exit 1 + } + ;; + *) + echo "Unsupported release mode." >&2 + exit 1 + ;; + esac + + - name: Log in to the named ECR registry + uses: aws-actions/amazon-ecr-login@v2 + + - name: Resolve immutable image locations + id: images + run: | + set -euo pipefail + api_uri="$( + aws ecr describe-repositories \ + --repository-names "${API_REPOSITORY}" \ + --query 'repositories[0].repositoryUri' \ + --output text + )" + web_uri="$( + aws ecr describe-repositories \ + --repository-names "${WEB_REPOSITORY}" \ + --query 'repositories[0].repositoryUri' \ + --output text + )" + echo "api=${api_uri}:${GITHUB_SHA}" >> "${GITHUB_OUTPUT}" + echo "web=${web_uri}:${GITHUB_SHA}" >> "${GITHUB_OUTPUT}" + if aws ecr describe-images \ + --repository-name "${API_REPOSITORY}" \ + --image-ids "imageTag=${GITHUB_SHA}" >/dev/null 2>&1; then + echo "api_exists=true" >> "${GITHUB_OUTPUT}" + else + echo "api_exists=false" >> "${GITHUB_OUTPUT}" + fi + if aws ecr describe-images \ + --repository-name "${WEB_REPOSITORY}" \ + --image-ids "imageTag=${GITHUB_SHA}" >/dev/null 2>&1; then + echo "web_exists=true" >> "${GITHUB_OUTPUT}" + else + echo "web_exists=false" >> "${GITHUB_OUTPUT}" + fi + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build and publish backend image + if: steps.images.outputs.api_exists != 'true' + uses: docker/build-push-action@v7.2.0 + with: + context: . + file: Dockerfile + target: production + platforms: linux/amd64 + push: true + tags: ${{ steps.images.outputs.api }} + provenance: mode=max + sbom: true + cache-from: type=gha,scope=changeops-api + cache-to: type=gha,mode=max,scope=changeops-api + + - name: Build and publish frontend image + if: steps.images.outputs.web_exists != 'true' + uses: docker/build-push-action@v7.2.0 + with: + context: web + file: web/Dockerfile + platforms: linux/amd64 + push: true + tags: ${{ steps.images.outputs.web }} + provenance: mode=max + sbom: true + cache-from: type=gha,scope=changeops-web + cache-to: type=gha,mode=max,scope=changeops-web + + - name: Register reviewed task-definition revisions + id: task_definitions + env: + API_IMAGE: ${{ steps.images.outputs.api }} + WEB_IMAGE: ${{ steps.images.outputs.web }} + run: | + set -euo pipefail + aws ecs describe-task-definition \ + --task-definition "${APPLICATION_TASK_FAMILY}" \ + > application-described.json + python scripts/render_ecs_task_definition.py \ + --input application-described.json \ + --output application-release.json \ + --container "api=${API_IMAGE}" \ + --container "web=${WEB_IMAGE}" + application_arn="$( + aws ecs register-task-definition \ + --cli-input-json file://application-release.json \ + --tags key=Application,value=ChangeOps key=Environment,value=demo \ + --query 'taskDefinition.taskDefinitionArn' \ + --output text + )" + + aws ecs describe-task-definition \ + --task-definition "${MIGRATION_TASK_FAMILY}" \ + > migration-described.json + python scripts/render_ecs_task_definition.py \ + --input migration-described.json \ + --output migration-release.json \ + --container "migration=${API_IMAGE}" + migration_arn="$( + aws ecs register-task-definition \ + --cli-input-json file://migration-release.json \ + --tags key=Application,value=ChangeOps key=Environment,value=demo \ + --query 'taskDefinition.taskDefinitionArn' \ + --output text + )" + + aws ecs describe-task-definition \ + --task-definition "${BOOTSTRAP_TASK_FAMILY}" \ + > bootstrap-described.json + python scripts/render_ecs_task_definition.py \ + --input bootstrap-described.json \ + --output bootstrap-release.json \ + --container "bootstrap=${API_IMAGE}" + bootstrap_arn="$( + aws ecs register-task-definition \ + --cli-input-json file://bootstrap-release.json \ + --tags key=Application,value=ChangeOps key=Environment,value=demo \ + --query 'taskDefinition.taskDefinitionArn' \ + --output text + )" + { + echo "application=${application_arn}" + echo "migration=${migration_arn}" + echo "bootstrap=${bootstrap_arn}" + } >> "${GITHUB_OUTPUT}" + + - name: Run controlled migration and fictional-data bootstrap + env: + MIGRATION_TASK_DEFINITION: ${{ steps.task_definitions.outputs.migration }} + BOOTSTRAP_TASK_DEFINITION: ${{ steps.task_definitions.outputs.bootstrap }} + run: | + set -euo pipefail + network_configuration="$( + aws ecs describe-services \ + --cluster "${ECS_CLUSTER}" \ + --services "${ECS_SERVICE}" \ + --query 'services[0].networkConfiguration' \ + --output json + )" + + run_controlled_task() { + local label="$1" + local task_definition="$2" + local container_name="$3" + local command_json="$4" + local log_group="$5" + local overrides task_arn task_id description exit_code reason + overrides="$( + jq -cn --arg name "${container_name}" --argjson command "${command_json}" \ + '{containerOverrides:[{name:$name,command:$command}]}' + )" + task_arn="$( + aws ecs run-task \ + --cluster "${ECS_CLUSTER}" \ + --launch-type FARGATE \ + --task-definition "${task_definition}" \ + --network-configuration "${network_configuration}" \ + --overrides "${overrides}" \ + --started-by "github-${GITHUB_RUN_ID}" \ + --tags key=Application,value=ChangeOps key=Environment,value=demo \ + --query 'tasks[0].taskArn' \ + --output text + )" + test -n "${task_arn}" && test "${task_arn}" != "None" + aws ecs wait tasks-stopped --cluster "${ECS_CLUSTER}" --tasks "${task_arn}" + description="$( + aws ecs describe-tasks --cluster "${ECS_CLUSTER}" --tasks "${task_arn}" + )" + exit_code="$( + jq -r --arg name "${container_name}" \ + '.tasks[0].containers[] | select(.name==$name) | .exitCode // -1' \ + <<<"${description}" + )" + reason="$( + jq -r --arg name "${container_name}" \ + '.tasks[0].containers[] | select(.name==$name) | .reason // "none"' \ + <<<"${description}" + )" + if test "${exit_code}" != "0"; then + task_id="${task_arn##*/}" + aws logs get-log-events \ + --log-group-name "${log_group}" \ + --log-stream-name "${container_name}/${container_name}/${task_id}" \ + --query 'events[].message' \ + --output text || true + echo "${label} failed with exit ${exit_code}; reason=${reason}." >&2 + exit 1 + fi + echo "${label} succeeded." + } + + run_controlled_task \ + migration "${MIGRATION_TASK_DEFINITION}" migration \ + '["alembic","upgrade","head"]' /changeops/demo/migration + run_controlled_task \ + bootstrap "${BOOTSTRAP_TASK_DEFINITION}" bootstrap \ + '["python","-m","changeops.deployment_bootstrap"]' /changeops/demo/bootstrap + + - name: Roll out the application revision + id: deploy + env: + APPLICATION_TASK_DEFINITION: ${{ steps.task_definitions.outputs.application }} + run: | + set -euo pipefail + previous="$( + aws ecs describe-services \ + --cluster "${ECS_CLUSTER}" \ + --services "${ECS_SERVICE}" \ + --query 'services[0].taskDefinition' \ + --output text + )" + echo "previous=${previous}" >> "${GITHUB_OUTPUT}" + aws ecs update-service \ + --cluster "${ECS_CLUSTER}" \ + --service "${ECS_SERVICE}" \ + --task-definition "${APPLICATION_TASK_DEFINITION}" \ + --force-new-deployment >/dev/null + aws ecs wait services-stable --cluster "${ECS_CLUSTER}" --services "${ECS_SERVICE}" + + - name: Verify HTTPS readiness, authentication, and correlation + if: inputs.release_mode == 'deploy' + run: | + set -euo pipefail + request_id="deployment-${GITHUB_SHA}" + response_headers="$(mktemp)" + ready_payload="$( + curl --fail --silent --show-error \ + --dump-header "${response_headers}" \ + --header "X-Request-ID: ${request_id}" \ + "${APPLICATION_URL%/}/readyz" + )" + test "$(jq -r '.status' <<< "${ready_payload}")" = "ok" + test "$(jq -r '.api' <<< "${ready_payload}")" = "ok" + grep -Eiq '^strict-transport-security: max-age=31536000; includesubdomains' \ + "${response_headers}" || { + echo "The public readiness response is missing the reviewed HSTS policy." >&2 + exit 1 + } + root_status="$( + curl --silent --show-error --output /dev/null --write-out '%{http_code}' \ + "${APPLICATION_URL%/}/" + )" + test "${root_status}" = "302" || { + echo "The product root did not enforce Cognito authentication." >&2 + exit 1 + } + + found=0 + for attempt in $(seq 1 10); do + found="$( + aws logs filter-log-events \ + --log-group-name /changeops/demo/api \ + --filter-pattern "\"${request_id}\"" \ + --query 'length(events)' \ + --output text + )" + test "${found}" -gt 0 && break + echo "Waiting for correlated CloudWatch log event (${attempt}/10)." + sleep 5 + done + test "${found}" -gt 0 || { + echo "The readiness request ID did not reach structured API logs." >&2 + exit 1 + } + + - name: Roll back application revision after a failed rollout or smoke check + if: failure() && steps.deploy.outputs.previous != '' + env: + PREVIOUS_TASK_DEFINITION: ${{ steps.deploy.outputs.previous }} + run: | + aws ecs update-service \ + --cluster "${ECS_CLUSTER}" \ + --service "${ECS_SERVICE}" \ + --task-definition "${PREVIOUS_TASK_DEFINITION}" \ + --force-new-deployment >/dev/null + aws ecs wait services-stable --cluster "${ECS_CLUSTER}" --services "${ECS_SERVICE}" diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 4cc2f37..7ded7cf 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -97,15 +97,18 @@ jobs: - name: Build Compose services run: docker compose build + - name: Build production backend image + run: docker build --target production --tag changeops-api:production . + - name: Save API image cache if: steps.api-image-cache.outputs.cache-hit != 'true' run: docker image save changeops-api:latest --output /tmp/changeops-api-image.tar - name: Lint - run: docker compose run --rm api ruff check src migrations tests + run: docker compose run --rm api ruff check src migrations scripts tests - name: Check formatting - run: docker compose run --rm api ruff format --check src migrations tests + run: docker compose run --rm api ruff format --check src migrations scripts tests - name: Run test suite run: docker compose run --rm api pytest diff --git a/.github/workflows/verify-production.yml b/.github/workflows/verify-production.yml new file mode 100644 index 0000000..cf0b2e5 --- /dev/null +++ b/.github/workflows/verify-production.yml @@ -0,0 +1,255 @@ +name: Verify production flagship + +on: + workflow_dispatch: + inputs: + release_sha: + description: Full 40-character commit SHA currently deployed + required: true + type: string + policy_analysis_run_id: + description: Run ID created through the authenticated deployed UI + required: true + type: string + confirmation: + description: Enter verified-live-human-journey after completing the run + required: true + type: string + +permissions: + contents: read + id-token: write + +concurrency: + group: changeops-production-release + cancel-in-progress: false + +env: + AWS_REGION: ${{ vars.AWS_REGION }} + APPLICATION_URL: ${{ vars.APPLICATION_URL }} + ECS_CLUSTER: changeops-demo-cluster + ECS_SERVICE: changeops-demo-service + BOOTSTRAP_TASK_FAMILY: changeops-demo-bootstrap + API_REPOSITORY: changeops-demo-api + WEB_REPOSITORY: changeops-demo-web + +jobs: + verify: + name: verify human journey and audit evidence + if: github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + timeout-minutes: 20 + environment: production + + steps: + - name: Validate protected verification request + env: + AWS_DEPLOY_ROLE_ARN: ${{ vars.AWS_DEPLOY_ROLE_ARN }} + RELEASE_SHA: ${{ inputs.release_sha }} + POLICY_ANALYSIS_RUN_ID: ${{ inputs.policy_analysis_run_id }} + CONFIRMATION: ${{ inputs.confirmation }} + run: | + set -euo pipefail + test -n "${AWS_DEPLOY_ROLE_ARN}" + test -n "${AWS_REGION}" + test "${CONFIRMATION}" = "verified-live-human-journey" + [[ "${RELEASE_SHA}" =~ ^[0-9a-f]{40}$ ]] + [[ "${POLICY_ANALYSIS_RUN_ID}" =~ ^[0-9a-fA-F-]{36}$ ]] + case "${APPLICATION_URL}" in + https://*) ;; + *) echo "Verification requires an HTTPS APPLICATION_URL." >&2; exit 1 ;; + esac + + - name: Obtain short-lived AWS credentials + uses: aws-actions/configure-aws-credentials@v6.1.2 + with: + role-to-assume: ${{ vars.AWS_DEPLOY_ROLE_ARN }} + aws-region: ${{ env.AWS_REGION }} + + - name: Verify the requested immutable release is running + id: release + env: + RELEASE_SHA: ${{ inputs.release_sha }} + run: | + set -euo pipefail + aws ecs wait services-stable --cluster "${ECS_CLUSTER}" --services "${ECS_SERVICE}" + service="$( + aws ecs describe-services \ + --cluster "${ECS_CLUSTER}" \ + --services "${ECS_SERVICE}" + )" + test "$(jq -r '.services[0].desiredCount' <<<"${service}")" = "1" + test "$(jq -r '.services[0].runningCount' <<<"${service}")" = "1" + test "$(jq -r '.services[0].pendingCount' <<<"${service}")" = "0" + jq -e ' + .services[0].deployments as $deployments + | ($deployments | length) == 1 + and $deployments[0].status == "PRIMARY" + and $deployments[0].rolloutState == "COMPLETED" + and $deployments[0].runningCount == 1 + and $deployments[0].pendingCount == 0 + ' <<<"${service}" >/dev/null + task_definition="$(jq -r '.services[0].taskDefinition' <<<"${service}")" + definition="$( + aws ecs describe-task-definition --task-definition "${task_definition}" + )" + api_uri="$( + aws ecr describe-repositories \ + --repository-names "${API_REPOSITORY}" \ + --query 'repositories[0].repositoryUri' \ + --output text + )" + web_uri="$( + aws ecr describe-repositories \ + --repository-names "${WEB_REPOSITORY}" \ + --query 'repositories[0].repositoryUri' \ + --output text + )" + jq -e \ + --arg api "${api_uri}:${RELEASE_SHA}" \ + --arg web "${web_uri}:${RELEASE_SHA}" \ + ' + (.taskDefinition.containerDefinitions | + map(select(.name == "api" and .image == $api)) | length) == 1 + and + (.taskDefinition.containerDefinitions | + map(select(.name == "web" and .image == $web)) | length) == 1 + ' <<<"${definition}" >/dev/null + task_arn="$( + aws ecs list-tasks \ + --cluster "${ECS_CLUSTER}" \ + --service-name "${ECS_SERVICE}" \ + --desired-status RUNNING \ + --query 'taskArns[0]' \ + --output text + )" + test -n "${task_arn}" && test "${task_arn}" != "None" + running_task="$( + aws ecs describe-tasks \ + --cluster "${ECS_CLUSTER}" \ + --tasks "${task_arn}" \ + --output json + )" + test "$(jq -r '.tasks[0].taskDefinitionArn' <<<"${running_task}")" = \ + "${task_definition}" + release_not_before="$(jq -r '.tasks[0].startedAt' <<<"${running_task}")" + test -n "${release_not_before}" && test "${release_not_before}" != "None" + echo "not_before=${release_not_before}" >> "${GITHUB_OUTPUT}" + + - name: Resolve the matching evidence-task revision + id: evidence_task + env: + RELEASE_SHA: ${{ inputs.release_sha }} + run: | + set -euo pipefail + api_uri="$( + aws ecr describe-repositories \ + --repository-names "${API_REPOSITORY}" \ + --query 'repositories[0].repositoryUri' \ + --output text + )" + expected_image="${api_uri}:${RELEASE_SHA}" + evidence_task="" + while IFS= read -r task_definition; do + image="$( + aws ecs describe-task-definition \ + --task-definition "${task_definition}" \ + --query "taskDefinition.containerDefinitions[?name=='bootstrap'].image | [0]" \ + --output text + )" + if test "${image}" = "${expected_image}"; then + evidence_task="${task_definition}" + break + fi + done < <( + aws ecs list-task-definitions \ + --family-prefix "${BOOTSTRAP_TASK_FAMILY}" \ + --sort DESC \ + --query 'taskDefinitionArns[]' \ + --output text | tr '\t' '\n' + ) + test -n "${evidence_task}" || { + echo "No evidence task matches the requested release SHA." >&2 + exit 1 + } + echo "task_definition=${evidence_task}" >> "${GITHUB_OUTPUT}" + + - name: Verify persisted human and execution evidence + env: + EVIDENCE_TASK_DEFINITION: ${{ steps.evidence_task.outputs.task_definition }} + POLICY_ANALYSIS_RUN_ID: ${{ inputs.policy_analysis_run_id }} + RELEASE_NOT_BEFORE: ${{ steps.release.outputs.not_before }} + run: | + set -euo pipefail + network_configuration="$( + aws ecs describe-services \ + --cluster "${ECS_CLUSTER}" \ + --services "${ECS_SERVICE}" \ + --query 'services[0].networkConfiguration' \ + --output json + )" + command_json="$( + jq -cn \ + --arg run "${POLICY_ANALYSIS_RUN_ID}" \ + --arg release_not_before "${RELEASE_NOT_BEFORE}" \ + '[ + "python", "-m", "changeops.evaluation.deployed_evidence", + "--run-id", $run, + "--release-not-before", $release_not_before + ]' + )" + overrides="$( + jq -cn --argjson command "${command_json}" \ + '{containerOverrides:[{name:"bootstrap",command:$command}]}' + )" + task_arn="$( + aws ecs run-task \ + --cluster "${ECS_CLUSTER}" \ + --launch-type FARGATE \ + --task-definition "${EVIDENCE_TASK_DEFINITION}" \ + --network-configuration "${network_configuration}" \ + --overrides "${overrides}" \ + --started-by "github-verify-${GITHUB_RUN_ID}" \ + --tags key=Application,value=ChangeOps key=Environment,value=demo \ + --query 'tasks[0].taskArn' \ + --output text + )" + test -n "${task_arn}" && test "${task_arn}" != "None" + aws ecs wait tasks-stopped --cluster "${ECS_CLUSTER}" --tasks "${task_arn}" + description="$( + aws ecs describe-tasks --cluster "${ECS_CLUSTER}" --tasks "${task_arn}" + )" + exit_code="$( + jq -r \ + '.tasks[0].containers[] | select(.name=="bootstrap") | .exitCode // -1' \ + <<<"${description}" + )" + if test "${exit_code}" != "0"; then + task_id="${task_arn##*/}" + aws logs get-log-events \ + --log-group-name /changeops/demo/bootstrap \ + --log-stream-name "bootstrap/bootstrap/${task_id}" \ + --query 'events[].message' \ + --output text || true + echo "Deployed evidence check failed." >&2 + exit 1 + fi + + - name: Verify the public deployed boundary + run: | + set -euo pipefail + response_headers="$(mktemp)" + ready_payload="$( + curl --fail --silent --show-error \ + --dump-header "${response_headers}" \ + "${APPLICATION_URL%/}/readyz" + )" + test "$(jq -r '.status' <<<"${ready_payload}")" = "ok" + test "$(jq -r '.api' <<<"${ready_payload}")" = "ok" + grep -Eiq '^strict-transport-security: max-age=31536000; includesubdomains' \ + "${response_headers}" + root_status="$( + curl --silent --show-error --output /dev/null --write-out '%{http_code}' \ + "${APPLICATION_URL%/}/" + )" + test "${root_status}" = "302" diff --git a/Dockerfile b/Dockerfile index 6a026a9..0d38994 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,18 +1,47 @@ -FROM python:3.12-slim +FROM python:3.12-slim-bookworm@sha256:4766d8b510c428e595d74b9cc5bbb2fae8e26316fffb4adc89908d79aacd58a2 AS package -ENV PYTHONDONTWRITEBYTECODE=1 \ - PYTHONUNBUFFERED=1 - -WORKDIR /app +ENV PIP_DISABLE_PIP_VERSION_CHECK=1 \ + PIP_NO_CACHE_DIR=1 +WORKDIR /build COPY pyproject.toml README.md ./ COPY src ./src -RUN python -m pip install --no-cache-dir ".[dev]" +RUN python -m pip wheel --no-deps --wheel-dir /wheels . + +FROM python:3.12-slim-bookworm@sha256:4766d8b510c428e595d74b9cc5bbb2fae8e26316fffb4adc89908d79aacd58a2 AS runtime-base + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PIP_DISABLE_PIP_VERSION_CHECK=1 \ + PIP_NO_CACHE_DIR=1 + +RUN groupadd --system --gid 10001 changeops \ + && useradd --system --uid 10001 --gid changeops --home-dir /app changeops + +WORKDIR /app +COPY requirements.lock ./ +COPY --from=package /wheels /wheels +RUN python -m pip install --require-hashes --only-binary=:all: -r requirements.lock \ + && python -m pip install --no-deps /wheels/changeops-*.whl \ + && rm -rf /wheels COPY alembic.ini ./ COPY migrations ./migrations -COPY tests ./tests +USER changeops EXPOSE 8000 +HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \ + CMD ["python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/healthz')"] +CMD ["uvicorn", "changeops.main:app", "--host", "0.0.0.0", "--port", "8000", "--no-access-log"] + +FROM runtime-base AS development +USER root +COPY pyproject.toml README.md ./ +COPY src ./src +COPY scripts ./scripts +COPY tests ./tests +RUN python -m pip install ".[dev]" \ + && chown -R changeops:changeops /app +USER changeops -CMD ["uvicorn", "changeops.main:app", "--host", "0.0.0.0", "--port", "8000"] +FROM runtime-base AS production diff --git a/README.md b/README.md index dd46eff..7d52b1c 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ See: - `docs/milestone-7-enterprise-knowledge-catalog.md` - `docs/demo-scenario.md` - `docs/decisions.md` +- [`docs/aws-deployment.md`](docs/aws-deployment.md) - [Contributing and engineering standards](CONTRIBUTING.md) ## Project direction diff --git a/compose.yaml b/compose.yaml index 7572567..057d922 100644 --- a/compose.yaml +++ b/compose.yaml @@ -20,7 +20,9 @@ services: retries: 15 migrate: - build: . + build: + context: . + target: development command: ["alembic", "upgrade", "head"] environment: DATABASE_URL: postgresql+psycopg://${POSTGRES_USER:-changeops}:${POSTGRES_PASSWORD:-changeops}@db:5432/${POSTGRES_DB:-changeops} @@ -29,7 +31,9 @@ services: condition: service_healthy seed: - build: . + build: + context: . + target: development command: ["python", "-m", "changeops.seed"] environment: DATABASE_URL: postgresql+psycopg://${POSTGRES_USER:-changeops}:${POSTGRES_PASSWORD:-changeops}@db:5432/${POSTGRES_DB:-changeops} @@ -38,7 +42,9 @@ services: condition: service_completed_successfully api: - build: . + build: + context: . + target: development environment: DATABASE_URL: postgresql+psycopg://${POSTGRES_USER:-changeops}:${POSTGRES_PASSWORD:-changeops}@db:5432/${POSTGRES_DB:-changeops} TEST_DATABASE_URL: postgresql+psycopg://${POSTGRES_USER:-changeops}:${POSTGRES_PASSWORD:-changeops}@db:5432/changeops_test diff --git a/docs/architecture.md b/docs/architecture.md index 8642b1d..0ff3ae0 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -18,11 +18,25 @@ ALB target, PostgreSQL remains private RDS, and a separate one-off task definiti execution boundary. Terraform defaults to the declared off state: ECS desired count zero with no ALB or application -alias. Enabling a demo creates Cognito-authenticated TLS ingress and exactly one task. Applying -the infrastructure remains prohibited until ADR-0022's authentication, trusted-header, -database-configuration, logging, health, and sizing prerequisites are implemented and tested. -Deployment, migrations, database-role bootstrap, seed, and operational lifecycle workflows remain -separate follow-up work. +alias. Enabling a demo creates Cognito-authenticated TLS ingress and exactly one task. The +application gate is implemented by signed ALB/Cognito claim verification in Next.js, strict +replacement of browser identity headers, TLS-required database URL assembly from injected secret +fields, structured request-correlated JSON logs, separate health/readiness routes, and locked +non-root production containers. + +The protected production workflow publishes commit-SHA images, registers new ECS task-definition +revisions, runs Alembic with migration authority and initializes fictional demo data with +restricted runtime authority as separate controlled tasks, then updates the service. Its prepare +mode operates at desired count zero, so the first release exists before the demo window starts. +A separate protected evidence workflow rejects the seeded run and validates persisted identities, +approval, immutable preparation, explicit execution, and provenance from a human-performed +deployed journey created after the currently running release task started. Actor subjects remain +in the business audit trail and are not copied into operational logs. Next.js applies HSTS and +reviewed browser security headers to every route. Terraform +ignores only the service's release revision so a later +infrastructure apply cannot silently roll back application code; it continues to own service +existence, desired count, networking, identity, secrets, and every other infrastructure control. +See [`docs/aws-deployment.md`](aws-deployment.md) for deployment and recovery. ## High-level architecture diff --git a/docs/aws-deployment.md b/docs/aws-deployment.md new file mode 100644 index 0000000..da89673 --- /dev/null +++ b/docs/aws-deployment.md @@ -0,0 +1,187 @@ +# AWS application deployment and recovery + +This runbook implements ADR-0022 without changing ChangeOps product boundaries. Terraform owns +the AWS environment; the protected `Deploy production` workflow owns immutable application +releases. No workflow creates infrastructure, changes database roles, reads the RDS administrator +secret, or silently runs migrations during application startup. + +## Release controls + +- The `production` GitHub Environment must require a human reviewer, prevent unreviewed branches, + and allow only `main`. +- GitHub Actions uses the Terraform-created OIDC role. Do not configure AWS access-key secrets. +- ECR image tags are the full Git commit SHA in immutable repositories. +- Alembic runs as a separate Fargate task with the migration-only database secret. The service is + updated only after migration and a separate runtime-authority fictional-data setup task both + exit successfully. +- The first release is prepared while ECS desired count is zero. This prevents an empty ECR + repository and placeholder task revision from creating a first-deployment deadlock. +- ECS health checks, ALB target health, the deployment circuit breaker, and the post-deployment + smoke check gate success. A failed rollout is returned to the previous task definition. +- A second protected workflow accepts only a non-seeded run ID and checks the persisted identities + and artifacts from a human-performed deployed journey. Fixture records are never accepted as + live journey evidence. +- The long-running task receives only the restricted runtime database secret and configured + provider secrets. It never receives the migration or RDS administrator credential. + +## One-time environment and database bootstrap + +Complete the safe off-state provisioning in +[`infra/terraform/README.md`](../infra/terraform/README.md) first. Applying a saved Terraform plan +still requires explicit human review of the exact account, domain, resources, and cost impact. + +The private RDS database needs two non-administrative logins before the first release. Keep the +RDS-managed administrator credential out of ECS: + +1. Create an AWS CloudShell VPC environment in one Terraform output `public_subnet_ids` subnet and + attach the Terraform output `task_security_group_id`. Delete that CloudShell VPC environment + after bootstrap. This gives the short-lived operator shell the same database network path as + the application without making RDS public. After entering that VPC environment, use the + CloudShell **Actions → Upload file** control to upload the reviewed + `scripts/bootstrap_database_roles.sql`; do not depend on internet access or persistent storage. + Compare `sha256sum scripts/bootstrap_database_roles.sql` there with the checksum from the + reviewed `main` checkout before executing it. +2. In CloudShell, confirm the account and retrieve the non-secret outputs: + + ```bash + aws sts get-caller-identity + aws rds describe-db-instances \ + --db-instance-identifier changeops-demo-postgres \ + --query 'DBInstances[0].{host:Endpoint.Address,port:Endpoint.Port,public:PubliclyAccessible}' + ``` + + Stop unless `public` is `false` and the account is the reviewed portfolio account. +3. Install the PostgreSQL 17 client if the CloudShell image does not already provide `psql`. + Retrieve the AWS-managed administrator secret into shell variables without printing it: + + ```bash + admin_secret_arn="PASTE_TERRAFORM_OUTPUT_database_admin_secret_arn" + admin_secret="$(aws secretsmanager get-secret-value \ + --secret-id "${admin_secret_arn}" \ + --query SecretString --output text)" + export PGUSER="$(jq -r .username <<<"${admin_secret}")" + export PGPASSWORD="$(jq -r .password <<<"${admin_secret}")" + export PGHOST="PASTE_PRIVATE_DATABASE_ENDPOINT" + export PGPORT=5432 + export PGDATABASE=changeops + export PGSSLMODE=require + ``` +4. Generate two distinct high-entropy passwords and store them temporarily in mode-`0600` JSON + files. Run the reviewed role script and paste the matching value at each masked `\password` + prompt: + + ```bash + psql --file scripts/bootstrap_database_roles.sql + ``` + + The script creates fixed `changeops_schema_owner` and `changeops_runtime` roles, transfers + schema ownership to the migration role, removes runtime DDL authority, and establishes only + the required DML/default privileges. +5. Put `{"username":"changeops_schema_owner","password":"..."}` in the migration secret and + `{"username":"changeops_runtime","password":"..."}` in the runtime secret. Populate the model + provider secret and optional dedicated Jira/Confluence secrets using the shapes in the + infrastructure runbook. Never use customer or production-enterprise credentials. +6. Test both non-administrative credentials over TLS, unset `PGPASSWORD`, clear the administrator + JSON variable, securely remove temporary files, close CloudShell, and delete its VPC + environment. + +## Configure protected deployment + +Create the repository environment named `production` with required reviewers and a `main`-only +deployment branch rule. Add these non-secret environment variables: + +| Variable | Value | +|---|---| +| `AWS_DEPLOY_ROLE_ARN` | Terraform output `github_deploy_role_arn` | +| `AWS_REGION` | `us-east-1` | +| `APPLICATION_URL` | Terraform output `application_url` while the demo window is enabled | + +The Terraform OIDC trust policy expects +`repo:jaydreyer/ChangeOps:environment:production`. The workflow declares +`id-token: write` and obtains short-lived credentials; no long-lived AWS key is stored by GitHub. + +## Prepare and deploy + +1. Review and merge the release to `main`. +2. Keep `demo_enabled = false`, make the private RDS instance available, and confirm the ECS + service desired count is zero. In GitHub Actions, run **Deploy production** from `main` with + `release_mode=prepare` and approve the protected environment gate. `APPLICATION_URL` is not + needed in this phase. +3. The prepare phase: + - builds reproducible Linux/AMD64 backend and frontend images from locked dependencies and + digest-pinned base images; + - publishes both images under the full commit SHA; + - registers application, migration, and restricted fixture task-definition revisions without + changing reviewed environment or secret wiring; + - runs `alembic upgrade head` as an explicit one-off task; + - initializes and validates the idempotent fictional catalog and demo timeline with restricted + runtime DB authority; and + - points the zero-count ECS service at the prepared application revision. +4. Create and review the exact Terraform plan that changes `demo_enabled` to `true`. Apply only + that saved, human-approved plan. Confirm desired task count is one, the ALB and application + alias exist, and the protected `APPLICATION_URL` matches the Terraform output. +5. Run **Deploy production** again with `release_mode=deploy`. This idempotently repeats migration + and fixture setup, rolls the two-container service, waits for ECS/ALB health, and: + - verifies public HTTPS Next.js-to-FastAPI readiness, Cognito enforcement on product routes, + required security headers, and the same request ID in CloudWatch API logs. + +## Verify the deployed flagship journey + +The fictional fixture makes the international-travel story immediately inspectable, including a +deterministic Jira receipt and prevented replay. It is data setup, not proof that authentication, +human approval, and execution work in the deployed application. + +After a successful deploy: + +1. Sign in through Cognito as an invited `reviewer`. Create and complete a new international-travel + policy analysis, decide every proposed action, and save the new policy-analysis run ID. Do not + use the pre-seeded run. +2. Sign in as an invited `admin`. Open that completed approval, prepare the immutable commands, + explicitly execute at least one simulated learning assignment, inspect its result and the + Unified Audit Timeline. +3. Confirm the seeded Jira item is clearly labeled fictional/deterministic and its timeline + contains the Jira receipt and replay prevention. +4. From the deployed commit on `main`, run **Verify production flagship**. Supply the full deployed + commit SHA, new policy-analysis run ID, and the exact confirmation + `verified-live-human-journey`. Approve the protected environment gate. Actor subjects remain + only in the immutable business audit trail and are not copied into workflow or CloudWatch logs. +5. Preserve the successful workflow URL as acceptance evidence. The workflow fails unless the + named SHA is running, the run was created after the currently running task started, the run is + not the deterministic seed, review decisions have one authenticated reviewer identity and role, + immutable preparation and explicit successful execution have one authenticated admin identity + and role, the simulated side effect and provenance exist, the deterministic Jira fixture exists, + and the public HTTPS/authentication boundary remains healthy. + +Do not report the flagship deployment verified until this protected workflow succeeds. + +## Failure and rollback + +- A migration or fixture-setup failure stops the workflow before the service changes. Inspect + `/changeops/demo/migration`; fix forward and rerun. Never represent a failed task as successful. +- An unhealthy task triggers the ECS circuit breaker and automatic rollback. A failure in the + post-deployment smoke check explicitly restores the prior task-definition revision. +- Database migrations are forward-only during automated rollback. Do not automatically downgrade. + If a reviewed migration is incompatible with the previous application, ship a compatible + forward fix or use the documented RDS snapshot recovery procedure. +- For database corruption or loss, restore the latest verified snapshot as described in + ADR-0022, reapply private networking, TLS, backup, deletion-protection, and log-export controls, + then repeat migration, fixture setup, deployment, and protected flagship verification. Retain + the predecessor snapshot until the restored audit timeline passes. +- Between demos, follow the safe off-state procedure in the Terraform runbook. Confirm ECS counts + are zero, the ALB is absent, and RDS is stopped for at most seven days or replaced by a verified + retained snapshot. An unavailable URL alone is not proof that billing has stopped. + +## Acceptance evidence + +| Requirement | Evidence | +|---|---| +| Reproducible production containers | Digest-pinned base images, `requirements.lock` hashes, `package-lock.json`, non-root runtime stages, CI image builds | +| Runtime configuration and secrets | ECS non-secret environment fields plus separately scoped Secrets Manager injections | +| Controlled migration | Protected one-off migration task completes before service update | +| Health and HTTPS architecture | ECS container checks, ALB target check, public `/readyz` through Next.js to task-local FastAPI, and tested HSTS/security headers | +| Authentication boundary | ES256 ALB claim verification, Cognito access-token verification, exact one-group mapping, proxy header replacement tests | +| Observability | Structured JSON stdout, CloudWatch log groups, response/request correlation check | +| Keyless CI/CD | GitHub Environment approval plus AWS OIDC role | +| Fictional demo setup | Restricted, idempotent setup validates the deterministic Jira receipt, replay prevention, and immediately inspectable Unified Audit Timeline without claiming live proof | +| Flagship journey | Protected post-deployment evidence task rejects the seed and checks a human-performed analysis, reviewer identity, approval, immutable preparation, admin identity, explicit successful execution, simulated side effect, and audit provenance | +| Recovery | ECS automatic/manual application rollback and reviewed forward-only database recovery above | diff --git a/docs/decisions.md b/docs/decisions.md index 67bdc90..9d21e39 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -893,7 +893,7 @@ aggregate can support it. ## Status -Proposed +Accepted ## Date diff --git a/infra/terraform/README.md b/infra/terraform/README.md index a8e8a12..2f76158 100644 --- a/infra/terraform/README.md +++ b/infra/terraform/README.md @@ -5,9 +5,9 @@ Terraform is the source of truth for managed AWS resources. It does not deploy a releases, run Alembic, create database roles, seed data, or implement the later demo-lifecycle workflows. -## Hard deployment gate +## Application deployment gate -Do not apply either root until ADR-0022's application prerequisites are implemented and tested: +Issue #45 implements and tests ADR-0022's application prerequisites: 1. Next.js verifies ALB and Cognito claims and fails closed. 2. The proxy replaces, rather than forwards, browser actor and role headers. @@ -17,8 +17,12 @@ Do not apply either root until ADR-0022's application prerequisites are implemen 5. Next.js exposes the configured health endpoint. 6. Production container sizing and the bounded synchronous path are verified. -Issue #44 establishes reproducible infrastructure definitions only. The safe default is -`demo_enabled = false`: no ALB or application DNS alias and ECS desired count zero. +The protected two-phase release workflow, initial database-role bootstrap, live human-journey +evidence gate, and recovery steps are in +[`docs/aws-deployment.md`](../../docs/aws-deployment.md). This does not authorize an +unreviewed Terraform apply: the exact saved plan, account, domain, and cost impact still require +human approval. The safe default is `demo_enabled = false`: no ALB or application DNS alias and +ECS desired count zero. ## Root layout and provisioned controls @@ -33,7 +37,8 @@ The demo root provisions: - security groups allowing only ALB-to-Next.js on 3000 and task-to-RDS on 5432, with no public FastAPI or RDS ingress; - immutable scan-on-push ECR repositories; -- one two-container Fargate application task, one separate migration task, and one ECS service; +- one two-container Fargate application task, separate migration and fictional-bootstrap task + definitions with distinct DB authority, and one ECS service; - encrypted private Single-AZ PostgreSQL 17 RDS, 20 GiB gp3 storage, 50 GiB autoscaling cap, seven-day backups, TLS enforcement, log exports, deletion protection, and an AWS-managed break-glass master password; @@ -370,5 +375,6 @@ explicitly authorize browser assistance while already signed in. Do not share th password. Secret values and Cognito users are not needed to provision the safe off-state infrastructure, and Codex should not receive real secret values in chat. -At present, ADR-0022's hard deployment gate prevents any AWS apply. This repository change -therefore performs validation and planning logic only and creates no cloud resources. +Repository changes and CI perform validation only and create no cloud resources. Apply a saved +plan only through the reviewed operator procedure above, then use the protected application +deployment workflow and recovery runbook. diff --git a/infra/terraform/environments/demo/iam.tf b/infra/terraform/environments/demo/iam.tf index d37c8f6..b5d8cec 100644 --- a/infra/terraform/environments/demo/iam.tf +++ b/infra/terraform/environments/demo/iam.tf @@ -66,6 +66,7 @@ data "aws_iam_policy_document" "application_execution" { resources = [ "${aws_cloudwatch_log_group.api.arn}:*", "${aws_cloudwatch_log_group.web.arn}:*", + "${aws_cloudwatch_log_group.bootstrap.arn}:*", ] } @@ -208,6 +209,19 @@ data "aws_iam_policy_document" "github_deploy" { ] } + statement { + sid = "ReadNamedImageMetadata" + effect = "Allow" + actions = [ + "ecr:DescribeImages", + "ecr:DescribeRepositories", + ] + resources = [ + aws_ecr_repository.api.arn, + aws_ecr_repository.web.arn, + ] + } + statement { sid = "RegisterReviewedTaskDefinitions" effect = "Allow" @@ -225,10 +239,13 @@ data "aws_iam_policy_document" "github_deploy" { } statement { - sid = "RunNamedTaskDefinitions" - effect = "Allow" - actions = ["ecs:RunTask"] - resources = ["arn:${data.aws_partition.current.partition}:ecs:${var.aws_region}:${data.aws_caller_identity.current.account_id}:task-definition/${local.migration_family}:*"] + sid = "RunNamedTaskDefinitions" + effect = "Allow" + actions = ["ecs:RunTask"] + resources = [ + "arn:${data.aws_partition.current.partition}:ecs:${var.aws_region}:${data.aws_caller_identity.current.account_id}:task-definition/${local.migration_family}:*", + "arn:${data.aws_partition.current.partition}:ecs:${var.aws_region}:${data.aws_caller_identity.current.account_id}:task-definition/${local.bootstrap_family}:*", + ] condition { test = "ArnEquals" @@ -264,6 +281,7 @@ data "aws_iam_policy_document" "github_deploy" { "ecs:DescribeServices", "ecs:DescribeTaskDefinition", "ecs:DescribeTasks", + "ecs:ListTaskDefinitions", "ecs:ListTasks", ] resources = ["*"] @@ -280,6 +298,7 @@ data "aws_iam_policy_document" "github_deploy" { "${aws_cloudwatch_log_group.api.arn}:*", "${aws_cloudwatch_log_group.web.arn}:*", "${aws_cloudwatch_log_group.migration.arn}:*", + "${aws_cloudwatch_log_group.bootstrap.arn}:*", ] } diff --git a/infra/terraform/environments/demo/ingress.tf b/infra/terraform/environments/demo/ingress.tf index 096a7ff..4483606 100644 --- a/infra/terraform/environments/demo/ingress.tf +++ b/infra/terraform/environments/demo/ingress.tf @@ -90,6 +90,27 @@ resource "aws_lb_listener" "https" { } } +resource "aws_lb_listener_rule" "public_health" { + count = var.demo_enabled ? 1 : 0 + listener_arn = aws_lb_listener.https[0].arn + priority = 10 + + action { + type = "forward" + target_group_arn = aws_lb_target_group.web.arn + } + + condition { + path_pattern { + values = ["/healthz", "/readyz"] + } + } + + tags = { + Name = "${local.name_prefix}-public-health" + } +} + resource "aws_route53_record" "app_ipv4" { count = var.demo_enabled ? 1 : 0 diff --git a/infra/terraform/environments/demo/locals.tf b/infra/terraform/environments/demo/locals.tf index 4e8819d..4e795e4 100644 --- a/infra/terraform/environments/demo/locals.tf +++ b/infra/terraform/environments/demo/locals.tf @@ -22,6 +22,7 @@ locals { ecs_service_arn = "arn:${data.aws_partition.current.partition}:ecs:${var.aws_region}:${data.aws_caller_identity.current.account_id}:service/${local.ecs_cluster_name}/${local.ecs_service_name}" application_family = "${local.name_prefix}-application" migration_family = "${local.name_prefix}-migration" + bootstrap_family = "${local.name_prefix}-bootstrap" database_identifier = "${local.name_prefix}-postgres" secret_arns = { diff --git a/infra/terraform/environments/demo/outputs.tf b/infra/terraform/environments/demo/outputs.tf index 7b6a999..f59f1ba 100644 --- a/infra/terraform/environments/demo/outputs.tf +++ b/infra/terraform/environments/demo/outputs.tf @@ -93,6 +93,11 @@ output "migration_task_definition_arn" { value = aws_ecs_task_definition.migration.arn } +output "bootstrap_task_definition_arn" { + description = "Separate one-off fictional-data bootstrap task using only runtime DB authority." + value = aws_ecs_task_definition.bootstrap.arn +} + output "database_identifier" { description = "Private RDS instance identifier." value = aws_db_instance.main.identifier diff --git a/infra/terraform/environments/demo/runtime.tf b/infra/terraform/environments/demo/runtime.tf index be0c377..b62086e 100644 --- a/infra/terraform/environments/demo/runtime.tf +++ b/infra/terraform/environments/demo/runtime.tf @@ -13,6 +13,11 @@ resource "aws_cloudwatch_log_group" "migration" { retention_in_days = 30 } +resource "aws_cloudwatch_log_group" "bootstrap" { + name = "/changeops/${var.environment}/bootstrap" + retention_in_days = 30 +} + resource "aws_cloudwatch_log_group" "rds_postgresql" { name = "/aws/rds/instance/${local.database_identifier}/postgresql" retention_in_days = 30 @@ -154,6 +159,10 @@ locals { name = "CHANGEOPS_API_BASE_URL" value = "http://127.0.0.1:8000" }, + { + name = "CHANGEOPS_IDENTITY_MODE" + value = "alb" + }, { name = "AWS_REGION" value = var.aws_region @@ -182,6 +191,16 @@ locals { valueFrom = "${aws_secretsmanager_secret.migration_db.arn}:password::" }, ] + + bootstrap_environment = [ + for item in local.api_environment : item + if contains(["APP_NAME", "AWS_REGION", "DB_HOST", "DB_PORT", "DB_NAME", "DB_SSLMODE"], item.name) + ] + + bootstrap_secrets = [ + for item in local.api_secrets : item + if contains(["DB_USERNAME", "DB_PASSWORD"], item.name) + ] } resource "aws_ecs_task_definition" "application" { @@ -326,6 +345,44 @@ resource "aws_ecs_task_definition" "migration" { } } +resource "aws_ecs_task_definition" "bootstrap" { + family = local.bootstrap_family + network_mode = "awsvpc" + requires_compatibilities = ["FARGATE"] + cpu = 512 + memory = 1024 + execution_role_arn = aws_iam_role.application_execution.arn + task_role_arn = aws_iam_role.application_task.arn + + container_definitions = jsonencode([ + { + name = "bootstrap" + image = "${aws_ecr_repository.api.repository_url}:${var.container_image_tag}" + essential = true + command = ["python", "-m", "changeops.deployment_bootstrap"] + environment = local.bootstrap_environment + secrets = local.bootstrap_secrets + logConfiguration = { + logDriver = "awslogs" + options = { + awslogs-group = aws_cloudwatch_log_group.bootstrap.name + awslogs-region = var.aws_region + awslogs-stream-prefix = "bootstrap" + } + } + }, + ]) + + runtime_platform { + cpu_architecture = "X86_64" + operating_system_family = "LINUX" + } + + tags = { + Name = local.bootstrap_family + } +} + resource "aws_ecs_service" "app" { name = local.ecs_service_name cluster = aws_ecs_cluster.main.id @@ -364,4 +421,10 @@ resource "aws_ecs_service" "app" { tags = { Name = local.ecs_service_name } + + lifecycle { + # Terraform owns the service and its safe desired-count state. The protected release + # workflow owns immutable application task-definition revisions. + ignore_changes = [task_definition] + } } diff --git a/infra/terraform/environments/demo/tests/architecture.tftest.hcl b/infra/terraform/environments/demo/tests/architecture.tftest.hcl index 1f819c1..f749d91 100644 --- a/infra/terraform/environments/demo/tests/architecture.tftest.hcl +++ b/infra/terraform/environments/demo/tests/architecture.tftest.hcl @@ -139,6 +139,14 @@ run "adr_0022_off_state" { ]) error_message = "The migration task must receive only the migration database secret." } + + assert { + condition = length(local.bootstrap_secrets) == 2 && alltrue([ + for secret in local.bootstrap_secrets : + strcontains(secret.valueFrom, aws_secretsmanager_secret.runtime_db.arn) + ]) + error_message = "The fictional bootstrap must use only restricted runtime DB authority." + } } run "declared_demo_window" { @@ -175,6 +183,23 @@ run "declared_demo_window" { ]) == aws_lb.app[0].arn error_message = "Next.js must verify the signed OIDC signer against the load balancer ARN." } + + assert { + condition = one([ + for item in local.web_environment : + item.value if item.name == "CHANGEOPS_IDENTITY_MODE" + ]) == "alb" + error_message = "The deployed proxy must fail closed on verified ALB and Cognito identity." + } + + assert { + condition = sort(flatten([ + for listener_condition in aws_lb_listener_rule.public_health[0].condition : flatten([ + for pattern in listener_condition.path_pattern : pattern.values + ]) + ])) == sort(["/healthz", "/readyz"]) + error_message = "Only the non-product health and readiness routes may bypass Cognito." + } } run "rejects_wrong_region" { diff --git a/requirements.lock b/requirements.lock new file mode 100644 index 0000000..37bf3d5 --- /dev/null +++ b/requirements.lock @@ -0,0 +1,1709 @@ +# +# This file is autogenerated by pip-compile with Python 3.12 +# by the following command: +# +# pip-compile --generate-hashes --output-file=requirements.lock --strip-extras pyproject.toml +# +alembic==1.18.5 \ + --hash=sha256:06d8ba9d04558022f5395e9317de03d270f3dced49cee01f89fe7a13c26f14bc \ + --hash=sha256:1554982221dd17e9a749b53902407578eb305e453f71999e8c7f0a48389fff8e + # via changeops (pyproject.toml) +annotated-doc==0.0.5 \ + --hash=sha256:117bac03a25ede5df5440e855b32d556049ca169ead221505badf432fed4b101 \ + --hash=sha256:c7e58ce09192557605d8bbd92836d7e1d520ac9580096042c0bfd197efacf1bb + # via fastapi +annotated-types==0.8.0 \ + --hash=sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7 \ + --hash=sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0 + # via pydantic +anyio==4.14.2 \ + --hash=sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494 \ + --hash=sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f + # via + # httpx + # langsmith + # openai + # starlette +certifi==2026.7.22 \ + --hash=sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775 \ + --hash=sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55 + # via + # httpcore + # httpx + # requests +charset-normalizer==3.4.9 \ + --hash=sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380 \ + --hash=sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62 \ + --hash=sha256:04ce310cb89c15df659582aee80a0603788732a5e017d5bd5c81158106ce249c \ + --hash=sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226 \ + --hash=sha256:0e94703ec9684807f20cfb5eed95c70f67f2a8f21ad620146d7b5a13677b93e5 \ + --hash=sha256:0fa1aec2d32bcc03c8fa0f6f1712caad1adc38509f31142112e5c9daf5b9c833 \ + --hash=sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b \ + --hash=sha256:16d10d789dd9bcca1173c95af82c58433122564b7bc39385124be735a35cbe99 \ + --hash=sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501 \ + --hash=sha256:1d22856ffbe153a602df38e4a5464f0b748a54002e0d69ac6d2ad0a197cc99ec \ + --hash=sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698 \ + --hash=sha256:231ddcbb35e2ff8973e1365db41fe0572662893b99a05deb183b68ad4c0c8bd4 \ + --hash=sha256:253a4a220747e8b5faf57ec320c4f5efb0cef05f647420bf267143ec15dba10a \ + --hash=sha256:280081916dc341820640489a66e4696049401ef1cf6dd672f672e70ad915aca3 \ + --hash=sha256:2a441ea71902098ffe78c5abe6c494f44160b4af614ed16c3d9a3b1d17fd8ee2 \ + --hash=sha256:304b13570067b2547562e308af560b3963857b1fa90bd6afd978130130fe2d6a \ + --hash=sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e \ + --hash=sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4 \ + --hash=sha256:375b83ed0aecfce76c16d198fbc21f3b11b337d68662bea0a995046682a11419 \ + --hash=sha256:3c09a49d6cde137258beb3d551994a2927fd35ad5cf96aed573f61bbd67c5f84 \ + --hash=sha256:3d92613ec25e43b05f042302531ec0f00b8445190e43325880cbd6ab7c2581da \ + --hash=sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519 \ + --hash=sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe \ + --hash=sha256:432786d3561e69aeeae6c7e8648964ce0ad05736120135601f87ac26b9c83381 \ + --hash=sha256:43b9e366a31fdd1c87d0eb08f579b4a82b723ea54338f040d6b4e518a026ea29 \ + --hash=sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614 \ + --hash=sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0 \ + --hash=sha256:476743fe6dfe14a2da12e3ac79125dc84a3b2cf8094369a47a1529b0cd8549fe \ + --hash=sha256:4773092f8019072343a7447203308b176e10199920eb02d6195e81bbb3274c29 \ + --hash=sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0 \ + --hash=sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917 \ + --hash=sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9 \ + --hash=sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32 \ + --hash=sha256:58150c9f9b9a552505912d182ccdf26f6396fb6094816ceebcbb20eecabaed94 \ + --hash=sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63 \ + --hash=sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd \ + --hash=sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198 \ + --hash=sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde \ + --hash=sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012 \ + --hash=sha256:6366a16e1a25018694d6a5d784d09b046edc9eac40ea2b54065c3052672516a1 \ + --hash=sha256:65a7ff3f705e57d392f7261b6d0550fe137c3019477431f1c355e0db0a7d3e15 \ + --hash=sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b \ + --hash=sha256:67830fc78e67501f47bb950471b2dcb9b35b140084429318e862895a8e89c993 \ + --hash=sha256:68ce9f4d6b26d5ccbf7fd4459bf75f74a0a146677ebba80597df60cbdb20e6f4 \ + --hash=sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5 \ + --hash=sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8 \ + --hash=sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35 \ + --hash=sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642 \ + --hash=sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2 \ + --hash=sha256:79580094b00d1789d1f93ea55bc43cb2f611910c72235b7657f3482ddcc1b22d \ + --hash=sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9 \ + --hash=sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c \ + --hash=sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33 \ + --hash=sha256:871ff67ea1aad4dfd91736464934d56b32dac49f9fbe16cddba36198a7b3a0db \ + --hash=sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf \ + --hash=sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9 \ + --hash=sha256:8c041122946b7ba21bb32c45b1aa57b1be35527690aeb3c5c234521085632eee \ + --hash=sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84 \ + --hash=sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44 \ + --hash=sha256:920079c3f7456fa213e0829ed2073aaa727fd39d889ead5b4f35d0de5460d04f \ + --hash=sha256:93d59d504b230e83c7a843251681959a0b6a9cd76f6e146ce1b8a80eb8739af9 \ + --hash=sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9 \ + --hash=sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177 \ + --hash=sha256:9bb41182d93ea91f60b4bc8fbf4c820c69ef8a12ab2d917f3f1834f1acad07e8 \ + --hash=sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b \ + --hash=sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39 \ + --hash=sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41 \ + --hash=sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0 \ + --hash=sha256:aa99adc8f081b475a12843953db36831eaf83ec33eb46a90629ca6a5de45a616 \ + --hash=sha256:ac351b3b8014eead140e77e9717e2992c6bbe30b63bc3422422eb84865412e3d \ + --hash=sha256:ad41ba96094304aa090f5a30cb6e4fb3b3f1c264c523394b4c39bbacc4dc92ba \ + --hash=sha256:b5314963fce9b0b12743891de876e724997864ee22aa496f903f426c7e2fa5b2 \ + --hash=sha256:bcf74c1df76758a395bf0af608c04c82257523f55c9868b334f06270d0f2112b \ + --hash=sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9 \ + --hash=sha256:c0323c9daef75ef2e5083624b4585018a0c9d5e3b40f607eed81a311270b934b \ + --hash=sha256:c1225416b463483160e4af85d5fc3a9690ccb53fd4b1865a6437825f5ede3209 \ + --hash=sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48 \ + --hash=sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046 \ + --hash=sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632 \ + --hash=sha256:cd6280cf040f233bd7d3407b743b4b4c74f70e8e1c4199cb112a62c941c0772a \ + --hash=sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2 \ + --hash=sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1 \ + --hash=sha256:ddf4af30b417d9fe16481e9b81c27ab2a7cde1ff7ba3e85653b02db7d145dc7b \ + --hash=sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990 \ + --hash=sha256:df7276909358e5635ae203673ab7e509ddd224225a8d6b0790bf13eb2bde1cc5 \ + --hash=sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b \ + --hash=sha256:e9701d0049d92c16703a42771b98d560b95248949f23f8cf7b4eddd201814fb9 \ + --hash=sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534 \ + --hash=sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81 \ + --hash=sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a \ + --hash=sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d \ + --hash=sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf \ + --hash=sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115 + # via requests +click==8.4.2 \ + --hash=sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6 \ + --hash=sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76 + # via uvicorn +distro==1.9.0 \ + --hash=sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed \ + --hash=sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2 + # via + # langsmith + # openai +fastapi==0.141.1 \ + --hash=sha256:bfb91aa2d334c61cb35ba9a116fc123b3d3df31640b801cf57a7a78ec3f603b3 \ + --hash=sha256:e8822fc40db1e1858054d7a949a888695bc9bdce70139178e33bd2871a453ca1 + # via changeops (pyproject.toml) +greenlet==3.5.4 \ + --hash=sha256:0232ae1de90a8e07867bb127d7a6ba2301e859145489f25cda8a6096dabe1d20 \ + --hash=sha256:07bd44616608d873d06735b63ef1a88191d6ca57c8d291d6559c71bc14c0893c \ + --hash=sha256:08fc36de8442d5c3e95b044550dbea9bf144d31ec0cc58e36fb241cb6ef6a994 \ + --hash=sha256:0fa53040b78b578120eecdc0265e3f1051487cc425d11a2b7c761daadf4feaa8 \ + --hash=sha256:123aa379c962ed5fe90a880327e0c3066124ac64ec99e12a238be9fd8eb3db3d \ + --hash=sha256:12cda9122e03341f1cb6b8207a19d7a9d375e52f1b4e9243918375f40fd7b4b9 \ + --hash=sha256:13b980043cb1b3134e81ea469da1250ddcc6bfe6d245bbaa59168d9cdc8f228f \ + --hash=sha256:178111881dd7a6c946471fda85485ec796e1043c2b939f694b096e2ecf986809 \ + --hash=sha256:1833637f17d5e7472548a48575c394fe39f1b1890d676d162d86593610f44d8c \ + --hash=sha256:188e4d142f243051d92a1f5c244a741da02dddc070a0620c842804d7b56d008c \ + --hash=sha256:1e1a4a684b16c45ba324e60b32a4386a87722bcb815d2a149d2182f9b401ca72 \ + --hash=sha256:1f17e362d78e37559e0506c5a7d066bdd45073c36a0127a543e8a0df27242ff3 \ + --hash=sha256:24e61b88cb7e1b1d794b32a10cc346ac779681d6d74ff137a3e0a444d2bf1f02 \ + --hash=sha256:27d3f00718634d4520a3a150154ac5da36f257869d41321953375b90bfbbc72c \ + --hash=sha256:2a924f15d17957e252a810acefcb5942f5ca712298e8b6fcaed9a307d357522c \ + --hash=sha256:2cdaadc3d31445a8f782bde3cd37e49a2c2a9c6da6daf76a3e34c683b271a3c7 \ + --hash=sha256:2ffbc533e0eaf8e80d8471411646ab88fe58f641d508c0b02b24494479f4d9ec \ + --hash=sha256:305f69e6c4523d7f6979ed001cff4e5853c063e5da04880296603aa0227e544c \ + --hash=sha256:32802705c2c1ff25e8237b3bdacf2594fa02be80af8a66703eb7853ea7e68686 \ + --hash=sha256:3529a8a933582ad19e224792cac7372489526576b75b4c124e8e4f29948f4861 \ + --hash=sha256:39169a11d87a6a263afda3e9a27d1df16d0f919d40a4837cc73986c9884c0dd8 \ + --hash=sha256:394de08dad5ffcb1f50c2159d93e398d9d2da3ed437645eaa54771fa720db9f0 \ + --hash=sha256:3d66250e8b09f182ede05490998c818b5961f7a3640332d44c4927caec7bbfe4 \ + --hash=sha256:3ef964f56dfcb6f9bbef2a190d9126795eac408716aeae47b5e7c73c32aafca9 \ + --hash=sha256:41ddab54e4b238f4a6c323f39b4e59e176affd5a94d461a9fb7583dac74240a3 \ + --hash=sha256:42afdc1ab5f66da8c586c32af9224a74a706b4f0ea0dc3a4188a0860a09c65c9 \ + --hash=sha256:4ab9f0704bccf6d3b38e0d2130b7b33271cff11453690da074fa280c3aa8e8e7 \ + --hash=sha256:57aa201b351f7c7c75627c60d29e4d5b97a07d37efeb62b903466fca42c097d7 \ + --hash=sha256:58023945f421093de5e6fa108c0985a8659d43f49e0216da25099369a121bcbd \ + --hash=sha256:60149df8f462d1b230038e6590c23c3b4768bb5d6c022b3b6e82532b34b0b8a3 \ + --hash=sha256:60e0bc961d367df506660e9ac0177a76bc6d81305300704b0977d1634f76efe2 \ + --hash=sha256:69173331fbc5d64bfac0065d7e22c39cfcd089e9b18d125bdcd5079363b09616 \ + --hash=sha256:70bdfacdc183dac838b2a0aaff2dd6134a457c52fe68a9c6bbab435483d2b9df \ + --hash=sha256:73b37afe369021423ea53dd3123e04bffa7e93ac64429b9f50835b2e4fcae7cf \ + --hash=sha256:77d6ce04fed0d9aeed42e0f37923cc43eba9b027bdd9c34546bb4ccd143d0fe0 \ + --hash=sha256:791fdfeeb9c6e0c7b10fa151bf110d2a6974866f13dcb5b1c7efae698245893a \ + --hash=sha256:7c1303791d603080cac6fc3b34df51c3b75b723739c282c8029e48a0d241672f \ + --hash=sha256:7e8afa5eac028f8140ceafe5ceec66e6aa127ddcb21452d2a564dcd2900b5f22 \ + --hash=sha256:870d730fec833f5a06906a32596cc099b9161594642a92a520b7a88911c95356 \ + --hash=sha256:89f3738167bab8c1084b94e23023d41d247117ac149fa0fbcb5bd4cf6262b353 \ + --hash=sha256:91c26423753b92caf41ab3f98fd547d7374d4d9fc2d85be041886c1579d9255e \ + --hash=sha256:9667862a2e38ad379f11b845daeda22c8989186def44f06962c9c4c05e556da7 \ + --hash=sha256:99e8f8c4ebc4fd80aa26c1280ae9ad43a0976e786349703a181cf0bae60413e5 \ + --hash=sha256:9c53ff01a5c53a40f2c16820ebc56d7c61a77f5fbe009dadd96292d5682f80f8 \ + --hash=sha256:9f1467de1bb767f75db0aa34c195e3a496d8d1278c796e70c24ce205d3e99cde \ + --hash=sha256:a2d614cb2372c7101a12ea8b96dd56f81c986d247c5a73db67063f3ed1ca4a52 \ + --hash=sha256:ac5bf81d79d2c8eeb2ef6359b2e1687a1e9ebf46c2b1f970da9a9255df51d190 \ + --hash=sha256:adf2244d7f69409925a8f22ed22cc5f93cdfe5c9dc87ff3476be2c2aaae61a05 \ + --hash=sha256:ae53534b5dec0f4c2ec26f898f538dc8ea1ca3ef2927d597a9439e40a09da937 \ + --hash=sha256:b3dabe3e2809013052c68bdf0b7fa5f5f2859c43a80803131ad61af9cabd7867 \ + --hash=sha256:b7a5f095767c4493afcd06067f2bb3b8716e3f3f9e92b99c88e7e99f885b3d4d \ + --hash=sha256:b7c895310363f310361e0fe2072af85269d2a2a285cd04c0c59e79a5e3670dcf \ + --hash=sha256:bae2728e1897aa8df8cb1af38cd48b3a743aefe29372de7b8b7a9f532501e69f \ + --hash=sha256:bd3d1145f603b2db19feb9078c2e6855eb7c67e15580c010ed815cee519b86fd \ + --hash=sha256:c38c902a0986eba1f6e7ba1ab39ad5195926abde90f3fe080e08212db62176da \ + --hash=sha256:c3fe76c2cac86b4f7a1e92865ac0a54384deb05c92986287c1a7110d9bd53071 \ + --hash=sha256:c883d61f2282d72c767a14936641b3efcbde9d82f1080712aaea0b1d3126cb88 \ + --hash=sha256:c90e930c9c192e5b3ee9fb8bcd920ea3926155e2e3ded39fc697323addecee17 \ + --hash=sha256:ca5726c0b08ca35ae873557266a78b2c3f3b2b7d7401aa5ff886c2045dd0111c \ + --hash=sha256:cbd60b5763c6543c1827e48faaf14ea9bfbad245f52b1a4d76a2a2d8884c6c66 \ + --hash=sha256:cd320d998cbaa032932830448e39abf3c6a12901295e386e8114db926e10cffb \ + --hash=sha256:cef589bc65fae02d10bca2ac341191c5b33acc2967892ebf4fcbd10eabb7a74c \ + --hash=sha256:d83ae0e32d14957ab7170785a20f582635c8474deab1bfbb552b17e769a6ce25 \ + --hash=sha256:d84d993f6e575c950d91a23c1345d18fe1a4310d447bf630849d7809196b52f0 \ + --hash=sha256:d92df08dd65fede97fc37aad36c2e9dcda3b31c467f8e0c2c096456cb818e927 \ + --hash=sha256:dc418cf4c873357964d6624445ed09472e50def990c65dd4e76fc3ba8cd9cef6 \ + --hash=sha256:dfc41ae893d9ceaf22c824f2153a88b30651b20e8758c2cd9ac143f23640563c \ + --hash=sha256:e849e6e139b9671adeac505f72fc05f4af7fd1921faef40295e214fc3b361b59 \ + --hash=sha256:e883de250e299654b1f1680f72a1a9f9ba62c9bd1bce84099c90657349a8dfbb \ + --hash=sha256:e9a5e3406e3ed8125ae1a3b37c12f3434e2b1f0fa053197c5557895b4fb09606 \ + --hash=sha256:ec5ff0d1878df6af3bf9b638a5a92a7d5693291de77c91bff10fa48519c604ef \ + --hash=sha256:ecca4d80d55a01ad6b23b33262662956149fbb7b2c6be2910f1705921958cbf3 \ + --hash=sha256:ed17e5f3420360d5b459de8462efb52060399a5326a613d4cde31cef63ef95da \ + --hash=sha256:ee032b91fd8ec29ec6c4cea2b8c561b178435134bd0752c7334b94e9c736c132 \ + --hash=sha256:f00f910f0e7b35416c63b23ad78b769aeccfc1775f712b43c4ee525624a2eef7 \ + --hash=sha256:f260930bbbbcf9caee661211235a5111c86dfe5832fdf6ae4570da1e0995320f \ + --hash=sha256:f680e549edb3eaf21eea4e7fe101e15ec180c74b7879ab46adc080f22d4015d2 \ + --hash=sha256:f88193799d43dbf8c8a806d6405c9c52fe2af40bf75072a606357b33cc336c7f \ + --hash=sha256:f908898d6fa484ce4b6f447ce70ea99b52c503fee419e53cf74d60a16bc9e667 + # via sqlalchemy +h11==0.16.0 \ + --hash=sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1 \ + --hash=sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86 + # via + # httpcore + # uvicorn +httpcore==1.0.9 \ + --hash=sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55 \ + --hash=sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8 + # via httpx +httpx==0.28.1 \ + --hash=sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc \ + --hash=sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad + # via + # langgraph-sdk + # langsmith + # openai +idna==3.18 \ + --hash=sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2 \ + --hash=sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848 + # via + # anyio + # httpx + # requests +jiter==0.16.0 \ + --hash=sha256:0542a7189c26920778658fc8fcf2af8bae05bae9924577f71804acef37996536 \ + --hash=sha256:0758ab7747a984797cf048e8eedea1d8ef39d7994b25611daf5b48fc903e8873 \ + --hash=sha256:0df91907609837f33341b8e6fe73b95991fdaa57caf1a0fbd343dffe826f386f \ + --hash=sha256:0e2e9efbe042210df657bade597f66d6d75723e3d8f45a12ea6d8167ff8bbce3 \ + --hash=sha256:0fa25b09b13075c46f5bc174f2690525a925a4fc2f7c82969a2bbabff22386ce \ + --hash=sha256:10438939205546132189c8e74a2d536a707841f3a25cd7c74ee91fe503407a26 \ + --hash=sha256:12288303c9844e61e1651d02a9a6f6633e47d39f897d6991d1427161ce6b746e \ + --hash=sha256:17ca7fae79f6d99cd9a042b75f917eaada7b895cfc7dd2ee3a16089dcaec7a85 \ + --hash=sha256:1878349266f8ee36ecb1375cc5ba2f115f35fd9f0a1a4119e725e379126647f7 \ + --hash=sha256:1a0f05e229edb29e68cdd0ccb83cea13b64263416120cf943767a6fd72e6787f \ + --hash=sha256:2577196f4474ef3fc4779a088a23b0897bbf86f9ea3679c372d45b8383b43207 \ + --hash=sha256:26798522707abb47d767db536e4148ceac1b14446bf028ee85e579a2e043cfe5 \ + --hash=sha256:26d122613ada2b708eb714695446f40fce5bdf2edb4b02116dec62faa62dfab3 \ + --hash=sha256:2a0d46ef67cc58d906a6132dd3040ca70ae4f0b0d7c9c052fe432c658a69b3f6 \ + --hash=sha256:2a8e9e39cf083016137aa5cadafe3188adc2ba6ba1fbf1e5d18889ad3e9ad056 \ + --hash=sha256:2c842cbf374a8daf50b2c04212995bee34ca2ac2cdc29a901b4cdb072c9c4131 \ + --hash=sha256:2ed5738ae4af18271a51a528b8811b0cbfa4a1858de9d83359e4169855d6a331 \ + --hash=sha256:3126a5dbad56401989ac769aca0cb56005bfb3e2366eea0ca99d1a91c3c1ee03 \ + --hash=sha256:31aaee5b80f672c1dc21272bcfb9cbdcfc1ea04ff50f00ed5af500b80c44fa93 \ + --hash=sha256:3a23f0e4f957e1be65752d2dfac9a5a06b1917af8dc85deb639c3b9d02e31290 \ + --hash=sha256:3b21b412b899fd8bd51a3046934b59a3bb068b79f70a5c6010053ac77cc53f0c \ + --hash=sha256:3c1fd2dbe1b0af19e987f03fe66c5f5bd105a2229c1aff4ab14890b24f41d21a \ + --hash=sha256:3f4d9e473a5ce7d27fef8b848df4dc16e283893d3f53b4a585e72c9595f3c284 \ + --hash=sha256:3fb7bc819187b56dc48aa5c833aaf92257da8e07efdb9306156667bd2eeb491c \ + --hash=sha256:41977aa5654023948c2dae2a81cbf9c43343954bef1cd59a154dd15a4d84c195 \ + --hash=sha256:44d019fa8cdaf89bf29c71b39e3712143fdd0ac76725c6ef954f9957a5ea8730 \ + --hash=sha256:46add52f4ad47a08bfb1219f3e673da972191489a33016edefdb5ea55bfa8c48 \ + --hash=sha256:49060fd70737fad59d33ba9dcc0d83247dc9e77187de26053a19c16c9f32bd69 \ + --hash=sha256:491e7d072a253b156fff46b78bceac4652a697aa8d7082c9c18c03d7b7917d24 \ + --hash=sha256:4a1d68ff7ca1d3b5dee20a97a3decda7d5f15003823bf6d140c81f8561d3bc5c \ + --hash=sha256:51d7b836acb0108d7c77df1742332cac2a1fa04a74d6dacec46e7091f0e91274 \ + --hash=sha256:560b2cf3fb03240cd34f27409a238547488708f05b7c3924f571a60422251ec7 \ + --hash=sha256:561926e0573ffe4a32498420a76d64b16c513e1ab413b9d28158a8764ac701e5 \ + --hash=sha256:57b37fc887a32d44798e4d8ebfa7c9683ff3da1d5bf38f08d1bb3573ccb39106 \ + --hash=sha256:5a71d0d2014c3275043e1170bf3d4e771493cb0dcf07be54c567155f4d8ee64b \ + --hash=sha256:5ab4f50ff971b611d656554ea10b75f80097392c827bc32923c6eeb6386c8b00 \ + --hash=sha256:5af7780e4a26bd7d0d989592bf9ef12ebf806b74ab709223ecca37c749872ea9 \ + --hash=sha256:5cf109d010b4b05a105afb3d43be36a21322d345ad3111e13d15f680afef0e5b \ + --hash=sha256:5ed466aee31294d7cdcd4d37dfe5c42c97bc29d9a5f00eacf24504358309cb9b \ + --hash=sha256:616e89e008a93c01104161c75b4988e58716b01d62307ebfe161e52a56d2a818 \ + --hash=sha256:62c1b7fe1f77925acf5af68b6140b8810fa87dfd4dc0a9c8568ec2fa2a10429c \ + --hash=sha256:63efadc657488f45db1c676d81e704cac2abf3fdb892def1faea61db053127e2 \ + --hash=sha256:63ffdbdae7d4499f4cda14eadc12ddcabef0fc0c081191bdc2247489cb698077 \ + --hash=sha256:64c0203212098470032aabcde9356fc168f377aade3e43def61dfe17e92f2037 \ + --hash=sha256:64d613743df53199b1aa256a7d328340da6d7078aac7705a7db9d7a791e9cfd2 \ + --hash=sha256:659039cc50b5addcc35fcc87ae2c1833b7c0a8e5326ef631a75e4478447bcf84 \ + --hash=sha256:6722bcef4ffc86c835574b1b2fac6b33b9fb4a889c781e67950e891591f3c55a \ + --hash=sha256:67c3bc1760f8c99d805dcab4e644027142a53b1d5d861f18780ebdbd5d40b72a \ + --hash=sha256:67fddeda1688f0cce2d2ae83ccf8a80f79936f2d2997d6cc2261f82fdb54a4d3 \ + --hash=sha256:6edb63a46e65a82c26800a868e49b2cac30dd5a4218b88d74bc2c848c8ad60bb \ + --hash=sha256:70a490b55634dc0d2606ce8a8e01b1d62459011beb368d15d76e1eaf62460e3d \ + --hash=sha256:710cc51d4ebdcd3c1f70b232c1db1ea1344a075770422bbd4bede5708335acbe \ + --hash=sha256:741eed508c233a76313a1c7b001f8f21b82f14327e9196ae8bd29a2cc164ae84 \ + --hash=sha256:7b24c3492c5f4f84a37946ad9cf504910cf6a782d6a4e0689b6673c5894b4a1c \ + --hash=sha256:7c9610fd25ebccb43fca584136f5c2fbb26802447eccd430dfdbab95a0fd5126 \ + --hash=sha256:850ccb1d7eedb4200f4014b1c0e8a577de114fc3cd88faad646dcc9bc4bb12ad \ + --hash=sha256:8597d23c87f59294f83bcb6229b9ed1fccee13dbba967b46930d2f1759466fee \ + --hash=sha256:8d031aabecc4f1b6276adfb42e3aabb77c89d468bf616600e8d3a11328929053 \ + --hash=sha256:8d30a4a1c87713060c8d1cc59a7b6c8fb6b8ef0a6900368014c76c87922a2929 \ + --hash=sha256:8fb8de1e23a0cb2a7f53c335049c7b72b6db41aa6227cdcc0972a1de5cb39450 \ + --hash=sha256:8fc4d94713c4697347e38faf7d6ef91547c142219bdcfc7220c4870879974244 \ + --hash=sha256:96e38eea538c8ddf853a35727c7be0741c76c13f04148ac5c116222f50ece3b3 \ + --hash=sha256:9acf1b2faec82d998811ecce7ae84d9005e53410773e9d37d61cdc424ba4581b \ + --hash=sha256:9c8a956fd72c2cf1e730d01ea080341f13aa0a97a4a33b51abebe725b7ae9ca9 \ + --hash=sha256:9ec553a99b0987efd7a3645a1a825cf29c224e494db267a83369fcc8da9aeda5 \ + --hash=sha256:a111256a7193bea0759267b10385e5870949c239ed7b6ddbaaf57573edb38734 \ + --hash=sha256:a32d2027a9fa67f109ff245a3252ece3ccc32cc56703e1deab6cc846a59e0585 \ + --hash=sha256:a5c54ef4ff776d9675837ef535b3308d6e31c208d43ebc44a0f7ab8a208c68f7 \ + --hash=sha256:adbb8edeadd431bc4477879d5d371ece7cb1334486584e0f252656dd7ffada29 \ + --hash=sha256:adff21bc78edfe086c15eb495b900306076de378dc2337c132401fc39bd79c91 \ + --hash=sha256:b1e7923093a376d93c6eb507c77045ae258d689ba577392846a1b3f10d0b09a9 \ + --hash=sha256:b2c61484666ad42726029af0c00ef4541f0f3b5cdc550221f56c2343208018ee \ + --hash=sha256:b42e9ff5376819c053da25809a8d4b6fa6e473b4856ebe42e298ac958be3d7f9 \ + --hash=sha256:b72d0b2990ca754a9102779ac98d8597b7cb31678958562214a007f909eab78e \ + --hash=sha256:b7dc85ea77d4abbae8bad0d3538678aedee75bceec4e2f6c8dfb1c74772e5aa5 \ + --hash=sha256:b8a3f9a6008048fe9def7bf465180564a6e458047d2ce499149cfbe73c3ae9db \ + --hash=sha256:baad945ed47f163ad833314f8e3288c396118934f94e7bbb9e243ce4b341a4fd \ + --hash=sha256:bae96332410f866e5900d809298b1ed82735932986c672495f9701daacd80620 \ + --hash=sha256:bc837c1b9631be10abfe0191537fe8009838204cec7e44827401ace390ddb567 \ + --hash=sha256:bf3ea07d9bc8e7d03a9fbc051295462e6dbc295b894fd72457c3136e3e43d898 \ + --hash=sha256:c22a488f7b9218e245a0025a9ba6b100e2e54700831cf4cf16833a27fba3ad01 \ + --hash=sha256:c4b4717bdb35ae456f831a6b08d01880fff399887a6bbc526a583a406e484eea \ + --hash=sha256:c5fc4f8def331036a7b8e981b4347ebe409981edbc8308a5ea842b8c3614fa6c \ + --hash=sha256:c682bea068a90b764577bdb78a60a4c1d1606daf9cd4c893832a37c7cc9d9026 \ + --hash=sha256:c90c0f63df322be920eda6ce622e3083d8906ba267f8220fe7873213b8b4430e \ + --hash=sha256:c9c53be232c2e206ef9cdbad81a48bfa74c3d3f08bcf8124630a8a748aad993e \ + --hash=sha256:cbd18dd5e2df96b580487b5745adf57ef64ad89ba2d9662fc3c19386acce7db8 \ + --hash=sha256:cf0d73f50e7b6935677854f6e8e31d499ca7064dd24734f703e060f5b237d883 \ + --hash=sha256:d284fb8d94d5855d60c44fefcab4bf966f1da6fada73992b01f6f0c9bc0c6702 \ + --hash=sha256:d28bb3c26762358dadf3e5bf0bccd29ae987d65e6988d2e6f49829c76b003c09 \ + --hash=sha256:d5bf78d0e05e45cfdd66558893938d59afe3d1b1a824a202039b20e607d25a72 \ + --hash=sha256:d5f91b1c27fc22a57993d5a5cb8a627cb8ed4b10502716fac1ffbfe1d19d84e8 \ + --hash=sha256:d8f80521644426d451e70f00c7974240cab8f6ee088aedaa9af2697153ab7805 \ + --hash=sha256:da3d7ec75dc83bb18bca888b5edfae0656a26849056c59e05a7728badd17e7af \ + --hash=sha256:dab907db06fc593645e73109acf4581ba5b548897d28b9348dc41ddc8343b2d3 \ + --hash=sha256:de5ba8763e56b793561f43bed197c9ea55776daa5e9a6b91eed68a909bc9cdbf \ + --hash=sha256:e03a5f21a5ce96a9441b8cb32719a8b88ed5388f53e0f339c5bcf54f1317f9d0 \ + --hash=sha256:e34e97bda77eb63242a410243c071e28ac7e0d8c0948c5ee658498690a4b2f2f \ + --hash=sha256:e431cfc9caf44c1d5459ff77d4e64cbf85fddb6a35dad836a15c6a9ec23087c1 \ + --hash=sha256:eab2cd170150e70153de16896a1774e3a1dca80154c56b54d7a812c479a7165e \ + --hash=sha256:ee6162b77d49a9939229df666dfa8af3e656b6701b54c4c84966d740e189264e \ + --hash=sha256:f17d61a28b4b3e0e3e2ba98490c70501403b4d196f78732439160e7fd3678127 \ + --hash=sha256:f3bd327cdfa118bc1ce69c214c2678571d5bd39b8ccd0ebf43a54db00541ba9a \ + --hash=sha256:f4444a83f946605990c98f625cdd3d2725bfb818158760c5748c653170a20e0e \ + --hash=sha256:fb08c276dd02dac3a284acdd02cacc630d2e3cd6572a4b85519f35cbd133c3de + # via openai +jsonpatch==1.33 \ + --hash=sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade \ + --hash=sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c + # via langchain-core +jsonpointer==3.1.1 \ + --hash=sha256:0b801c7db33a904024f6004d526dcc53bbb8a4a0f4e32bfd10beadf60adf1900 \ + --hash=sha256:8ff8b95779d071ba472cf5bc913028df06031797532f08a7d5b602d8b2a488ca + # via jsonpatch +langchain-core==1.5.3 \ + --hash=sha256:48b56fa580277209594dd7baf837f5b9a2a3651613f34ff9fb1728b429df015f \ + --hash=sha256:a56457ac444fef41e9404443c187f0ecea708d36e816ea4ba9573c027f7d1a2d + # via + # changeops (pyproject.toml) + # langchain-openai + # langgraph + # langgraph-checkpoint + # langgraph-prebuilt +langchain-openai==1.4.1 \ + --hash=sha256:6d16be615d997db80294731b8e768783f1fb8e0313668e64acd50cd68acbad20 \ + --hash=sha256:8528bb34cc78fdfd2d895573c7917f9441cbb82db5f18ae0e6b3b75d95bdefb3 + # via changeops (pyproject.toml) +langchain-protocol==0.0.18 \ + --hash=sha256:70b53a86fbf9cedc863555effe44da192ab02d556ddbf2cf95b8873adcf41b5a \ + --hash=sha256:ec3e11782f1ed0c9db38e5a9ed01b0e7a0d3fba406faa8aef6594b73c56a63e6 + # via langchain-core +langgraph==1.0.8 \ + --hash=sha256:2630fc578846995114fd659f8b14df9eff5a4e78c49413f67718725e88ceb544 \ + --hash=sha256:da737177c024caad7e5262642bece4f54edf4cba2c905a1d1338963f41cf0904 + # via changeops (pyproject.toml) +langgraph-checkpoint==4.1.1 \ + --hash=sha256:25d29144b082827218e7bc3f1e9b0566a4bb007895cd6cc26f66a8428739f56e \ + --hash=sha256:6c2bdb530c91f91d7d9c1bd100925d0fc4f498d418c17f3587d1526279482a25 + # via + # langgraph + # langgraph-prebuilt +langgraph-prebuilt==1.0.13 \ + --hash=sha256:7055e9fad41fbd3593800aed0aea0a6e974b17f33ed51b80d3d3a031212dd7c0 \ + --hash=sha256:ad219782a80e1718e7e7794de49e0ae307111d45cbcffab9a52725a66a609456 + # via langgraph +langgraph-sdk==0.3.15 \ + --hash=sha256:29e805003d2c6e296823dd71992610976fd0428cefaa8b3304fd91f2247037de \ + --hash=sha256:3838773acf7456d158165385d49f48f1e856f28b56ccd99ea139a8f27004815d + # via langgraph +langsmith==0.10.16 \ + --hash=sha256:7dc5ff6477d50101b4944a985773bf9a3e2a9374e5d71ed2549ada929ca500b2 \ + --hash=sha256:f0b3882e0d2d69bda596c7b452a7857943aefb2c531c047677470b0a394cc490 + # via langchain-core +mako==1.4.1 \ + --hash=sha256:a359d9a94a541213958742b2698d0a7757bb83551767bc468a74b9905aba9617 \ + --hash=sha256:d7904710b662996425a21627710c4777c45053146942cf8a7aebf757c92b8c27 + # via alembic +markupsafe==3.0.3 \ + --hash=sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f \ + --hash=sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a \ + --hash=sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf \ + --hash=sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19 \ + --hash=sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf \ + --hash=sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c \ + --hash=sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175 \ + --hash=sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219 \ + --hash=sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb \ + --hash=sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6 \ + --hash=sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab \ + --hash=sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26 \ + --hash=sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1 \ + --hash=sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce \ + --hash=sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218 \ + --hash=sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634 \ + --hash=sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695 \ + --hash=sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad \ + --hash=sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73 \ + --hash=sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c \ + --hash=sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe \ + --hash=sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa \ + --hash=sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559 \ + --hash=sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa \ + --hash=sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37 \ + --hash=sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758 \ + --hash=sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f \ + --hash=sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8 \ + --hash=sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d \ + --hash=sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c \ + --hash=sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97 \ + --hash=sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a \ + --hash=sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19 \ + --hash=sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9 \ + --hash=sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9 \ + --hash=sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc \ + --hash=sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2 \ + --hash=sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4 \ + --hash=sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354 \ + --hash=sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50 \ + --hash=sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698 \ + --hash=sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9 \ + --hash=sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b \ + --hash=sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc \ + --hash=sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115 \ + --hash=sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e \ + --hash=sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485 \ + --hash=sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f \ + --hash=sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12 \ + --hash=sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025 \ + --hash=sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009 \ + --hash=sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d \ + --hash=sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b \ + --hash=sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a \ + --hash=sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5 \ + --hash=sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f \ + --hash=sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d \ + --hash=sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1 \ + --hash=sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287 \ + --hash=sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6 \ + --hash=sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f \ + --hash=sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581 \ + --hash=sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed \ + --hash=sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b \ + --hash=sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c \ + --hash=sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026 \ + --hash=sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8 \ + --hash=sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676 \ + --hash=sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6 \ + --hash=sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e \ + --hash=sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d \ + --hash=sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d \ + --hash=sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01 \ + --hash=sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7 \ + --hash=sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419 \ + --hash=sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795 \ + --hash=sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1 \ + --hash=sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5 \ + --hash=sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d \ + --hash=sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42 \ + --hash=sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe \ + --hash=sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda \ + --hash=sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e \ + --hash=sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737 \ + --hash=sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523 \ + --hash=sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591 \ + --hash=sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc \ + --hash=sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a \ + --hash=sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50 + # via mako +openai==2.53.0 \ + --hash=sha256:baf5802ad08980e1d9d561e1b996e800c8bcd14af5847c6d0e7a5cc59e4d4116 \ + --hash=sha256:c694ffc747a3c4d1663ef2b07b811315a476164ee5efa3a993967349ebca7618 + # via langchain-openai +orjson==3.11.9 \ + --hash=sha256:011382e2a60fda9d46f1cdee31068cfc52ffe952b587d683ec0463002802a0f4 \ + --hash=sha256:03db380e3780fa0015ed776a90f20e8e20bb11dde13b216ce19e5718e3dfba62 \ + --hash=sha256:051b102c93b4f634e89f3866b07b9a9a98915ada541f4ec30f177067b2694979 \ + --hash=sha256:08f4d8ebb44925c794e535b2bebc507cebf32209df81de22ae285fb0d8d66de0 \ + --hash=sha256:0b34789fa0da61cf7bef0546b09c738fb195331e017e477096d129e9105ab03d \ + --hash=sha256:0e4eed3b200023042814d2fc8a5d2e880f13b52e1ed2485e83da4f3962f7dc1a \ + --hash=sha256:115ab5f5f4a0f203cc2a5f0fb09aee503a3f771aa08392949ab5ca230c4fbdbd \ + --hash=sha256:135869ef917b8704ea0a94e01620e0c05021c15c52036e4663baffe75e72f8ce \ + --hash=sha256:147302878da387104b66bb4a8b0227d1d487e976ce41a8501916161072ed87b1 \ + --hash=sha256:14ed654580c1ed2bc217352ec82f91b047aef82951aa71c7f64e0dcb03c0e180 \ + --hash=sha256:16969c9d369c98eb084889c6e4d2d39b77c7eb38ceccf8da2a9fff62ae908980 \ + --hash=sha256:19b72ed11572a2ee51a67a903afbe5af504f84ed6f529c0fe44b0ab3fb5cc697 \ + --hash=sha256:231742b4a11dad8d5380a435962c57e91b7c37b79be858f4ef1c0df1a259897e \ + --hash=sha256:25e4aed0312d292c09f61af25bba34e0b2c88546041472b09088c39a4d828af1 \ + --hash=sha256:26a473dbb4162108b27901492546f83c76fdcea3d0eadff00ae7a07e18dcce09 \ + --hash=sha256:277fefe9d76ee17eb14debf399e3533d4d63b5f677a4d3719eb763536af1f4bd \ + --hash=sha256:2d057a602cdd19a0ad680417527c45b6961a095081c0f46fe0e03e304aac6470 \ + --hash=sha256:32ef5f4283a3be81913947d19608eacb7c6608026851123790cd9cc8982af34b \ + --hash=sha256:33d7d766701847dc6729846362dc27895d2f2d2251264f9d10e7cb9878194877 \ + --hash=sha256:34fd2317602587321faab75ab76c623a0117e80841a6413654f04e47f339a8fb \ + --hash=sha256:3513550321f8c8c811a7c3297b8a630e82dc08e4c10216d07703c997776236cd \ + --hash=sha256:380cdce7ba24989af81d0a7013d0aaec5d0e2a21734c0e2681b1bc4f141957fe \ + --hash=sha256:3a81d52442a7c99b3662333235b3adf96a1715864658b35bb797212be7bddb97 \ + --hash=sha256:3ebca4179031ee716ed076ffadc29428e900512f6fccee8614c9983157fcf19c \ + --hash=sha256:48ee05097750de0ff69ed5b7bbcf0732182fd57a24043dcc2a1da780a5ead3a5 \ + --hash=sha256:4bab1b2d6141fe7b32ae71dac905666ece4f94936efbfb13d55bb7739a3a6021 \ + --hash=sha256:4d4e98d6f3b8afed8bc8cd9718ec0cdf46661826beefb53fe8eafb37f2bf0362 \ + --hash=sha256:4d7fde5501b944f83b3e665e1b31343ff6e154b15560a16b7130ea1e594a4206 \ + --hash=sha256:4da3c38a2083ca4aaf9c2a36776cce3e9328e6647b10d118948f3cfb4913ffe4 \ + --hash=sha256:4e39364e726a8fff737309aff059ff67d8a8c8d5b677be7bb49a8b3e84b7e218 \ + --hash=sha256:4fd66214623f1b17501df9f0543bef0b833979ab5b6ded1e1d123222866aa8c9 \ + --hash=sha256:4fef17e1f8722c11587a6ef18e35902450221da0028e65dbaaa543619e68e48f \ + --hash=sha256:53b50b0e14084b8f7e29c5ce84c5af0f1160169b30d8a6914231d97d2fe297d4 \ + --hash=sha256:57ea77fb70a448ce87d18fca050193202a3da5e54598f6501ca5476fb66cfe02 \ + --hash=sha256:59e403b1cc5a676da8eaf31f6254801b7341b3e29efa85f92b48d272637e77be \ + --hash=sha256:5b192c6cf397e4455b11523c5cf2b18ed084c1bbd61b6c0926344d2129481972 \ + --hash=sha256:5f63aaf97afd9f6dec5b1a68e1b8da12bfccb4cb9a9a65c3e0b6c847849e7586 \ + --hash=sha256:63e0efbc991250c0b3143488fa57d95affcabbfc63c99c48d625dd37779aafe2 \ + --hash=sha256:6cc7923789694fd58f001cbcac7e47abc13af4d560ebbfcf3b41a8b1a0748124 \ + --hash=sha256:71e63adb0e1f1ed5d9e168f50a91ceb93ae6420731d222dc7da5c69409aa47aa \ + --hash=sha256:71f3db16e69b667b132e0f305a833d5497da302d801508cbb051ed9a9819da47 \ + --hash=sha256:844417969855fc7a41be124aafe83dc424592a7f77cd4501900c67307122b92c \ + --hash=sha256:8697ab6a080a5c46edaad50e2bc5bd8c7ca5c66442d24104fa44ec74910a8244 \ + --hash=sha256:87e4d4ab280b0c87424d47695bec2182caf8cfc17879ea78dab76680194abc13 \ + --hash=sha256:8aff7da9952a5ad1cef8e68017724d96c7b9a66e99e91d6252e1b133d67a7b10 \ + --hash=sha256:8ecc30f10465fa1e0ce13fd01d9e22c316e5053a719a8d915d4545a09a5ff677 \ + --hash=sha256:97d0d932803c1b164fde11cb542a9efcb1e0f63b184537cca65887147906ff48 \ + --hash=sha256:97db4c94a7db398a5bd636273324f0b3fd58b350bbbac8bb380ceb825a9b40f4 \ + --hash=sha256:9af678d6488357948f1f84c6cd1c1d397c014e1ae2f98ae082a44eb48f602624 \ + --hash=sha256:9ef6fe90aadef185c7b128859f40beb24720b4ecea95379fc9000931179c3a49 \ + --hash=sha256:9f78cf8fec5bd627f4082b8dfeac7871b43d7f3274904492a43dab39f18a19a0 \ + --hash=sha256:a028425d1b440c5d92a6be1e1a020739dfe67ea87d96c6dbe828c1b30041728b \ + --hash=sha256:a6082706765a95a6680d812e1daf1c0cfe8adec7831b3ff3b625693f3b461b1c \ + --hash=sha256:a8f5f8bc7ce7d59f08d9f99fa510c06496164a24cb5f3d34537dbd9ca30132e2 \ + --hash=sha256:aaea64f3f467d22e70eeed68bdccb3bc4f83f650446c4a03c59f2cba28a108db \ + --hash=sha256:ace6c58523302d3b97b6ac5c38a5298a54b473762b6be82726b4265c41029f92 \ + --hash=sha256:b3afcf569c15577a9fe64627292daa3e6b3a70f4fb77a5df246a87ec21681b94 \ + --hash=sha256:b6ef1979adc4bc243523f1a2ba91418030a8e29b0a99cbe7e0e2d6807d4dce6e \ + --hash=sha256:be4fa4f0af7fa18951f7ab3fc2148e223af211bf03f59e1c6034ec3f97f21d61 \ + --hash=sha256:c2d3dc759490128c5c1711a53eeaa8ee1d437fd0038ffd2b6008abf46db3f882 \ + --hash=sha256:c5d001196b89fa9cf0a4ab79766cd835b991a166e4b621ba95089edc50c429ff \ + --hash=sha256:cce9127885941bd28f080cecf1f1d288336b7e0d812c345b08be88b572796254 \ + --hash=sha256:cde1a448023ba7d5bb4c01c5afb48894380b5e4956e0627266526587ef4e535f \ + --hash=sha256:d4087e5c0209a0a8efe4de3303c234b9c44d1174161dcd851e8eea07c7560b32 \ + --hash=sha256:d8ea516b3726d190e1b4297e6f4e7a8650347ae053868a18163b4dd3641d1fff \ + --hash=sha256:e30ab17845bb9fa54ccf67fa4f9f5282652d54faa6d17452f47d0f369d038673 \ + --hash=sha256:e5c9b8f28e726e97d97696c826bc7bea5d71cecd63576dba92924a32c1961291 \ + --hash=sha256:ea407d4ccf5891d667d045fecae97a7a1e5e87b3b97f97ae1803c2e741130be0 \ + --hash=sha256:ea5c46eb2d3af39e806b986f4b09d5c2706a1f5afde3cbf7544ce6616127173c \ + --hash=sha256:eebdbdeef0094e4f5aefa20dcd4eb2368ab5e7a3b4edea27f1e7b2892e009cf9 \ + --hash=sha256:f01c4818b3fc9b0da8e096722a84318071eaa118df35f6ed2344da0e73a5444f \ + --hash=sha256:f36b7f32c7c0db4a719f1fc5824db4a9c6f8bd1a354debb91faf26ebf3a4c71e \ + --hash=sha256:f5d89a2ed90731df3be64bab0aa44f78bff39fdc9d71c291f4a8023aa46425b7 \ + --hash=sha256:ffe02797b5e9f3a9d8292ddcd289b474ad13e81ad83cd1891a240811f1d2cb81 + # via + # langgraph-sdk + # langsmith +ormsgpack==1.12.2 \ + --hash=sha256:0b39e629fd2e1c5b2f46f99778450b59454d1f901bc507963168985e79f09c5d \ + --hash=sha256:118576ea6006893aea811b17429bfc561b4778fad393f5f538c84af70b01260c \ + --hash=sha256:18867233df592c997154ff942a6503df274b5ac1765215bceba7a231bea2745d \ + --hash=sha256:1dcc17d92b6390d4f18f937cf0b99054824a7815818012ddca925d6e01c2e49e \ + --hash=sha256:21f4276caca5c03a818041d637e4019bc84f9d6ca8baa5ea03e5cc8bf56140e9 \ + --hash=sha256:292410a7d23de9b40444636b9b8f1e4e4b814af7f1ef476e44887e52a123f09d \ + --hash=sha256:29a9f17a3dac6054c0dce7925e0f4995c727f7c41859adf9b5572180f640d172 \ + --hash=sha256:34d5b28b3570e9fed9a5a76528fc7230c3c76333bc214798958e58e9b79cc18a \ + --hash=sha256:3708693412c28f3538fb5a65da93787b6bbab3484f6bc6e935bfb77a62400ae5 \ + --hash=sha256:39c1bd2092880e413902910388be8715f70b9f15f20779d44e673033a6146f2d \ + --hash=sha256:43013a3f3e2e902e1d05e72c0f1aeb5bedbb8e09240b51e26792a3c89267e181 \ + --hash=sha256:458e4568be13d311ef7d8877275e7ccbe06c0e01b39baaac874caaa0f46d826c \ + --hash=sha256:46d084427b4132553940070ad95107266656cb646ea9da4975f85cb1a6676553 \ + --hash=sha256:4ee766d2e78251b7a63daf1cddfac36a73562d3ddef68cacfb41b2af64698033 \ + --hash=sha256:50b7249244382209877deedeee838aef1542f3d0fc28b8fe71ca9d7e1896a0d7 \ + --hash=sha256:58d379d72b6c5e964851c77cfedfb386e474adee4fd39791c2c5d9efb53505cc \ + --hash=sha256:59f5da97000c12bc2d50e988bdc8576b21f6ab4e608489879d35b2c07a8ab51a \ + --hash=sha256:5af04800d844451cf102a59c74a841324868d3f1625c296a06cc655c542a6685 \ + --hash=sha256:5ea60cb5f210b1cfbad8c002948d73447508e629ec375acb82910e3efa8ff355 \ + --hash=sha256:5f13034dc6c84a6280c6c33db7ac420253852ea233fc3ee27c8875f8dd651163 \ + --hash=sha256:7121b3d355d3858781dc40dafe25a32ff8a8242b9d80c692fd548a4b1f7fd3c8 \ + --hash=sha256:73670ed0375ecc303858e3613f407628dd1fca18fe6ac57b7b7ce66cc7bb006c \ + --hash=sha256:7a1c460655d7288407ffa09065e322a7231997c0d62ce914bf3a96ad2dc6dedd \ + --hash=sha256:7a29d09b64b9694b588ff2f80e9826bdceb3a2b91523c5beae1fab27d5c940e7 \ + --hash=sha256:7c8b1667a72cbba74f0ae7ecf3105a5e01304620ed14528b2cb4320679d2869b \ + --hash=sha256:837dd316584485b72ef451d08dd3e96c4a11d12e4963aedb40e08f89685d8ec2 \ + --hash=sha256:8463a3fc5f09832e67bdb0e2fda6d518dc4281b133166146a67f54c08496442e \ + --hash=sha256:8cde5eaa6c6cbc8622db71e4a23de56828e3d876aeb6460ffbcb5b8aff91093b \ + --hash=sha256:944a2233640273bee67521795a73cf1e959538e0dfb7ac635505010455e53b33 \ + --hash=sha256:958dcb270d30a7cb633a45ee62b9444433fa571a752d2ca484efdac07480876e \ + --hash=sha256:9e4459c3f27066beadb2b81ea48a076a417aafffff7df1d3c11c519190ed44f2 \ + --hash=sha256:ae3aba7eed4ca7cb79fd3436eddd29140f17ea254b91604aa1eb19bfcedb990f \ + --hash=sha256:b009049086ddc6b8f80c76b3955df1aa22a5fbd7673c525cd63bf91f23122ede \ + --hash=sha256:baca4b6773d20a82e36d6fd25f341064244f9f86a13dead95dd7d7f996f51709 \ + --hash=sha256:bc68dd5915f4acf66ff2010ee47c8906dc1cf07399b16f4089f8c71733f6e36c \ + --hash=sha256:bd5f4bf04c37888e864f08e740c5a573c4017f6fd6e99fa944c5c935fabf2dd9 \ + --hash=sha256:c010da16235806cf1d7bc4c96bf286bfa91c686853395a299b3ddb49499a3e13 \ + --hash=sha256:c1429217f8f4d7fcb053523bbbac6bed5e981af0b85ba616e6df7cce53c19657 \ + --hash=sha256:c2be829954434e33601ae5da328cccce3266b098927ca7a30246a0baec2ce7bd \ + --hash=sha256:c6a4c34ddef109647c769d69be65fa1de7a6022b02ad45546a69b3216573eb4a \ + --hash=sha256:cec70477d4371cd524534cd16472d8b9cc187e0e3043a8790545a9a9b296c258 \ + --hash=sha256:d024b40828f1dde5654faebd0d824f9cc29ad46891f626272dd5bfd7af2333a4 \ + --hash=sha256:da538c542bac7d1c8f3f2a937863dba36f013108ce63e55745941dda4b75dbb6 \ + --hash=sha256:dc7a33be14c347893edbb1ceda89afbf14c467d593a5ee92c11de4f1666b4d4f \ + --hash=sha256:df6961442140193e517303d0b5d7bc2e20e69a879c2d774316125350c4a76b92 \ + --hash=sha256:eddffb77eff0bad4e67547d67a130604e7e2dfbb7b0cde0796045be4090f35c6 \ + --hash=sha256:f04b5e896d510b07c0ad733d7fce2d44b260c5e6c402d272128f8941984e4285 \ + --hash=sha256:f3601f19afdbea273ed70b06495e5794606a8b690a568d6c996a90d7255e51c1 \ + --hash=sha256:fcd55e5f6ba0dbce624942adf9f152062135f991a0126064889f68eb850de0dd + # via langgraph-checkpoint +packaging==26.3 \ + --hash=sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79 \ + --hash=sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c + # via + # langchain-core + # langsmith +psycopg==3.3.4 \ + --hash=sha256:b6bbc25ccf05c8fad3b061d9db2ef0909a555171b84b07f29458a447253d679a \ + --hash=sha256:e21207764952cff81b6b8bdacad9a3939f2793367fdac2987b3aac36a651b5bc + # via changeops (pyproject.toml) +psycopg-binary==3.3.4 \ + --hash=sha256:018fbed325936da502feb546642c982dcc4b9ffdea32dfef78dbf3b7f7ad4070 \ + --hash=sha256:0579252a1202cd73e4da137a1426e2dae993ae44e757605344282af3a082848c \ + --hash=sha256:136f199a407b5348b9b857c504aff60c77622a28482e7195839ce1b51238c4cc \ + --hash=sha256:13a7f380824c35896dcac7fe0f61440f7ca49d6dc73f3c13a9a4471e6a3b302e \ + --hash=sha256:17a21953a9e5ff3a16dab692625a3676e2f101db5e40072f39dbee2250194d68 \ + --hash=sha256:1dc1f79fd16bb1f3f4421417a514607539f17804d95c7ed617265369d1981cae \ + --hash=sha256:1fbaa292a3c8bb61b45df1ad3da1908ccee7cb889db9425e3557d9e34e2a4829 \ + --hash=sha256:22cdbf5f91ef7bb91fe0c5757e1962d3127a8010256eefd9c61fcaf441802097 \ + --hash=sha256:26df2717e59c0473e4465a97dfb1b7afebaa479277870fd5784d1436470db47c \ + --hash=sha256:276904e3452d6a23d474ef9a21eee19f20eed3d53ddd2576af033827e0ba0992 \ + --hash=sha256:28b7398fdd19db3232c884fb24550bdfe951221f510e195e233299e4c9b78f97 \ + --hash=sha256:2c09aad7051326e7603c14e50636db9c01f78272dc54b3accff03d46370461e6 \ + --hash=sha256:32a6fbf8481e3a370d0d72b860d35948a693cb01281da217f7b2f307636e591a \ + --hash=sha256:41f2ec0fea529832982bcb6c9415de3c86264ebe562b77a467c0fbcd7efbba8d \ + --hash=sha256:46893c26858be12cc49ca4226ed6a60b4bfccadd946b3bebb783a60b38788228 \ + --hash=sha256:47c656a8a7ba6eb0cff1801a4caaa9c8bdc12d03080e273aff1c8ac39971a77e \ + --hash=sha256:494ca54901be8cf9eb7e02c25b731f2317c378efa44f43e8f9bd0e1184ae7be4 \ + --hash=sha256:514404ed543efd620c85602b747df2a23cf1241b4067199e1a66f2d2757aaa41 \ + --hash=sha256:574ea21a9651958f1535c5a1c649c7409e9168bcbffa29a3f2f961f58b322949 \ + --hash=sha256:580ae30a5f95ccd90008ec697d3ed6a4a2047a516407ad904283fa42086936e9 \ + --hash=sha256:5ab28a2a7649df3b72e6b674b4c190e448e8e77cf496a65bd846472048de2089 \ + --hash=sha256:5c4ab71be17bdca30cb34c34c4e1496e2f5d6f20c199c12bad226070b22ef9bf \ + --hash=sha256:612a627d733f695b1de1f9b4bd511c15f999a5d8b915d444bbd7dd71cf3370da \ + --hash=sha256:6402a9d8146cf4b3974ded3fd28a971e83dc6a0333eb7822524a3aa20b546578 \ + --hash=sha256:6b9016b1714da4dd5ecaaa75b82098aa5a0b87854ce9b092e21c27c4ae23e014 \ + --hash=sha256:71e55ccbdfae79a2ed9c6369c3008a3025817ff9d7e27b32a2d84e2a4267e66e \ + --hash=sha256:7465bfe6087d2d5b42d4c53b9b11ca9f218e477317a4a162a10e3c19e984ba8e \ + --hash=sha256:75a9067e236f9b9ae3535b66fe99bddb33d39c0de10112e49b9ab11eee53dc31 \ + --hash=sha256:773d573e11f437ce0bdb95b7c18dc58390494f96d43f8b45b9760436114f7652 \ + --hash=sha256:77df19583501ea288eaf15ac0fe7ad01e6d8091a91d5c41df5c718f307d8e31b \ + --hash=sha256:7f7668f30b9dd5163197e5cbf4e0efd54e00f0a859cc566ce56cfc31f4054839 \ + --hash=sha256:8c0056529e68dbe9184cd4019a1f3d8f3a4ead2f6fc7a5afcf27d3314edd1277 \ + --hash=sha256:94596f9e7633ee3f6440711d43bb70aa31cc0a46a900ab8b4201a366ace5c9e7 \ + --hash=sha256:ab8cca8ef8fb1ccf5b048ae5bd78ba55b9e4b5d472e3ce5ca39ff4d2a9c249e4 \ + --hash=sha256:ad3bc94054876155549fdaedf4a46d1ec69d39a5bcee377148afe498e84c4b8e \ + --hash=sha256:b56b603ebcea8aa10b46228b8410ba7f13e7c2ee54389d4d9be0927fd8ce2a70 \ + --hash=sha256:b6f5a29e9c775b9f12a1a717aa7a2c80f9e1db6f27ba44a5b59c80ac61d2ffcf \ + --hash=sha256:b7bfff1ca23732b488cbca3076fc11bc98d520ee122514fdb17a8e20d3338f5a \ + --hash=sha256:bdef84570ebbce1d42b4e7ea952d21c414c5f118ad02fee00c5625f35e134429 \ + --hash=sha256:c37e024c07308cd06cf3ec51bfd0e7f6157585a4d84d1bce4a7f5f7913719bf8 \ + --hash=sha256:c677c4ad433cb7150c8cd304a0769ae3bcfbe5ea0676eb53faa7b1443b16d0d3 \ + --hash=sha256:cf7f73a4a792bc5db58a4b385d8a1467e8d468f7548702fb0ed1e9b7501b1c13 \ + --hash=sha256:cffc3408d77a27973f33e5d909b624cce683db5fc25964b02fe0aae7886c1007 \ + --hash=sha256:d7b4d40c153fa352ab3cca530f3a0baedf7621b2ebcbd7f084009522c21788fc \ + --hash=sha256:dbfdb9b6cc79f31104a7b162a2b921b765fcc62af6c00540a167a8de47e4ed38 \ + --hash=sha256:df1d567fc430f6df15c9fcf67d87685fc49bdb325adc0db5af1adfb2f44eb5c9 \ + --hash=sha256:e2631da29253a98bd496e6c4813b24e09a4fe3fb2a9e88513305d6f8747cce95 \ + --hash=sha256:e7510c37550f91a187e3660a8cc50d4b760f8c3b8b2f89ebc5698cd2c7f2c85d \ + --hash=sha256:eb05ee1c2b817d27c537333224c9e83c7afb86fe7296ba970990068baf819b16 \ + --hash=sha256:eb4eed2079c01a4850bf467deacfab56d356d4225040170af03dc9958321242d \ + --hash=sha256:ee17a2cf4943cde261adfad1bbc5bf38d6b3776d7afff74c7cabcbeaeb08c260 \ + --hash=sha256:f80e3f2b5331dbbf0901bcb658056c03eeb2c1ef31d774afb0d61598b242e744 \ + --hash=sha256:f9b1c2533af01cd7648378599f82b0b8ae32f293296e6eec5753a625bc97ef28 \ + --hash=sha256:fa1cbc10768a796c96d3243656016bf4e337c81c71097270bb7b0ad6210d9765 \ + --hash=sha256:fbd1d4ed566895ad2d3bf4ddfd8bae90026930ddf29df3b9d91d32c8c47866a7 + # via psycopg +pydantic==2.13.4 \ + --hash=sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba \ + --hash=sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6 + # via + # fastapi + # langchain-core + # langgraph + # langsmith + # openai + # pydantic-settings +pydantic-core==2.46.4 \ + --hash=sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0 \ + --hash=sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262 \ + --hash=sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda \ + --hash=sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0 \ + --hash=sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e \ + --hash=sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b \ + --hash=sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594 \ + --hash=sha256:10e17cbb10a330363733efc4d7c4d0dd827ac0909b8f6a6542298fed1ea62f29 \ + --hash=sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2 \ + --hash=sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c \ + --hash=sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d \ + --hash=sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398 \ + --hash=sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d \ + --hash=sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3 \ + --hash=sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f \ + --hash=sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb \ + --hash=sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7 \ + --hash=sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5 \ + --hash=sha256:228ee9bae8bef5b1e97ec58302f80357c37199e0d0a99174e138d28e6957b9d9 \ + --hash=sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462 \ + --hash=sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4 \ + --hash=sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b \ + --hash=sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d \ + --hash=sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df \ + --hash=sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2 \ + --hash=sha256:3447661d99f75a3683a4cf5c87da72f2161964611864dbbeac7fbb118bb4bfc0 \ + --hash=sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519 \ + --hash=sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd \ + --hash=sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7 \ + --hash=sha256:3be77f45df024d789a672ae34f8b06fb346c4f9f46ea714956660ea4862e89ac \ + --hash=sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6 \ + --hash=sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565 \ + --hash=sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898 \ + --hash=sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb \ + --hash=sha256:432c179df7874eeb73307aad2df0755e1ae0efa61ff0ea89b93e194411ae3928 \ + --hash=sha256:4a05d69cba51d852c5c3e92758653245a50c0b646ced0cf05bd793ed592839d6 \ + --hash=sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3 \ + --hash=sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a \ + --hash=sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596 \ + --hash=sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987 \ + --hash=sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e \ + --hash=sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d \ + --hash=sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712 \ + --hash=sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008 \ + --hash=sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd \ + --hash=sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1 \ + --hash=sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be \ + --hash=sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea \ + --hash=sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292 \ + --hash=sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33 \ + --hash=sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3 \ + --hash=sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4 \ + --hash=sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b \ + --hash=sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826 \ + --hash=sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac \ + --hash=sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7 \ + --hash=sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d \ + --hash=sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf \ + --hash=sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4 \ + --hash=sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc \ + --hash=sha256:8b9bab013d1c7a79d3501ff86d0bc9c31bf587db4551677b96bec07df78c6b15 \ + --hash=sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3 \ + --hash=sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b \ + --hash=sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914 \ + --hash=sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04 \ + --hash=sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c \ + --hash=sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b \ + --hash=sha256:91a06d2e259ecfbd8c901d70c3c507900458498142b3026a296b7de4d1322cc9 \ + --hash=sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce \ + --hash=sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4 \ + --hash=sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a \ + --hash=sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f \ + --hash=sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424 \ + --hash=sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894 \ + --hash=sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9 \ + --hash=sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76 \ + --hash=sha256:9f444c499b3eefd3a92e348059471ea0c3a6e303d9c1cec09fa748fd9f895201 \ + --hash=sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb \ + --hash=sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109 \ + --hash=sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4 \ + --hash=sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848 \ + --hash=sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526 \ + --hash=sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0 \ + --hash=sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01 \ + --hash=sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458 \ + --hash=sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e \ + --hash=sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba \ + --hash=sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a \ + --hash=sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39 \ + --hash=sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c \ + --hash=sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000 \ + --hash=sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b \ + --hash=sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf \ + --hash=sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4 \ + --hash=sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd \ + --hash=sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28 \ + --hash=sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9 \ + --hash=sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30 \ + --hash=sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983 \ + --hash=sha256:d80ee3d731373b24cebbc10d689ca4ee1875caf0d5703a245db18efd4dd37fc1 \ + --hash=sha256:d995260fdf4e1db774581b4900e0f832abe3c7c84996726bbc161b19c8f29e76 \ + --hash=sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5 \ + --hash=sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4 \ + --hash=sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7 \ + --hash=sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c \ + --hash=sha256:e68b7a074f65a2fd746c52a7ce6142ab7006074ac269ace0c25cd8ba171f8066 \ + --hash=sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3 \ + --hash=sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02 \ + --hash=sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89 \ + --hash=sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50 \ + --hash=sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76 \ + --hash=sha256:f13a646d65d09fbf1bc6b3a9635d30095c8e7e5cc419ff35ecc563c5fd04cd49 \ + --hash=sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b \ + --hash=sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d \ + --hash=sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7 \ + --hash=sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4 \ + --hash=sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c \ + --hash=sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e \ + --hash=sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff \ + --hash=sha256:fd8b3d9fd264be37976686c7f65cd52a83f5e84f4bfd2adf9c1d469676bbb6ae + # via pydantic +pydantic-settings==2.14.2 \ + --hash=sha256:a20c97b37910b6550d5ea50fbcc2d4187defe58cd57070b73863d069419c9440 \ + --hash=sha256:c19dd64b19097f1de80184f0cc7b0272a13ae6e170cbf240a3e27e381ed14a5f + # via changeops (pyproject.toml) +python-dotenv==1.2.2 \ + --hash=sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a \ + --hash=sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3 + # via pydantic-settings +pyyaml==6.0.3 \ + --hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \ + --hash=sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a \ + --hash=sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3 \ + --hash=sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956 \ + --hash=sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6 \ + --hash=sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c \ + --hash=sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65 \ + --hash=sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a \ + --hash=sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0 \ + --hash=sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b \ + --hash=sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1 \ + --hash=sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6 \ + --hash=sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7 \ + --hash=sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e \ + --hash=sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007 \ + --hash=sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310 \ + --hash=sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4 \ + --hash=sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9 \ + --hash=sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295 \ + --hash=sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea \ + --hash=sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0 \ + --hash=sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e \ + --hash=sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac \ + --hash=sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9 \ + --hash=sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7 \ + --hash=sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35 \ + --hash=sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb \ + --hash=sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b \ + --hash=sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69 \ + --hash=sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5 \ + --hash=sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b \ + --hash=sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c \ + --hash=sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369 \ + --hash=sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd \ + --hash=sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824 \ + --hash=sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198 \ + --hash=sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065 \ + --hash=sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c \ + --hash=sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c \ + --hash=sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764 \ + --hash=sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196 \ + --hash=sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b \ + --hash=sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00 \ + --hash=sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac \ + --hash=sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8 \ + --hash=sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e \ + --hash=sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28 \ + --hash=sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3 \ + --hash=sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5 \ + --hash=sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4 \ + --hash=sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b \ + --hash=sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf \ + --hash=sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5 \ + --hash=sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702 \ + --hash=sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8 \ + --hash=sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788 \ + --hash=sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da \ + --hash=sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d \ + --hash=sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc \ + --hash=sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c \ + --hash=sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba \ + --hash=sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f \ + --hash=sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917 \ + --hash=sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5 \ + --hash=sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26 \ + --hash=sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f \ + --hash=sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b \ + --hash=sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be \ + --hash=sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c \ + --hash=sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3 \ + --hash=sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6 \ + --hash=sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926 \ + --hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0 + # via langchain-core +regex==2026.7.19 \ + --hash=sha256:062f8cb7a9739c4835d22bd96f370c59aba89f257adcfa53be3cc209e08d3ae0 \ + --hash=sha256:064f1760a5a4ade65c5419be23e782f29147528e8a66e0c42dd4cedb8d4e9fc6 \ + --hash=sha256:09523a592938aa9f587fb74467c63ff0cf88fc3df14c82ab0f0517dcf76aaa62 \ + --hash=sha256:09d3007fc76249a83cdd33de160d50e6cb77f54e09d8fa9e7148e10607ce24af \ + --hash=sha256:09f3e5287f94f17b709dc9a9e70865855feee835c861613be144218ce4ca82cc \ + --hash=sha256:0c41c63992bf1874cebb6e7f56fd7d3c007924659a604ae3d90e427d40d4fd13 \ + --hash=sha256:0e9554c8785eac5cffe6300f69a91f58ba72bc88a5f8d661235ad7c6aa5b8ccd \ + --hash=sha256:1123ef4211d763ee771d47916a1596e2f4915794f7aabdc1adcb20e4249a6951 \ + --hash=sha256:15b364b9b98d6d2fe1a85034c23a3180ff913f46caddc3895f6fd65186255ccc \ + --hash=sha256:1649eb39fcc9ea80c4d2f110fde2b8ab2aef3877b98f02ab9b14e961f418c511 \ + --hash=sha256:17ed5692f6acc4183e98331101a5f9e4f64d72fe58b753da4d444a2c77d05b12 \ + --hash=sha256:199535629f25caf89698039af3d1ad5fcae7f933e2112c73f1cdf49165c99518 \ + --hash=sha256:1c398716054621aa300b3d411f467dda903806c5da0df6945ab73982b8d115db \ + --hash=sha256:1d3372064506b94dd2c67c845f2db8062e9e9ba84d04e33cb96d7d33c11fe1ae \ + --hash=sha256:1d58561843f0ff7dc78b4c28b5e2dc388f3eff94ebc8a232a3adba961fc00009 \ + --hash=sha256:1d793a7988e04fcb1e2e135567443d82173225d657419ec09414a9b5a145b986 \ + --hash=sha256:1ebac3474b8589fce2f9b225b650afd61448f7c73a5d0255a10cc6366471aed1 \ + --hash=sha256:20568e182eb82d39a6bf7cff3fd58566f14c75c6f74b2c8c96537eecf9010e3a \ + --hash=sha256:22a992de9a0d91bda927bf02b94351d737a0302905432c88a53de7c4b9ce62e2 \ + --hash=sha256:2955907b7157a6660f27079edf7e0229e9c9c5325c77a2ef6a890cba91efa6f0 \ + --hash=sha256:2c4e61e2e1be56f63ec3cc618aa9e0de81ef6f43d177205451840022e24f5b78 \ + --hash=sha256:2cc3460cedf7579948486eab03bc9ad7089df4d7281c0f47f4afe03e8d13f02d \ + --hash=sha256:2ce9e679f776649746729b6c86382da519ef649c8e34cc41df0d2e5e0f6c36d4 \ + --hash=sha256:2ef7eeb108c47ce7bcc9513e51bcb1bf57e8f483d52fce68a8642e3527141ae0 \ + --hash=sha256:3080a7fd38ef049bd489e01c970c97dd84ff446a885b0f1f6b26d9b1ad13ce11 \ + --hash=sha256:343a4504e3fb688c47cad451221ca5d4814f42b1e16c0065bde9cbf7f473bd52 \ + --hash=sha256:36aacfb15faaff3ced55afbf35ec72f50d4aee22082c4f7fe0573a33e2fca92e \ + --hash=sha256:3d3143f159261b1ce5b24c261c590e5913370c3200c5e9ebbb92b5aa5e111902 \ + --hash=sha256:40b34dd88658e4fedd2fddbf0275ac970d00614b731357f425722a3ed1983d11 \ + --hash=sha256:4458124d71339f505bf1fb94f69fd1bb8fa9d2481eebfef27c10ef4f2b9e12f6 \ + --hash=sha256:4896db1f4ce0576765b8272aa922df324e0f5b9bb2c3d03044ff32a7234a9aba \ + --hash=sha256:4a0530bb1b8c1c985e7e2122e2b4d3aedd8a3c21c6bfddae6767c4405668b56e \ + --hash=sha256:4aa5435cdb3eb6f55fe98a171b05e3fbcd95fadaa4aa32acf62afd9b0cfdbcac \ + --hash=sha256:4c3501bfa814ab07b5580741f9bf78dfdfe146a04057f82df9e2402d2a975939 \ + --hash=sha256:4e5413bd5f13d3a4e3539ca98f70f75e7fca92518dd7f117f030ebedd10b60cb \ + --hash=sha256:4e6883a021db30511d9fb8cfb0f222ce1f2c369f7d4d8b0448f449a93ba0bdfc \ + --hash=sha256:52579c60a6078be70a0e49c81d6e56d677f34cd439af281a0083b8c7bc75c095 \ + --hash=sha256:555497390743af1a65045fa4527782d10ff5b88970359412baa4a1e628fe393b \ + --hash=sha256:56ad4d9f77df871a99e25c37091052a02528ec0eb059de928ee33956b854b45b \ + --hash=sha256:571fde9741eb0ccde23dd4e0c1d50fbae910e901fa7e629faf39b2dda740d220 \ + --hash=sha256:572fc57b0009c735ee56c175ea021b637a15551a312f56734277f923d6fd0f6c \ + --hash=sha256:59787bd5f8c70aa339084e961d2996b53fbdeab4d5393bba5c1fe1fc32e02bae \ + --hash=sha256:5a2721c8720e2cb3c209925dfb9200199b4b07361c9e01d321719404b21458b3 \ + --hash=sha256:5cc26a66e212fa5d6c6170c3a40d99d888db3020c6fdab1523250d4341382e44 \ + --hash=sha256:5ebee1ee89c39c953baac6924fcde08c5bb427c4057510862f9d7c7bdb3d8665 \ + --hash=sha256:60be8693a1dadc210bbcbc0db3e26da5f7d01d1d5a3da594e99b4fa42df404f5 \ + --hash=sha256:618a0aed532be87294c4477b0481f3aa0f1520f4014a4374dd4cf789b4cd2c97 \ + --hash=sha256:61bb1bd45520aacd56dd80943bd34991fb5350afdd1f36f2282230fd5154a218 \ + --hash=sha256:6383cd2ed53a646c659ba1fe65727db76437fdaa069e697a0b44a51d5843d864 \ + --hash=sha256:64729333167c2dcaaa56a331d40ee097bd9c5617ffd51dabb09eaddafb1b532e \ + --hash=sha256:64b6ca7391a1395c2638dd5c7456d67bea44fc6c5e8e92c5dc8aa6a8f23292b4 \ + --hash=sha256:65dcd28d3eba2ab7c2fd906485cc301392b47cc2234790d27d4e4814e02cdfda \ + --hash=sha256:65fa6cb38ed5e9c3637e68e544f598b39c3b86b808ed0627a67b68320384b459 \ + --hash=sha256:66bd62c59a5427746e8c44becae1d9b99d22fb13f30f492083dfb9ad7c45cc18 \ + --hash=sha256:6e44c0e7c5664be20aee92085153150c0a7967310a73a43c0f832b7cd35d0dd3 \ + --hash=sha256:6f8c6e7a1cfa3dc9d0ee2de0e65e834537fa29992cc3976ffec914afc35c5dd5 \ + --hash=sha256:7322ec6cc9fba9d49ab888bb82d67ac5625627aa168f0165139b17018df3fb8a \ + --hash=sha256:73b133a9e6fb512858e7f065e96f1180aa46646bc74a83aea62f1d314f3dd035 \ + --hash=sha256:73f272fba87b8ccfe70a137d02a54af386f6d27aa509fbffdd978f5947aae1aa \ + --hash=sha256:7e77b324909c1617cbb4c668677e2c6ae13f44d7c1de0d4f15f2e3c10f3315b5 \ + --hash=sha256:80115dd39481fd3a4b4080220799dbcacb921a844de4b827264ececacbe17c78 \ + --hash=sha256:87ccab0db8d5f4fbb0272642113c1adb2ffc698c16d3a0944580222331fa7a20 \ + --hash=sha256:89dfee3319f5ae3f75ebd5c2445a809bb320252ba5529ffdafea4ef25d79cf1a \ + --hash=sha256:8ac59a0900474a52b7c04af8196affc22bd9842acb0950df12f7b813e983609a \ + --hash=sha256:8cae6fd77a5b72dae505084b1a2ee0360139faf72fedbab667cd7cc65aae7a6a \ + --hash=sha256:8d3469c91dd92ee41b7c95280edbd975ef1ba9195086686623a1c6e8935ce965 \ + --hash=sha256:90c633e7e8d6bf4e992b8b36ce69e018f834b641dd6de8cea6d78c06ffa119c5 \ + --hash=sha256:93db40c8de0815baab96a06e08a984bac71f989d13bab789e382158c5d426797 \ + --hash=sha256:9724e6cb5e478cd7d8cabf027826178739cb18cf0e117d0e32814d479fa02276 \ + --hash=sha256:98c6ac18480fcdb33f35439183f1d2e79760ab41930309c6d951cb1f8e46694c \ + --hash=sha256:9a15e785f244f3e07847b984ce8773fc3da10a9f3c131cc49a4c5b4d672b4547 \ + --hash=sha256:9b60d7814174f059e5de4ab98271cc5ba9259cfea55273a81544dceea32dc8d9 \ + --hash=sha256:9be2a6647740dd3cca6acb24e87f03d7632cd280dbce9bbe40c26353a215a45d \ + --hash=sha256:9c7472192ebfad53a6be7c4a8bfb2d64b81c0e93a1fc8c57e1dd0b638297b5d1 \ + --hash=sha256:9dce8ec9695f531a1b8a6f314fd4b393adcccf2ea861db480cdf97a301d01a68 \ + --hash=sha256:9e50d748a32da622f256e8d505867f5d3c43a837c6a9f0efb149655fadd1042a \ + --hash=sha256:a81758ed242b861b72e778ba34d41366441a2e10b16b472784c88da2dea7e2dd \ + --hash=sha256:ac777001cdfc28b72477d93c8564bb7583081ea8fb45cdca3d568e0a4f87183c \ + --hash=sha256:b2b506b1788df5fecd270a10d5e70a95fe77b87ea2b370a318043f6f5f817ee6 \ + --hash=sha256:b2ea4a3e8357be8849e833beeae757ac3c7a6b3fc055c03c808a53c91ad30d82 \ + --hash=sha256:bf1516fe58fc104f39b2d1dbe2d5e27d0cd45c4be2e42ba6ee0cc763701ec3c7 \ + --hash=sha256:c0d702548d89d572b2929879bc883bb7a4c4709efafe4512cadee56c55c9bd15 \ + --hash=sha256:c10b82c2634df08dfb13b1f04e38fe310d086ee092f4f69c0c8da234251e556e \ + --hash=sha256:c42572142ed0b9d5d261ba727157c426510da78e20828b66bbb855098b8a4e38 \ + --hash=sha256:c4585c3e64b4f9e583b4d2683f18f5d5d872b3d71dcf24594b74ecc23602fa96 \ + --hash=sha256:c639ea314df70a7b2811e8020448c75af8c9445f5a60f8a4ced81c306a9380c2 \ + --hash=sha256:c670fe7be5b6020b76bc6e8d2196074657e1327595bca93a389e1a76ab130ad8 \ + --hash=sha256:cc1b2440423a851fad781309dd87843868f4f66a6bcd1ddb9225cf4ec2c84732 \ + --hash=sha256:cd3584591ea4429026cdb931b054342c2bcf189b44ff367f8d5c15bc092a2966 \ + --hash=sha256:d15df07081d91b76ff20d43f94592ee110330152d617b730fdbe5ef9fb680053 \ + --hash=sha256:d19662dbedbe783d323196312d38f5ba53cf56296378252171985da6899887d3 \ + --hash=sha256:d24ecb4f5e009ea0bd275ee37ad9953b32005e2e5e60f8bbae16da0dbbf0d3a0 \ + --hash=sha256:d446c6ac40bb6e05025ccee55b84d80fe9bf8e93010ffc4bb9484f13d498835f \ + --hash=sha256:d51ffd3427640fa2da6ade574ceba932f210ad095f65fcc450a2b0a0d454868e \ + --hash=sha256:d6ce43a0269d68cee79a7d1ade7def53c20f8f2a047b92d7b5d5bcc73ae88327 \ + --hash=sha256:d721e53758b2cca74990185eb0671dd466d7a388a1a45d0c6f4c13cef41a68ac \ + --hash=sha256:d7da47a0f248977f08e2cb659ff3c17ddc13a4d39b3a7baa0a81bf5b415430f6 \ + --hash=sha256:db47b561c9afd884baa1f96f797c9ca369872c4b65912bc691cfa99e68340af2 \ + --hash=sha256:dbe6493fbd27321b1d1f2dd4f5c7e5bd4d8b1d7cab7f32fd67db3d0b2ed8248a \ + --hash=sha256:dbece16025afda5e3031af0c4059207e61dcf73ef13af844964f57f387d1c435 \ + --hash=sha256:ddd67571c10869f65a5d7dde536d1e066e306cc90de57d7de4d5f34802428bb5 \ + --hash=sha256:de9208bb427130c82a5dbfd104f92c8876fc9559278c880b3002755bbbe9c83d \ + --hash=sha256:e30d40268a28d54ce0437031750497004c22602b8e3ab891f759b795a003b312 \ + --hash=sha256:e8b0abe7d870f53ca5143895fef7d1041a0c831a140d3dc2c760dd7ba25d4a8b \ + --hash=sha256:f035d9dc1d25eff9d361456572231c7d27b5ccd473ca7dc0adfce732bd006d40 \ + --hash=sha256:f04b9f56b0e0614c0126be12c2c2d9f8850c1e57af302bd0a63bed379d4af974 \ + --hash=sha256:f0fa4fa9c3632d708742baf2282f2055c11d888a790362670a403cbf48a2c404 \ + --hash=sha256:f2e7f8e2ab6c2922be02c7ec45185aa5bd771e2e57b95455ee343a44d8130dff \ + --hash=sha256:f8f6fa298bb4f7f58a33334406218ba74716e68feddf5e4e54cd5d8082705abf \ + --hash=sha256:fbf300e2070bb35038660b3be1be4b91b0024edb41517e6996320b49b92b4175 \ + --hash=sha256:fce7760bf283405b2c7999cab3da4e72f7deca6396013115e3f7a955db9760da \ + --hash=sha256:fcee38cd8e5089d6d4f048ba1233b3ad76e5954f545382180889112ff5cb712d \ + --hash=sha256:fe31f28c94402043161876a258a9c6f757cb485905c7614ce8d6cd40e6b7bdc1 \ + --hash=sha256:ffd8893ccc1c2fce6e0d6ca402d716fe1b29db70c7132609a05955e31b2aa8f2 + # via tiktoken +requests==2.34.2 \ + --hash=sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0 \ + --hash=sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed + # via + # langsmith + # requests-toolbelt + # tiktoken +requests-toolbelt==1.0.0 \ + --hash=sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6 \ + --hash=sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06 + # via langsmith +sniffio==1.3.1 \ + --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \ + --hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc + # via + # langsmith + # openai +sqlalchemy==2.0.51 \ + --hash=sha256:0378d055e9e8cd6ce4d8dff683bdd3d7d413533c4ee51d67a2b1e0f9eacc0f23 \ + --hash=sha256:0592bdadf86ddcabfd72d9ab66ea8a5d8d2cc6be1cc51fa7e66c03868ac5eac1 \ + --hash=sha256:08a204d8b5638717c26a24df18fcf40af45a6b22e35b70b1d62f0113c2e278e8 \ + --hash=sha256:0c2c62877097e1a0db401fba5cb4debee33265e5b2a55c4ccb489c02c53b4f72 \ + --hash=sha256:0e8203d2fbd5c6254692ef0a72c740d75b2f3c7ca345404f4c1a4604813c77c0 \ + --hash=sha256:0f053118c30e53161857a953e4de667d90e274980dccbe5dd3829bbbeece72a5 \ + --hash=sha256:0f6bcad487aee1c638d707235682fc96f741de00663619881ab235400d03289e \ + --hash=sha256:111604e637da87031255ddc26c7d7bc22bc6af6f5d459ccff3af1b4660233a85 \ + --hash=sha256:1181256e0f16479691b5616d36375dc2620ad8332b25978763c3d206ad3f3f1d \ + --hash=sha256:159bb6ba32059f57ad7375a8f50d844dd2f19d14954ecf820cd33e20debd46b2 \ + --hash=sha256:1aa10c0daee6705294d181daadaa793221e1a59ed55000a3fab1d42b088ce4ba \ + --hash=sha256:1af05726b3d0cdba1c55284bf408fd3b792e690fe2399bfb8304565551cda652 \ + --hash=sha256:1bed1ee8b01da6088210aa9412023326fb98a599ba502e6118308601dcbef77f \ + --hash=sha256:1d21ce524ab86c23046e992a5b81cb54c21079c6df6e78b8fc77d77cac70a6b9 \ + --hash=sha256:1e47b1199c2e832e325eacabc8d32d2487f58c9358f97e9a00f5eb93c5680d84 \ + --hash=sha256:247acaa29ccef6250dfd6a3eedf8f94ddf23564180a39fe362e32ae9dbdbde46 \ + --hash=sha256:2a97eaad21c84b4ef8010b11eeba9fe6153eb0b3df3ff8b6abc309df1b978ef7 \ + --hash=sha256:2cf39aabdf48e87c1c2c2ed6d20d33ffa0733b3071ce9c5f66357947dd009080 \ + --hash=sha256:2e54ff2dd657f2e3e0fbf2b097db1182f7bfea263eca4353f00065bae2a67c3d \ + --hash=sha256:39a76529db6305693d8d4affa58ad5b5e2e18edd62daea628b29b97930b3513d \ + --hash=sha256:4004ada0aafe8ae1991b2cd1d99c6d9146126e123bd6f883c260d974aa012e54 \ + --hash=sha256:436728ce18a80f6951a1e11cc6112c2ede9faf20766f1a26195a7c441ca12dbd \ + --hash=sha256:483b11bd46bf35fc14c52faf338b04300c9e6ce554bce9b11be85bfec3bc3195 \ + --hash=sha256:4a011ea4510683319ce4ed274b56ee05194b39b6da9d09ca7a39388f0fa84dcc \ + --hash=sha256:581921d849d6e6f994d560389192955e80e2950e18fcdfe2ccea863e01158e6e \ + --hash=sha256:59cab3686b1bc039dd9cded2f8d0c08a246e84e76bd4ab5b4f18c7cdae293825 \ + --hash=sha256:6b588fd681ddf0c196b8df1ea49a8913514894b2b8f945a9511b4b48871f99c8 \ + --hash=sha256:6e46fc36029eff666391e0531e5387b62ce6c4f1d8e50b3fb3099eaca1b42522 \ + --hash=sha256:6ea306caaae6bd5afd0a46050003c88f6bf33227377a49298c498c3cb88ff491 \ + --hash=sha256:72ca54c952107ba5cd58854b67a5a6268631289d21651a1235396f3b98b47400 \ + --hash=sha256:740cf6f35351b1ac3d82369152acf1d51d37e3dcf85d4dc0a22ca01410eabe2a \ + --hash=sha256:7c2056838b6685b72fdb36c99996cf862753461a62f2e84f4196371d3b2d6a07 \ + --hash=sha256:7c6b36ed71f41942bdcd2ad2522be46bfce09d5705be5640ecf19bbc7660e4b7 \ + --hash=sha256:7d78702b26ba1c18b2d0fb2ea940ba7f17a9581b42e8361ff93920ebbee1235a \ + --hash=sha256:804dccd8a4a6242c4e30ad961e540e18a588f6527202f2d6791b01845d59fdc9 \ + --hash=sha256:9161cfc9efce70d1715f47d6ff40f79c6778c00d53be4fbc09d70301e4b83ba7 \ + --hash=sha256:96747bfbadb055466e5b46d572618170046b45ce5a4879167f50d70a5319a499 \ + --hash=sha256:9f380393be5abeb6815f68fd39271b95127173511b6706b0a630a9995d53f8f5 \ + --hash=sha256:a42ad6afcbaaa777241e347aa2e29155993045a0d6b7db74da61053ffe875fe0 \ + --hash=sha256:a5b2ed6d828f1f09bd812861f4f59ca3bc3803f9df871f4555187f0faf018604 \ + --hash=sha256:a6d26094615306d116dd5e4a51b0304c99dd2356fc569eed6922a80a6bd3b265 \ + --hash=sha256:aa18ae738b5170e253ad0bb6c4b0f07585081e8a6e50893e4d911d47b39a0904 \ + --hash=sha256:ad30ae663711786303fbcd46a47516302d201ee49a877cb3fac61f672895110a \ + --hash=sha256:b21f0e7efc7a5c509e953784e9d1575ebb8b4318960e7e7d7a93bb803626cf64 \ + --hash=sha256:b3e693d15533a45cd5906f0589f9c35090bef6ef45bf1e8195c424aa0ae06a8d \ + --hash=sha256:b7f08588854bbb724041d9ae9d980d40040c922382e1d9a2ecb390edc4fd5032 \ + --hash=sha256:b93ab07b5292dbe7e6b8da89475275e7042744283921344b56105f3eeb0f828b \ + --hash=sha256:bb024d8b621d0be75f4f44ecc7c950450026e76d66dc8f791bb5331d7fed59d5 \ + --hash=sha256:bb1f5062f98b0b3290e72b707747fdd7e0f22d6956b236ba7ca7f5c9971d2da2 \ + --hash=sha256:c45a496d6bc05dec41dcd4c3a2b183723f47473255c159cd80b503c8f246424d \ + --hash=sha256:c5d98a2709840027f5a347c3af0a7c3d5f6c1ff93af2ca1c54494e23cba8f389 \ + --hash=sha256:c68568f3facf8f66fa76c60e0ced69b67666ffa9941d1d0a3756fda196049080 \ + --hash=sha256:c95ef01f53233a305a874a44a63fbfb1d81cd79b49de0f8529b3548cde437e37 \ + --hash=sha256:ca216e8af5c05e326efc7e28716ac2381a7cf9791749f5ee1849dccdc99c9b00 \ + --hash=sha256:ca8435d13829b92f4a97362d91975154a4015db3a2634154e1754e9a915e6b86 \ + --hash=sha256:dc261707bf5739aea8a541593f3cc1d463c2701fb05fbcbba0ce031b69a21260 \ + --hash=sha256:e5ea1a213be1fcd5e49d9904c3b9939211ded90bc2a64e93f4c01963474285de \ + --hash=sha256:fa268106c8987639a17a18514cfe0cd9bf17420ab887e1e1bf486da8836135b1 + # via + # alembic + # changeops (pyproject.toml) +starlette==1.4.1 \ + --hash=sha256:7d078e0fbefae0d2cecfb80a799d6fb84b1c0c6acd4f14ac79d17d0e7ec27f19 \ + --hash=sha256:b7332de6e9375593a29ba9eee1e6ecfeb3eb2043e2e19a13b4b71da73ff35540 + # via fastapi +tenacity==9.1.4 \ + --hash=sha256:6095a360c919085f28c6527de529e76a06ad89b23659fa881ae0649b867a9d55 \ + --hash=sha256:adb31d4c263f2bd041081ab33b498309a57c77f9acf2db65aadf0898179cf93a + # via langchain-core +tiktoken==0.13.0 \ + --hash=sha256:059c8ecf554eb5b41e6e054ba467b871b03277d267dee7244380aca4359747d4 \ + --hash=sha256:115c4f26ffa11caac8b54eea35c2ad38c612c20a48d35dd15d70a02ac6f51f58 \ + --hash=sha256:125bc05005e747f993a83dc67934249932d6e4209854452cd4c0b1d53fba3ba2 \ + --hash=sha256:165cf1820ea4a354985c2490a5205d4cc74661c934aca79dd0368232fff94e0f \ + --hash=sha256:2a3b536c55802fe42f4b4644d2be4f04bf788506b48de0a0a658cb58f8bce232 \ + --hash=sha256:2b920b35805cd64585a37c3dc7ce65fba4d2d36016be01e1d7942482ca29093a \ + --hash=sha256:2c397ddda233208345b01bd30f2fca79ff730e55731d0108a603f9bc57f6af3b \ + --hash=sha256:303f7d91b4fce3baddbcde05c139091d4caa5026ac7214c1dc7ff7a71ee429ff \ + --hash=sha256:32ac870a806cfb260a02d0cb70426aef02e038297f8ad50df5040bb5af360791 \ + --hash=sha256:32e0c12305105002c047b3bb1070b0dd9a73b0cb3b2856a8972b810e7a4f5881 \ + --hash=sha256:35e1ea1e0631c04f551297284a1ab7e1f65a3c55a9a48728d5e0f66b4527c04a \ + --hash=sha256:36217497eaffc158607a3b26f065300db2aefd43b115263f3b9688ce38146173 \ + --hash=sha256:3f277ebea5edd7b8bf03c6f9431e1d67d517530115572b2dc1d465326e8f88c7 \ + --hash=sha256:43cee3e5400573b2046fbf092cc7a5bc30164f9e4c95ce20714da929df48737a \ + --hash=sha256:44733b99bfd72b590cd0936b1c01b3b4dd73122db2d544bc1ceeb18a7678c910 \ + --hash=sha256:472527e9132952f2fbf77cd290658bacf003d4d5a3fabc18e5fbd407cbae4d9b \ + --hash=sha256:477c9a38e20d0ed248090509acf1e839ad3967a4f00b4b0f958210049f656dee \ + --hash=sha256:47b1df8d73390a24f94980c75158cdd5c56d256f16d55f30cb49c230caba9ba4 \ + --hash=sha256:493af3aa28a4aaf2e3d2600a2ee717252c9bf5ab38fff94eb5a02db5ab77e5ad \ + --hash=sha256:4d9980f11429ed2d737c463bb1fb78cf330caa026adf002f714aced7849a687b \ + --hash=sha256:4e2f67d27c9626cdd25fe33d9313c5cdb3d8d82da646b68d6eb8e7e9c20e6448 \ + --hash=sha256:51384448aa508e4df84c0f7c1dc3211c7f7b8096325660ee5fc82f3e11b381ce \ + --hash=sha256:5ba5fd62507a932d1241346179e3b39bc7bf7408f03c272652d93b3bedf5db24 \ + --hash=sha256:5cb65b60b9408563676d874a3a4ee573370066f0dc4e29d84e82e989c6517424 \ + --hash=sha256:5d48843bee149630eb735a99e1f4a85b47308d21868ea63163f6e87768d3cfed \ + --hash=sha256:5df5d1507bd245f1ccad4a074698240021239e455eb0bb4ced4e3d7181872154 \ + --hash=sha256:5e6358911cab4adee6712da27d65573496a4f68cf8a2b5fca6a4ad10fc5748cf \ + --hash=sha256:6644c9c2b5cf3916f5a3641d7d12fdb3f006a7b3d9ff6acdaec44e29ab1ff91e \ + --hash=sha256:6b1615f0ff71953d19729ceb18865429c185b0a23c5353f1bbca34a394bf60f7 \ + --hash=sha256:6c43a675ca14f6f2749ba7f12075d37456015a24b859f2517b9beb4ef30807ec \ + --hash=sha256:6eb4a5bfbc6426938026b1a334e898ac53541360d62d8c689870160cc80abd67 \ + --hash=sha256:75ab9bc99fa020a4c283424590ecd7f3afd70c1c281cb3fa3192a6c3af9f9615 \ + --hash=sha256:7ab10f4a21c2999846940113f6dbd72e0fa06a24119feddd74cc47e85818e06d \ + --hash=sha256:7bfe1849caa65d1e1d9871817170ec497bbb7984e182012e1bdce72f66608cdb \ + --hash=sha256:7d40c6c5aab171dcd6eb8455bc567bde404bb9def60cdb8c1299cc782b242bb9 \ + --hash=sha256:7de52e3f566d19b3b11bd37eea552c6c305ad74081f736882bd44d148ed4c48d \ + --hash=sha256:85b78cc3a2c3d48723ca751fa981f1fedccd54194ca0471b957364353a898b07 \ + --hash=sha256:8f2d16e7a7c783ad81f36e457d046d1f1c8af70b22aec8a13238efe531977c41 \ + --hash=sha256:8fe806a50664e83a6ffd56cbd1e4f5dcc6cd32a3e7538f70dc38b1a271384545 \ + --hash=sha256:91c180fe255bd5a86d8316210d2833a1d4d33d026cd86a67812f4773743c8d26 \ + --hash=sha256:95097e4f89b06403976e498abf61a0ee73a7497e73fb599cb211d8197a054d91 \ + --hash=sha256:975cbd78d085d75d26b59660e262736dcaed1e35f8f142cd6291025c01d25486 \ + --hash=sha256:9b842981fa91accdffd48ff6408a977b7a91c3fbda55d353c3c68114d5c9d69e \ + --hash=sha256:9b8858b29804b3a0add25ce9e62fb00f89f621dc754d75d03ca419d17e8ddf67 \ + --hash=sha256:a116178fa7e1b4065bff05214360373a65cac22f965be7b3f73d00a0dbfe7649 \ + --hash=sha256:a2937ad042d49d50eac6e1ba07c5661d4bd3942a5b1e0c0d08475c4df83676e1 \ + --hash=sha256:b8ac2d6420ff05841a89ba5205c6d45f56c4f6843454f3c884b7eb1a2a8dddb2 \ + --hash=sha256:b967dfb9d0adf9a631953b1b40717684f04478270fc51bbccdd2f838d67a2f00 \ + --hash=sha256:c9435714c3a84c2319499de9a300c0e604449dd0799ff246458b3bb6a7f433c1 \ + --hash=sha256:ca8b310bd93b3772cb1b7922d915446864860f562bdfe4825c63a0aed3fb28cd \ + --hash=sha256:cb99cb5127449f58d0a2d5f5ccfb390d8dbdfd919c221246caaee29d8725ed51 \ + --hash=sha256:d108bc2d470fc53c8ecd24f2c0fd2b5f98c33e87cdb6aa2e9b8c5dced703d273 \ + --hash=sha256:da86f8c96ac1c235d7a3b3eebff1eacfdbcfb8ad792706943268d4d2938fbafe \ + --hash=sha256:e28157350f7ebf35008dd8e9e0fdb621f976e4230c881099c85e8cf07eaa50e2 \ + --hash=sha256:eaaaef47c2406277181d2086484c317bf7fc433e2d5d03ff94f56b0dcec87471 \ + --hash=sha256:ed5a30027cb4d8c7ca8b273d4766f3db3cf58fad9e9f3b1a68a351ffb54873d5 \ + --hash=sha256:fc1c44cd37b43fc46bae593129164f4f281e82ea116b57a85aa81bda57eafc94 + # via langchain-openai +tqdm==4.70.0 \ + --hash=sha256:55b0b0dbd97462d06ebee91e4dac24ed4d4702be82b24f07e6c1d27e08cea220 \ + --hash=sha256:7f585706bfddbdebf89daac705b2dfcc16890130727d3197ca62c732b4310953 + # via openai +typing-extensions==4.16.0 \ + --hash=sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8 \ + --hash=sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5 + # via + # alembic + # anyio + # fastapi + # langchain-core + # langchain-protocol + # langsmith + # openai + # psycopg + # pydantic + # pydantic-core + # sqlalchemy + # starlette + # typing-inspection +typing-inspection==0.4.2 \ + --hash=sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7 \ + --hash=sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464 + # via + # fastapi + # pydantic + # pydantic-settings +urllib3==2.7.0 \ + --hash=sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c \ + --hash=sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897 + # via requests +uuid-utils==0.17.0 \ + --hash=sha256:03815cea572c8a693cab5475b9d750cc161470961c7defa27e9286cad62f38f5 \ + --hash=sha256:04452640d8b6920c480c16e5afe91ff896d236e0c972830f9247e0898d38c803 \ + --hash=sha256:09a55b7a5ae764985cb46467496a1787678d0a1400356157a080ad95b1a36869 \ + --hash=sha256:0ab4a66e7a035ad6625cfc1fbdb34f5c2d25a80ae1ef4bfee458ea2036333c6d \ + --hash=sha256:0bc4c431ccd59c764080ceb43b126043325fe17861b87759d026a0cdd8423bb2 \ + --hash=sha256:0f3729e839209f3457d0d8b6a35a376fdf65577a5aecaf4cc3587d3305759ba6 \ + --hash=sha256:0fcca4e838af9ac9243b3358d7c14afa4dca286a87781124c272d6c4cad9c968 \ + --hash=sha256:1019476b6bdc047216ef7414be5babe0fa5ccfde977c0cac4fd6c75ddec66ff7 \ + --hash=sha256:14dc2f46abb1091260c0d203fcbdf4e045042cc07e49183fd3b255904b95eb70 \ + --hash=sha256:1776a80d16369999b21627028cc5dbce819be83e1e079fdd7a51b587d2916db9 \ + --hash=sha256:1edf2f8732e4ed95bd7b65f2658f4aa072efaaff321144f4e0d4bf6a22709263 \ + --hash=sha256:1fd6f0e8a162dc0e9255b6aebe3cd175e76c33202f1bf39da9e6294b93db0099 \ + --hash=sha256:21c79b61ff750abcf057163dd764ccb6196cde7a26cda1b31b45cd97769e03b3 \ + --hash=sha256:220b52746d99e11964badac3c0869016e0c24bafb70a7dd5c2c072a6be3da9cc \ + --hash=sha256:237722b6581bb5b4eb4cefbcbe5c6e2980a440aabe781fbe50ebf1cb71eee4cc \ + --hash=sha256:239d8a281fe10bae33205b5d43185834d556b18434e0a113b5dc1dfb2fd97e91 \ + --hash=sha256:29179ffb7b317239b6d6afb100d14c439c728770460718280b9c0a42d2561ec2 \ + --hash=sha256:2db386941cfdecdd0b5a8ceeed5cf7479c83d1730dcf64a48d43cfa018cc3310 \ + --hash=sha256:2dd4a21baaac9a88486f0dd166c5793feb101a0bb9f006f2c401657fff5a1343 \ + --hash=sha256:309a35f12d99dde19032bc2259cda6431c85eeac0879134dc777cc3087d7e1cb \ + --hash=sha256:3150d836290c88f1d26eb59c4db280d87417dd3bfaadd2889c77416c8f0ff6fa \ + --hash=sha256:32abaafc8e91928b3d9f4d82e42d2094041e38ad6bb964066faadff28e4162f1 \ + --hash=sha256:32df1944808877702ceea398c103881c09a679bb672a215e01c2a84231266bf9 \ + --hash=sha256:344f7c755e280ea0ba6aeb08022190d867a80000b1715cacded54fc4b5633607 \ + --hash=sha256:351462debd866f1f25e4d4f5c7fac89525b52151f0102a1bdfe94a999b046f5f \ + --hash=sha256:375cde148430d60a4a07c03abaa0774c4fddfdd90de99b4ba02f24088bc9d750 \ + --hash=sha256:387cf7437c94ddec08651a0f1081381299c7075bc48a6251d8922bf39973378a \ + --hash=sha256:3dac0ad0cd9a2818d1775215365a4e8c2f8ada215529dd26f3f8cceeb67a6988 \ + --hash=sha256:405233a5f625b3d995648f4647fa6befa4567cf3f74e1f6b9837e16f7310f0e0 \ + --hash=sha256:4134353bfe3026ddab8e886002dc52bc5a0ab04611aabb0eaae23c32e6e57f64 \ + --hash=sha256:42275ebd0e8e74e32cdbfb8bd88fc99576567d51d54a508020611fd8f4f463a0 \ + --hash=sha256:4441600447d340ae103a353f01dbcd22ff680e5ee1a22988efe8d7b791d8fdb3 \ + --hash=sha256:46a73cacdf512f473a81f65dbf84186e08cfe6e9118fa582b6c6b33a8288a30d \ + --hash=sha256:4bf4d9cd1e80e73922073b9b27c143bedeb109d65f94cd12712e2c87118f2b7d \ + --hash=sha256:4e2ac1c0b56f2c91b6f158e29ed96b1503223fe8aa6e79b1be1dc55bd8a5131c \ + --hash=sha256:52db0e471d3d2632d35445af352591f40a8f32959a412981d9f51e068bb9514b \ + --hash=sha256:53ce348ef4c6e98c02c19c522af01334fe94476ce9af0db8c4482f9f142ae9c1 \ + --hash=sha256:5641071337eb11d61a001ea08793bf72216f3241f0a433ed2764804b2a3e3cc7 \ + --hash=sha256:5670c52a438e21483ce715776144914a4e2a2a5c62d9dee15f8a3e90cf128ae6 \ + --hash=sha256:56aa6488b931246fae11924e4bd0e2b32677e63945eecb71c29e3c2ca0dc3131 \ + --hash=sha256:570db214f6d8507587a8faa968a3fe65e957daeb7bc48b27dc7f69bc3ecdd6f1 \ + --hash=sha256:58838921e377791ef22c64cc92141bfae030f43651ff9272f0f28a208a9e6a5a \ + --hash=sha256:589d9da7de8fa7f739bb970ac4632c9a268213117d634e1c4a58c1c1e821ca05 \ + --hash=sha256:5a4370089c8b2e42f1db51d76408c7fa8eaa2934bf854d17983d16179c07c098 \ + --hash=sha256:622cdde768300591ac79bfcd7bb3468e4b191b1105d5dbfe8d87c39d8f63dd46 \ + --hash=sha256:673d89cc434cc9b97a0b4cf61272f6fca70a81f64eb0afbface2a0d9f77f06cd \ + --hash=sha256:6a019a31bc4db89a0903a3e4f6b218571f3a6ff0ad4b3d3fe1c8f91a05ff6e3e \ + --hash=sha256:6c142bd0cb4dba31c10babe00d59f7ef6460f0ef55eaa9c1a9da270684af996a \ + --hash=sha256:6f29689a76fe7a49cbd629a794d0ec1eab48814e323a00a146a741b0195bde68 \ + --hash=sha256:75d7411e8eb9259764dd60310738540649057cda4509b4af14b36b7f663bfeb0 \ + --hash=sha256:793229621e1ad6cac55f015cfa9f4eff102accbc3da25d607b91c6b0bec167fb \ + --hash=sha256:7a49f47ac26df3e431c56b825c1bae8e6d3d591fdbb7438c227cc9845a7e3d73 \ + --hash=sha256:7b9044ce4acbf392d4b3a503fe377641f4deff82e6c341c36ef27af0dea76cdf \ + --hash=sha256:7c89359affecebe2e39e6a116d069b363c936511a9572b308402489a26957d89 \ + --hash=sha256:84ed3a2d5cd3ae6db87af20bfed3331116195ba4757ad7177fc8f12c1bbce2a9 \ + --hash=sha256:89a0980d49683c00539c59cd9f46b1908c538e6b5b0a48ad12187bb856d0f391 \ + --hash=sha256:8b72c2002202038666bf647f9a790906214c7c11cd0d6efef77b7d07bef3034a \ + --hash=sha256:8eb3e5caca8d3a6f72ea4cce024583f989f6f2e9186f98800213fff0176e8bcc \ + --hash=sha256:9082e709014946b1f6e96ae6ecd93652efca2d2a6a3ab67dbe151c8b4bf193a4 \ + --hash=sha256:9205068badf453d2f0821fd5d340389b4679992d7ff79d4f3e5608996dd1b287 \ + --hash=sha256:9472a8de37faf8bd216c628e0e68c8f6bef730d3ba0a5060f3b0fa460c992ac2 \ + --hash=sha256:967955620df45e6cffe2e9950cb9903cb455649396f896b26b04363a91a5054b \ + --hash=sha256:975c17da26c5b9d46c336b03c52a057ac28378d6f9d98b58d32a038589bb3912 \ + --hash=sha256:981cc10163988defea96e8d6c507df151eab8f483e7df9ae543d5a41a4be073b \ + --hash=sha256:98c88d3edd08e7245562e9815996dbc6f0bd4745e1c76462f24af5ae4e187dd1 \ + --hash=sha256:9a91c4814c7150a4d798da691b7804eacd78c4b84fb392a60fa0de21341861eb \ + --hash=sha256:9e311f908d2f842fca4c7dcebc4f10306b8089b204ef04cf6704b4332c9ff6ff \ + --hash=sha256:9e753e81457241e2200c56a898e268e8fa25796271af0489c608f24d8e631eed \ + --hash=sha256:a46bedc273b6f58f11dee816ff74999625ef8d007890f411b7a4975bf1c89330 \ + --hash=sha256:abb5667a36119019b3fa320c4d10c21ebccfcc87c8a739e6a0056cee7f48dde2 \ + --hash=sha256:b3131a82d0c7611f0aa480a6d36929e001a3f54ba0fc029a8118a5863cce513c \ + --hash=sha256:b5d11cccba076a32321ef1380dea956821f0b51794ef59df64e58fb1cd543aae \ + --hash=sha256:b6c5d2d71e1f17329150ad9427d27f4a3f29a01792e7ecdc64a98ac5368fc4d5 \ + --hash=sha256:b776c7fc8755c7de06dd5a22b47c40ae84f67d13277ebb233cc84933ba4dcbcd \ + --hash=sha256:c00d182e31034250690f417b9068b78eab423c10d76766664e82d9860c340479 \ + --hash=sha256:c351737e2e65497c7200ab4ffb8af97e9f48be6488309abdd265fe08d66ee92f \ + --hash=sha256:c4f845166b09acc65c5213a35551a7f81c17fa010ab467229b5813f79d17fe13 \ + --hash=sha256:c589f5023d471ce75dd2cce61acb25ed6347e562041588a1a366808f22d7176c \ + --hash=sha256:cee808b405e9095506f4e4e89924bec7ea77eac3129b6fe36eda04364b3b343b \ + --hash=sha256:d11a7bc1e02da8984d32e6de9e0826c6edac00eac17de270f372bf32f9a0af63 \ + --hash=sha256:d27c531edb8d1f38ca2eddaa1fa24913a460aeb721f2efd4ef42a124ce94e354 \ + --hash=sha256:d2d9a63a9e6f2416ace8c109043a9280d6b34f34bb2e5421903e149403db40a6 \ + --hash=sha256:d561a4c5747a1e6c7fa7c49a0292e78b4e8c456332caa084fc7abad8de828652 \ + --hash=sha256:d63010803d7c368963bbe6f7ec379593e76dd581d7db0f29118d88713c9e0354 \ + --hash=sha256:dd741c73440b328f937dc53b344ecadc46bc4f0cec0333a8f42b55f3468ce7ec \ + --hash=sha256:de1064663aa7c839286488a319d2b3b478ca5ab5b2091ade888ed0eeca11a98a \ + --hash=sha256:e252db239eb41c32248e096e0d170bce5896a4fd3405556362bc3dd83d912206 \ + --hash=sha256:e288a06cbbbcd01b44386e767985c9e21d2ad9bf59829aa7058d9a2a494804ab \ + --hash=sha256:e59b60a0a4cb7541480e02090d37dc2df3b72df4c2e776fff64ce3a4e3dd4637 \ + --hash=sha256:e671b2322ef09106ecb1ca0f4c398b134d5e2c1f80d7a4f3336847a3072c0e94 \ + --hash=sha256:e7b04935a79c03c41ad08d0a5f390aac968bfb561f1268897bc5b0f077971efd \ + --hash=sha256:f7e9b8728ba07a3cb2f29d5aa1a266c2664eb8ef0fd43afa34627c92f7fac8f0 \ + --hash=sha256:f9b093cb3b6c9d6233ef45a05cab064d2aa0a8cb3c5777084c9e20fcb77c2371 \ + --hash=sha256:fae8b282f0cb22a5de222999f7723f4e5ec04f6fcdf4aaef879b5b36625ae2b0 + # via + # langchain-core + # langsmith +uvicorn==0.52.0 \ + --hash=sha256:3d887809810b89ed33501bcf0a9aba469b06ecd608158efce04bd6b48d8c9b08 \ + --hash=sha256:ca8876ad6c1983f394157c168b39d52f6dd56dabf5602fa0982751cffc2293ae + # via changeops (pyproject.toml) +websockets==17.0.1 \ + --hash=sha256:02ed63bf26dda9fa27df730a41f6664586c4ee05972c8fb667ce1725b3fd13d3 \ + --hash=sha256:02f0b037a737d0cb0c33866c97bcd1a0b73170dfbf42d69d8fb86f51002fd5ae \ + --hash=sha256:038cfad5d5417f8bb09295abe986029a26d22f34bda622ccc79b670efd4dab56 \ + --hash=sha256:07abc3bd196a48af476a82fd47f3f79a6a3f70937a9f930cef703cfa0c9d83b6 \ + --hash=sha256:07d78a509c3333f5908c83d7f78144ea68a6c9ec28110f5c54d81d8fcdc262c4 \ + --hash=sha256:0b52c76b8a870b141b7ca0705289452183ce7a523101954ccfe29a25986a673f \ + --hash=sha256:10b1587c599fa0f2c89154587c80e0fda98ade6c9fa8c0260a2823fb1800b685 \ + --hash=sha256:10f461191125c63902ea7394ae9e752b1b5785641850c1d365bb30b0f88bc53f \ + --hash=sha256:15920057a6b723f84734f0641403bca163a4b176e5af809ee4f0c4a1e75e9fed \ + --hash=sha256:17ac37716c0244e82c9e384c41653c090b1864c6610224ca3857e7f7b58fce10 \ + --hash=sha256:18ded646ce98cdd3c0235825b3252f1df55765ba49b616bb10282f758667b4d0 \ + --hash=sha256:1b363bfd72a52c0658a3154a4cff219f15a474b35a235057d38853bf151acce7 \ + --hash=sha256:1bdd8c4be420905dd732e00dcd669852d8128cc723efa585a0c0e51adb00a28a \ + --hash=sha256:1d4cf7e8e5b8b1fa40758ac7524843a00237b124ab217e227542cafcfeb7a946 \ + --hash=sha256:1df81d174c1561292de9e40b141cafc04f69077272f6c352afe1d743e20810df \ + --hash=sha256:20a92f78ac8250984ed459faa9ca48c285adbfc0038ddc3fdac6046990a9c9ed \ + --hash=sha256:22bd00f8bae2bccdb5dbe41e20f58ba44ca9fff0b4b561aaf39099c35da762ed \ + --hash=sha256:2437d4ca208cc0f246d3a2297ae7474b4ba18261aaf5b9c79c84c031ecf348e1 \ + --hash=sha256:246927ae9ae06ca0d42a483a4bdb80d4862e1ee5b4cab37c354a5e1ad8356448 \ + --hash=sha256:2503c7e2a5049a12d5dac917a46d5d52591283a766165b8176bb167560421b38 \ + --hash=sha256:2604de7228506b13a44a256a9d223943340c0e725af5d367dc068e192b027761 \ + --hash=sha256:28012a54510fe8301bb893ef143cec30a2780a2d3bc20b7bbdf4379d7a63945d \ + --hash=sha256:2a855b6dfe21c4d3420be265ae031829ba8ba0be0ea350d9f7c3ef30ae63ebe2 \ + --hash=sha256:2abb1ba0a5133b7d2ef3c1c9f4b0c1e8a101012dce0b594ab2b2888d9a64820e \ + --hash=sha256:2b3f3020171202b135ca078e20434977c6b2b02af647130d6980c9e39b9462e3 \ + --hash=sha256:2bc14b481e05e331811108daa1aeb41a5e237a5564ef2f02ec5a356a0f102f78 \ + --hash=sha256:2fa2cb465a131c347ba6717a78c887746e73edb1c131d01c982d6ef0d68b82e0 \ + --hash=sha256:409d93efcaa14f7a99592c5baaef5ec6ca94fba0f5aec1a86f693977c69c9c1c \ + --hash=sha256:41d6aa06b5ab832aee72fedf47a149535b121ac900b6bb4d3fe14712afac9a79 \ + --hash=sha256:49266e4488309b38783257293a38298942b9a03aa106fcb45195377a77c0c1e2 \ + --hash=sha256:4d1d99db29b5444e3982f1ce2ba8a833508ad44b2f1fbd0bd99e81d825c0b461 \ + --hash=sha256:4d41c0a1d47a478bc432b3b9068097bee1ce0c5b19327ea6f75c2ab34ab1f2fb \ + --hash=sha256:5033ffe6804dd53afafa7d08e8c3eef2d2431f34d58ca30507a8442dd04a033a \ + --hash=sha256:53b90c00bc6201ab6695c7ff51a04d0e425514c37515e9eeecd2c1b978ac6c0e \ + --hash=sha256:54cdcaa56f5d3eafd57058f0fa4a3de93a310b43a3c4699f06efc4c0bd054a5a \ + --hash=sha256:5508f38c98ac29def9e747b87543b008a58b075df6da70b2cf2e0b47073d33bb \ + --hash=sha256:55383d8177b3c99fd873ee5db0e0193f4c1dd4a3feaccf1a4a03c1b7cf539cac \ + --hash=sha256:55b12e47dcee83673a40d07686cfb6f9d6dfc285976ade9463f61d2bef3fad22 \ + --hash=sha256:5661f868ef191d33dfc6a0cc7c5b3d495f0cc8bb3f8b30d87bda8755c61c95f5 \ + --hash=sha256:57d2ee9b24b404ce75f3814f92073c0ed88106c950148d2427fe8d25ca254d1f \ + --hash=sha256:599b03beb77633bffc095334338fad79cafc2b01fbd58953838130a9ae967d7b \ + --hash=sha256:5baa9bc0dfbae8c507e51c8cf1b6d4628086f7a87bbd3a9952bd5f035451f1cc \ + --hash=sha256:5f33a649bfcb8312524173cc4bbafa7dbb236e18eee9aa31a1d324ca0ddda28c \ + --hash=sha256:6740be6d1bab69f08ab52cb15b08f76c143b6fe61c580ba62bd929f3ab7a1d42 \ + --hash=sha256:6a434e59962a4fb9016bea327e1d14d6cd67670ecfb8942b4f4a0c24036634ce \ + --hash=sha256:6db9e5bf3649ab506c6ae8a3ac85a00fb1ae3816d75962771b2df8adbc5d40d2 \ + --hash=sha256:6fd88365da261c53d3e943fb37e0d0721b9cde119f6b2e3fc84369b6ab234d63 \ + --hash=sha256:7002d5f9e1c3ddd991cdfdbfee18cc8c8b196b2445022892badacd6cb338bbbc \ + --hash=sha256:70d438268e49f1a4bd096b6b6f7010f3ab48b5db2574dbf7d8c864c46ce7a06a \ + --hash=sha256:72d7f2a5aeb4e82daa4ee18f125b4277f427033359be5c745ad709608446cc2c \ + --hash=sha256:733e3cc7171fa1b899edbe725ef9382d0e960657dc1fd933f3281ae910c01dab \ + --hash=sha256:734d20364dc2cfe03674883cafcf580b6e431c5ce42b476312b9285310230cf9 \ + --hash=sha256:759adeb5b0c5775b563254ec63b5b79089fc0045b479143a0b1b8c0ebaae1253 \ + --hash=sha256:769ce7e2acfd9a89f2bed3a9c0da229459516bbc00bd4c9e2ca492c613ae4861 \ + --hash=sha256:810cb3fb5fa6e447216f4e82d9a85cb8aed0929ae3538153ddfe8a6e3121a58d \ + --hash=sha256:81ce19c6046ace11da7001781be7317bb1dc389f399af4b2ed962190f76f9add \ + --hash=sha256:846a4a8b0833e3cad57523d9e3bd50ec8ea05ab9d06c582f82a1340ba096af5f \ + --hash=sha256:872273e629ca7e3d35f16a2dc6ede84e1d5c831e616b8277de6e4f83114e7c58 \ + --hash=sha256:8848c207049ad49d318e5f64a3d4d7bb189f8328d0d98e65647788f2a085785c \ + --hash=sha256:884af729b8ab50486acd94d9768c2b60914bf39b579ebba0a5cb73bfdfd61fd2 \ + --hash=sha256:8c07f145d0b9e90cbd96035f31fb79199aef4da1872854e36ebeb258e3d57594 \ + --hash=sha256:8cd3369e42c0246afaf9d669cfc19797e3a49e8c0a639544459c57597108b966 \ + --hash=sha256:8e387adb0c692c6b5571bdeafc8ac9d1901ea30f10309134780b16ecd35e6605 \ + --hash=sha256:90246fa9e6cb192a778ce6ce024057ec54317a894db7899c922dcdc1f4cbf6a5 \ + --hash=sha256:90973a3a00f23afdfd1c9b06fb84289bf0220f247ef8a62501a1967c7af54f7b \ + --hash=sha256:9493314a99e599163c854fb5900ad7f7ea38c5cb9d9103aa30b3c6b8181c01fa \ + --hash=sha256:9f7747d3daa41a11f25f7cca5dc988fc51da97b311bed4c9d843860f79779283 \ + --hash=sha256:a39ce3a7b0e6059be093213d637963101380157bcbad355916738fafb490698d \ + --hash=sha256:a60fa1a25cca1bcc2bf87b8d6be37a741f0a3239fb5e9cfb7a37173b68ffcf87 \ + --hash=sha256:a68e604c6d1b0338e46652e2688cbce8096ad9c03548b075fda9e2ea19a9b7dd \ + --hash=sha256:a8af570fc29cd998a921c7131c8ac81d9434466d6d25300cb12a690fb56a8a08 \ + --hash=sha256:aadc298969ad229d8e3029fc5cc751fdad286696230f9cf014e90ff9cd8e6ea0 \ + --hash=sha256:ab56439c9f74c52770690c7b2f616b3bf775cb3920453ee355ac765c032d8bbf \ + --hash=sha256:ab9f962a5b64a5c3c845d556b7dc4e6fb683f7b67179f8205e814bb2e0213ffe \ + --hash=sha256:afbce6e3f0fac32dc87c2a0d84869d1a706460d64f39f3889386413e6e4d3d26 \ + --hash=sha256:b3ff0ad440ad52dda64138f16895f66403f40192365e39b1010e889f289746b0 \ + --hash=sha256:b580794e926cab7ff42ee4371ef14e0b22cb2bb722a607f77769136468f49a3f \ + --hash=sha256:b85b960a4507b0714c0a1246d031be9118d908ee974dc085257297a955205f1d \ + --hash=sha256:b98860aefbd3d9bc8e3c7f0eefb83b11142b16110739c68cd33d3b4d6e84e536 \ + --hash=sha256:bb31f42ea095ea826463c770829aa188a86c9a5c976b1467cbbf583c811de833 \ + --hash=sha256:bc0bca48ba24c6c866847fd20478a51dd547fa0ad258dab9615c414ec534bbc0 \ + --hash=sha256:bd1470d2c53fe53269bf5619da7725d30dd9b9693f1689f7a85eab8dea734442 \ + --hash=sha256:c09e097d0e46e3c289bedab9a475ae344b70c30ff5646e46af22b4e6fdc97b21 \ + --hash=sha256:c1bec5d6a19f5fbe87e4940739cfc65e7bb53d8b353e1029b8037a1653b321bc \ + --hash=sha256:c1c118a6b0e25bfc9a6802075d748fa6321714ffbdf3c88d29d9a0e3c7386c75 \ + --hash=sha256:c23e532c8a2325a1e7486de8763a60dc43e83f01bcaeca07e3ba79652c156db1 \ + --hash=sha256:c356dbddab0a529ed7574f78f559d75a223735c321c28f6f587fbf02b11ed301 \ + --hash=sha256:c38515cb54902f7e97d0239e81ef46c4444f9475f4807fb9bbdb789b4089abcf \ + --hash=sha256:c395bda8e7d8f51a02e80261fb57127979e5c472675d9a96b2860619ad47da48 \ + --hash=sha256:c6be9cba65c65cc76dfa3d4619e359ff02a4476c74e179b215236c11a0b32345 \ + --hash=sha256:cd526c8228e759c1006c4b7c9ac71dc4e925ced1a6a6a5a8e94643709738f63e \ + --hash=sha256:cddc675ec31bca65473321f9a9794e488b43b3b8de5d02c8ef4810c5d5792163 \ + --hash=sha256:cffc84ddec6da7f447677266fee2a3c40ecc78172f00752aa1150b8a8d65df1d \ + --hash=sha256:d41e9845514754a42d1d83b2fca9d27fee2ca7b3b0bee6843ba5a9bb2b6e25ac \ + --hash=sha256:d69fd559f9f0e8a52d2fce6f04ee143f86e70df0a189cd95164eddac599e810f \ + --hash=sha256:d7d72843691f50b91127c50688df10cb72ec6f4c4b1d7e2c11ab33b16acf8e51 \ + --hash=sha256:d9aac6081513f02eac3f8caace800dbfc5c608b69e4a7bef69e414eabfc95aa1 \ + --hash=sha256:dbfae8e75b342e31fc6fd1a8bbb393b7cbb91d6cfd581650300a94381e7b7e2b \ + --hash=sha256:e8208f2729cba030ff872a92064c97584eeb9502f53d32a05a0f05d5a17ca6c6 \ + --hash=sha256:e95e321d0d763f2b6633512605f6112ebd70d5746f3ce05c941909d4a25233f2 \ + --hash=sha256:e98ec9ec61cce5bc4b8b218322ad090b0994eb060bb04da704c62ef0a3d864e6 \ + --hash=sha256:eab6de8a98b9a7772cf686d00b4de439fc7efb8ab05ae106ef227291d06f87c5 \ + --hash=sha256:efe0ae052a8d023b87198921e8a7ce1dc7768816bcd2fbc20df171ac73a04891 \ + --hash=sha256:f11a398d8170b7ac5000baf7f258dcda579ef3ea744e0cc6a165e0dfbc0d3198 \ + --hash=sha256:f3fd9a1f87f8f0f3f8e9f9bd0195f7516562d13f5b178db8c5784d1f60b60bed \ + --hash=sha256:f47b0815af3948ec6a440b3afa02f05b18cc0939549e91b5c677b5d9c2c8472a \ + --hash=sha256:f991247276797d0c61ab7770bc9791eadc16f683b4d83517f624932adc1a8bab \ + --hash=sha256:ffad64ce7ad3703d652a3fd9af26238377d24ce52c6ad8ff35d26d82f61f493f + # via langsmith +xxhash==3.8.1 \ + --hash=sha256:000435984a0469b0f822fe76f35bddea0f96a4d6521b3339a60a6428cdee1edc \ + --hash=sha256:00de40f3b42240db23a82a5c682b55d7263d84a26a953240c1aee463409660e3 \ + --hash=sha256:01cab782f8a0a05ecad2c63d7ef10f7ab475f660e0d6419d069418c14d88de7c \ + --hash=sha256:0204701e6d01f64254e0e5ff4255812b1febe027ddd7dda63372e27f98b5e91f \ + --hash=sha256:027dee4355f3fcc41481650d846cf6cfc895c85a1ab7acd063063821a0df5b4c \ + --hash=sha256:036a024d8b9c01f70782e09ed98d532e76fd23f950ae7154bd950fe94e90ebec \ + --hash=sha256:0418ec8b2331b9d4d575fc9284427e8e69449d7172e99e1a86fcdd1f51a0a937 \ + --hash=sha256:08ea2081f5e88615fec8622a9f87fbe21b8ea58d88cfc02163ca11026ee62a92 \ + --hash=sha256:095e1323fa108be1292c54c86da3ef3c7a7dc015b105a52133973bc07a6ad11a \ + --hash=sha256:09a204dd4bb0823daf938cdd0dc8057d5f1e14fe3cbde929424255f23f9de872 \ + --hash=sha256:0dfdf19b0d5433a75d61f19dc85737af0f0b95e445c1ad69c855115d05efed45 \ + --hash=sha256:0fe37f72a207223d22a4eddc3149d4298993385aa9daef25c039246ca5a309f3 \ + --hash=sha256:10e4393ec33633c2f05ad01869e546ad080b1a18f2650503731f153774608b31 \ + --hash=sha256:1153265daa10750a9bf8e9b01753d7618024a300925591efaf16b1b7fa536699 \ + --hash=sha256:115772daeb71b2f3b9381177017f53e6cf3f3439c840737fdabd21aba6e54920 \ + --hash=sha256:12a3cf79dadbab9631230ebc4c51c7c60f1e9cdfb890c15fb733eaafe2e7713c \ + --hash=sha256:12eaeaa9ab8b9e6033a1fa5f6b338aaf55ff4df4bee11b59fd6ee03b19186ee4 \ + --hash=sha256:131324f719957b988861714de7d6ddf57b47abec3b0cc691302ffeaba0e05e10 \ + --hash=sha256:15790b686f8723b845fec6f612a343beb815a25c83117a7fa408d7c8ee5aa8fd \ + --hash=sha256:1731407102b9332cd3c9dadee07db498bc3d437b95d752b5b1a5f7eb730a3738 \ + --hash=sha256:1b86ae798a976ccbc1d02af6ccb98f5b4d24756b1f65e995f11d10fe071f486f \ + --hash=sha256:1c332dd48b8cb050da2bb2a3c96d72b1664168650a250ef9718e423df7989e05 \ + --hash=sha256:1da930bbcac3e8fbe2191850e2abb57977a99348c12c4b385e1058ac1b0a9ecc \ + --hash=sha256:1f44275ddb0978b67a58a951501903f04d49335a91f7681c9ce122ecb8ccb329 \ + --hash=sha256:1ffcc98d8878e449e86dec008cea6f44cfd3a954d2ef24ae7d1cc9f725beec7d \ + --hash=sha256:220d68130f83f7cc86d6edfdeab176adc73d7200bf3a8ec10c629e8cf605c215 \ + --hash=sha256:2256e80e4960ee282f63428adb349cb7f8bd8efe4db770d88eb815f4b9860724 \ + --hash=sha256:23e710118a5778a45db740b431943a3f2a82a571a052c2768cce6544d9c8c62e \ + --hash=sha256:264710bd335016f303763ce1275c6486df30bb57c2245c91b224c983d7ac39b8 \ + --hash=sha256:2666f059a1588a99267e33605365ed89cea92f424b3522806a9f4bd8ad2e3d62 \ + --hash=sha256:27a9e475157f7315826118e3f3127909a0fe25f1b43d3d3be9c584f9d265f937 \ + --hash=sha256:27cfc2f1ed76f956f36dfe0c56e5f5a3e94cd91eb78b893f63e2ef2ae404fcdf \ + --hash=sha256:2bc7113e6f2b6b3922dd61796ca9f36af09da3773898e7003038dc992fc83b8d \ + --hash=sha256:2e32855b6f9e5b18f449e59d45e3d5778bdeb660632ef2693cca267a11246c75 \ + --hash=sha256:2f1c68394818e0595569c2ff3cbc1e6d5a36a434e796f5c526b987b80c8a8c62 \ + --hash=sha256:2f8c25a7061d952de589bd0ea0eaadee32378ff83dd6a677b267f9cd86f401f8 \ + --hash=sha256:314d05fbc55719ae2438eaaba77bf2508ca4f030b26fa4c9c8c380e81c48fa33 \ + --hash=sha256:32a94ad2763e0263d9102037d349002c3d3c401e42770542c3eeb4801f311661 \ + --hash=sha256:32ab1e5432690276e71192be7401b55f96db2d0eedea5d44eb1f164505669cc0 \ + --hash=sha256:345b07b78e2bf583d71682aa34ae5b5fab575f7a1cb31e10263ebbc6f89f8c42 \ + --hash=sha256:3557bec8fcb11738a8920eeb68974bc76b75262f6947998d3147954ce0a4b893 \ + --hash=sha256:358650d5bda9c635da699c53adf4e8134af492ecc79c960f917eebf088bb6799 \ + --hash=sha256:36fc69160465ae75c6ec4ac9f781bb2aa16ae7ff869e73c26fee85fbb11b9887 \ + --hash=sha256:37d5a56c36dcc0b9a87b814cd992598d33863ff683749de6c86081f278d5e629 \ + --hash=sha256:38c887aedb696ef8bca19983206d270848558cfae4a91afa6a2fb05dde58ffc5 \ + --hash=sha256:39c9d5b61508b0bb68f29e54546de0ed2a74943c6a18585535a7e37356f1dd12 \ + --hash=sha256:3a800912a2e5e975d4128969d645c4a2a80aa886ccd6c9b1c6f44529e327e8cf \ + --hash=sha256:3bc0fa90830df1e1277f33cc6e55de9990b83c0319fd8c7412866cfde38b025e \ + --hash=sha256:3c0d84c5f2e086b120bae4e7f551cbda804c1deb10d958478bed4f89ba286dfe \ + --hash=sha256:3c682fcd96eb4bf64be32a4d95f96107e1588005831bd8a741b324fdda01b913 \ + --hash=sha256:402db908ea70eaf9800d9182a66596fc86f36655df8f63fdecf7c11da741d86f \ + --hash=sha256:445e0f5a31f2f3546ae0895d4811e159518cdc9d824c11419898d40cfadb677e \ + --hash=sha256:4482380b462ca9e59994d072a877ecadd1cf51102daeeab2db696f96ab763723 \ + --hash=sha256:44c89d915a75c11d2547eaee9098fcd80398987c4bff2974a0497a925bf92c07 \ + --hash=sha256:454d78e786602278a2a4383d08048482052f4f0c61fa677ca590af08914d9bca \ + --hash=sha256:460261045936975193bfd20549a0de1cd52a33b405cbb972f0d80940c42266cd \ + --hash=sha256:46b39976d008e2a845758650f0ff7136bca004f40da0c8798bd37ac37860154f \ + --hash=sha256:498017fbf2d13a768b3110d084bde39f2bd8664c1de0b8084f8ccc84425b7c88 \ + --hash=sha256:49aa8692507835dcc1e8ad8021f20c74c2dc13d83b5112e87877faa2a0035b20 \ + --hash=sha256:4b512261801b1e5fde7b6ebf2fef7977339c620cbbca88a0040ad9ad134f4d02 \ + --hash=sha256:4bec8b2c909bcfae9a0dc702346007e02a8c9ba5bbde83ffb224aa194f4f9efc \ + --hash=sha256:4d365ee1892c1fa803536f8c6ce21d24b29c9718ec75eb856095c07830f8c478 \ + --hash=sha256:4d6e88ddb3c741fbf29e1e7faf429880f8cd1d7aff4303247435a549726b4fb1 \ + --hash=sha256:4df57c0b161ec1b3ed0526a67b0db0914b557e86ee8aae51887aec941b261542 \ + --hash=sha256:4e0e1b0fb0259c1b75d1251ac0bb4d7ab675d36f7a6bf4ba6aa630dae94f9ffa \ + --hash=sha256:4ef09bbc2519a93cd0f95f2ceb5f7b85919dffea643278e02362bf40e3c4bed1 \ + --hash=sha256:5013be3bea7612852c62a7437f3302c1cfb91ca7e703b194459db0b2b2e0d792 \ + --hash=sha256:5177aa44eddaa97c6ef0cc00c6d540edb64d51781d2f8fb941612ec61a92c9ed \ + --hash=sha256:51f71a6e2ad071e70c937e41fcb6c19f82c3f9f49831eba850ed4a106ffbb647 \ + --hash=sha256:538f5f865df6cd8c32dd63158a0e5b4f5dd08d732a7da8b7228a5a0776c8ce55 \ + --hash=sha256:53f3ed9118397074ff63a79b66b7fec1c84c782eecde35c5bc94e420a971c231 \ + --hash=sha256:559e3cabe522231909f9de98ef06929edbd53782046bd21aae0c72db6f2a0775 \ + --hash=sha256:57189a69c0891e4818853feaa521c972d22c880a001453addea015f48e3c3398 \ + --hash=sha256:57f80a898544db78ec6b0be6183bd1bc008933193d4199f5cde36b0e6bd5e062 \ + --hash=sha256:58346024d47e84f7d8b3e7f5d6faa1d58acbbe49a8771497872059f58c1d8ea5 \ + --hash=sha256:594131ce1aad18db3689781f806db1b065cdaa04f4df36b4c038d2013aefd0bf \ + --hash=sha256:5b96f0024e9840f449bd91b2d005c921a4b666055a0d1b6492463799f32aae22 \ + --hash=sha256:5c566b123dce7e4867ca518434cdfb9f84e5023771235b2e3107a26c9a41cbd8 \ + --hash=sha256:5d3dfb1f0ff146da7952867a9414f0c7a29762f8825a84879592612fd6139342 \ + --hash=sha256:5da703225374e3a4c8d4fd90e26fe7213a52004ec77f88b42b42e9e86d8c6d57 \ + --hash=sha256:5db43f249b4be9f99ef4b967863f37094fb40e67effafb78ba4f0356b6396104 \ + --hash=sha256:5eed32dad81d6ba8e62dc7b9ffa0500199385d7810a8dd9d4eafaceb8c6e20bb \ + --hash=sha256:602efcad4a42c184e81d43a2b7e6e4f524d619878f2b6ee2ba469011f47c8147 \ + --hash=sha256:614bca2c7cfa87ec95b703e691c3c5eb6c448b6dabbe9776ac53883152951729 \ + --hash=sha256:632a34590c090d1285ed5efa5a02be919f3f9a56a64bd25f693fe1e2d27a27fb \ + --hash=sha256:64af54dd1c3a45a27c04942f9a1a4683322bdd127f4745cca4e02549c1d2d2bb \ + --hash=sha256:6536d8677d2fff7e64cd0b98b976df9de7aee0e69590044c2af5f51b76b7a170 \ + --hash=sha256:656256c9f9303e47f07d5cb8ae4468285370adfafd7ba48aea33a458e7697626 \ + --hash=sha256:6696c8752aded28ff3b16f33ef28ce28fb5d209b80c206746f943199fcf5fd65 \ + --hash=sha256:6c7574528bc922f8757f34dd78ed60ab52b1c7973b630f5eae7ba33ec133ce71 \ + --hash=sha256:6cee733fe4ccb1737e0997135283c82341e5cfa9cf214b165f9087fb663aaf4f \ + --hash=sha256:6cf633fe83b1d4e6519d7259b33afe40fbba5d3f438730156971dd0cf7730610 \ + --hash=sha256:714503083a1f2065c9ad15340dd49ac8a8e948a505a705ffa1750cb951519113 \ + --hash=sha256:717b12fdc51819833704e85e6926d76981ffa3f780ef92e33ebb8b26d46bb230 \ + --hash=sha256:7258ee276e8772599bc19e14b36f6260306e21b637190cd7cb489a2449d48684 \ + --hash=sha256:72eb5ae575cc7ae2b23f6f8064a8b10f638c7149819ae9cc6d20ebd4d37a1629 \ + --hash=sha256:7345007c12780985de4fd740148776d1eee18c0d41407c6fa1e48c5450304fe5 \ + --hash=sha256:747476436f6891b9773374ce8d48edcc8b12cb5b61b67c6fb6289633747d088f \ + --hash=sha256:77f74e45a1e5574bbbf80181c8027b3a4c65c2248fffbd557bd596fff13102f9 \ + --hash=sha256:7801b7223db017b9c0c9ccf37e44524edb35a1544a1c032add22c061c6af0276 \ + --hash=sha256:78c794b643d214f1522e7a288bcf5a2de120d26cd170516749a4009dc92722c9 \ + --hash=sha256:7b5f97ecfede10d5b2870383620e2d25c8561e217c7bf9081073802b54248d2b \ + --hash=sha256:7dc4bdf008f77c88d544849c48c1a40faf25a5eff6cc466de2e8edc37c191fce \ + --hash=sha256:81f4ed9ca9644bc95cd976bfe10f7a4cafab8ffdc3aed52877d4600e445be7ef \ + --hash=sha256:82c0cedd280eab2e8291270e6c04894dbc096f8159a39dcf1807429f026ca3cc \ + --hash=sha256:8304be0982130954b7fd3aad18e2c6f8ee40254bc3d2e635991c16d77c91e2bd \ + --hash=sha256:83697b0ea1f10e7f5d8b26a4906fa851393c61546c63839643a2b7fe2d868061 \ + --hash=sha256:836f11d4474d3228e9909d97216faa4f7505df41cfaf3927eb29809de785a78d \ + --hash=sha256:83b9130b80b216d56fdf9e87131946b353c9627930c061955a101ea82b09fed9 \ + --hash=sha256:83d879362ddd0fedd3f2ab8ce7cce3da2049a6d51d16da8af73011c6edf4752f \ + --hash=sha256:848182a391fffdc25605443e832f5b443f25498edeccf9a64343fd84421ca04b \ + --hash=sha256:852bfe059720632e2f16a6a4745e41d20937b2bf2a42a401e2412046bb6971cc \ + --hash=sha256:868a8dcaff1a84ba78038e1cef14fc88ccf84d9b4d12ea604696e0693296aa56 \ + --hash=sha256:89b11a5cdd441aa463f6d34ca0241602bc09b001a76994b6059828494108c673 \ + --hash=sha256:89df64c10adfe340fb00330042537cdd6bf0d8d78bad73f29cfe5427eed7b084 \ + --hash=sha256:8ea8a141eeced4f6262ab6dd71c681ac546a558c30bb586abe087d814b5f85ea \ + --hash=sha256:8f454166c2ffed45636c8d501741e649851ba2f346c4eb73a64c07ac00428f20 \ + --hash=sha256:8f759eed402448c2bdbb492e4fba1f20668ffe29688605ea61f0f67f9e4e386d \ + --hash=sha256:9043877a917be88ccf230aa5667c1bd059bce80f4c2727e4defa1b29b7f48b08 \ + --hash=sha256:942bc86e9be6fdd6e1175048f5fe8f8fdaaf2309dd1323ef1e155a69cd346780 \ + --hash=sha256:947a585bcaa235702b7c59433b485489397f9a163b3f56058b9463a46fd9b74c \ + --hash=sha256:950ac754d16daea42038f38e7465eb84cda4d08d7343c1c915771b29470f065a \ + --hash=sha256:98d8ac1129b4dd39098cffed94d1284aceb61c3aa396757ccc736ac392e4cee5 \ + --hash=sha256:98ee81b4b7f3023c9cb04a78cc67610baffcb5812d92f2096cb5a5efc6f19437 \ + --hash=sha256:9b2ce44bf8f4a1d01f418b3110ff8dff32fd3f3e836c0e06333c3725f243fa6c \ + --hash=sha256:9d45eee3a95a8b61e5b568580caac91f1502ddb731aaf8f4aa448a98660b2fb4 \ + --hash=sha256:9db455cb649dcfe4504d6d68a6d83a7315a99a3ca59871dc3ff840671f99adba \ + --hash=sha256:9df56e6df96a60590935e22373041cccc91fd55858763dcffb55bf63b3a2b396 \ + --hash=sha256:9e80238259655bf69d7bcd08226a970d7f42605f3157786bfa76dd13472d7fa0 \ + --hash=sha256:9f23083e1bd9d901f844af7a126727c486e7eada9a1a6791c8f7e73f94fac656 \ + --hash=sha256:a2489d3a776fa380cb8e71f54c7fda268a9baf3de9b1395093fd280f95735907 \ + --hash=sha256:a5cd96f6dcdf4fa657b2d95668d71d58455248f98712ecffaa9c528edf40ccae \ + --hash=sha256:a5eed9d41995a83f3332b4e3396abb7f433cac584222bd7e305b606d8353861e \ + --hash=sha256:a6617f30641ba0d8baa1635fbefb1dffc5165ec36d26921bd5cee13497cd937a \ + --hash=sha256:a6e088bd7870775624256a0d84c2a6714afd223b2eeb56b0ca58398e52a32fda \ + --hash=sha256:a98b2f95cab589e0f5e92c48431afb4d56238b8bf6668edcc66166180e9b509b \ + --hash=sha256:ad52a0e4bcc0ba956a953a169d1feec2734a64981d689e4fc8f490f7bf91af60 \ + --hash=sha256:af0c9fedc4a2c24e8664953882fe8185f3790b8338c9c700f76f5ad660817711 \ + --hash=sha256:afe6380a0e9653a87aa1e6e88fb47718113e5563c7a1cb2bcc23c1d8e17e3961 \ + --hash=sha256:affb37f152e55b5e4494bb9d0107f7bb08515c6704fbed82d9f61214d74adc17 \ + --hash=sha256:b0093cf7eeb91b84776e8742113afa4bdf47533d36cf719179aaaf1f56f6f8bf \ + --hash=sha256:b0de4bf3aa66363552d52c6a89003c479911f12098cd48a53d44a0f7a25f7c46 \ + --hash=sha256:b30e01a0b97a4bc3f519a4d7a82da3dc53251fb0de5eeea8660dcd4ff094c0c2 \ + --hash=sha256:b3ba794c3d885803db6c3116686923f1ec13bc86e621e169a375282b63ea1cc6 \ + --hash=sha256:b3e1107fe5ca030f946dfa59fdbb66b5df121c8432f14b0bdd282d17b297f4eb \ + --hash=sha256:b5196cc2574cfec572a5f3fb7cfa5ade27305ae3d06516a082132441aff4c83a \ + --hash=sha256:b6fa3116e40e14e7782fb1a9f872f94b5997de21127c95545ce40196ac1351c5 \ + --hash=sha256:bb70573d2995d23932e2871120f78d798ebc3572e54c09e694a18ced95c5f8d9 \ + --hash=sha256:bbcdf9c92d21c65bc75426eecea724c8fa0d35a6e201fdf1630011d4cc3aa685 \ + --hash=sha256:bcab50a389cc04d87f90092af78a6adba2ab3deca63175a3344ca83514045315 \ + --hash=sha256:bf28f55e427e0483acb1f666bd0d869b6d5e5a716680c216ad7befe3d4cfba2e \ + --hash=sha256:bfcd82852c62a60e314670a9602de354c4460f8adad916e2e42a20860c7870bc \ + --hash=sha256:c240939e963653054fc7e4a17c382829cda4aa88a7daf0af841715dbded1b497 \ + --hash=sha256:c31a2649bcf1fe97cf11c79848d761df33ac46b3896942d31b640557b486ff6b \ + --hash=sha256:c4ed42965c2cd9081f011be22f69d0e65d3b6165fe7734072fd0c232840bbd4e \ + --hash=sha256:c73b6f652f0745425aa6378319c331293b5341756262e9408ed3d45f183375e6 \ + --hash=sha256:c85949d02c85adf6d786eb94858e124989a632a4e65739835b2fc5761827fac3 \ + --hash=sha256:c919f38cd3f0b5e8d30b81fd6cac688cf9221560340f0c35cbbb8b2bd77ad6ac \ + --hash=sha256:c959f88160b13b4e730b0d75b459b7929fc0d2225c284c9683ac95d6feeeac6a \ + --hash=sha256:cb3fe820c27593f170770d6c8d791936cf6275d9269405fbb7b30a55363c10c8 \ + --hash=sha256:cf399fac542a1c7a4734a435b93df2c55e858c7d31abf6c1bdf46f9ae67fbfd0 \ + --hash=sha256:d0b48cdf690a64cedf7258c3dc9506cc41fc86edd7739c40e3098952265dc068 \ + --hash=sha256:d247b34bf433c92b41689318fd25d246313cab2275a6a47e2efac178b80d6efe \ + --hash=sha256:d48acabb1e5cb0071009f80d71d7f01b6ba2c1d4b869b1352bb5df3f11bf7dfd \ + --hash=sha256:d5006c65ec507a333479e76e00e2c368781f16c24ededa764763956b32a0e93e \ + --hash=sha256:d58ce8b6cfa9c4d2f230557f69caf7c06369e318015d0b19485095bc2c5963ab \ + --hash=sha256:d59e71153fe9ff85648d00e18649b07e9b22c797291abb7e27274fa06df8b838 \ + --hash=sha256:d6a5c0bce213b23b0166fe0d35bcbbe23ce4b968f257cc7eb6fd57cb8e1e6297 \ + --hash=sha256:daa86e4b68221d38e669bb236ba112d0335353829fb627c82e5909e4bbe8694c \ + --hash=sha256:db77278a6eddadbf44ce5aae2fee5ebb4d061f026b1ce2130d058cd4d7a7b670 \ + --hash=sha256:dfe0580fbfd5e4af87d0cc52d2044f155d55ebd8c8a93568758a2ea7d8e15975 \ + --hash=sha256:e14800b9b10bb39d7a60ad4a310e403164d7b8988a27ae933d4e40618a44088e \ + --hash=sha256:e2a845687219ba3214126f14a8a5861f97c9e065a7d0b8252adb6df13eea86fb \ + --hash=sha256:e3b87cbd974512c0c5fc7b469c36b2cdc9ee6d76e4ec78bccb2c7184611c49b0 \ + --hash=sha256:e4a6443968c4e8dc69967e12776776a5952c119cc1bd94168ad1c5ad667c2be1 \ + --hash=sha256:e605e0b8abca9457abd5bee737e086ab145a20c25083ef1113013612268872ff \ + --hash=sha256:e6e49370822c1f4d8d90e678b06dbcb08b51a026a7c4b55479e7d467f2e813bc \ + --hash=sha256:e710ad822c493fb80a4fbc1e3d0a807b1422cb90adbe64378f98291b7fa48fef \ + --hash=sha256:ea6a3e734b0fd41b82784a400be946821900daebe610c050a5e0760838a34f99 \ + --hash=sha256:ec55d80e9b8a519d742669e0b49e8ce9e6747be42bf3c138158b6543a9c8e489 \ + --hash=sha256:ed8bcdab6692fd4ad0dd6241807a24a640a376764460023b8d462d745e6b7b27 \ + --hash=sha256:f377012b86c0a23a1df0cf5a1b05aa7187649e472f71c7892e5f2c2815bbe74f \ + --hash=sha256:f6114692261eff4266386cdec0f7d87eee24e317ab397c218b7ae6a76b4c6339 \ + --hash=sha256:f8044cf4c77f37968b8c4cbcbf7a0f355d8a437877ae18eba23e3aad953a6cc7 \ + --hash=sha256:f8ed8940435834141061da26d27c4dd0d18fb69777bf431f5c6cc46b43349113 \ + --hash=sha256:f93e408255ddce525189bf11feaa1be7ee35e55f486c299c97d9caa68d724a5b \ + --hash=sha256:fb9e256a357dfcede7818c6d34e70db2d6b664394803d1de4b6984d2de76c0f1 + # via + # langgraph + # langsmith +zstandard==0.25.0 \ + --hash=sha256:011d388c76b11a0c165374ce660ce2c8efa8e5d87f34996aa80f9c0816698b64 \ + --hash=sha256:01582723b3ccd6939ab7b3a78622c573799d5d8737b534b86d0e06ac18dbde4a \ + --hash=sha256:05353cef599a7b0b98baca9b068dd36810c3ef0f42bf282583f438caf6ddcee3 \ + --hash=sha256:05df5136bc5a011f33cd25bc9f506e7426c0c9b3f9954f056831ce68f3b6689f \ + --hash=sha256:06acb75eebeedb77b69048031282737717a63e71e4ae3f77cc0c3b9508320df6 \ + --hash=sha256:07b527a69c1e1c8b5ab1ab14e2afe0675614a09182213f21a0717b62027b5936 \ + --hash=sha256:0bbc9a0c65ce0eea3c34a691e3c4b6889f5f3909ba4822ab385fab9057099431 \ + --hash=sha256:0be7622c37c183406f3dbf0cba104118eb16a4ea7359eeb5752f0794882fc250 \ + --hash=sha256:106281ae350e494f4ac8a80470e66d1fe27e497052c8d9c3b95dc4cf1ade81aa \ + --hash=sha256:10ef2a79ab8e2974e2075fb984e5b9806c64134810fac21576f0668e7ea19f8f \ + --hash=sha256:1673b7199bbe763365b81a4f3252b8e80f44c9e323fc42940dc8843bfeaf9851 \ + --hash=sha256:172de1f06947577d3a3005416977cce6168f2261284c02080e7ad0185faeced3 \ + --hash=sha256:181eb40e0b6a29b3cd2849f825e0fa34397f649170673d385f3598ae17cca2e9 \ + --hash=sha256:1869da9571d5e94a85a5e8d57e4e8807b175c9e4a6294e3b66fa4efb074d90f6 \ + --hash=sha256:19796b39075201d51d5f5f790bf849221e58b48a39a5fc74837675d8bafc7362 \ + --hash=sha256:1cd5da4d8e8ee0e88be976c294db744773459d51bb32f707a0f166e5ad5c8649 \ + --hash=sha256:1f3689581a72eaba9131b1d9bdbfe520ccd169999219b41000ede2fca5c1bfdb \ + --hash=sha256:1f830a0dac88719af0ae43b8b2d6aef487d437036468ef3c2ea59c51f9d55fd5 \ + --hash=sha256:223415140608d0f0da010499eaa8ccdb9af210a543fac54bce15babbcfc78439 \ + --hash=sha256:22a06c5df3751bb7dc67406f5374734ccee8ed37fc5981bf1ad7041831fa1137 \ + --hash=sha256:22a086cff1b6ceca18a8dd6096ec631e430e93a8e70a9ca5efa7561a00f826fa \ + --hash=sha256:23ebc8f17a03133b4426bcc04aabd68f8236eb78c3760f12783385171b0fd8bd \ + --hash=sha256:25f8f3cd45087d089aef5ba3848cd9efe3ad41163d3400862fb42f81a3a46701 \ + --hash=sha256:2b6bd67528ee8b5c5f10255735abc21aa106931f0dbaf297c7be0c886353c3d0 \ + --hash=sha256:2e54296a283f3ab5a26fc9b8b5d4978ea0532f37b231644f367aa588930aa043 \ + --hash=sha256:3756b3e9da9b83da1796f8809dd57cb024f838b9eeafde28f3cb472012797ac1 \ + --hash=sha256:37daddd452c0ffb65da00620afb8e17abd4adaae6ce6310702841760c2c26860 \ + --hash=sha256:3a39c94ad7866160a4a46d772e43311a743c316942037671beb264e395bdd611 \ + --hash=sha256:3b870ce5a02d4b22286cf4944c628e0f0881b11b3f14667c1d62185a99e04f53 \ + --hash=sha256:3c83b0188c852a47cd13ef3bf9209fb0a77fa5374958b8c53aaa699398c6bd7b \ + --hash=sha256:4203ce3b31aec23012d3a4cf4a2ed64d12fea5269c49aed5e4c3611b938e4088 \ + --hash=sha256:457ed498fc58cdc12fc48f7950e02740d4f7ae9493dd4ab2168a47c93c31298e \ + --hash=sha256:474d2596a2dbc241a556e965fb76002c1ce655445e4e3bf38e5477d413165ffa \ + --hash=sha256:4b14abacf83dfb5c25eb4e4a79520de9e7e205f72c9ee7702f91233ae57d33a2 \ + --hash=sha256:4b6d83057e713ff235a12e73916b6d356e3084fd3d14ced499d84240f3eecee0 \ + --hash=sha256:4d441506e9b372386a5271c64125f72d5df6d2a8e8a2a45a0ae09b03cb781ef7 \ + --hash=sha256:4f187a0bb61b35119d1926aee039524d1f93aaf38a9916b8c4b78ac8514a0aaf \ + --hash=sha256:51526324f1b23229001eb3735bc8c94f9c578b1bd9e867a0a646a3b17109f388 \ + --hash=sha256:53e08b2445a6bc241261fea89d065536f00a581f02535f8122eba42db9375530 \ + --hash=sha256:53f94448fe5b10ee75d246497168e5825135d54325458c4bfffbaafabcc0a577 \ + --hash=sha256:5a56ba0db2d244117ed744dfa8f6f5b366e14148e00de44723413b2f3938a902 \ + --hash=sha256:5f1ad7bf88535edcf30038f6919abe087f606f62c00a87d7e33e7fc57cb69fcc \ + --hash=sha256:5f5e4c2a23ca271c218ac025bd7d635597048b366d6f31f420aaeb715239fc98 \ + --hash=sha256:6a573a35693e03cf1d67799fd01b50ff578515a8aeadd4595d2a7fa9f3ec002a \ + --hash=sha256:6c0e5a65158a7946e7a7affa6418878ef97ab66636f13353b8502d7ea03c8097 \ + --hash=sha256:6dffecc361d079bb48d7caef5d673c88c8988d3d33fb74ab95b7ee6da42652ea \ + --hash=sha256:7030defa83eef3e51ff26f0b7bfb229f0204b66fe18e04359ce3474ac33cbc09 \ + --hash=sha256:7149623bba7fdf7e7f24312953bcf73cae103db8cae49f8154dd1eadc8a29ecb \ + --hash=sha256:72d35d7aa0bba323965da807a462b0966c91608ef3a48ba761678cb20ce5d8b7 \ + --hash=sha256:75ffc32a569fb049499e63ce68c743155477610532da1eb38e7f24bf7cd29e74 \ + --hash=sha256:7713e1179d162cf5c7906da876ec2ccb9c3a9dcbdffef0cc7f70c3667a205f0b \ + --hash=sha256:78228d8a6a1c177a96b94f7e2e8d012c55f9c760761980da16ae7546a15a8e9b \ + --hash=sha256:7b3c3a3ab9daa3eed242d6ecceead93aebbb8f5f84318d82cee643e019c4b73b \ + --hash=sha256:809c5bcb2c67cd0ed81e9229d227d4ca28f82d0f778fc5fea624a9def3963f91 \ + --hash=sha256:81dad8d145d8fd981b2962b686b2241d3a1ea07733e76a2f15435dfb7fb60150 \ + --hash=sha256:85304a43f4d513f5464ceb938aa02c1e78c2943b29f44a750b48b25ac999a049 \ + --hash=sha256:89c4b48479a43f820b749df49cd7ba2dbc2b1b78560ecb5ab52985574fd40b27 \ + --hash=sha256:8e735494da3db08694d26480f1493ad2cf86e99bdd53e8e9771b2752a5c0246a \ + --hash=sha256:913cbd31a400febff93b564a23e17c3ed2d56c064006f54efec210d586171c00 \ + --hash=sha256:9174f4ed06f790a6869b41cba05b43eeb9a35f8993c4422ab853b705e8112bbd \ + --hash=sha256:9300d02ea7c6506f00e627e287e0492a5eb0371ec1670ae852fefffa6164b072 \ + --hash=sha256:933b65d7680ea337180733cf9e87293cc5500cc0eb3fc8769f4d3c88d724ec5c \ + --hash=sha256:9654dbc012d8b06fc3d19cc825af3f7bf8ae242226df5f83936cb39f5fdc846c \ + --hash=sha256:98750a309eb2f020da61e727de7d7ba3c57c97cf6213f6f6277bb7fb42a8e065 \ + --hash=sha256:99c0c846e6e61718715a3c9437ccc625de26593fea60189567f0118dc9db7512 \ + --hash=sha256:a1a4ae2dec3993a32247995bdfe367fc3266da832d82f8438c8570f989753de1 \ + --hash=sha256:a3f79487c687b1fc69f19e487cd949bf3aae653d181dfb5fde3bf6d18894706f \ + --hash=sha256:a4089a10e598eae6393756b036e0f419e8c1d60f44a831520f9af41c14216cf2 \ + --hash=sha256:a51ff14f8017338e2f2e5dab738ce1ec3b5a851f23b18c1ae1359b1eecbee6df \ + --hash=sha256:a5a419712cf88862a45a23def0ae063686db3d324cec7edbe40509d1a79a0aab \ + --hash=sha256:a9ec8c642d1ec73287ae3e726792dd86c96f5681eb8df274a757bf62b750eae7 \ + --hash=sha256:aaf21ba8fb76d102b696781bddaa0954b782536446083ae3fdaa6f16b25a1c4b \ + --hash=sha256:ab85470ab54c2cb96e176f40342d9ed41e58ca5733be6a893b730e7af9c40550 \ + --hash=sha256:b9af1fe743828123e12b41dd8091eca1074d0c1569cc42e6e1eee98027f2bbd0 \ + --hash=sha256:bfc4e20784722098822e3eee42b8e576b379ed72cca4a7cb856ae733e62192ea \ + --hash=sha256:bfd06b1c5584b657a2892a6014c2f4c20e0db0208c159148fa78c65f7e0b0277 \ + --hash=sha256:c19bcdd826e95671065f8692b5a4aa95c52dc7a02a4c5a0cac46deb879a017a2 \ + --hash=sha256:c2ba942c94e0691467ab901fc51b6f2085ff48f2eea77b1a48240f011e8247c7 \ + --hash=sha256:c8e167d5adf59476fa3e37bee730890e389410c354771a62e3c076c86f9f7778 \ + --hash=sha256:ca54090275939dc8ec5dea2d2afb400e0f83444b2fc24e07df7fdef677110859 \ + --hash=sha256:d7541afd73985c630bafcd6338d2518ae96060075f9463d7dc14cfb33514383d \ + --hash=sha256:d8c56bb4e6c795fc77d74d8e8b80846e1fb8292fc0b5060cd8131d522974b751 \ + --hash=sha256:da469dc041701583e34de852d8634703550348d5822e66a0c827d39b05365b12 \ + --hash=sha256:daab68faadb847063d0c56f361a289c4f268706b598afbf9ad113cbe5c38b6b2 \ + --hash=sha256:e05ab82ea7753354bb054b92e2f288afb750e6b439ff6ca78af52939ebbc476d \ + --hash=sha256:e09bb6252b6476d8d56100e8147b803befa9a12cea144bbe629dd508800d1ad0 \ + --hash=sha256:e29f0cf06974c899b2c188ef7f783607dbef36da4c242eb6c82dcd8b512855e3 \ + --hash=sha256:e59fdc271772f6686e01e1b3b74537259800f57e24280be3f29c8a0deb1904dd \ + --hash=sha256:e7360eae90809efd19b886e59a09dad07da4ca9ba096752e61a2e03c8aca188e \ + --hash=sha256:e96594a5537722fdfb79951672a2a63aec5ebfb823e7560586f7484819f2a08f \ + --hash=sha256:ea9d54cc3d8064260114a0bbf3479fc4a98b21dffc89b3459edd506b69262f6e \ + --hash=sha256:ec996f12524f88e151c339688c3897194821d7f03081ab35d31d1e12ec975e94 \ + --hash=sha256:f27662e4f7dbf9f9c12391cb37b4c4c3cb90ffbd3b1fb9284dadbbb8935fa708 \ + --hash=sha256:f373da2c1757bb7f1acaf09369cdc1d51d84131e50d5fa9863982fd626466313 \ + --hash=sha256:f5aeea11ded7320a84dcdd62a3d95b5186834224a9e55b92ccae35d21a8b63d4 \ + --hash=sha256:f604efd28f239cc21b3adb53eb061e2a205dc164be408e553b41ba2ffe0ca15c \ + --hash=sha256:f67e8f1a324a900e75b5e28ffb152bcac9fbed1cc7b43f99cd90f395c4375344 \ + --hash=sha256:fd7a5004eb1980d3cefe26b2685bcb0b17989901a70a1040d1ac86f1d898c551 \ + --hash=sha256:ffef5a74088f1e09947aecf91011136665152e0b4b359c42be3373897fb39b01 + # via langsmith diff --git a/scripts/__init__.py b/scripts/__init__.py new file mode 100644 index 0000000..3bee7bf --- /dev/null +++ b/scripts/__init__.py @@ -0,0 +1 @@ +"""Repository automation helpers covered by the Python test suite.""" diff --git a/scripts/bootstrap_database_roles.sql b/scripts/bootstrap_database_roles.sql new file mode 100644 index 0000000..5e88b27 --- /dev/null +++ b/scripts/bootstrap_database_roles.sql @@ -0,0 +1,42 @@ +\set ON_ERROR_STOP on + +\if :{?DBNAME} +\else +\echo 'Connect to the intended ChangeOps RDS database before running this script.' +\quit +\endif + +SELECT format('CREATE ROLE %I LOGIN', 'changeops_schema_owner') +WHERE NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'changeops_schema_owner') +\gexec + +SELECT format('CREATE ROLE %I LOGIN', 'changeops_runtime') +WHERE NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'changeops_runtime') +\gexec + +\echo 'Enter the migration secret password. Input is masked by psql.' +\password changeops_schema_owner +\echo 'Enter the distinct runtime secret password. Input is masked by psql.' +\password changeops_runtime + +REVOKE CREATE ON DATABASE changeops FROM PUBLIC; +GRANT CONNECT ON DATABASE changeops TO changeops_schema_owner, changeops_runtime; + +REVOKE CREATE ON SCHEMA public FROM PUBLIC; +ALTER SCHEMA public OWNER TO changeops_schema_owner; +GRANT USAGE, CREATE ON SCHEMA public TO changeops_schema_owner; +GRANT USAGE ON SCHEMA public TO changeops_runtime; + +ALTER DEFAULT PRIVILEGES FOR ROLE changeops_schema_owner IN SCHEMA public + GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO changeops_runtime; +ALTER DEFAULT PRIVILEGES FOR ROLE changeops_schema_owner IN SCHEMA public + GRANT USAGE, SELECT, UPDATE ON SEQUENCES TO changeops_runtime; +ALTER DEFAULT PRIVILEGES FOR ROLE changeops_schema_owner IN SCHEMA public + GRANT EXECUTE ON FUNCTIONS TO changeops_runtime; + +GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO changeops_runtime; +GRANT USAGE, SELECT, UPDATE ON ALL SEQUENCES IN SCHEMA public TO changeops_runtime; +GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA public TO changeops_runtime; + +REVOKE CREATE ON DATABASE changeops FROM changeops_runtime; +REVOKE CREATE ON SCHEMA public FROM changeops_runtime; diff --git a/scripts/render_ecs_task_definition.py b/scripts/render_ecs_task_definition.py new file mode 100644 index 0000000..8850974 --- /dev/null +++ b/scripts/render_ecs_task_definition.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 +"""Render a reviewed ECS task definition with immutable release images.""" + +import argparse +import json +from pathlib import Path +from typing import Any + +READ_ONLY_FIELDS = { + "compatibilities", + "deregisteredAt", + "registeredAt", + "registeredBy", + "requiresAttributes", + "revision", + "status", + "taskDefinitionArn", +} + + +def render_task_definition( + describe_payload: dict[str, Any], + image_replacements: dict[str, str], +) -> dict[str, Any]: + task_definition = describe_payload.get("taskDefinition") + if not isinstance(task_definition, dict): + raise ValueError("ECS describe payload does not contain a taskDefinition object.") + rendered = {key: value for key, value in task_definition.items() if key not in READ_ONLY_FIELDS} + containers = rendered.get("containerDefinitions") + if not isinstance(containers, list): + raise ValueError("Task definition does not contain containerDefinitions.") + found: set[str] = set() + for container in containers: + name = container.get("name") + if name in image_replacements: + container["image"] = image_replacements[name] + found.add(name) + missing = set(image_replacements) - found + if missing: + raise ValueError(f"Task definition is missing containers: {', '.join(sorted(missing))}.") + return rendered + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--input", required=True, type=Path) + parser.add_argument("--output", required=True, type=Path) + parser.add_argument( + "--container", + action="append", + default=[], + metavar="NAME=IMAGE", + help="Replace one named container image. Repeat for multiple containers.", + ) + args = parser.parse_args() + replacements: dict[str, str] = {} + for item in args.container: + name, separator, image = item.partition("=") + if not separator or not name or not image: + parser.error("--container values must use NAME=IMAGE.") + replacements[name] = image + if not replacements: + parser.error("At least one --container replacement is required.") + + payload = json.loads(args.input.read_text()) + rendered = render_task_definition(payload, replacements) + args.output.write_text(json.dumps(rendered, separators=(",", ":"), sort_keys=True)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/changeops/config.py b/src/changeops/config.py index e6cad08..cad3258 100644 --- a/src/changeops/config.py +++ b/src/changeops/config.py @@ -1,12 +1,19 @@ from functools import lru_cache -from pydantic import SecretStr +from pydantic import SecretStr, model_validator from pydantic_settings import BaseSettings, SettingsConfigDict +from sqlalchemy import URL class Settings(BaseSettings): app_name: str = "ChangeOps" - database_url: str = "postgresql+psycopg://changeops:changeops@localhost:5432/changeops" + database_url: str | None = "postgresql+psycopg://changeops:changeops@localhost:5432/changeops" + db_host: str | None = None + db_port: int = 5432 + db_name: str | None = None + db_username: str | None = None + db_password: SecretStr | None = None + db_sslmode: str = "prefer" extraction_model_provider: str = "openai" extraction_model: str = "gpt-5.6-luna" extraction_model_timeout_seconds: float = 120.0 @@ -30,6 +37,28 @@ class Settings(BaseSettings): model_config = SettingsConfigDict(env_file=".env", extra="ignore") + @model_validator(mode="after") + def assemble_database_url(self) -> "Settings": + component_values = (self.db_host, self.db_name, self.db_username, self.db_password) + if not any(value is not None for value in component_values): + if not self.database_url: + raise ValueError("DATABASE_URL or the separate DB_* settings are required.") + return self + if not all(value is not None for value in component_values): + raise ValueError("DB_HOST, DB_NAME, DB_USERNAME, and DB_PASSWORD must be set together.") + if self.db_sslmode != "require": + raise ValueError("Component-based database configuration requires DB_SSLMODE=require.") + self.database_url = URL.create( + "postgresql+psycopg", + username=self.db_username, + password=self.db_password.get_secret_value(), + host=self.db_host, + port=self.db_port, + database=self.db_name, + query={"sslmode": "require"}, + ).render_as_string(hide_password=False) + return self + @lru_cache def get_settings() -> Settings: diff --git a/src/changeops/deployment_bootstrap.py b/src/changeops/deployment_bootstrap.py new file mode 100644 index 0000000..4bacdd8 --- /dev/null +++ b/src/changeops/deployment_bootstrap.py @@ -0,0 +1,83 @@ +"""Idempotent setup and validation of the fictional demo fixture.""" + +import json +import sys +from dataclasses import asdict, dataclass + +from changeops.db.session import SessionLocal +from changeops.services.audit_timeline_projection_service import get_audit_timeline +from changeops.services.demo_assessment_seed_service import ( + DEMO_PROPOSED_POLICY_ANALYSIS_RUN_ID, +) +from changeops.services.demo_audit_timeline_seed_service import seed_demo_audit_timeline +from changeops.services.seed_service import seed_database + +REQUIRED_FIXTURE_EVENTS = { + "extraction_attempted", + "extraction_accepted", + "impact_assessment_completed", + "comparison_created", + "impact_delta_created", + "human_action_decision_recorded", + "approval_completed", + "execution_command_prepared", + "execution_requested", + "execution_succeeded", + "jira_issue_created", + "execution_replay_prevented", +} + + +@dataclass(frozen=True) +class DeploymentBootstrapSummary: + status: str + policy_analysis_run_id: str + approval_run_id: str + event_count: int + jira_mode: str + + +def bootstrap_demo_fixture() -> DeploymentBootstrapSummary: + with SessionLocal.begin() as session: + seed_database(session) + with SessionLocal() as session: + approval_run_id = seed_demo_audit_timeline(session) + timeline = get_audit_timeline(session, DEMO_PROPOSED_POLICY_ANALYSIS_RUN_ID) + + event_types = {entry.event_type.value for entry in timeline.entries} + missing = REQUIRED_FIXTURE_EVENTS - event_types + if missing: + raise RuntimeError(f"demo_fixture_incomplete:{','.join(sorted(missing))}") + if not any( + entry.event_type.value == "human_action_decision_recorded" + and entry.actor_category.value == "human" + for entry in timeline.entries + ): + raise RuntimeError("demo_fixture_human_decision_missing") + if not all(entry.artifact.artifact_id for entry in timeline.entries): + raise RuntimeError("demo_fixture_audit_provenance_incomplete") + + return DeploymentBootstrapSummary( + status="initialized", + policy_analysis_run_id=str(DEMO_PROPOSED_POLICY_ANALYSIS_RUN_ID), + approval_run_id=str(approval_run_id), + event_count=len(timeline.entries), + jira_mode="deterministic_simulation", + ) + + +def main() -> int: + try: + summary = bootstrap_demo_fixture() + except Exception as error: + code = str(error) + if not code.startswith("demo_fixture_"): + code = "demo_fixture_bootstrap_failed" + print(json.dumps({"status": "failed", "failure_code": code}, sort_keys=True)) + return 1 + print(json.dumps(asdict(summary), sort_keys=True)) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/src/changeops/evaluation/deployed_evidence.py b/src/changeops/evaluation/deployed_evidence.py new file mode 100644 index 0000000..f4a9a9c --- /dev/null +++ b/src/changeops/evaluation/deployed_evidence.py @@ -0,0 +1,167 @@ +"""Read-only evidence check for a human-performed deployed journey.""" + +import argparse +import json +import sys +import uuid +from dataclasses import asdict, dataclass +from datetime import UTC, datetime + +from changeops.db.session import SessionLocal +from changeops.schemas.audit_timeline import AuditActorCategory, AuditEventType +from changeops.services.audit_timeline_projection_service import get_audit_timeline +from changeops.services.demo_assessment_seed_service import ( + DEMO_PROPOSED_POLICY_ANALYSIS_RUN_ID, +) + +REQUIRED_LIVE_EVENTS = { + AuditEventType.EXTRACTION_ATTEMPTED, + AuditEventType.EXTRACTION_ACCEPTED, + AuditEventType.IMPACT_ASSESSMENT_COMPLETED, + AuditEventType.HUMAN_ACTION_DECISION_RECORDED, + AuditEventType.APPROVAL_COMPLETED, + AuditEventType.EXECUTION_COMMAND_PREPARED, + AuditEventType.EXECUTION_REQUESTED, + AuditEventType.EXECUTION_SUCCEEDED, + AuditEventType.SIMULATED_SIDE_EFFECT_CREATED, +} + + +class DeployedEvidenceError(Exception): + """A stable, non-sensitive deployed-evidence failure.""" + + +@dataclass(frozen=True) +class DeployedEvidenceSummary: + status: str + policy_analysis_run_id: str + approval_run_id: str + event_count: int + human_decision_count: int + execution_request_count: int + release_bound: bool + jira_fixture_mode: str + + +def verify_deployed_journey( + run_id: uuid.UUID, + *, + release_not_before: datetime, +) -> DeployedEvidenceSummary: + if run_id == DEMO_PROPOSED_POLICY_ANALYSIS_RUN_ID: + raise DeployedEvidenceError("live_run_must_not_be_seeded_fixture") + if release_not_before.tzinfo is None: + raise DeployedEvidenceError("release_timestamp_must_include_timezone") + release_boundary = release_not_before.astimezone(UTC) + + with SessionLocal() as session: + live_timeline = get_audit_timeline(session, run_id) + fixture_timeline = get_audit_timeline(session, DEMO_PROPOSED_POLICY_ANALYSIS_RUN_ID) + + analysis_requested = next( + entry + for entry in live_timeline.entries + if entry.event_type == AuditEventType.ANALYSIS_REQUESTED + ) + if analysis_requested.occurred_at.astimezone(UTC) < release_boundary: + raise DeployedEvidenceError("live_run_predates_deployed_task") + + event_types = {entry.event_type for entry in live_timeline.entries} + missing = REQUIRED_LIVE_EVENTS - event_types + if missing: + names = ",".join(sorted(event.value for event in missing)) + raise DeployedEvidenceError(f"live_journey_incomplete:{names}") + + decisions = [ + entry + for entry in live_timeline.entries + if entry.event_type == AuditEventType.HUMAN_ACTION_DECISION_RECORDED + ] + decision_identities = {entry.actor_identity for entry in decisions} + if ( + not decisions + or any( + entry.actor_category != AuditActorCategory.HUMAN + or not entry.actor_identity + or entry.metadata.get("reviewer_role") != "reviewer" + for entry in decisions + ) + or len(decision_identities) != 1 + ): + raise DeployedEvidenceError("reviewer_evidence_invalid") + + prepared = [ + entry + for entry in live_timeline.entries + if entry.event_type == AuditEventType.EXECUTION_COMMAND_PREPARED + ] + requested = [ + entry + for entry in live_timeline.entries + if entry.event_type == AuditEventType.EXECUTION_REQUESTED + ] + prepared_identities = {entry.actor_identity for entry in prepared} + requested_identities = {entry.actor_identity for entry in requested} + if not prepared or None in prepared_identities or len(prepared_identities) != 1: + raise DeployedEvidenceError("command_preparer_evidence_invalid") + if ( + not requested + or any( + entry.actor_category != AuditActorCategory.HUMAN + or not entry.actor_identity + or entry.metadata.get("attempted_role") != "admin" + for entry in requested + ) + or requested_identities != prepared_identities + ): + raise DeployedEvidenceError("execution_actor_evidence_invalid") + if not all(entry.artifact.artifact_id for entry in live_timeline.entries): + raise DeployedEvidenceError("live_audit_provenance_incomplete") + + fixture_events = {entry.event_type for entry in fixture_timeline.entries} + if AuditEventType.JIRA_ISSUE_CREATED not in fixture_events: + raise DeployedEvidenceError("deterministic_jira_fixture_missing") + + approval = next( + entry + for entry in live_timeline.entries + if entry.event_type == AuditEventType.APPROVAL_COMPLETED + ) + return DeployedEvidenceSummary( + status="verified", + policy_analysis_run_id=str(run_id), + approval_run_id=approval.artifact.artifact_id, + event_count=len(live_timeline.entries), + human_decision_count=len(decisions), + execution_request_count=len(requested), + release_bound=True, + jira_fixture_mode="deterministic_simulation", + ) + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--run-id", required=True, type=uuid.UUID) + parser.add_argument( + "--release-not-before", + required=True, + type=lambda value: datetime.fromisoformat(value.replace("Z", "+00:00")), + ) + args = parser.parse_args() + try: + summary = verify_deployed_journey( + args.run_id, + release_not_before=args.release_not_before, + ) + except Exception as error: + code = str(error) + if not isinstance(error, DeployedEvidenceError): + code = "deployed_evidence_check_failed" + print(json.dumps({"status": "failed", "failure_code": code}, sort_keys=True)) + return 1 + print(json.dumps(asdict(summary), sort_keys=True)) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/src/changeops/main.py b/src/changeops/main.py index 455488b..d4c83c2 100644 --- a/src/changeops/main.py +++ b/src/changeops/main.py @@ -1,5 +1,3 @@ -import logging - from fastapi import FastAPI from changeops.api.action_approval import router as action_approval_router @@ -14,22 +12,14 @@ from changeops.api.policy_comparisons import router as policy_comparisons_router from changeops.api.policy_extractions import router as policy_extractions_router from changeops.api.policy_interpretation import router as policy_interpretation_router +from changeops.observability import configure_structured_logging, install_request_logging - -def _configure_application_logging() -> None: - application_logger = logging.getLogger("changeops") - application_logger.setLevel(logging.INFO) - uvicorn_handlers = logging.getLogger("uvicorn").handlers - if uvicorn_handlers: - application_logger.handlers = uvicorn_handlers - application_logger.propagate = False - - -_configure_application_logging() +configure_structured_logging() def create_app() -> FastAPI: app = FastAPI(title="ChangeOps", version="0.1.0") + install_request_logging(app) app.include_router(health_router) app.include_router(assessments_router) app.include_router(catalog_router) diff --git a/src/changeops/observability.py b/src/changeops/observability.py new file mode 100644 index 0000000..6310dd7 --- /dev/null +++ b/src/changeops/observability.py @@ -0,0 +1,93 @@ +import json +import logging +import re +import sys +import uuid +from contextvars import ContextVar +from time import monotonic + +from fastapi import FastAPI, Request + +REQUEST_ID_PATTERN = re.compile(r"^[A-Za-z0-9._:-]{1,100}$") +request_id_context: ContextVar[str | None] = ContextVar("request_id", default=None) +logger = logging.getLogger("changeops.http") + + +class JsonLogFormatter(logging.Formatter): + _extra_fields = ( + "elapsed_ms", + "event", + "method", + "path", + "request_id", + "status_code", + ) + + def format(self, record: logging.LogRecord) -> str: + payload: dict[str, object] = { + "level": record.levelname.lower(), + "logger": record.name, + "message": record.getMessage(), + "request_id": getattr(record, "request_id", None) or request_id_context.get(), + } + for field in self._extra_fields: + value = getattr(record, field, None) + if value is not None: + payload[field] = value + return json.dumps(payload, separators=(",", ":"), sort_keys=True) + + +def configure_structured_logging() -> None: + handler = logging.StreamHandler(sys.stdout) + handler.setFormatter(JsonLogFormatter()) + root = logging.getLogger() + root.handlers = [handler] + root.setLevel(logging.INFO) + for logger_name in ("changeops", "uvicorn", "uvicorn.error", "uvicorn.access"): + configured = logging.getLogger(logger_name) + configured.handlers = [] + configured.propagate = True + + +def normalized_request_id(value: str | None) -> str: + candidate = (value or "").strip() + return candidate if REQUEST_ID_PATTERN.fullmatch(candidate) else str(uuid.uuid4()) + + +def install_request_logging(app: FastAPI) -> None: + @app.middleware("http") + async def correlate_and_log(request: Request, call_next): + request_id = normalized_request_id(request.headers.get("x-request-id")) + token = request_id_context.set(request_id) + request.state.request_id = request_id + started_at = monotonic() + try: + response = await call_next(request) + except Exception: + logger.error( + "Request failed.", + extra={ + "event": "request_failed", + "method": request.method, + "path": request.url.path, + "request_id": request_id, + "elapsed_ms": round((monotonic() - started_at) * 1000), + }, + ) + raise + else: + response.headers["X-Request-ID"] = request_id + logger.info( + "Request completed.", + extra={ + "event": "request_completed", + "method": request.method, + "path": request.url.path, + "request_id": request_id, + "status_code": response.status_code, + "elapsed_ms": round((monotonic() - started_at) * 1000), + }, + ) + return response + finally: + request_id_context.reset(token) diff --git a/tests/integration/test_deployed_evidence.py b/tests/integration/test_deployed_evidence.py new file mode 100644 index 0000000..98e021f --- /dev/null +++ b/tests/integration/test_deployed_evidence.py @@ -0,0 +1,82 @@ +import uuid +from datetime import UTC, datetime, timedelta + +import pytest +from sqlalchemy import select + +from changeops.db.models import ActionApprovalRun, ExecutionCommand +from changeops.db.session import SessionLocal +from changeops.deployment_bootstrap import bootstrap_demo_fixture +from changeops.evaluation.deployed_evidence import ( + DeployedEvidenceError, + verify_deployed_journey, +) +from changeops.services.demo_assessment_seed_service import ( + DEMO_PROPOSED_POLICY_ANALYSIS_RUN_ID, +) +from tests.integration.test_execution_commands_api import ADMIN_HEADERS +from tests.integration.test_learning_execution_api import _prepared_command + + +def test_verifies_nonseeded_human_journey_and_fixture_jira(client) -> None: + bootstrap_demo_fixture() + _, command = _prepared_command(client) + executed = client.post( + f"/api/v1/execution-commands/{command['id']}/execute", + headers=ADMIN_HEADERS, + ) + assert executed.status_code == 201 + + with SessionLocal() as session: + command_row = session.get(ExecutionCommand, uuid.UUID(command["id"])) + run_id = session.scalar( + select(ActionApprovalRun.policy_analysis_run_id).where( + ActionApprovalRun.id == command_row.approval_run_id + ) + ) + + summary = verify_deployed_journey( + run_id, + release_not_before=datetime.now(UTC) - timedelta(minutes=5), + ) + + assert summary.status == "verified" + assert summary.policy_analysis_run_id == str(run_id) + assert summary.jira_fixture_mode == "deterministic_simulation" + + +def test_rejects_seeded_fixture_as_live_evidence() -> None: + bootstrap_demo_fixture() + + with pytest.raises( + DeployedEvidenceError, + match="live_run_must_not_be_seeded_fixture", + ): + verify_deployed_journey( + DEMO_PROPOSED_POLICY_ANALYSIS_RUN_ID, + release_not_before=datetime.now(UTC) - timedelta(minutes=5), + ) + + +def test_rejects_journey_that_predates_deployed_task(client) -> None: + bootstrap_demo_fixture() + _, command = _prepared_command(client) + executed = client.post( + f"/api/v1/execution-commands/{command['id']}/execute", + headers=ADMIN_HEADERS, + ) + assert executed.status_code == 201 + + with SessionLocal() as session: + command_row = session.get(ExecutionCommand, uuid.UUID(command["id"])) + run_id = session.scalar( + select(ActionApprovalRun.policy_analysis_run_id).where( + ActionApprovalRun.id == command_row.approval_run_id + ) + ) + + with pytest.raises(DeployedEvidenceError, match="live_run_predates_deployed_task"): + verify_deployed_journey( + run_id, + release_not_before=datetime.now(UTC) + timedelta(minutes=1), + ) diff --git a/tests/integration/test_deployment_bootstrap.py b/tests/integration/test_deployment_bootstrap.py new file mode 100644 index 0000000..c794ab3 --- /dev/null +++ b/tests/integration/test_deployment_bootstrap.py @@ -0,0 +1,21 @@ +from changeops.db.session import SessionLocal +from changeops.deployment_bootstrap import ( + REQUIRED_FIXTURE_EVENTS, + bootstrap_demo_fixture, +) +from changeops.services.audit_timeline_projection_service import get_audit_timeline +from changeops.services.demo_assessment_seed_service import ( + DEMO_PROPOSED_POLICY_ANALYSIS_RUN_ID, +) + + +def test_deployment_bootstrap_is_idempotent_fixture_setup() -> None: + first = bootstrap_demo_fixture() + second = bootstrap_demo_fixture() + + assert first == second + assert first.status == "initialized" + assert first.jira_mode == "deterministic_simulation" + with SessionLocal() as session: + timeline = get_audit_timeline(session, DEMO_PROPOSED_POLICY_ANALYSIS_RUN_ID) + assert {entry.event_type.value for entry in timeline.entries} >= REQUIRED_FIXTURE_EVENTS diff --git a/tests/integration/test_request_correlation.py b/tests/integration/test_request_correlation.py new file mode 100644 index 0000000..0575245 --- /dev/null +++ b/tests/integration/test_request_correlation.py @@ -0,0 +1,13 @@ +def test_request_id_is_preserved_on_api_response(client) -> None: + response = client.get("/healthz", headers={"X-Request-ID": "deployment-smoke-123"}) + + assert response.status_code == 200 + assert response.headers["X-Request-ID"] == "deployment-smoke-123" + + +def test_unsafe_request_id_is_replaced(client) -> None: + response = client.get("/healthz", headers={"X-Request-ID": "unsafe value"}) + + assert response.status_code == 200 + assert response.headers["X-Request-ID"] != "unsafe value" + assert len(response.headers["X-Request-ID"]) == 36 diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py new file mode 100644 index 0000000..122a937 --- /dev/null +++ b/tests/unit/test_config.py @@ -0,0 +1,56 @@ +from sqlalchemy import make_url + +from changeops.config import Settings + + +def test_component_database_settings_build_tls_required_encoded_url() -> None: + settings = Settings( + database_url=None, + db_host="private-db.example.internal", + db_port=5432, + db_name="changeops", + db_username="runtime user", + db_password="p@ss:/word", + db_sslmode="require", + _env_file=None, + ) + + url = make_url(settings.database_url) + assert url.host == "private-db.example.internal" + assert url.database == "changeops" + assert url.username == "runtime user" + assert url.password == "p@ss:/word" + assert url.query == {"sslmode": "require"} + + +def test_component_database_settings_fail_closed_when_incomplete() -> None: + try: + Settings( + database_url=None, + db_host="private-db.example.internal", + db_name="changeops", + db_username="runtime", + db_sslmode="require", + _env_file=None, + ) + except ValueError as error: + assert "must be set together" in str(error) + else: + raise AssertionError("Incomplete component configuration was accepted.") + + +def test_component_database_settings_reject_non_tls_connection() -> None: + try: + Settings( + database_url=None, + db_host="private-db.example.internal", + db_name="changeops", + db_username="runtime", + db_password="secret", + db_sslmode="disable", + _env_file=None, + ) + except ValueError as error: + assert "DB_SSLMODE=require" in str(error) + else: + raise AssertionError("A non-TLS deployed database URL was accepted.") diff --git a/tests/unit/test_observability.py b/tests/unit/test_observability.py new file mode 100644 index 0000000..0bd8a47 --- /dev/null +++ b/tests/unit/test_observability.py @@ -0,0 +1,39 @@ +import json +import logging + +from changeops.observability import JsonLogFormatter, normalized_request_id + + +def test_request_id_accepts_safe_correlation_value_and_replaces_control_characters() -> None: + assert normalized_request_id("request-123") == "request-123" + generated = normalized_request_id("unsafe\nvalue") + assert generated != "unsafe\nvalue" + assert len(generated) == 36 + + +def test_json_formatter_emits_only_whitelisted_structured_fields() -> None: + record = logging.LogRecord( + name="changeops.http", + level=logging.INFO, + pathname=__file__, + lineno=1, + msg="Request completed.", + args=(), + exc_info=None, + ) + record.request_id = "request-123" + record.event = "request_completed" + record.path = "/healthz" + record.authorization = "Bearer must-not-appear" + + payload = json.loads(JsonLogFormatter().format(record)) + + assert payload == { + "event": "request_completed", + "level": "info", + "logger": "changeops.http", + "message": "Request completed.", + "path": "/healthz", + "request_id": "request-123", + } + assert "must-not-appear" not in json.dumps(payload) diff --git a/tests/unit/test_render_ecs_task_definition.py b/tests/unit/test_render_ecs_task_definition.py new file mode 100644 index 0000000..2a3eb38 --- /dev/null +++ b/tests/unit/test_render_ecs_task_definition.py @@ -0,0 +1,44 @@ +import pytest + +from scripts.render_ecs_task_definition import render_task_definition + + +def test_render_replaces_only_named_images_and_removes_read_only_fields() -> None: + rendered = render_task_definition( + { + "taskDefinition": { + "family": "changeops-demo-application", + "taskDefinitionArn": "arn:old", + "revision": 4, + "status": "ACTIVE", + "registeredAt": "timestamp", + "containerDefinitions": [ + {"name": "api", "image": "api:old", "secrets": [{"name": "DB_PASSWORD"}]}, + {"name": "web", "image": "web:old"}, + ], + "networkMode": "awsvpc", + } + }, + {"api": "api:commit-sha", "web": "web:commit-sha"}, + ) + + assert rendered["containerDefinitions"] == [ + { + "name": "api", + "image": "api:commit-sha", + "secrets": [{"name": "DB_PASSWORD"}], + }, + {"name": "web", "image": "web:commit-sha"}, + ] + assert rendered["family"] == "changeops-demo-application" + assert "taskDefinitionArn" not in rendered + assert "revision" not in rendered + assert "registeredAt" not in rendered + + +def test_render_fails_when_reviewed_container_name_is_missing() -> None: + with pytest.raises(ValueError, match="missing containers: web"): + render_task_definition( + {"taskDefinition": {"containerDefinitions": [{"name": "api", "image": "api:old"}]}}, + {"web": "web:commit-sha"}, + ) diff --git a/web/.dockerignore b/web/.dockerignore index 4f6ed21..28737f1 100644 --- a/web/.dockerignore +++ b/web/.dockerignore @@ -1,3 +1,6 @@ +.env +.env.* +.git .next node_modules npm-debug.log diff --git a/web/Dockerfile b/web/Dockerfile index fc0baab..ed49447 100644 --- a/web/Dockerfile +++ b/web/Dockerfile @@ -1,21 +1,24 @@ -FROM node:24-alpine AS dependencies +FROM node:24-alpine@sha256:d32cdf619f63fe0471182d08996dd516c6275bb5fd31ae06e55a570bd9e1ad43 AS dependencies WORKDIR /app COPY package.json package-lock.json ./ RUN npm ci -FROM node:24-alpine AS builder +FROM node:24-alpine@sha256:d32cdf619f63fe0471182d08996dd516c6275bb5fd31ae06e55a570bd9e1ad43 AS builder WORKDIR /app COPY --from=dependencies /app/node_modules ./node_modules COPY . . RUN npm run build -FROM node:24-alpine AS runner +FROM node:24-alpine@sha256:d32cdf619f63fe0471182d08996dd516c6275bb5fd31ae06e55a570bd9e1ad43 AS runner ENV NODE_ENV=production \ HOSTNAME=0.0.0.0 \ PORT=3000 WORKDIR /app -COPY --from=builder /app/public ./public -COPY --from=builder /app/.next/standalone ./ -COPY --from=builder /app/.next/static ./.next/static +COPY --from=builder --chown=node:node /app/public ./public +COPY --from=builder --chown=node:node /app/.next/standalone ./ +COPY --from=builder --chown=node:node /app/.next/static ./.next/static +USER node EXPOSE 3000 +HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \ + CMD wget -q -O /dev/null http://127.0.0.1:3000/healthz || exit 1 CMD ["node", "server.js"] diff --git a/web/app/api/[...path]/route.test.ts b/web/app/api/[...path]/route.test.ts new file mode 100644 index 0000000..75160be --- /dev/null +++ b/web/app/api/[...path]/route.test.ts @@ -0,0 +1,65 @@ +import { NextRequest } from "next/server"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { IdentityVerificationError, verifiedIdentity } from "@/lib/identity"; +import { GET } from "./route"; + +vi.mock("@/lib/identity", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + identityMode: vi.fn(() => "alb"), + verifiedIdentity: vi.fn(), + }; +}); + +afterEach(() => { + vi.restoreAllMocks(); + vi.unstubAllGlobals(); +}); + +describe("deployed API proxy identity boundary", () => { + it("replaces browser-controlled actor headers with verified claim values", async () => { + vi.mocked(verifiedIdentity).mockResolvedValue({ + actor: "cognito-subject", + role: "reviewer", + }); + const upstream = vi.fn().mockResolvedValue(Response.json({ status: "ok" })); + vi.stubGlobal("fetch", upstream); + const request = new NextRequest("https://changeops.example/api/v1/example", { + headers: { + "x-changeops-actor": "browser-attacker", + "x-changeops-role": "admin", + "x-request-id": "request-123", + }, + }); + + const response = await GET(request, { params: Promise.resolve({ path: ["v1", "example"] }) }); + + expect(response.status).toBe(200); + const forwarded = new Headers(upstream.mock.calls[0][1].headers); + expect(forwarded.get("x-changeops-actor")).toBe("cognito-subject"); + expect(forwarded.get("x-changeops-role")).toBe("reviewer"); + expect(forwarded.get("x-request-id")).toBe("request-123"); + expect(response.headers.get("x-request-id")).toBe("request-123"); + }); + + it("does not call FastAPI when claim verification fails", async () => { + vi.mocked(verifiedIdentity).mockRejectedValue(new IdentityVerificationError()); + const upstream = vi.fn(); + vi.stubGlobal("fetch", upstream); + const request = new NextRequest("https://changeops.example/api/v1/example", { + headers: { + "x-changeops-actor": "browser-attacker", + "x-changeops-role": "admin", + }, + }); + + const response = await GET(request, { params: Promise.resolve({ path: ["v1", "example"] }) }); + + expect(response.status).toBe(401); + expect(upstream).not.toHaveBeenCalled(); + await expect(response.json()).resolves.toMatchObject({ + detail: { code: "identity_verification_failed" }, + }); + }); +}); diff --git a/web/app/api/[...path]/route.ts b/web/app/api/[...path]/route.ts index 5043de3..1cd0e2a 100644 --- a/web/app/api/[...path]/route.ts +++ b/web/app/api/[...path]/route.ts @@ -1,15 +1,58 @@ import { NextRequest, NextResponse } from "next/server"; import { apiBaseUrl } from "@/lib/api"; +import { + IdentityVerificationError, + identityMode, + verifiedIdentity, +} from "@/lib/identity"; +import { requestId } from "@/lib/request-id"; async function proxy(request: NextRequest, context: { params: Promise<{ path: string[] }> }) { + const correlationId = requestId(request.headers); const { path } = await context.params; const upstream = new URL(`/api/${path.join("/")}`, apiBaseUrl()); upstream.search = request.nextUrl.search; const headers = new Headers(); - for (const name of ["content-type", "x-changeops-actor", "x-changeops-role"]) { - const value = request.headers.get(name); - if (value) headers.set(name, value); + const contentType = request.headers.get("content-type"); + if (contentType) headers.set("content-type", contentType); + headers.set("x-request-id", correlationId); + + try { + if (identityMode() === "alb") { + const identity = await verifiedIdentity(request.headers); + headers.set("x-changeops-actor", identity.actor); + headers.set("x-changeops-role", identity.role); + } else { + for (const name of ["x-changeops-actor", "x-changeops-role"]) { + const value = request.headers.get(name); + if (value) headers.set(name, value); + } + } + } catch (error) { + if (error instanceof IdentityVerificationError) { + console.warn( + JSON.stringify({ + event: "proxy_identity_rejected", + level: "warning", + method: request.method, + path: request.nextUrl.pathname, + request_id: correlationId, + }), + ); + return NextResponse.json( + { + detail: { + code: "identity_verification_failed", + message: "A valid authenticated ChangeOps identity is required.", + }, + }, + { status: 401, headers: { "x-request-id": correlationId } }, + ); + } + throw error; } + + const startedAt = performance.now(); try { const response = await fetch(upstream, { method: request.method, @@ -17,19 +60,43 @@ async function proxy(request: NextRequest, context: { params: Promise<{ path: st body: request.method === "GET" || request.method === "HEAD" ? undefined : await request.text(), cache: "no-store", }); + console.info( + JSON.stringify({ + event: "proxy_request_completed", + level: "info", + method: request.method, + path: request.nextUrl.pathname, + request_id: correlationId, + status_code: response.status, + elapsed_ms: Math.round(performance.now() - startedAt), + }), + ); return new NextResponse(response.body, { status: response.status, - headers: { "content-type": response.headers.get("content-type") ?? "application/json" }, + headers: { + "content-type": response.headers.get("content-type") ?? "application/json", + "x-request-id": correlationId, + }, }); } catch { + console.error( + JSON.stringify({ + event: "proxy_request_failed", + level: "error", + method: request.method, + path: request.nextUrl.pathname, + request_id: correlationId, + elapsed_ms: Math.round(performance.now() - startedAt), + }), + ); return NextResponse.json( { detail: { code: "api_unavailable", - message: "The ChangeOps API is unavailable. Check that the local stack is running.", + message: "The ChangeOps API is unavailable.", }, }, - { status: 503 }, + { status: 503, headers: { "x-request-id": correlationId } }, ); } } diff --git a/web/app/healthz/route.ts b/web/app/healthz/route.ts new file mode 100644 index 0000000..56561e1 --- /dev/null +++ b/web/app/healthz/route.ts @@ -0,0 +1,12 @@ +export const dynamic = "force-dynamic"; + +export function GET() { + return Response.json( + { status: "ok" }, + { + headers: { + "cache-control": "no-store", + }, + }, + ); +} diff --git a/web/app/readyz/route.test.ts b/web/app/readyz/route.test.ts new file mode 100644 index 0000000..0a354c0 --- /dev/null +++ b/web/app/readyz/route.test.ts @@ -0,0 +1,32 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { GET } from "./route"; + +afterEach(() => { + vi.unstubAllGlobals(); +}); + +describe("deployed readiness route", () => { + it("proves the Next.js server can reach the task-local API", async () => { + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue(Response.json({ status: "ok" })), + ); + + const response = await GET(); + + expect(response.status).toBe(200); + await expect(response.json()).resolves.toEqual({ status: "ok", api: "ok" }); + }); + + it("fails without exposing upstream details", async () => { + vi.stubGlobal("fetch", vi.fn().mockRejectedValue(new Error("secret upstream detail"))); + + const response = await GET(); + + expect(response.status).toBe(503); + await expect(response.json()).resolves.toEqual({ + status: "unavailable", + api: "unavailable", + }); + }); +}); diff --git a/web/app/readyz/route.ts b/web/app/readyz/route.ts new file mode 100644 index 0000000..8e2e91c --- /dev/null +++ b/web/app/readyz/route.ts @@ -0,0 +1,29 @@ +import { apiBaseUrl } from "@/lib/api"; +import { requestId } from "@/lib/request-id"; + +export const dynamic = "force-dynamic"; + +export async function GET(request: Request = new Request("http://localhost/readyz")) { + const correlationId = requestId(request.headers); + try { + const response = await fetch(new URL("/healthz", apiBaseUrl()), { + cache: "no-store", + headers: { "x-request-id": correlationId }, + signal: AbortSignal.timeout(3_000), + }); + const payload = await response.json(); + if (!response.ok || payload?.status !== "ok") throw new Error("api_not_ready"); + return Response.json( + { status: "ok", api: "ok" }, + { headers: { "cache-control": "no-store", "x-request-id": correlationId } }, + ); + } catch { + return Response.json( + { status: "unavailable", api: "unavailable" }, + { + status: 503, + headers: { "cache-control": "no-store", "x-request-id": correlationId }, + }, + ); + } +} diff --git a/web/lib/identity.test.ts b/web/lib/identity.test.ts new file mode 100644 index 0000000..38c3fdc --- /dev/null +++ b/web/lib/identity.test.ts @@ -0,0 +1,146 @@ +// @vitest-environment node + +import { + SignJWT, + exportJWK, + exportSPKI, + generateKeyPair, +} from "jose"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { + IdentityVerificationError, + clearIdentityCachesForTests, + verifiedIdentity, +} from "./identity"; + +const REGION = "us-east-1"; +const USER_POOL = "us-east-1_fixture"; +const ISSUER = `https://cognito-idp.${REGION}.amazonaws.com/${USER_POOL}`; +const CLIENT = "fixture-client"; +const SIGNER = "arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/test/1"; +const SUBJECT = "fixture-subject"; + +async function authenticationHeaders(options?: { + groups?: string[]; + signer?: string; + subjectHeader?: string; +}) { + const albKeys = await generateKeyPair("ES256"); + const cognitoKeys = await generateKeyPair("RS256"); + const albKeyId = "alb-key"; + const cognitoKeyId = "cognito-key"; + const now = Math.floor(Date.now() / 1000); + const claims = await new SignJWT({}) + .setProtectedHeader({ + alg: "ES256", + kid: albKeyId, + signer: options?.signer ?? SIGNER, + client: CLIENT, + iss: ISSUER, + exp: now + 300, + }) + .setSubject(SUBJECT) + .setIssuedAt(now) + .sign(albKeys.privateKey); + const access = await new SignJWT({ + client_id: CLIENT, + token_use: "access", + "cognito:groups": options?.groups ?? ["admin"], + }) + .setProtectedHeader({ alg: "RS256", kid: cognitoKeyId }) + .setIssuer(ISSUER) + .setSubject(SUBJECT) + .setIssuedAt(now) + .setExpirationTime(now + 300) + .sign(cognitoKeys.privateKey); + const albPublicKey = await exportSPKI(albKeys.publicKey); + const cognitoPublicKey = { + ...(await exportJWK(cognitoKeys.publicKey)), + kid: cognitoKeyId, + alg: "RS256", + use: "sig", + }; + vi.stubGlobal( + "fetch", + vi.fn(async (input: string | URL | Request) => { + const url = String(input); + if (url.includes("public-keys.auth.elb")) return new Response(albPublicKey); + if (url.endsWith("/.well-known/jwks.json")) { + return Response.json({ keys: [cognitoPublicKey] }); + } + throw new Error(`Unexpected fixture URL: ${url}`); + }), + ); + return new Headers({ + "x-amzn-oidc-data": claims, + "x-amzn-oidc-identity": options?.subjectHeader ?? SUBJECT, + "x-amzn-oidc-accesstoken": access, + "x-changeops-actor": "browser-controlled", + "x-changeops-role": "admin", + }); +} + +afterEach(() => { + vi.unstubAllGlobals(); + vi.unstubAllEnvs(); + clearIdentityCachesForTests(); +}); + +function configureAlbMode() { + vi.stubEnv("CHANGEOPS_IDENTITY_MODE", "alb"); + vi.stubEnv("AWS_REGION", REGION); + vi.stubEnv("ALB_EXPECTED_SIGNER_ARN", SIGNER); + vi.stubEnv("COGNITO_USER_POOL_ID", USER_POOL); + vi.stubEnv("COGNITO_CLIENT_ID", CLIENT); +} + +describe("verifiedIdentity", () => { + it("maps signed ALB and Cognito claims to the immutable subject and one role", async () => { + configureAlbMode(); + + await expect(verifiedIdentity(await authenticationHeaders())).resolves.toEqual({ + actor: SUBJECT, + role: "admin", + }); + }); + + it("fails closed when the ALB signer does not match the configured load balancer", async () => { + configureAlbMode(); + + await expect( + verifiedIdentity(await authenticationHeaders({ signer: `${SIGNER}-forged` })), + ).rejects.toBeInstanceOf(IdentityVerificationError); + }); + + it("fails closed for missing, multiple, or unknown privileged groups", async () => { + configureAlbMode(); + + for (const groups of [[], ["admin", "reviewer"], ["operator"]]) { + clearIdentityCachesForTests(); + await expect( + verifiedIdentity(await authenticationHeaders({ groups })), + ).rejects.toBeInstanceOf(IdentityVerificationError); + } + }); + + it("requires the signed subject to match the ALB subject header", async () => { + configureAlbMode(); + + await expect( + verifiedIdentity(await authenticationHeaders({ subjectHeader: "different-subject" })), + ).rejects.toBeInstanceOf(IdentityVerificationError); + }); + + it("retains explicit local demonstration headers outside deployed mode", async () => { + vi.stubEnv("CHANGEOPS_IDENTITY_MODE", "demo"); + + await expect( + verifiedIdentity( + new Headers({ + "x-changeops-actor": "local-reviewer", + "x-changeops-role": "reviewer", + }), + ), + ).resolves.toEqual({ actor: "local-reviewer", role: "reviewer" }); + }); +}); diff --git a/web/lib/identity.ts b/web/lib/identity.ts new file mode 100644 index 0000000..eb1a1b3 --- /dev/null +++ b/web/lib/identity.ts @@ -0,0 +1,193 @@ +import { + createRemoteJWKSet, + decodeProtectedHeader, + importSPKI, + jwtVerify, + type JWTPayload, +} from "jose"; + +const ALLOWED_ROLES = new Set(["reviewer", "admin"]); +const SAFE_KEY_ID = /^[A-Za-z0-9_-]{1,200}$/; +const SAFE_REGION = /^[a-z]{2}(?:-gov)?-[a-z]+-\d$/; +const CERTIFICATE_LIMIT_BYTES = 16_384; + +export type ChangeOpsRole = "reviewer" | "admin"; + +export type VerifiedIdentity = { + actor: string; + role: ChangeOpsRole; +}; + +type IdentityMode = "demo" | "alb"; + +type IdentityConfiguration = { + mode: IdentityMode; + awsRegion?: string; + expectedSignerArn?: string; + cognitoClientId?: string; + cognitoIssuer?: string; +}; + +export class IdentityVerificationError extends Error { + constructor() { + super("identity_verification_failed"); + this.name = "IdentityVerificationError"; + } +} + +const albCertificateCache = new Map(); +const cognitoJwksCache = new Map>(); + +function configuration(environment: NodeJS.ProcessEnv = process.env): IdentityConfiguration { + const mode = environment.CHANGEOPS_IDENTITY_MODE ?? "demo"; + if (mode === "demo") return { mode }; + if (mode !== "alb") throw new IdentityVerificationError(); + + const awsRegion = environment.AWS_REGION; + const expectedSignerArn = environment.ALB_EXPECTED_SIGNER_ARN; + const cognitoClientId = environment.COGNITO_CLIENT_ID; + const userPoolId = environment.COGNITO_USER_POOL_ID; + if ( + !awsRegion || + !SAFE_REGION.test(awsRegion) || + !expectedSignerArn || + !cognitoClientId || + !userPoolId + ) { + throw new IdentityVerificationError(); + } + return { + mode, + awsRegion, + expectedSignerArn, + cognitoClientId, + cognitoIssuer: `https://cognito-idp.${awsRegion}.amazonaws.com/${userPoolId}`, + }; +} + +function requiredHeader(headers: Headers, name: string): string { + const value = headers.get(name)?.trim(); + if (!value) throw new IdentityVerificationError(); + return value; +} + +async function albPublicKey(token: string, awsRegion: string): Promise { + const protectedHeader = decodeProtectedHeader(token); + const keyId = protectedHeader.kid; + if (protectedHeader.alg !== "ES256" || typeof keyId !== "string" || !SAFE_KEY_ID.test(keyId)) { + throw new IdentityVerificationError(); + } + const cached = albCertificateCache.get(keyId); + if (cached) return cached; + + const response = await fetch( + `https://public-keys.auth.elb.${awsRegion}.amazonaws.com/${keyId}`, + { + cache: "force-cache", + signal: AbortSignal.timeout(5_000), + }, + ); + if (!response.ok) throw new IdentityVerificationError(); + const certificate = await response.text(); + if ( + certificate.length > CERTIFICATE_LIMIT_BYTES || + !certificate.startsWith("-----BEGIN PUBLIC KEY-----") + ) { + throw new IdentityVerificationError(); + } + const key = await importSPKI(certificate, "ES256"); + albCertificateCache.set(keyId, key); + return key; +} + +function cognitoJwks(issuer: string) { + const cached = cognitoJwksCache.get(issuer); + if (cached) return cached; + const jwks = createRemoteJWKSet(new URL(`${issuer}/.well-known/jwks.json`), { + timeoutDuration: 5_000, + cooldownDuration: 30_000, + cacheMaxAge: 3_600_000, + }); + cognitoJwksCache.set(issuer, jwks); + return jwks; +} + +function roleFromClaims(payload: JWTPayload): ChangeOpsRole { + const groups = payload["cognito:groups"]; + if (!Array.isArray(groups)) throw new IdentityVerificationError(); + const roles = groups.filter( + (group): group is ChangeOpsRole => typeof group === "string" && ALLOWED_ROLES.has(group), + ); + if (roles.length !== 1 || groups.length !== 1) throw new IdentityVerificationError(); + return roles[0]; +} + +export async function verifiedIdentity(headers: Headers): Promise { + const config = configuration(); + if (config.mode !== "alb") { + const actor = requiredHeader(headers, "x-changeops-actor"); + const role = requiredHeader(headers, "x-changeops-role"); + if (!ALLOWED_ROLES.has(role)) throw new IdentityVerificationError(); + return { actor, role: role as ChangeOpsRole }; + } + + try { + const claimsToken = requiredHeader(headers, "x-amzn-oidc-data"); + const subjectHeader = requiredHeader(headers, "x-amzn-oidc-identity"); + const accessToken = requiredHeader(headers, "x-amzn-oidc-accesstoken"); + const claimsKey = await albPublicKey(claimsToken, config.awsRegion!); + const { payload: claims, protectedHeader: claimsHeader } = await jwtVerify( + claimsToken, + claimsKey, + { + algorithms: ["ES256"], + requiredClaims: ["sub"], + }, + ); + const headerExpiry = + typeof claimsHeader.exp === "number" + ? claimsHeader.exp + : typeof claimsHeader.exp === "string" + ? Number(claimsHeader.exp) + : Number.NaN; + if ( + claimsHeader.signer !== config.expectedSignerArn || + claimsHeader.client !== config.cognitoClientId || + claimsHeader.iss !== config.cognitoIssuer || + !Number.isFinite(headerExpiry) || + headerExpiry <= Math.floor(Date.now() / 1000) || + claims.sub !== subjectHeader + ) { + throw new IdentityVerificationError(); + } + + const { payload: access } = await jwtVerify( + accessToken, + cognitoJwks(config.cognitoIssuer!), + { + algorithms: ["RS256"], + issuer: config.cognitoIssuer, + requiredClaims: ["exp", "iss", "sub", "client_id", "token_use"], + }, + ); + if ( + access.sub !== subjectHeader || + access.client_id !== config.cognitoClientId || + access.token_use !== "access" + ) { + throw new IdentityVerificationError(); + } + return { actor: subjectHeader, role: roleFromClaims(access) }; + } catch { + throw new IdentityVerificationError(); + } +} + +export function identityMode(): IdentityMode { + return configuration().mode; +} + +export function clearIdentityCachesForTests(): void { + albCertificateCache.clear(); + cognitoJwksCache.clear(); +} diff --git a/web/lib/request-id.ts b/web/lib/request-id.ts new file mode 100644 index 0000000..687cf1f --- /dev/null +++ b/web/lib/request-id.ts @@ -0,0 +1,6 @@ +const SAFE_REQUEST_ID = /^[A-Za-z0-9._:-]{1,100}$/; + +export function requestId(headers: Headers): string { + const supplied = headers.get("x-request-id")?.trim(); + return supplied && SAFE_REQUEST_ID.test(supplied) ? supplied : crypto.randomUUID(); +} diff --git a/web/next.config.test.ts b/web/next.config.test.ts new file mode 100644 index 0000000..cfce280 --- /dev/null +++ b/web/next.config.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, it } from "vitest"; +import nextConfig from "./next.config"; + +describe("production security headers", () => { + it("applies the reviewed browser controls to every route", async () => { + const configuredHeaders = await nextConfig.headers?.(); + + expect(configuredHeaders).toEqual([ + { + source: "/:path*", + headers: expect.arrayContaining([ + { + key: "Strict-Transport-Security", + value: "max-age=31536000; includeSubDomains", + }, + { key: "X-Content-Type-Options", value: "nosniff" }, + { key: "X-Frame-Options", value: "DENY" }, + { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" }, + { + key: "Permissions-Policy", + value: "camera=(), geolocation=(), microphone=()", + }, + ]), + }, + ]); + }); +}); diff --git a/web/next.config.ts b/web/next.config.ts index 68a6c64..f762104 100644 --- a/web/next.config.ts +++ b/web/next.config.ts @@ -2,6 +2,26 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { output: "standalone", + async headers() { + return [ + { + source: "/:path*", + headers: [ + { + key: "Strict-Transport-Security", + value: "max-age=31536000; includeSubDomains", + }, + { key: "X-Content-Type-Options", value: "nosniff" }, + { key: "X-Frame-Options", value: "DENY" }, + { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" }, + { + key: "Permissions-Policy", + value: "camera=(), geolocation=(), microphone=()", + }, + ], + }, + ]; + }, }; export default nextConfig; diff --git a/web/package-lock.json b/web/package-lock.json index 9e749d5..6f6d36a 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@fontsource-variable/source-sans-3": "^5.3.0", "@fontsource/ibm-plex-mono": "^5.3.0", + "jose": "^6.2.8", "next": "16.2.12", "react": "19.2.0", "react-dom": "19.2.0" @@ -5315,6 +5316,15 @@ "node": ">= 0.4" } }, + "node_modules/jose": { + "version": "6.2.8", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.8.tgz", + "integrity": "sha512-Bsdjwm3Qsd/P0jR+BHDe3LytDfY7WBq2HmCCLIwuVRHMuEC9ae7/R474GIUdF1NgCyZjzVo/A9DOiOBtXq8ZoQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", diff --git a/web/package.json b/web/package.json index b1766b3..f391252 100644 --- a/web/package.json +++ b/web/package.json @@ -14,6 +14,7 @@ "dependencies": { "@fontsource-variable/source-sans-3": "^5.3.0", "@fontsource/ibm-plex-mono": "^5.3.0", + "jose": "^6.2.8", "next": "16.2.12", "react": "19.2.0", "react-dom": "19.2.0"