=== STEP_037A4 SUBSCRIPTION SCHEMA DIAGNOSTICS === timestamp=20260708-133834 mode=read-only diagnostics after HTTP 422 / no known-good request forbidden: POST create, agent run, selector changes, peer changes, backend restart === VM121 read-only diagnostics === date=Wed Jul 8 13:38:34 UTC 2026 === current API counts === peers_curl_rc=0 jobs_curl_rc=0 peer_count=21 enabled_peer_count=1 job_count=37 pending_job_count=0 ENABLED_PEER id=376dfa07-0ba4-4182-af39-389a2cb3badd tunnel_ip=10.253.1.10 node_id=ddn-test === latest A2/A3 private response/error bodies, sanitized === --- backup_dir=/root/router-ops-backups/canary-test-subscription-cs2-knowngood-20260708-133626 --- jobs-before.json peers-before.json --- backup_dir=/root/router-ops-backups/canary-test-subscription-cs2-fixed-20260708-133412 --- create-full.private.json create-request.public.json cs2-peer.safe.env jobs-after.json jobs-before.json peers-after.json peers-before.json --- create-request.public.json --- { "email": "wgpay-cs2-canary+20260708-133412@router.local", "months": 1, "node_id": "ddn-test", "auto_renew": false } --- create-full.private.json sanitized/error preview --- { "detail": [ { "type": "value_error", "loc": [ "body", "email" ], "msg": "value is not a valid email address: The part after the @-sign is a special-use or reserved name that cannot be used with email.", "input": "wgpay-cs2-canary+20260708-133412@router.local", "ctx": { "reason": "The part after the @-sign is a special-use or reserved name that cannot be used with email." } } ] } === OpenAPI subscription endpoint schema === openapi_curl_rc=0 openapi_post_admin_subscriptions_found=True openapi_operation_id=create_subscription_admin_subscriptions_post openapi_request_body_keys=content,required openapi_content_type=application/json openapi_request_schema={"$ref": "#/components/schemas/SubscriptionCreateRequest"} openapi_request_model=SubscriptionCreateRequest openapi_request_model_json={ "properties": { "email": { "anyOf": [ { "type": "string", "format": "email" }, { "type": "null" } ], "title": "Email" }, "months": { "type": "integer", "maximum": 36.0, "minimum": 1.0, "title": "Months", "default": 1 }, "plan_code": { "type": "string", "title": "Plan Code", "default": "manual" }, "node_id": { "type": "string", "title": "Node Id", "default": "ddn-test" }, "auto_renew": { "type": "boolean", "title": "Auto Renew", "default": false } }, "type": "object", "title": "SubscriptionCreateRequest" } openapi_responses=200,422 === backend code snippets around subscription create === --- file=/opt/wg-access/backend/app/api/admin.py --- 66: node_id: str 84:class SubscriptionCreateRequest(BaseModel): 85: email: EmailStr | None = None 86: months: int = Field(default=1, ge=1, le=36) 87: plan_code: str = "manual" 88: node_id: str = "ddn-test" 89: auto_renew: bool = False 92:class SubscriptionCreateResponse(BaseModel): 97: node_id: str 106:@router.post("/subscriptions", response_model=SubscriptionCreateResponse) 107:def create_subscription(payload: SubscriptionCreateRequest, db: Session = Depends(get_db)): 111: paid_until = datetime.now(timezone.utc) + timedelta(days=30 * payload.months) 113: user = User(email=payload.email) 120: plan_code=payload.plan_code, 121: auto_renew=payload.auto_renew, 127: tunnel_ip = next_tunnel_ip(db, payload.node_id) 134: node_id=payload.node_id, 145: node_id=payload.node_id, 164: return SubscriptionCreateResponse( 169: node_id=peer.node_id, 181:class ExpireSubscriptionsResponse(BaseModel): 189:def expire_subscriptions(db: Session = Depends(get_db)): 204: subscription.auto_renew = False 219: node_id=peer.node_id, 245:class CancelSubscriptionResponse(BaseModel): 255:def cancel_subscription(subscription_id: UUID, db: Session = Depends(get_db)): 265: subscription.auto_renew = False 282: node_id=peer.node_id, 313: node_id: str 330: node_id=peer.node_id, 348: node_id=peer.node_id, 358: node_id: str 375:class AdminSubscriptionResponse(BaseModel): 379: plan_code: str 380: auto_renew: bool 392: node_id: str 406: node_id: str 420: email: str | None 432: email: str | None 455: email=user.email, 490: email=user.email, 498:@router.get("/users/by-email/{email}", response_model=AdminUserDetailResponse) 499:def get_user_by_email(email: str, db: Session = Depends(get_db)): 500: decoded_email = unquote(email) 504: .where(User.email == decoded_email) --- file=/opt/wg-access/backend/app/schemas.py --- missing --- file=/opt/wg-access/backend/app/models.py --- missing --- file=/opt/wg-access/backend/app/main.py --- === broader grep for request model === /opt/wg-access/backend/app/models/core.py:48: plan_code: Mapped[str] = mapped_column(String(64), nullable=False) /opt/wg-access/backend/app/models/core.py:49: months: Mapped[int] = mapped_column(Integer, nullable=False) /opt/wg-access/backend/app/models/core.py:69: plan_code: Mapped[str] = mapped_column(String(64), nullable=False) /opt/wg-access/backend/app/models/core.py:70: auto_renew: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) /opt/wg-access/backend/app/api/admin.py:7:from pydantic import BaseModel, EmailStr, Field /opt/wg-access/backend/app/api/admin.py:84:class SubscriptionCreateRequest(BaseModel): /opt/wg-access/backend/app/api/admin.py:85: email: EmailStr | None = None /opt/wg-access/backend/app/api/admin.py:86: months: int = Field(default=1, ge=1, le=36) /opt/wg-access/backend/app/api/admin.py:87: plan_code: str = "manual" /opt/wg-access/backend/app/api/admin.py:89: auto_renew: bool = False /opt/wg-access/backend/app/api/admin.py:92:class SubscriptionCreateResponse(BaseModel): /opt/wg-access/backend/app/api/admin.py:106:@router.post("/subscriptions", response_model=SubscriptionCreateResponse) /opt/wg-access/backend/app/api/admin.py:107:def create_subscription(payload: SubscriptionCreateRequest, db: Session = Depends(get_db)): /opt/wg-access/backend/app/api/admin.py:111: paid_until = datetime.now(timezone.utc) + timedelta(days=30 * payload.months) /opt/wg-access/backend/app/api/admin.py:120: plan_code=payload.plan_code, /opt/wg-access/backend/app/api/admin.py:121: auto_renew=payload.auto_renew, /opt/wg-access/backend/app/api/admin.py:164: return SubscriptionCreateResponse( /opt/wg-access/backend/app/api/admin.py:204: subscription.auto_renew = False /opt/wg-access/backend/app/api/admin.py:265: subscription.auto_renew = False /opt/wg-access/backend/app/api/admin.py:379: plan_code: str /opt/wg-access/backend/app/api/admin.py:380: auto_renew: bool /opt/wg-access/backend/app/api/dev.py.BEFORE_CLIENT_CONFIG_RESPONSE.20260704-151636:9:from pydantic import BaseModel, EmailStr /opt/wg-access/backend/app/api/dev.py.BEFORE_CLIENT_CONFIG_RESPONSE.20260704-151636:20: email: EmailStr | None = None /opt/wg-access/backend/app/api/dev.py.BEFORE_CLIENT_CONFIG_RESPONSE.20260704-151636:21: months: int = 2 /opt/wg-access/backend/app/api/dev.py.BEFORE_CLIENT_CONFIG_RESPONSE.20260704-151636:77: paid_until = datetime.now(timezone.utc) + timedelta(days=30 * payload.months) /opt/wg-access/backend/app/api/dev.py:5:from pydantic import BaseModel, EmailStr /opt/wg-access/backend/app/api/dev.py:27: email: EmailStr | None = None /opt/wg-access/backend/app/api/dev.py:28: months: int = 2 /opt/wg-access/backend/app/api/dev.py:57: paid_until = datetime.now(timezone.utc) + timedelta(days=30 * payload.months) /opt/wg-access/backend/app/api/dev.py.BEFORE_REAL_WG_KEYS.20260704-132545:6:from pydantic import BaseModel, EmailStr /opt/wg-access/backend/app/api/dev.py.BEFORE_REAL_WG_KEYS.20260704-132545:17: email: EmailStr | None = None /opt/wg-access/backend/app/api/dev.py.BEFORE_REAL_WG_KEYS.20260704-132545:18: months: int = 2 /opt/wg-access/backend/app/api/dev.py.BEFORE_REAL_WG_KEYS.20260704-132545:50: paid_until = datetime.now(timezone.utc) + timedelta(days=30 * payload.months) === backend endpoint env === backend_container=wgaccess-backend WG_CLIENT_ENDPOINT=wg-studio.secret-studio.ru:51830 === no forbidden action proof === no_post_create=1 no_agent_run=1 no_selector_change=1 no_peer_change=1 no_backend_restart=1 vm121_diagnostics_done=1 === VM100 read-only state === vm100_wg_paid_peer_count=1 vm100_existing_canary_visible_rc=0 vm100_selector_active_entry_count=1 vm100_selector_rule_visible_rc=0 no_remote_write_vm100=1 no_selector_change_vm100=1 no_wg_reload_vm100=1 vm100_readonly_done=1 === STEP_037A4 RESULT === peer_count=21 enabled_peer_count=1 pending_job_count=0 openapi_curl_rc=0 openapi_post_admin_subscriptions_found=True a2_422_email_hint_lines=26 plan_required_hint_lines=12 months_hint_lines=14 vm100_wg_paid_peer_count=1 vm100_existing_canary_visible_rc=0 vm100_selector_active_entry_count=1 vm100_selector_rule_visible_rc=0 next_action=craft_correct_create_payload_from_schema decision=PASS_SUBSCRIPTION_SCHEMA_DIAGNOSTICS