nene2-python 1.8.59__tar.gz → 1.8.62__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {nene2_python-1.8.59 → nene2_python-1.8.62}/CLAUDE.md +42 -2
- {nene2_python-1.8.59 → nene2_python-1.8.62}/PKG-INFO +1 -1
- nene2_python-1.8.62/docs/field-trials/2026-05-field-trial-189.md +485 -0
- nene2_python-1.8.62/docs/field-trials/2026-05-field-trial-190.md +220 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/INDEX.md +5 -3
- nene2_python-1.8.62/docs/how-to/decimal-unicode-input.md +61 -0
- nene2_python-1.8.62/docs/how-to/email-address-parsing.md +57 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/todo/current.md +9 -19
- {nene2_python-1.8.59 → nene2_python-1.8.62}/pyproject.toml +1 -1
- {nene2_python-1.8.59 → nene2_python-1.8.62}/.env.example +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/.github/workflows/ci.yml +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/.github/workflows/docs.yml +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/.github/workflows/publish.yml +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/.gitignore +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/.vitepress/config.mts +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/.vitepress/theme/custom.css +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/.vitepress/theme/index.ts +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/AGENTS.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/CHANGELOG.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/Dockerfile +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/LICENSE +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/README.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/alembic/README +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/alembic/env.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/alembic/script.py.mako +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/alembic/versions/001_create_notes_and_tags_tables.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/alembic.ini +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/compose.yaml +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/adr/0001-toolchain.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/adr/0002-clean-architecture.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/adr/0003-security-first.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/adr/0004-ai-first-design.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/adr/0005-logging.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/adr/0006-rate-limiting.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/adr/0009-mcp-design.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/adr/0010-async-use-case.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/adr/0011-mcp-as-core-dependency.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/de/index.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/de/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/explanation/architecture.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/explanation/design-philosophy.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-1.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-10.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-100.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-101.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-102.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-103.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-104.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-105.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-106.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-107.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-108.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-109.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-11.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-110.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-111.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-112.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-113.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-114.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-115.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-116.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-117.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-118.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-119.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-12.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-120.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-121.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-122.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-123.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-124.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-125.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-126.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-127.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-128.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-129.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-13.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-130.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-131.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-132.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-133.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-134.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-135.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-136.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-137.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-138.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-139.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-14.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-140.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-141.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-142.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-143.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-144.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-145.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-146.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-147.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-148.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-149.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-15.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-150.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-151.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-152.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-153.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-154.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-155.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-156.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-157.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-158.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-159.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-16.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-160.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-161.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-162.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-163.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-164.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-165.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-166.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-167.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-168.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-169.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-17.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-170.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-171.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-172.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-173.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-174.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-175.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-176.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-177.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-178.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-179.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-18.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-180.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-181.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-182.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-183.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-184.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-185.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-186.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-187.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-188.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-19.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-2.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-20.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-21.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-22.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-23.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-24.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-25.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-26.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-27.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-28.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-29.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-3.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-30.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-31.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-32.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-33.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-34.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-35.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-36.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-37.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-38.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-39.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-4.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-40.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-41.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-42.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-43.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-44.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-45.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-46.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-47.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-48.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-49.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-5.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-50.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-51.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-52.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-53.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-54.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-55.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-56.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-57.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-58.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-59.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-6.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-60.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-61.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-62.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-63.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-64.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-65.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-66.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-67.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-68.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-69.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-7.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-70.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-71.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-72.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-73.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-74.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-75.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-76.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-77.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-78.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-79.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-8.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-80.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-81.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-82.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-83.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-84.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-85.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-86.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-87.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-88.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-89.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-9.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-90.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-91.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-92.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-93.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-94.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-95.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-96.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-97.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-98.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-99.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/fr/index.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/fr/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/how-to/add-new-domain.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/how-to/api-versioning.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/how-to/async-use-case.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/how-to/background-tasks.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/how-to/configure-auth.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/how-to/cors.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/how-to/custom-auth-middleware.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/how-to/dependency-injection.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/how-to/domain-events.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/how-to/file-upload.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/how-to/lifespan-and-app-state.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/how-to/middleware-stack.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/how-to/new-project.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/how-to/problem-details.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/how-to/response-patterns.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/how-to/run-tests.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/how-to/soft-delete.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/how-to/sqlalchemy-repository.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/how-to/streaming.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/how-to/structured-logging.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/how-to/validation.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/how-to/webhook.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/howto/mcp-setup.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/index.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/ja/explanation/architecture.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/ja/explanation/design-philosophy.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/ja/how-to/add-new-domain.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/ja/how-to/configure-auth.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/ja/how-to/new-project.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/ja/how-to/run-tests.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/ja/how-to/sqlalchemy-repository.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/ja/howto/mcp-setup.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/ja/index.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/ja/reference/api.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/ja/reference/configuration.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/ja/reference/framework-modules.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/ja/tutorials/first-domain.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/ja/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/pt-br/index.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/pt-br/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/reference/api.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/reference/configuration.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/reference/framework-modules.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/roadmap.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/templates/field-trial-report.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/tutorials/first-domain.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/zh/index.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/docs/zh/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/package-lock.json +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/package.json +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/src/example/__init__.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/src/example/__main__.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/src/example/app.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/src/example/comment/__init__.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/src/example/comment/entity.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/src/example/comment/exceptions.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/src/example/comment/handler.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/src/example/comment/repository.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/src/example/comment/sqlalchemy_repository.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/src/example/comment/use_case.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/src/example/mcp.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/src/example/note/__init__.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/src/example/note/async_use_case.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/src/example/note/entity.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/src/example/note/exceptions.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/src/example/note/handler.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/src/example/note/repository.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/src/example/note/sqlalchemy_repository.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/src/example/note/use_case.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/src/example/schema.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/src/example/tag/__init__.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/src/example/tag/entity.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/src/example/tag/exceptions.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/src/example/tag/handler.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/src/example/tag/repository.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/src/example/tag/sqlalchemy_repository.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/src/example/tag/use_case.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/src/nene2/__init__.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/src/nene2/auth/__init__.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/src/nene2/auth/api_key.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/src/nene2/auth/bearer_token.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/src/nene2/auth/deps.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/src/nene2/auth/exceptions.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/src/nene2/auth/interfaces.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/src/nene2/auth/local_verifier.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/src/nene2/cache/__init__.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/src/nene2/cache/ttl.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/src/nene2/config/__init__.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/src/nene2/config/settings.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/src/nene2/database/__init__.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/src/nene2/database/exceptions.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/src/nene2/database/health.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/src/nene2/database/interfaces.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/src/nene2/database/sqlalchemy_executor.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/src/nene2/database/utils.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/src/nene2/http/__init__.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/src/nene2/http/etag.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/src/nene2/http/health.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/src/nene2/http/pagination.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/src/nene2/http/problem_details.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/src/nene2/log/__init__.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/src/nene2/log/setup.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/src/nene2/mcp/__init__.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/src/nene2/mcp/http_client.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/src/nene2/mcp/server.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/src/nene2/middleware/__init__.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/src/nene2/middleware/domain_exception.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/src/nene2/middleware/error_handler.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/src/nene2/middleware/request_id.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/src/nene2/middleware/request_logging.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/src/nene2/middleware/request_size_limit.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/src/nene2/middleware/security_headers.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/src/nene2/middleware/setup.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/src/nene2/middleware/throttle.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/src/nene2/py.typed +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/src/nene2/security/__init__.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/src/nene2/security/webhook.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/src/nene2/use_case/__init__.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/src/nene2/use_case/protocols.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/src/nene2/validation/__init__.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/src/nene2/validation/exceptions.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/src/scripts/__init__.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/src/scripts/export_openapi.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/tests/__init__.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/tests/conftest.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/tests/example/__init__.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/tests/example/comment/__init__.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/tests/example/comment/test_comment_http.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/tests/example/comment/test_comment_repository.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/tests/example/comment/test_comment_use_case.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/tests/example/conftest.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/tests/example/note/__init__.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/tests/example/note/test_async_note_use_case.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/tests/example/note/test_list_notes.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/tests/example/note/test_note_repository.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/tests/example/tag/__init__.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/tests/example/tag/test_tag_repository.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/tests/example/tag/test_tags.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/tests/example/test_cors.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/tests/example/test_mcp.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/tests/nene2/__init__.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/tests/nene2/auth/__init__.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/tests/nene2/auth/test_api_key.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/tests/nene2/auth/test_bearer_token.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/tests/nene2/auth/test_make_require_auth.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/tests/nene2/auth/test_token_issuer.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/tests/nene2/cache/__init__.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/tests/nene2/cache/test_ttl.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/tests/nene2/config/__init__.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/tests/nene2/config/test_settings.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/tests/nene2/database/__init__.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/tests/nene2/database/test_transaction.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/tests/nene2/database/test_utils.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/tests/nene2/http/__init__.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/tests/nene2/http/test_etag.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/tests/nene2/http/test_health.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/tests/nene2/http/test_pagination.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/tests/nene2/http/test_problem_details.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/tests/nene2/log/__init__.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/tests/nene2/log/test_setup.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/tests/nene2/mcp/__init__.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/tests/nene2/mcp/test_http_client.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/tests/nene2/mcp/test_server.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/tests/nene2/middleware/__init__.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/tests/nene2/middleware/test_error_handler.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/tests/nene2/middleware/test_request_id.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/tests/nene2/middleware/test_request_logging.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/tests/nene2/middleware/test_request_size_limit.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/tests/nene2/middleware/test_security_headers.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/tests/nene2/middleware/test_setup_middlewares.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/tests/nene2/middleware/test_simple_domain_handler.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/tests/nene2/middleware/test_throttle.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/tests/nene2/security/__init__.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/tests/nene2/security/test_webhook.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/tests/nene2/use_case/__init__.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/tests/nene2/use_case/test_protocols.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/tests/nene2/use_case/test_run_in_threadpool.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/tests/nene2/validation/__init__.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/tests/nene2/validation/test_exceptions.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/tests/scripts/__init__.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/tests/scripts/test_export_openapi.py +0 -0
- {nene2_python-1.8.59 → nene2_python-1.8.62}/uv.lock +0 -0
|
@@ -130,6 +130,11 @@ print(sensitive_data) # logging モジュールを使う
|
|
|
130
130
|
- **セキュリティヘッダーをミドルウェアで付与**(X-Content-Type-Options, X-Frame-Options, etc.)
|
|
131
131
|
- **SQL はパラメータ化クエリのみ**。文字列フォーマット禁止
|
|
132
132
|
- **ファイルパスは `pathlib.Path` で操作**し、パストラバーサルを防ぐ
|
|
133
|
+
- **XML 処理には `defusedxml` を使用**。標準の `xml.etree.ElementTree` は XXE・展開爆弾に脆弱(FT180 で確認)
|
|
134
|
+
```bash
|
|
135
|
+
uv add defusedxml
|
|
136
|
+
```
|
|
137
|
+
`import xml.etree.ElementTree` の代わりに `import defusedxml.ElementTree` を使う。
|
|
133
138
|
|
|
134
139
|
### 依存関係の脆弱性スキャン
|
|
135
140
|
|
|
@@ -233,6 +238,37 @@ AI エージェント(Claude 等)がこのコードベースを正確に理
|
|
|
233
238
|
- `nene2.http.problem_details_response()` で RFC 9457 エラー応答
|
|
234
239
|
- `nene2.http.PaginationQueryParser` でページネーション
|
|
235
240
|
|
|
241
|
+
### APIRouter パターン(必須)
|
|
242
|
+
|
|
243
|
+
すべての FastAPI アプリで `APIRouter` + `create_app()` ファクトリパターンを使うこと。
|
|
244
|
+
|
|
245
|
+
```python
|
|
246
|
+
# ✅ 正しい構造 — app.py
|
|
247
|
+
router = APIRouter()
|
|
248
|
+
|
|
249
|
+
@router.post("/items") # ← すべてのルート定義は router に紐付ける
|
|
250
|
+
def create_item(...): ...
|
|
251
|
+
|
|
252
|
+
@router.get("/items/{item_id}")
|
|
253
|
+
def get_item(...): ...
|
|
254
|
+
|
|
255
|
+
def create_app() -> FastAPI: # ← create_app() はファイル末尾に定義する
|
|
256
|
+
application = FastAPI(title="...")
|
|
257
|
+
application.include_router(router)
|
|
258
|
+
return application
|
|
259
|
+
|
|
260
|
+
app = create_app() # ← モジュールレベルの app は最終行
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
**`create_app()` はファイルの末尾**(全 `@router.xxx()` デコレーター定義の後)に置くこと。
|
|
264
|
+
先に `app = create_app()` を呼ぶと `router` にルートが登録される前に `include_router()` が実行され、
|
|
265
|
+
エンドポイントが空になるバグが発生する(FT182 で発見)。
|
|
266
|
+
|
|
267
|
+
- `router = APIRouter()` → ファイル先頭の定数・モデル定義の後
|
|
268
|
+
- `@router.post(...)` デコレーター → ハンドラー関数の定義
|
|
269
|
+
- `create_app()` → ファイル末尾
|
|
270
|
+
- `app = create_app()` → ファイル最終行
|
|
271
|
+
|
|
236
272
|
### ミドルウェアスタック順序(重要)
|
|
237
273
|
|
|
238
274
|
`app.add_middleware()` は **LIFO**(後から追加したものが外側になる)。
|
|
@@ -378,7 +414,11 @@ Python 標準ライブラリ・サードパーティライブラリを nene2-pyt
|
|
|
378
414
|
テンプレート: docs/templates/field-trial-report.md
|
|
379
415
|
6. DX Review(6ペルソナ)を実施(後述)
|
|
380
416
|
7. FT番号が3の倍数なら セキュリティ診断 を実施(後述)
|
|
381
|
-
8. Follow-up Issues
|
|
417
|
+
8. Follow-up Issues をその場で修正してからクローズする
|
|
418
|
+
- 発見した問題(摩擦点・セキュリティ指摘)は FT PR に含めて修正する
|
|
419
|
+
- 修正 → テスト全通過 → PR に含める → GitHub Issue は PR 内でクローズ(Closes #NNN)
|
|
420
|
+
- CLAUDE.md 追記・docs 更新・サンドボックスのコード修正すべてを同じ PR に含める
|
|
421
|
+
- 「外部依存の修正待ち」など対応不可能な理由がある場合のみ Issue を残し、理由を PR 説明に記載する
|
|
382
422
|
9. まとめて main merge → パッチバージョン(v1.8.N)でリリース
|
|
383
423
|
```
|
|
384
424
|
|
|
@@ -418,7 +458,7 @@ Python 標準ライブラリ・サードパーティライブラリを nene2-pyt
|
|
|
418
458
|
|
|
419
459
|
**合否判定**:
|
|
420
460
|
- **合格**: 全カテゴリ問題なし
|
|
421
|
-
- **条件付き合格**: MEDIUM
|
|
461
|
+
- **条件付き合格**: MEDIUM 以下の指摘のみ → **同 FT の PR 内で修正してからマージ**
|
|
422
462
|
- **不合格**: HIGH/CRITICAL の指摘あり → main merge 前に必須修正
|
|
423
463
|
|
|
424
464
|
---
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nene2-python
|
|
3
|
-
Version: 1.8.
|
|
3
|
+
Version: 1.8.62
|
|
4
4
|
Summary: NENE2 Python — minimal API framework following NENE2's design philosophy
|
|
5
5
|
Project-URL: Homepage, https://github.com/hideyukiMORI/nene2-python
|
|
6
6
|
Project-URL: Repository, https://github.com/hideyukiMORI/nene2-python
|
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
# FT189: subprocess モジュール — 安全なプロセス実行・stdin/stdout 制御・ストリーミング
|
|
2
|
+
|
|
3
|
+
**日付**: 2026-05-21
|
|
4
|
+
**テーマ**: subprocess モジュールを使った安全なプロセス実行パターンと攻撃耐性の検証
|
|
5
|
+
**セキュリティ診断**: **あり**(189 % 3 = 0)
|
|
6
|
+
**クラッカーペンテスト**: なし(189 % 4 = 1)
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## 概要
|
|
11
|
+
|
|
12
|
+
`subprocess` モジュールは Python から外部プロセスを起動する最も強力な機能の一つだが、
|
|
13
|
+
誤用すると OS コマンドインジェクション・情報漏洩・DoS を引き起こす。
|
|
14
|
+
このFTでは `shell=False` + コマンドアローリスト + タイムアウト + 出力サイズ制限という
|
|
15
|
+
4 本柱の防御パターンを FastAPI サンドボックスで実装し、
|
|
16
|
+
security audit でその有効性を確認する。
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 実装したサンプルアプリ
|
|
21
|
+
|
|
22
|
+
**場所**: `/home/xi/docker/nene2-python-FT/ft189-subprocess/`
|
|
23
|
+
|
|
24
|
+
### 主要機能
|
|
25
|
+
|
|
26
|
+
| 関数/クラス | 概要 |
|
|
27
|
+
|---|---|
|
|
28
|
+
| `run_safe()` | コマンドを allowlist 検証 + `shell=False` で安全に実行 |
|
|
29
|
+
| `run_with_input()` | stdin にテキストを流してコマンドを実行 |
|
|
30
|
+
| `run_streaming()` | `Popen` で stdout を 1 行ずつ収集 |
|
|
31
|
+
| `run_in_directory()` | 許可ベースディレクトリ外を `Path.resolve()` で拒否 |
|
|
32
|
+
| `run_with_env()` | 最小限の環境変数のみで実行(親環境を継承しない) |
|
|
33
|
+
| `parse_command_line()` | `shlex.split()` でコマンドライン文字列を安全にトークン分割 |
|
|
34
|
+
|
|
35
|
+
### HTTP エンドポイント
|
|
36
|
+
|
|
37
|
+
| メソッド | パス | 概要 |
|
|
38
|
+
|---|---|---|
|
|
39
|
+
| POST | `/subprocess/run` | allowlist + `shell=False` で基本実行 |
|
|
40
|
+
| POST | `/subprocess/run-with-input` | stdin フィード実行 |
|
|
41
|
+
| POST | `/subprocess/stream` | Popen ストリーミング収集 |
|
|
42
|
+
| POST | `/subprocess/run-in-dir` | cwd 指定(パストラバーサル防止) |
|
|
43
|
+
| POST | `/subprocess/run-with-env` | 環境変数分離実行 |
|
|
44
|
+
| POST | `/subprocess/parse-command` | コマンドライン文字列のトークン分割 |
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## テスト結果
|
|
49
|
+
|
|
50
|
+
**57 passed**
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
57 passed in 0.38s
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## 摩擦ポイント
|
|
59
|
+
|
|
60
|
+
### F-1: ruff S603「`subprocess` call: check for execution of untrusted input」(深刻度: 低)
|
|
61
|
+
|
|
62
|
+
**事象**: `subprocess.run([command, *args], shell=False)` に対して ruff S603 が
|
|
63
|
+
「untrusted input の実行をチェックせよ」という警告を出す。
|
|
64
|
+
|
|
65
|
+
**原因**: S603 は `shell=False` でも `subprocess.run()` を呼ぶと発火する。
|
|
66
|
+
ruff はコマンドが信頼されているかどうかをコード構造から判定できないため、
|
|
67
|
+
原則として全 `subprocess.run()` を警告する。
|
|
68
|
+
|
|
69
|
+
**対応**: allowlist 検証済みの呼び出し箇所に `# noqa: S603` を付与。
|
|
70
|
+
コメントなしで `noqa` を使うと意図が不明になるため、
|
|
71
|
+
関数冒頭の docstring に「allowlist 検証済み」と明記する設計とした。
|
|
72
|
+
`# type: ignore` と同様、`noqa` も理由を docstring や周辺コメントで補足する慣行を推奨する。
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## 観察点
|
|
77
|
+
|
|
78
|
+
### 観察1: `shell=False` がコマンドインジェクションを防ぐ仕組み
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
# shell=True の場合 — シェルが ; を解釈してコマンドチェーン実行
|
|
82
|
+
subprocess.run("echo hello; cat /etc/passwd", shell=True) # ← 危険
|
|
83
|
+
|
|
84
|
+
# shell=False の場合 — ; はコマンドではなく echo への文字列引数
|
|
85
|
+
subprocess.run(["echo", "hello; cat /etc/passwd"], shell=False)
|
|
86
|
+
# stdout: "hello; cat /etc/passwd" — /etc/passwd は読まれない
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
`shell=False` でもコマンド名(`tokens[0]`)をアローリストで検証しなければ、
|
|
90
|
+
`subprocess.run(["rm", "-rf", "/"])` のような直接呼び出しは防げない。
|
|
91
|
+
**アローリスト + `shell=False` の組み合わせが必須**。
|
|
92
|
+
|
|
93
|
+
### 観察2: パストラバーサルは `Path.resolve()` で一貫して防ぐ
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
def run_in_directory(command, args, cwd: Path, ...) -> RunResult:
|
|
97
|
+
resolved = cwd.resolve() # /tmp/../etc → /etc に正規化
|
|
98
|
+
if not any(_is_within(resolved, allowed) for allowed in ALLOWED_BASE_DIRS):
|
|
99
|
+
raise ValueError(f"Directory not allowed: {resolved}")
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`Path("/tmp/../etc")` は `resolve()` 後に `Path("/etc")` となり、
|
|
103
|
+
`ALLOWED_BASE_DIRS = (Path("/tmp"), Path("/home"))` に含まれないため拒否される。
|
|
104
|
+
`cwd` を文字列のまま扱うと `..` のエスケープバリエーション(`%2e%2e` 等)が通り抜ける可能性がある。
|
|
105
|
+
|
|
106
|
+
### 観察3: 環境変数分離で秘密情報の子プロセス漏洩を防ぐ
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
# NG: 親の環境をそのまま継承(SECRET_KEY が子プロセスから見える)
|
|
110
|
+
subprocess.run(["env"], capture_output=True, text=True)
|
|
111
|
+
|
|
112
|
+
# OK: 最小環境セットのみ渡す
|
|
113
|
+
safe_env = {"PATH": "/usr/bin:/bin", **extra_env}
|
|
114
|
+
subprocess.run(["env"], capture_output=True, text=True, env=safe_env)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
FastAPI サーバーは `DB_PASSWORD`・`SECRET_KEY` 等を環境変数で受け取っていることが多い。
|
|
118
|
+
`env=None`(デフォルト)で子プロセスを起動すると、これらがすべて子プロセスに見える。
|
|
119
|
+
|
|
120
|
+
### 観察4: 出力サイズ制限で stdout バッファ DoS を防ぐ
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
MAX_OUTPUT_BYTES = 65536 # 64 KiB
|
|
124
|
+
|
|
125
|
+
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
|
|
126
|
+
stdout = proc.stdout[:MAX_OUTPUT_BYTES] # 上限クリップ
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
`capture_output=True` は stdout を全てメモリに保持する。
|
|
130
|
+
`cat /dev/zero` のような無限出力コマンドは `timeout` で制御するが、
|
|
131
|
+
タイムアウト前に大量の stdout が蓄積する可能性もあるため、
|
|
132
|
+
クリップによる二重防御が望ましい。
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## nene2-python フレームワークとの統合
|
|
137
|
+
|
|
138
|
+
- `subprocess` は CPU/I/O バウンドな外部コマンド実行に使う。
|
|
139
|
+
FastAPI の非同期ループ内で直接呼ぶとブロッキングになるため、
|
|
140
|
+
`asyncio.get_event_loop().run_in_executor()` 経由で呼ぶことを検討する。
|
|
141
|
+
- CLAUDE.md の「絶対禁止」リストに `subprocess.run(cmd, shell=True)` が明示されており、
|
|
142
|
+
ruff S602/S603 と二重に強制される設計になっている。
|
|
143
|
+
- コマンドアローリストは `frozenset` で定義し、定数として `demos.py` 上部に配置する。
|
|
144
|
+
アローリストの変更は PR レビューで明示的に確認できる。
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Developer Experience (DX) Review
|
|
149
|
+
|
|
150
|
+
### ペルソナ1: 初心者(Python 歴1年・独学中・女性・バックエンド志望)
|
|
151
|
+
|
|
152
|
+
`subprocess.run()` で外部コマンドを実行したいと考え、公式ドキュメントを参照している。
|
|
153
|
+
|
|
154
|
+
**ドキュメント理解**: 公式ドキュメントの最初の例が `shell=True` を使っているケースも多く、
|
|
155
|
+
「なぜ `shell=False` にするのか」の説明が不足しがち。
|
|
156
|
+
nene2-python の CLAUDE.md で `subprocess.run(cmd, shell=True)` が「絶対禁止」と明記されているため、
|
|
157
|
+
初心者が `shell=True` を選ぶ前に気づける設計になっている。
|
|
158
|
+
**事故リスク**: 高。`shell=True` を「動けばいい」と選んでしまうと OS コマンドインジェクションに直結する。
|
|
159
|
+
ruff S602 が `shell=True` の使用でエラーを出すため、静的解析を通している限りは防止される。
|
|
160
|
+
**規約の使いやすさ**: `run_safe(command, args)` という関数シグネチャは直感的で覚えやすい。
|
|
161
|
+
アローリストを広げるだけで機能拡張できるため、最初の壁は低い。
|
|
162
|
+
|
|
163
|
+
### ペルソナ2: ロースキル経験者(Python 歴3-4年・スクリプト系・男性・SES)
|
|
164
|
+
|
|
165
|
+
スクリプトで `os.system()` や `subprocess.run(cmd, shell=True)` を使ってきた経験を持つ。
|
|
166
|
+
|
|
167
|
+
**コピペ可能性**: `run_safe()` のパターンは ALLOWED_COMMANDS を変えるだけでコピー流用できる。
|
|
168
|
+
ただし、アローリストを変える際に `app.py` 側の `max_length` との整合を確認する必要があり、
|
|
169
|
+
この確認を怠りやすい。
|
|
170
|
+
**拡張時の罠**: `timeout` パラメータを削除すると無限待機になる。
|
|
171
|
+
`DEFAULT_TIMEOUT = 10.0` 定数は意図を伝えているが、「とりあえず大きい値に変えたい」と
|
|
172
|
+
`timeout=None` に設定するミスが起きやすい。mypy では `None` を `float` に渡せないため検出可能。
|
|
173
|
+
**セキュリティ的な事故リスク**: 高。アローリストを `frozenset(["*"])` や `set()` にしてしまうと
|
|
174
|
+
全コマンドが実行可能になる。コードレビューでアローリストの内容を必ず確認するルール化を推奨。
|
|
175
|
+
|
|
176
|
+
### ペルソナ3: フロントエンド寄り経験者(React/TS 歴4年・バックエンド転向中・ノンバイナリ)
|
|
177
|
+
|
|
178
|
+
API クライアント側の実装者として、各エンドポイントのエラーレスポンスを評価する。
|
|
179
|
+
|
|
180
|
+
**エラーレスポンスの質**: 許可されていないコマンドへの 400 エラーは
|
|
181
|
+
`{"detail": "Command not allowed: 'rm'"}` として返り、クライアント側で原因を特定しやすい。
|
|
182
|
+
Pydantic の 422 エラー(`timeout` が範囲外など)も自動で詳細なエラー構造が返る。
|
|
183
|
+
**Python 固有概念の学習コスト**: `subprocess` 自体の概念(プロセス・パイプ・PIPE・DEVNULL)は
|
|
184
|
+
Node.js の `child_process` と類似しており学習コストは低め。
|
|
185
|
+
`shlex.split()` の「クォートを正しく処理するシェル風トークナイザ」という概念は独特だが、
|
|
186
|
+
`parse_command_line()` として抽象化されているため直接触れる必要は少ない。
|
|
187
|
+
**事故リスク**: 低。HTTP 境界は Pydantic で保護されており、不正な入力は 400/422 で返る。
|
|
188
|
+
|
|
189
|
+
### ペルソナ4: バックエンド経験者(Django/FastAPI 歴5-6年・男性・リードエンジニア)
|
|
190
|
+
|
|
191
|
+
CI/CD パイプラインやビルドスクリプト連携で `subprocess` を頻繁に使う立場。
|
|
192
|
+
|
|
193
|
+
**他フレームワークとの差異**: Django の `management command` や
|
|
194
|
+
`os.system()` を直接呼ぶスクリプト系との比較で、
|
|
195
|
+
`run_safe()` のアローリスト + `shell=False` パターンは「FastAPI 境界に組み込んだ場合」の
|
|
196
|
+
標準的なアプローチとして評価できる。
|
|
197
|
+
**nene2-python の薄さへの評価**: subprocess のラップ関数が `demos.py` 層にあり、
|
|
198
|
+
HTTP ハンドラー(`app.py`)はリクエスト変換のみを担う設計は明確でレビューしやすい。
|
|
199
|
+
`run_safe()` を UseCase 層に組み込む場合も、HTTP 非依存の関数設計になっている。
|
|
200
|
+
**本番投入可能性**: 本番環境では `asyncio.get_event_loop().run_in_executor()` でスレッドプールに
|
|
201
|
+
オフロードすることと、コンテナ環境で実行可能なコマンドをより厳密に管理することが必要。
|
|
202
|
+
アローリストはハードコードではなく設定ファイルから読む設計も検討価値がある。
|
|
203
|
+
|
|
204
|
+
### ペルソナ5: シニアエンジニア(設計・コードレビュー担当・女性・10-12年)
|
|
205
|
+
|
|
206
|
+
チームでの security review を担当する立場でリスクを評価する。
|
|
207
|
+
|
|
208
|
+
**コードレビューチェックポイント**:
|
|
209
|
+
- [x] `shell=True` が使われていないか(ruff S602 で自動検出)
|
|
210
|
+
- [x] `ALLOWED_COMMANDS` が適切に絞られているか(最小権限の原則)
|
|
211
|
+
- [x] `timeout` が全ての `subprocess.run()` / `Popen` に設定されているか
|
|
212
|
+
- [x] `env=None`(デフォルト)で子プロセスを起動していないか(環境変数漏洩)
|
|
213
|
+
- [x] stdout のサイズが `MAX_OUTPUT_BYTES` でクリップされているか
|
|
214
|
+
- [x] `cwd` パスが `Path.resolve()` + ベースディレクトリ検証を経ているか
|
|
215
|
+
|
|
216
|
+
**チームでの安全な共有パターン**: `run_safe(command: str, args: list[str])` という
|
|
217
|
+
「コマンドと引数を分離した関数」を共通化し、直接 `subprocess.run()` を呼ぶことを禁止するルール化が効果的。
|
|
218
|
+
ruff S603 の `# noqa` を使う場合は PR 説明で理由を明記させる。
|
|
219
|
+
**ツール追加の必要性**: `bandit` の B603/B604 ルール(現行の ruff S603 に対応)は
|
|
220
|
+
`subprocess` の安全でない使用を検出するためすでに有効。
|
|
221
|
+
ただし `noqa` の乱用が増えた場合は `ruff.toml` で S603 を `per-file-ignores` で
|
|
222
|
+
特定ファイルのみに絞るか、別途 `semgrep` ルールで補強することを推奨。
|
|
223
|
+
|
|
224
|
+
### ペルソナ6: 設計者・ポリシー照合(nene2-python 設計ポリシー目線)
|
|
225
|
+
|
|
226
|
+
CLAUDE.md の設計ポリシーと FT189 の実装を照合する。
|
|
227
|
+
|
|
228
|
+
**ポリシー達成度**: 高
|
|
229
|
+
**「初心者でも安全な API」達成度**: 高
|
|
230
|
+
**設計上の負債・ドキュメント不足**:
|
|
231
|
+
- `run_safe()` の `noqa: S603` が何を意味するか(allowlist 検証済みだから安全)を
|
|
232
|
+
CLAUDE.md の「セキュリティポリシー」セクションに補足する価値がある(優先度: 低)。
|
|
233
|
+
- `timeout=None` を渡した場合の挙動(無限待機)を How-to ガイドに記載する価値がある(優先度: 低)。
|
|
234
|
+
**Follow-up Issue 候補**: なし(既存ポリシーの範囲内で解決済み)
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
## セキュリティ診断(FT189 % 3 = 0)
|
|
239
|
+
|
|
240
|
+
> **診断方針**: subprocess は OS コマンドインジェクションの主要な攻撃面。
|
|
241
|
+
> CLAUDE.md の「絶対禁止」リストに直接掲載されている機能のため、
|
|
242
|
+
> 防御が実際に機能しているかを重点的に確認する。
|
|
243
|
+
|
|
244
|
+
### 1. OWASP API Security Top 10 (2023)
|
|
245
|
+
|
|
246
|
+
#### API1: オブジェクトレベルの認可不備 (BOLA / IDOR)
|
|
247
|
+
- このFTでは認証・ユーザーデータが存在しないため N/A
|
|
248
|
+
- **結果**: N/A
|
|
249
|
+
|
|
250
|
+
#### API2: 認証の破損 (Broken Authentication)
|
|
251
|
+
- 認証機能なし(sandboxのため)
|
|
252
|
+
- **結果**: N/A(本番組み込み時は nene2 の `ApiKeyAuthMiddleware` を使用)
|
|
253
|
+
|
|
254
|
+
#### API3: オブジェクトプロパティレベルの認可不備 (Mass Assignment)
|
|
255
|
+
- Pydantic モデルに定義されていないフィールドを `{"command": "echo", "is_admin": true}` で送信
|
|
256
|
+
- Pydantic デフォルト設定で unknown フィールドは無視される
|
|
257
|
+
- **結果**: ✅ 不明フィールドは無視される
|
|
258
|
+
|
|
259
|
+
#### API4: 無制限リソース消費
|
|
260
|
+
- `args: ["x"] * 11` → Pydantic `max_length=10` で 422
|
|
261
|
+
- `timeout: 31.0` → Pydantic `le=30.0` で 422
|
|
262
|
+
- `input_text: "x" * 10001` → Pydantic `max_length=10000` で 422
|
|
263
|
+
- **結果**: ✅ 全て Pydantic で遮断
|
|
264
|
+
|
|
265
|
+
#### API5: 機能レベルの認可不備
|
|
266
|
+
- 管理者専用エンドポイントなし(sandbox なので N/A)
|
|
267
|
+
- **結果**: N/A
|
|
268
|
+
|
|
269
|
+
#### API6: SSRF
|
|
270
|
+
- このFTでは URL を扱わない(N/A)
|
|
271
|
+
- **結果**: N/A
|
|
272
|
+
|
|
273
|
+
#### API7: セキュリティ設定ミス
|
|
274
|
+
- sandbox のため SecurityHeadersMiddleware は未使用
|
|
275
|
+
- エラーレスポンスに内部パスは含まれない(`str(exc)` のみ返す)
|
|
276
|
+
- **結果**: ✅ コマンド名のみ返却、内部スタックトレース不露出
|
|
277
|
+
|
|
278
|
+
#### API8-10
|
|
279
|
+
- N/A(バージョニング・在庫管理・外部API消費なし)
|
|
280
|
+
|
|
281
|
+
---
|
|
282
|
+
|
|
283
|
+
### 2. インジェクション攻撃
|
|
284
|
+
|
|
285
|
+
#### コマンドインジェクション(最重要)
|
|
286
|
+
|
|
287
|
+
```bash
|
|
288
|
+
# テスト1: shell=True での ; インジェクション試み
|
|
289
|
+
run_safe("echo", ["hello; cat /etc/passwd"])
|
|
290
|
+
# → stdout: "hello; cat /etc/passwd"
|
|
291
|
+
# → /etc/passwd の内容は読まれない(shell=False のため ; が引数扱い)
|
|
292
|
+
# ✅ 防止
|
|
293
|
+
|
|
294
|
+
# テスト2: バッククォート展開
|
|
295
|
+
run_safe("echo", ["`id`"])
|
|
296
|
+
# → stdout: "`id`"
|
|
297
|
+
# → uid= は含まれない
|
|
298
|
+
# ✅ 防止
|
|
299
|
+
|
|
300
|
+
# テスト3: $() 展開
|
|
301
|
+
run_safe("echo", ["$(cat /etc/passwd)"])
|
|
302
|
+
# → stdout: "$(cat /etc/passwd)"
|
|
303
|
+
# ✅ 防止
|
|
304
|
+
|
|
305
|
+
# テスト4: コマンド名アローリスト外
|
|
306
|
+
run_safe("rm", ["-rf", "/"])
|
|
307
|
+
# → ValueError: Command not allowed: 'rm'
|
|
308
|
+
# ✅ 防止(アローリストで即時拒否)
|
|
309
|
+
|
|
310
|
+
# テスト5: curl による外部接続
|
|
311
|
+
run_safe("curl", ["http://evil.example.com"])
|
|
312
|
+
# → ValueError: Command not allowed: 'curl'
|
|
313
|
+
# ✅ 防止
|
|
314
|
+
|
|
315
|
+
# テスト6: python3 による任意コード実行
|
|
316
|
+
run_safe("python3", ["-c", "import os; os.system('id')"])
|
|
317
|
+
# → ValueError: Command not allowed: 'python3'
|
|
318
|
+
# ✅ 防止
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
- **結果**: ✅ 全コマンドインジェクション試みが防止される
|
|
322
|
+
|
|
323
|
+
#### SQL インジェクション
|
|
324
|
+
- SQL を扱わない(N/A)
|
|
325
|
+
|
|
326
|
+
#### パストラバーサル
|
|
327
|
+
|
|
328
|
+
```bash
|
|
329
|
+
# cwd での パストラバーサル試み
|
|
330
|
+
run_in_directory("ls", [], Path("/tmp/../etc"))
|
|
331
|
+
# Path.resolve() → /etc
|
|
332
|
+
# /etc は ALLOWED_BASE_DIRS に含まれない → ValueError
|
|
333
|
+
# ✅ 防止
|
|
334
|
+
|
|
335
|
+
run_in_directory("ls", [], Path("/"))
|
|
336
|
+
# /は /tmp でも /home でもない → ValueError
|
|
337
|
+
# ✅ 防止
|
|
338
|
+
|
|
339
|
+
run_in_directory("ls", [], Path("/etc/passwd"))
|
|
340
|
+
# Not a directory: /etc/passwd → ValueError
|
|
341
|
+
# ✅ 防止
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
- **結果**: ✅ `Path.resolve()` + ベースディレクトリ検証で防止
|
|
345
|
+
|
|
346
|
+
#### SSTI / HTTP ヘッダーインジェクション
|
|
347
|
+
- テンプレートエンジン・レスポンスヘッダー操作なし(N/A)
|
|
348
|
+
|
|
349
|
+
---
|
|
350
|
+
|
|
351
|
+
### 3. 認証・認可
|
|
352
|
+
|
|
353
|
+
- このFTは sandbox(認証なし)のため本格的な評価対象外
|
|
354
|
+
- `secrets` モジュールは使用していない(N/A)
|
|
355
|
+
- **結果**: N/A
|
|
356
|
+
|
|
357
|
+
---
|
|
358
|
+
|
|
359
|
+
### 4. 入力バリデーション
|
|
360
|
+
|
|
361
|
+
| 入力 | Pydantic バリデーション | 結果 |
|
|
362
|
+
|---|---|---|
|
|
363
|
+
| `"A" * 65` (command) | `max_length=64` → 422 | ✅ |
|
|
364
|
+
| `["x"] * 11` (args) | `max_length=10` → 422 | ✅ |
|
|
365
|
+
| `timeout=0.0` | `ge=0.1` → 422 | ✅ |
|
|
366
|
+
| `timeout=31.0` | `le=30.0` → 422 | ✅ |
|
|
367
|
+
| `input_text="x"*10001` | `max_length=10000` → 422 | ✅ |
|
|
368
|
+
| `max_lines=1001` | `le=1000` → 422 | ✅ |
|
|
369
|
+
| `env` × 21 個 | アプリ層で `len(body.env) > 20` → 400 | ✅ |
|
|
370
|
+
| Null バイト `"\x00evil"` in args | OS レベルで `OSError` → 500 相当(改善余地あり) | ⚠️ |
|
|
371
|
+
|
|
372
|
+
Null バイトを引数に含む場合、OS が `EINVAL` を返すため Python が `OSError` を送出する。
|
|
373
|
+
これをアプリが 500 として返す可能性があり、エラーメッセージに内部情報が含まれないよう
|
|
374
|
+
`try/except OSError` で 400 に変換することを推奨する(優先度: 中)。
|
|
375
|
+
|
|
376
|
+
- **結果**: ⚠️ 条件付き合格(Null バイト OSError のハンドリングが不完全)
|
|
377
|
+
|
|
378
|
+
---
|
|
379
|
+
|
|
380
|
+
### 5. 情報漏洩
|
|
381
|
+
|
|
382
|
+
- 500 時のスタックトレース: `run_safe()` は `try/except` で全例外を捕捉、
|
|
383
|
+
FastAPI が HTTPException の `detail` のみを返す → 内部情報不露出
|
|
384
|
+
- 環境変数漏洩: `run_with_env()` で `env=safe_env` を使い親環境を継承しない設計 → ✅
|
|
385
|
+
- `pip-audit` スキャン: PYSEC-2025-183 (PyJWT via mcp 推移的依存) のみ、許容済み
|
|
386
|
+
|
|
387
|
+
- **結果**: ✅ 情報漏洩なし(PYSEC-2025-183 は許容)
|
|
388
|
+
|
|
389
|
+
---
|
|
390
|
+
|
|
391
|
+
### 6. Python / FastAPI 固有の攻撃ベクター
|
|
392
|
+
|
|
393
|
+
#### ReDoS
|
|
394
|
+
- `shlex.split()` は正規表現を使っていないため ReDoS 非対象
|
|
395
|
+
- アローリスト検索は `frozenset` の `in` 演算(O(1) ハッシュ)で安全
|
|
396
|
+
- **結果**: ✅
|
|
397
|
+
|
|
398
|
+
#### pickle / yaml インジェクション
|
|
399
|
+
- `subprocess` モジュールは pickle/yaml を使わない(N/A)
|
|
400
|
+
|
|
401
|
+
#### 非同期レースコンディション
|
|
402
|
+
- `run_safe()` はグローバル状態を変更しない(stateless 関数設計)
|
|
403
|
+
- リクエストごとに独立した子プロセスを起動するため競合状態なし
|
|
404
|
+
- **結果**: ✅
|
|
405
|
+
|
|
406
|
+
#### Pydantic 型強制攻撃
|
|
407
|
+
|
|
408
|
+
```python
|
|
409
|
+
{"command": "echo", "args": [], "timeout": "1e2"}
|
|
410
|
+
# → Pydantic v2: "1e2" は float として解釈され 100.0 → le=30.0 で 422
|
|
411
|
+
# ✅ 許容範囲外は拒否
|
|
412
|
+
|
|
413
|
+
{"command": "echo", "args": [], "timeout": True}
|
|
414
|
+
# → Pydantic v2: True → 1.0 → ge=0.1 で合格(1.0 は有効値)
|
|
415
|
+
# ✅ 意図しない動作なし(True→1.0 は有効なタイムアウト)
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
- **結果**: ✅
|
|
419
|
+
|
|
420
|
+
#### subprocess 固有: `shell=True` バイパス試み
|
|
421
|
+
|
|
422
|
+
ruff S602 (`subprocess-run-without-check` with `shell=True`) が CI で強制されるため、
|
|
423
|
+
静的解析を通過した時点で `shell=True` は使われていない。
|
|
424
|
+
`# noqa: S602` を使っても PR レビューで発見される。
|
|
425
|
+
|
|
426
|
+
- **結果**: ✅ 二重防御(ruff + PR レビュー)
|
|
427
|
+
|
|
428
|
+
---
|
|
429
|
+
|
|
430
|
+
### 7. 依存関係の脆弱性スキャン
|
|
431
|
+
|
|
432
|
+
```
|
|
433
|
+
Name Version ID Fix Versions
|
|
434
|
+
pyjwt 2.12.1 PYSEC-2025-183 (未対応)
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
- **スキャン結果**: MEDIUM: 1件(PYSEC-2025-183)
|
|
438
|
+
- **対応方針**: mcp 推移的依存。mcp 側の修正待ち。文書化済み。許容。
|
|
439
|
+
|
|
440
|
+
---
|
|
441
|
+
|
|
442
|
+
### 診断サマリー
|
|
443
|
+
|
|
444
|
+
| カテゴリ | 結果 | 最重要発見 |
|
|
445
|
+
|---|---|---|
|
|
446
|
+
| OWASP API Security Top 10 | ✅ 全通過(N/A 除く) | - |
|
|
447
|
+
| コマンドインジェクション | ✅ 全通過 | shell=False + allowlist で完全防御 |
|
|
448
|
+
| パストラバーサル | ✅ 全通過 | Path.resolve() で ../etc 等を防止 |
|
|
449
|
+
| 入力バリデーション | ⚠️ 軽微指摘 | Null バイト args の OSError → 500 の改善余地 |
|
|
450
|
+
| 情報漏洩 | ✅ 全通過 | 環境変数分離で SECRET_KEY 漏洩なし |
|
|
451
|
+
| ReDoS | ✅ 全通過 | shlex/frozenset は正規表現不使用 |
|
|
452
|
+
| 非同期レースコンディション | ✅ 全通過 | stateless 関数設計 |
|
|
453
|
+
| 型強制攻撃 | ✅ 全通過 | Pydantic le=30.0 等が適切に働く |
|
|
454
|
+
| 依存関係 CVE | ⚠️ MEDIUM 1件 | PYSEC-2025-183 (PyJWT / 許容済み) |
|
|
455
|
+
|
|
456
|
+
**総合評価**: 条件付き合格(Null バイト OSError ハンドリングを次FTまでに改善)
|
|
457
|
+
**発見した問題**: 1件(MEDIUM: Null バイトを含む引数で OSError が 500 として露出する可能性)
|
|
458
|
+
**新規セキュリティ Issue**: #524
|
|
459
|
+
|
|
460
|
+
---
|
|
461
|
+
|
|
462
|
+
## Follow-up Issues
|
|
463
|
+
|
|
464
|
+
| 優先度 | タイトル | 種別 |
|
|
465
|
+
|---|---|---|
|
|
466
|
+
| 中 | [FT189] subprocess の args に Null バイトが含まれる場合の OSError を 400 で返す | fix |
|
|
467
|
+
|
|
468
|
+
---
|
|
469
|
+
|
|
470
|
+
## まとめ
|
|
471
|
+
|
|
472
|
+
FT189 では `subprocess` モジュールの 6 パターン(run_safe・run_with_input・run_streaming・
|
|
473
|
+
run_in_directory・run_with_env・parse_command_line)を FastAPI サンドボックスで実装した。
|
|
474
|
+
|
|
475
|
+
最重要の学習は **`shell=False` + コマンドアローリストの組み合わせ**。
|
|
476
|
+
どちらか片方だけでは不十分で、両方が揃うことでコマンドインジェクションを完全に防げる。
|
|
477
|
+
セキュリティ診断では Null バイトを含む引数による OSError の露出(MEDIUM)を発見し、
|
|
478
|
+
Follow-up Issue #524 として記録した。
|
|
479
|
+
|
|
480
|
+
ruff S603 の `# noqa` 使用については、周辺コメントや docstring で
|
|
481
|
+
「allowlist 検証済みであるため安全」という意図を補足する設計を採用した。
|
|
482
|
+
これは `# type: ignore[code]` の慣行と並ぶ「noqa の使用には理由を添える」パターンとして
|
|
483
|
+
CLAUDE.md への追記価値がある。
|
|
484
|
+
|
|
485
|
+
次の FT190 は `190 % 3 = 1` でセキュリティ診断なし、`190 % 4 = 2` でペンテストなし。
|