nerdit 0.9.0__tar.gz → 0.9.2__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.
- {nerdit-0.9.0 → nerdit-0.9.2}/PKG-INFO +6 -6
- {nerdit-0.9.0 → nerdit-0.9.2}/README.md +5 -5
- {nerdit-0.9.0 → nerdit-0.9.2}/pyproject.toml +1 -1
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/__init__.py +1 -1
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/commands/apply.py +4 -1
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/commands/deploy.py +13 -1
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/upload.py +21 -2
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/config/app_config.py +5 -1
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/config/build.py +4 -1
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/config/defaults.py +1 -1
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/config/project.py +43 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/app_build.py +43 -1
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/deploy_state.py +8 -1
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/link/apps.py +9 -6
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/link/client.py +2 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/link/mux.py +43 -3
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/services.py +120 -30
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/deploy_pipeline.py +359 -19
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/remediation.py +12 -2
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/routes/app_config.py +11 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/routes/deploy.py +15 -2
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/routes/projects.py +7 -2
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/routes/services.py +9 -2
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/routes/share.py +2 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/routes/system.py +3 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/schemas/exposure.py +10 -0
- nerdit-0.9.2/src/nerdit/daemon/web/dist/assets/index-Bo4WRmzm.js +265 -0
- nerdit-0.9.0/src/nerdit/daemon/web/dist/assets/index-CLfzcEzV.css → nerdit-0.9.2/src/nerdit/daemon/web/dist/assets/index-z-z38G0Q.css +1 -1
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/dist/index.html +2 -2
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/components/AuthGate.test.tsx +4 -1
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/components/AuthGate.tsx +19 -12
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/components/CommandPalette.tsx +4 -3
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/components/DaemonStatusBanner.tsx +11 -4
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/components/SecretsPanel.tsx +2 -3
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/components/ServiceShareCard.test.tsx +1 -1
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/components/ServiceShareCard.tsx +3 -7
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/components/bindings/GenericBindingsPanel.tsx +1 -3
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/components/bindings/sections.tsx +21 -9
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/components/settings/DoctorCard.tsx +33 -2
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/components/settings/RoutesCard.tsx +4 -11
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/components/ui.tsx +9 -18
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/lib/auth.ts +3 -0
- nerdit-0.9.2/src/nerdit/daemon/web/src/lib/button.ts +20 -0
- nerdit-0.9.2/src/nerdit/daemon/web/src/pages/AppStates.tsx +40 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/pages/Audit.tsx +9 -2
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/pages/Databases.tsx +13 -5
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/pages/Models.tsx +13 -5
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/pages/ProjectDetail.tsx +34 -35
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/pages/ProjectPage.test.tsx +1 -1
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/pages/ProjectPage.tsx +20 -10
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/pages/Projects.tsx +4 -2
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/pages/ServiceRedirect.tsx +10 -2
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/pages/Settings.tsx +3 -3
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/pages/Tokens.tsx +9 -2
- nerdit-0.9.2/src/nerdit/daemon/web/src/shell/AppShell.test.tsx +42 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/shell/AppShell.tsx +5 -4
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/shell/navItems.ts +9 -2
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/tests/e2e/01-login-and-overview.spec.ts +5 -4
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/tests/e2e/07-audit.spec.ts +1 -1
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/tests/e2e/10-settings.spec.ts +8 -1
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/tests/e2e/19-project-page.spec.ts +1 -1
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/tsconfig.tsbuildinfo +1 -1
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/db/queries/shares.py +32 -7
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/db/queries/workloads.py +14 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/mcp/tools/_shared.py +5 -4
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/mcp/tools/config.py +6 -4
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/mcp/tools/deploy.py +48 -11
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/mcp/tools/secrets.py +1 -1
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/mcp/tools/workspaces.py +14 -9
- nerdit-0.9.0/src/nerdit/daemon/web/dist/assets/index-DI2mvcfQ.js +0 -265
- {nerdit-0.9.0 → nerdit-0.9.2}/.gitignore +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/LICENSE +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/NOTICE +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/__main__.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/__init__.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/app.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/checks.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/client.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/commands/__init__.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/commands/ai.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/commands/backup.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/commands/capabilities.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/commands/check_deps.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/commands/config.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/commands/connect.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/commands/daemon.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/commands/db.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/commands/dev.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/commands/diagnose.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/commands/doctor.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/commands/domains.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/commands/events.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/commands/exit.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/commands/hooks.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/commands/init.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/commands/license.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/commands/link.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/commands/logs.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/commands/mcp.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/commands/models.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/commands/projects.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/commands/proxy.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/commands/routes.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/commands/secrets.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/commands/serve.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/commands/services.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/commands/share.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/commands/store.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/commands/system.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/commands/token.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/commands/trust.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/commands/uninstall.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/commands/update.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/commands/vars.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/display.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/config/__init__.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/config/app_templates/__init__.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/config/app_templates/builtin.json +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/config/app_templates/loader.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/config/redaction.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/config/settings.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/config/store.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/__init__.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/ai_gateway/__init__.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/ai_gateway/app.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/ai_gateway/keys.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/ai_gateway/proxy.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/backup.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/bindings/__init__.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/bindings/registry.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/bindings/secretref.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/builder.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/cutover.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/data/__init__.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/data/backend.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/data/binding.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/data/controller.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/discovery.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/dns.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/eventlog.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/events.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/gitsource.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/gitwatch.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/health.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/hooks.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/jobconfig.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/launch.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/license.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/link/__init__.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/link/capability.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/link/frames.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/link/hosted.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/link/identity.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/link/manager.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/models/__init__.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/models/backend.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/models/binding.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/models/controller.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/monitor.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/node_packages.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/node_runtime.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/notify.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/project_identity.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/proxy/__init__.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/proxy/admin.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/proxy/certs.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/proxy/domains.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/proxy/edgeauth.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/proxy/manager.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/proxy/routing.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/proxy/supervisor.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/proxy/tls.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/remediation_settle.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/resources/__init__.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/resources/controller.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/retention.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/runtime/__init__.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/runtime/container.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/runtime/docker.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/runtime/protocol.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/runtime/stub.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/sandbox.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/secrets.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/sweeper.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/variables.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/volumes.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/workload.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/workspaces.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/__init__.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/appfactory.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/audit.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/auth.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/bodylimit.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/bootstrap.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/errors.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/idempotency.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/imagegc.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/lifecycle.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/limits.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/middleware.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/project_delegation.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/routes/__init__.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/routes/_resources.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/routes/ai_gateway.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/routes/app_templates.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/routes/audit.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/routes/auth.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/routes/cluster.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/routes/config.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/routes/databases.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/routes/domains.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/routes/events.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/routes/gpus.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/routes/health.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/routes/hooks.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/routes/images.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/routes/license.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/routes/link.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/routes/models.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/routes/proxy.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/routes/secrets.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/routes/service_diagnose.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/routes/service_logs_stream.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/routes/service_run.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/routes/service_stats.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/routes/service_wait.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/routes/tokens.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/routes/workspaces.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/schemas/__init__.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/schemas/_base.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/schemas/audit.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/schemas/cluster.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/schemas/config.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/schemas/databases.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/schemas/deploy.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/schemas/events.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/schemas/gpus.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/schemas/health.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/schemas/license.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/schemas/link.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/schemas/models.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/schemas/proxy.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/schemas/service_diagnose.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/schemas/service_purge.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/schemas/service_run.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/schemas/service_stats.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/schemas/service_wait.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/schemas/services.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/schemas/system.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/schemas/tokens.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/schemas/workspaces.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/secret_scope.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/server.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/service_purge.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/sse.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/sweeps.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/uploads.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/views/__init__.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/views/hosted.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/views/service.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/dist/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/dist/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/dist/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/dist/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/dist/assets/geist-mono-cyrillic-ext-wght-normal-X_5orZeX.woff2 +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/dist/assets/geist-mono-cyrillic-wght-normal-DiZS0aHC.woff2 +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/dist/assets/geist-mono-latin-ext-wght-normal-Bwz-egvJ.woff2 +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/dist/assets/geist-mono-latin-wght-normal-XN7g48iV.woff2 +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/dist/assets/geist-mono-symbols2-wght-normal-CO5SzqOn.woff2 +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/dist/assets/geist-mono-vietnamese-wght-normal-DadHysG0.woff2 +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/dist/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/eslint.config.js +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/index.html +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/package-lock.json +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/package.json +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/playwright.config.ts +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/postcss.config.js +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/api/auditStream.ts +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/api/client.test.ts +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/api/client.ts +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/api/clusterEvents.ts +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/api/queries.test.ts +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/api/queries.ts +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/api/types.ts +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/components/ConfigurePanel.tsx +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/components/DeployDialog.tsx +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/components/DeployProgress.tsx +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/components/DiagnosePanel.tsx +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/components/Dropzone.tsx +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/components/ErrorBoundary.tsx +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/components/ServiceLogsPanel.tsx +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/components/ServicePublicUrl.tsx +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/components/TemplateDeployDialog.tsx +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/components/ToastViewport.tsx +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/components/UnlinkedBanner.test.tsx +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/components/UnlinkedBanner.tsx +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/components/bindings/AiEndpointFields.tsx +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/components/bindings/BindingSwitch.tsx +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/components/bindings/KeyFlow.tsx +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/components/bindings/useBindingDraft.ts +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/components/settings/AppearanceCard.tsx +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/components/settings/DiskCard.tsx +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/components/settings/GpuCard.tsx +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/components/settings/HostnameCard.tsx +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/components/settings/ProxyStatusCard.tsx +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/components/ui.test.tsx +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/lib/aiProviders.ts +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/lib/analytics.ts +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/lib/apiErrors.test.ts +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/lib/apiErrors.ts +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/lib/bindings.test.ts +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/lib/bindings.ts +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/lib/deployPhase.ts +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/lib/format.test.ts +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/lib/format.ts +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/lib/projects.test.ts +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/lib/projects.ts +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/lib/sse.test.ts +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/lib/sse.ts +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/lib/status.test.ts +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/lib/status.ts +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/lib/theme.test.ts +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/lib/theme.ts +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/main.tsx +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/pages/Login.test.tsx +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/pages/Login.tsx +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/pages/ProjectPolling.test.tsx +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/state/toastStore.ts +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/styles/globals.css +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/tailwind.config.js +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/tests/e2e/05-services.spec.ts +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/tests/e2e/06-models.spec.ts +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/tests/e2e/08-secrets.spec.ts +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/tests/e2e/11-configure.spec.ts +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/tests/e2e/12-deploy.spec.ts +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/tests/e2e/13-bindings.spec.ts +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/tests/e2e/14-responsive-shell.spec.ts +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/tests/e2e/17-projects.spec.ts +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/tests/e2e/18-navigation.spec.ts +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/tests/e2e/20-command-palette.spec.ts +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/tests/e2e/21-tokens.spec.ts +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/tests/e2e/22-system-cards.spec.ts +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/tests/e2e/fixtures.ts +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/tsconfig.json +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/vite.config.ts +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/db/__init__.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/db/database.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/db/enums.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/db/models.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/db/queries/__init__.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/db/queries/_base.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/db/queries/ai_gateway.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/db/queries/audit.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/db/queries/domains.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/db/queries/endpoints.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/db/queries/events.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/db/queries/gpus.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/db/queries/hooks.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/db/queries/idempotency.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/db/queries/logs.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/db/queries/projects.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/db/queries/secret_claims.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/db/queries/service_config.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/db/queries/services.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/db/queries/tokens.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/db/rows.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/mcp/__init__.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/mcp/errors.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/mcp/server.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/mcp/tools/__init__.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/mcp/tools/ai_gateway.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/mcp/tools/cluster.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/mcp/tools/databases.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/mcp/tools/exposure.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/mcp/tools/models.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/mcp/tools/projects.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/mcp/tools/services.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/mcp/tools/system.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/mcp/transport.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/utils/__init__.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/utils/certs.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/utils/disk.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/utils/frozen.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/utils/fs.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/utils/ids.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/utils/install_layout.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/utils/logging.py +0 -0
- {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/utils/names.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: nerdit
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.2
|
|
4
4
|
Summary: The local AI PaaS — deploy and operate apps on your own hardware, with first-class local AI
|
|
5
5
|
Project-URL: Homepage, https://github.com/nerdit-ai/nerdit
|
|
6
6
|
Project-URL: Repository, https://github.com/nerdit-ai/nerdit
|
|
@@ -160,10 +160,10 @@ token.
|
|
|
160
160
|
|
|
161
161
|
### Claude on the web or desktop (remote, through Nerdit Cloud)
|
|
162
162
|
|
|
163
|
-
Link the machine with `nerdit link`. Then open **
|
|
163
|
+
Link the machine with `nerdit link`. Then open **Agents** in
|
|
164
164
|
[app.nerdit.ai](https://app.nerdit.ai) and add the connection URL it shows as a
|
|
165
165
|
custom connector in Claude. You approve which machines or projects the
|
|
166
|
-
|
|
166
|
+
agent can reach. No token leaves your machine. See the
|
|
167
167
|
[guide](https://docs.nerdit.ai/app/connect-assistant).
|
|
168
168
|
|
|
169
169
|
### Over HTTP
|
|
@@ -272,10 +272,10 @@ Questions or feedback: [feedback@nerdit.ai](mailto:feedback@nerdit.ai).
|
|
|
272
272
|
|
|
273
273
|
## License
|
|
274
274
|
|
|
275
|
-
Apache-2.0, see [LICENSE](https://github.com/nerdit-ai/nerdit/blob/v0.9.
|
|
276
|
-
are not covered by the code license, see [NOTICE](https://github.com/nerdit-ai/nerdit/blob/v0.9.
|
|
275
|
+
Apache-2.0, see [LICENSE](https://github.com/nerdit-ai/nerdit/blob/v0.9.2/LICENSE). "Nerdit" and the logo are trademarks and
|
|
276
|
+
are not covered by the code license, see [NOTICE](https://github.com/nerdit-ai/nerdit/blob/v0.9.2/NOTICE).
|
|
277
277
|
|
|
278
278
|
## Contributing
|
|
279
279
|
|
|
280
|
-
Issues and pull requests are welcome. [CONTRIBUTING.md](https://github.com/nerdit-ai/nerdit/blob/v0.9.
|
|
280
|
+
Issues and pull requests are welcome. [CONTRIBUTING.md](https://github.com/nerdit-ai/nerdit/blob/v0.9.2/CONTRIBUTING.md)
|
|
281
281
|
explains how contributions land in releases.
|
|
@@ -101,10 +101,10 @@ token.
|
|
|
101
101
|
|
|
102
102
|
### Claude on the web or desktop (remote, through Nerdit Cloud)
|
|
103
103
|
|
|
104
|
-
Link the machine with `nerdit link`. Then open **
|
|
104
|
+
Link the machine with `nerdit link`. Then open **Agents** in
|
|
105
105
|
[app.nerdit.ai](https://app.nerdit.ai) and add the connection URL it shows as a
|
|
106
106
|
custom connector in Claude. You approve which machines or projects the
|
|
107
|
-
|
|
107
|
+
agent can reach. No token leaves your machine. See the
|
|
108
108
|
[guide](https://docs.nerdit.ai/app/connect-assistant).
|
|
109
109
|
|
|
110
110
|
### Over HTTP
|
|
@@ -213,10 +213,10 @@ Questions or feedback: [feedback@nerdit.ai](mailto:feedback@nerdit.ai).
|
|
|
213
213
|
|
|
214
214
|
## License
|
|
215
215
|
|
|
216
|
-
Apache-2.0, see [LICENSE](https://github.com/nerdit-ai/nerdit/blob/v0.9.
|
|
217
|
-
are not covered by the code license, see [NOTICE](https://github.com/nerdit-ai/nerdit/blob/v0.9.
|
|
216
|
+
Apache-2.0, see [LICENSE](https://github.com/nerdit-ai/nerdit/blob/v0.9.2/LICENSE). "Nerdit" and the logo are trademarks and
|
|
217
|
+
are not covered by the code license, see [NOTICE](https://github.com/nerdit-ai/nerdit/blob/v0.9.2/NOTICE).
|
|
218
218
|
|
|
219
219
|
## Contributing
|
|
220
220
|
|
|
221
|
-
Issues and pull requests are welcome. [CONTRIBUTING.md](https://github.com/nerdit-ai/nerdit/blob/v0.9.
|
|
221
|
+
Issues and pull requests are welcome. [CONTRIBUTING.md](https://github.com/nerdit-ai/nerdit/blob/v0.9.2/CONTRIBUTING.md)
|
|
222
222
|
explains how contributions land in releases.
|
|
@@ -19,6 +19,7 @@ import typer
|
|
|
19
19
|
from nerdit.cli.commands.deploy import (
|
|
20
20
|
_git_name_default,
|
|
21
21
|
_maybe_wait,
|
|
22
|
+
_note_skipped_excluded,
|
|
22
23
|
_note_skipped_secrets,
|
|
23
24
|
_render_dry_run,
|
|
24
25
|
)
|
|
@@ -130,8 +131,10 @@ def _folder_source(path: str | None, max_bytes: int) -> tuple[str, bytes]:
|
|
|
130
131
|
raise _fail(_plain(str(exc))) from exc
|
|
131
132
|
console.print(f"[dim]Creating archive of {directory}...[/dim]")
|
|
132
133
|
skipped_secrets: list[str] = []
|
|
133
|
-
|
|
134
|
+
skipped_excluded: list[str] = []
|
|
135
|
+
zip_bytes = create_dir_zip(directory, skipped_secrets, skipped_excluded)
|
|
134
136
|
_note_skipped_secrets(skipped_secrets)
|
|
137
|
+
_note_skipped_excluded(skipped_excluded)
|
|
135
138
|
try:
|
|
136
139
|
check_upload_size(zip_bytes, max_bytes)
|
|
137
140
|
except ValueError as exc:
|
|
@@ -23,6 +23,7 @@ from nerdit.cli.display import (
|
|
|
23
23
|
render_client_error,
|
|
24
24
|
wait_exit_code,
|
|
25
25
|
)
|
|
26
|
+
from nerdit.config.project import _DISPLAY_UNSAFE_RE
|
|
26
27
|
|
|
27
28
|
|
|
28
29
|
def parse_env_pairs(items: list[str]) -> dict[str, str]:
|
|
@@ -159,6 +160,15 @@ def _note_skipped_secrets(skipped: list[str]) -> None:
|
|
|
159
160
|
)
|
|
160
161
|
|
|
161
162
|
|
|
163
|
+
def _note_skipped_excluded(skipped: list[str]) -> None:
|
|
164
|
+
"""Name the default-excluded paths left out of the archive, capped at ten."""
|
|
165
|
+
if skipped:
|
|
166
|
+
# Names come from the deployed tree: strip terminal and bidi controls.
|
|
167
|
+
shown = ", ".join(_plain(_DISPLAY_UNSAFE_RE.sub("?", p)) for p in skipped[:10])
|
|
168
|
+
more = f" (+{len(skipped) - 10} more)" if len(skipped) > 10 else ""
|
|
169
|
+
console.print(f"[dim]Not uploaded (excluded by default): {shown}{more}[/dim]")
|
|
170
|
+
|
|
171
|
+
|
|
162
172
|
def _git_name_default(repo_url: str, subdir: str | None) -> str:
|
|
163
173
|
"""Derive a service name from a Git deploy: subdir basename, else repo slug.
|
|
164
174
|
|
|
@@ -420,8 +430,10 @@ async def _deploy_async(
|
|
|
420
430
|
|
|
421
431
|
console.print(f"[dim]Creating archive of {directory}...[/dim]")
|
|
422
432
|
skipped_secrets: list[str] = []
|
|
423
|
-
|
|
433
|
+
skipped_excluded: list[str] = []
|
|
434
|
+
zip_bytes = create_dir_zip(directory, skipped_secrets, skipped_excluded)
|
|
424
435
|
_note_skipped_secrets(skipped_secrets)
|
|
436
|
+
_note_skipped_excluded(skipped_excluded)
|
|
425
437
|
|
|
426
438
|
try:
|
|
427
439
|
# Ask the daemon for its cap only past the local default: an archive
|
|
@@ -57,11 +57,26 @@ def should_exclude(rel_path: str) -> bool:
|
|
|
57
57
|
return matches_exclude_pattern(Path(rel_path).parts)
|
|
58
58
|
|
|
59
59
|
|
|
60
|
-
def
|
|
60
|
+
def _excluded_top(rel: str) -> str:
|
|
61
|
+
"""Return the shortest excluded prefix of *rel*, ``dir/`` for a directory."""
|
|
62
|
+
parts = Path(rel).parts
|
|
63
|
+
for i, part in enumerate(parts):
|
|
64
|
+
if matches_exclude_pattern((part,)):
|
|
65
|
+
return "/".join(parts[: i + 1]) + ("/" if i < len(parts) - 1 else "")
|
|
66
|
+
return rel # only the symlink target matched
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def create_dir_zip(
|
|
70
|
+
directory: str | Path,
|
|
71
|
+
skipped_secrets: list[str] | None = None,
|
|
72
|
+
skipped_excluded: list[str] | None = None,
|
|
73
|
+
) -> bytes:
|
|
61
74
|
"""Create a ZIP archive of a whole directory.
|
|
62
75
|
|
|
63
76
|
Zips the directory contents recursively for `nerdit deploy`. Arcnames are
|
|
64
|
-
relative to the directory root and `ZIP_EXCLUDE_PATTERNS` apply
|
|
77
|
+
relative to the directory root and `ZIP_EXCLUDE_PATTERNS` apply; the top
|
|
78
|
+
excluded component of each dropped path (e.g. ``node_modules/``) is
|
|
79
|
+
appended once to *skipped_excluded* when given. Symlinks
|
|
65
80
|
resolving outside the directory are dropped (they would upload their
|
|
66
81
|
target), and so are `.env` / `.env.*` files, which would be baked into the
|
|
67
82
|
image; their relative paths are appended to *skipped_secrets* when given.
|
|
@@ -81,6 +96,10 @@ def create_dir_zip(directory: str | Path, skipped_secrets: list[str] | None = No
|
|
|
81
96
|
# path must pass the same filters as the link's own name.
|
|
82
97
|
target_rel = str(target.relative_to(base_dir))
|
|
83
98
|
if should_exclude(rel) or should_exclude(target_rel):
|
|
99
|
+
if skipped_excluded is not None:
|
|
100
|
+
top = _excluded_top(rel)
|
|
101
|
+
if top not in skipped_excluded:
|
|
102
|
+
skipped_excluded.append(top)
|
|
84
103
|
continue
|
|
85
104
|
if is_secret_basename(rel) or is_secret_basename(target_rel):
|
|
86
105
|
if skipped_secrets is not None:
|
|
@@ -59,6 +59,7 @@ def validate_deploy_fields(
|
|
|
59
59
|
# blob (a ZIP's nerdit.toml table, a PUT body value, or a persisted config
|
|
60
60
|
# key) and EdgeAuthConfig is the thing that decides whether it is one.
|
|
61
61
|
edge_auth: Any = None,
|
|
62
|
+
forward_authorization: bool | None = None,
|
|
62
63
|
) -> DeployConfig:
|
|
63
64
|
"""Validate effective deployment fields with DeployConfig.
|
|
64
65
|
|
|
@@ -81,6 +82,7 @@ def validate_deploy_fields(
|
|
|
81
82
|
cutover=cutover,
|
|
82
83
|
auto_deploy=auto_deploy,
|
|
83
84
|
edge_auth=edge_auth,
|
|
85
|
+
forward_authorization=forward_authorization,
|
|
84
86
|
)
|
|
85
87
|
except ValidationError as exc:
|
|
86
88
|
raise NerditError(
|
|
@@ -96,7 +98,8 @@ def validate_deploy_fields(
|
|
|
96
98
|
"release a single-line command (<=4096 chars); "
|
|
97
99
|
"cutover/auto_deploy booleans (null = daemon default); "
|
|
98
100
|
"edge_auth = {user = '<name>', password = '${secrets.KEY}'} "
|
|
99
|
-
"(a secret reference, never a literal password)
|
|
101
|
+
"(a secret reference, never a literal password); "
|
|
102
|
+
"forward_authorization a boolean, not combined with edge_auth."
|
|
100
103
|
),
|
|
101
104
|
) from exc
|
|
102
105
|
|
|
@@ -320,6 +323,7 @@ async def validate_app_config(
|
|
|
320
323
|
cutover=deploy.get("cutover"),
|
|
321
324
|
auto_deploy=deploy.get("auto_deploy"),
|
|
322
325
|
edge_auth=deploy.get("edge_auth"),
|
|
326
|
+
forward_authorization=deploy.get("forward_authorization"),
|
|
323
327
|
)
|
|
324
328
|
ai_section = effective_doc.get("ai")
|
|
325
329
|
bindings: dict[str, AiBindingConfig] | None = None
|
|
@@ -148,7 +148,10 @@ class BuildSettings(BaseModel):
|
|
|
148
148
|
raise ValueError(
|
|
149
149
|
"public_env values must be single lines of at most 4096 characters."
|
|
150
150
|
)
|
|
151
|
-
|
|
151
|
+
# Deferred: config.project imports this module.
|
|
152
|
+
from nerdit.config.project import ENV_REF_PREFIXES
|
|
153
|
+
|
|
154
|
+
if any(p in item for p in ENV_REF_PREFIXES):
|
|
152
155
|
raise ValueError(
|
|
153
156
|
"public_env values are embedded in public build output; secret references "
|
|
154
157
|
"are not allowed."
|
|
@@ -153,7 +153,7 @@ DEFAULT_LINK_RENEW_MARGIN_S = 120
|
|
|
153
153
|
DEFAULT_UPLOAD_DIR = Path("~/.nerdit/uploads")
|
|
154
154
|
DEFAULT_MAX_UPLOAD_BYTES = 500 * 1024 * 1024 # 500 MB
|
|
155
155
|
UPLOAD_WARNING_BYTES = 100 * 1024 * 1024 # 100 MB
|
|
156
|
-
ZIP_EXCLUDE_PATTERNS = {".git", "
|
|
156
|
+
ZIP_EXCLUDE_PATTERNS = {".git", "*.pyc", "__pycache__", ".venv", "node_modules"}
|
|
157
157
|
|
|
158
158
|
|
|
159
159
|
def matches_exclude_pattern(parts: Sequence[str], *, casefold: bool = False) -> bool:
|
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import os
|
|
6
6
|
import re
|
|
7
7
|
import tomllib
|
|
8
|
+
from collections.abc import Mapping
|
|
8
9
|
from pathlib import Path
|
|
9
10
|
from typing import Literal
|
|
10
11
|
from urllib.parse import urlsplit
|
|
@@ -73,6 +74,34 @@ def rewrite_vars_ref(value: str | None) -> str | None:
|
|
|
73
74
|
return value
|
|
74
75
|
|
|
75
76
|
|
|
77
|
+
#: Reserved `${…}` namespaces: a value containing one is a platform reference,
|
|
78
|
+
#: never text an app may receive (ordinary `${VAR:-x}` templates stay untouched).
|
|
79
|
+
ENV_REF_PREFIXES = ("${secrets.", "${vars.", "${github.")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def env_ref_problem(env: Mapping[str, str | None] | None) -> str | None:
|
|
83
|
+
"""Return why an app env map holds a malformed reserved reference, else `None`.
|
|
84
|
+
|
|
85
|
+
A value using a reserved namespace must be exactly one
|
|
86
|
+
`${secrets[.shared].KEY}` (or `${vars…}`) reference. The message names the
|
|
87
|
+
env key only, never its value.
|
|
88
|
+
"""
|
|
89
|
+
for key, value in (env or {}).items():
|
|
90
|
+
if value is None or not any(p in value for p in ENV_REF_PREFIXES):
|
|
91
|
+
continue
|
|
92
|
+
if "${github." in value:
|
|
93
|
+
return (
|
|
94
|
+
f"env {key}: ${{github.installation}} is only valid as a git token_ref, "
|
|
95
|
+
"not as an app env value."
|
|
96
|
+
)
|
|
97
|
+
if not SECRET_REF_RE.fullmatch(rewrite_vars_ref(value) or ""):
|
|
98
|
+
return (
|
|
99
|
+
f"env {key}: a ${{secrets.*}} reference must be the whole value: "
|
|
100
|
+
"${secrets.KEY} or ${secrets.shared.KEY}."
|
|
101
|
+
)
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
|
|
76
105
|
# [hooks.<name>] grammar (inbound webhooks). The name maps to the
|
|
77
106
|
# NERDIT_HOOK_<NAME>_SECRET / _URL keys: no '_', so the '-' -> '_' fold is injective.
|
|
78
107
|
HOOK_SENDERS = ("stripe", "github", "slack", "hmac", "none")
|
|
@@ -374,6 +403,20 @@ class DeployConfig(BaseModel):
|
|
|
374
403
|
# Like other non-form deployment fields, auth carries forward when omitted.
|
|
375
404
|
# Only an explicit edge_auth=null removes protection; no container restart needed.
|
|
376
405
|
edge_auth: EdgeAuthConfig | None = None
|
|
406
|
+
# Hand the browser's Authorization to the app on its hosted and cloud custom
|
|
407
|
+
# domain URLs (the cloud ships it in a carrier header the tunnel mux renames).
|
|
408
|
+
# Carries forward like cutover; clear it with deploy forward_authorization=null.
|
|
409
|
+
forward_authorization: bool | None = None
|
|
410
|
+
|
|
411
|
+
@model_validator(mode="after")
|
|
412
|
+
def _check_forward_authorization(self) -> DeployConfig:
|
|
413
|
+
"""Refuse forward_authorization alongside edge_auth, which owns Authorization."""
|
|
414
|
+
if self.forward_authorization and self.edge_auth is not None:
|
|
415
|
+
raise ValueError(
|
|
416
|
+
"Invalid [deploy] forward_authorization: cannot be combined with "
|
|
417
|
+
"edge_auth, which owns the Authorization header on the local proxy path."
|
|
418
|
+
)
|
|
419
|
+
return self
|
|
377
420
|
|
|
378
421
|
@field_validator("name")
|
|
379
422
|
@classmethod
|
|
@@ -22,11 +22,13 @@ from __future__ import annotations
|
|
|
22
22
|
import asyncio
|
|
23
23
|
import json
|
|
24
24
|
import logging
|
|
25
|
+
import os
|
|
25
26
|
import shutil
|
|
26
27
|
from datetime import UTC, datetime
|
|
27
28
|
from typing import TYPE_CHECKING
|
|
28
29
|
|
|
29
30
|
from nerdit.core.deploy_state import record_deploy_settled, stamp_last_deploy
|
|
31
|
+
from nerdit.core.eventlog import record_job_event
|
|
30
32
|
from nerdit.core.jobconfig import parse_job_config
|
|
31
33
|
from nerdit.core.launch import (
|
|
32
34
|
_SCRUB_MIN_VALUE_LEN,
|
|
@@ -175,6 +177,8 @@ def _sensitive_env_values(resolved: "ResolvedLaunchEnv") -> set[str]:
|
|
|
175
177
|
# Empty values are dropped here rather than downstream: a "" target would
|
|
176
178
|
# splice `***` between every character of every line.
|
|
177
179
|
values = {value for value in resolved.secret_env.values() if value}
|
|
180
|
+
# A resolved `${secrets…}` env ref is a declared secret under any key name.
|
|
181
|
+
values.update(v for k in resolved.ref_env_keys if (v := resolved.env.get(k)))
|
|
178
182
|
for key, value in resolved.env.items():
|
|
179
183
|
if value and len(value) >= _SCRUB_MIN_VALUE_LEN and _is_credential_key(key):
|
|
180
184
|
values.add(value)
|
|
@@ -270,6 +274,17 @@ class AppImageBuilder:
|
|
|
270
274
|
# must keep intercepting this predicate.
|
|
271
275
|
if await self._c._needs_build(job):
|
|
272
276
|
if not self._task_in_flight(job.id):
|
|
277
|
+
cfg = parse_job_config(job, warn=True)
|
|
278
|
+
if not os.path.isdir(str(cfg["build_context_dir"])):
|
|
279
|
+
# `build` removes the context on either outcome, so this
|
|
280
|
+
# generation already built and its image went missing
|
|
281
|
+
# afterwards (a woken box restores images after boot).
|
|
282
|
+
# Rebuilding from nothing fails the app, and launching burns
|
|
283
|
+
# the restart budget into crash_loop: hold until it returns.
|
|
284
|
+
await self._c._log_secret_wait(
|
|
285
|
+
job, f"Waiting for image {cfg['image']} to be present locally"
|
|
286
|
+
)
|
|
287
|
+
return True
|
|
273
288
|
# Boot touch-up: a row stuck at phase "building" with
|
|
274
289
|
# no live build task (e.g. a daemon restart mid-build) regresses
|
|
275
290
|
# to "queued" — the freshly spawned build re-stamps "building"
|
|
@@ -424,6 +439,7 @@ class AppImageBuilder:
|
|
|
424
439
|
# The build context is only needed for the build itself; drop it on EITHER
|
|
425
440
|
# outcome (success or a failed build) so successive deploys never leak
|
|
426
441
|
# extracted upload dirs.
|
|
442
|
+
cancelled = False
|
|
427
443
|
try:
|
|
428
444
|
try:
|
|
429
445
|
# Hold the global build semaphore only for the actual image
|
|
@@ -499,8 +515,14 @@ class AppImageBuilder:
|
|
|
499
515
|
if not await self._run_release(job, image):
|
|
500
516
|
return
|
|
501
517
|
await self._c._prune_old_images(job)
|
|
518
|
+
except asyncio.CancelledError:
|
|
519
|
+
# Shutdown cancelled the build: keep the context so the next boot
|
|
520
|
+
# rebuilds. A missing context means the build ran (`ensure_built`).
|
|
521
|
+
cancelled = True
|
|
522
|
+
raise
|
|
502
523
|
finally:
|
|
503
|
-
|
|
524
|
+
if not cancelled:
|
|
525
|
+
await self.cleanup_context(job, cleanup_dir)
|
|
504
526
|
|
|
505
527
|
async def _run_release(self, job: Job, image: str) -> bool:
|
|
506
528
|
"""Run the built generation's release and contain unexpected task failures.
|
|
@@ -1201,6 +1223,7 @@ class AppImageBuilder:
|
|
|
1201
1223
|
# never fire on a newer generation's behalf. The crash-loop sibling in
|
|
1202
1224
|
# `services.py` settles a DIFFERENT, deliberately
|
|
1203
1225
|
# unguarded scenario and is untouched here.
|
|
1226
|
+
prior_ld = parse_job_config(job).get("last_deploy")
|
|
1204
1227
|
if not await self._write_terminal_settle(
|
|
1205
1228
|
job.id,
|
|
1206
1229
|
target_version=target_version,
|
|
@@ -1222,6 +1245,7 @@ class AppImageBuilder:
|
|
|
1222
1245
|
# Fixed action on the terminal branch (never `audit_action`): a fresh
|
|
1223
1246
|
# deploy that never came up is a `service.failed`, whatever killed it.
|
|
1224
1247
|
await _audit_outcome("service.failed")
|
|
1248
|
+
await self._record_healthy_generation_failed(job, prior_ld, reason, error_class)
|
|
1225
1249
|
logger.warning("%s for service %s: %s", label, job.service_name or job.id, message)
|
|
1226
1250
|
# NOT reclaimable — the one branch that keeps the build markers. The
|
|
1227
1251
|
# row still carries `build_context_dir`, so removing the tag would
|
|
@@ -1231,6 +1255,24 @@ class AppImageBuilder:
|
|
|
1231
1255
|
# tag keeps layer 3 in charge: it re-settles the restart instead.
|
|
1232
1256
|
return False
|
|
1233
1257
|
|
|
1258
|
+
async def _record_healthy_generation_failed(
|
|
1259
|
+
self, job: Job, prior_ld: object, reason: str, error_class: ErrorClass
|
|
1260
|
+
) -> None:
|
|
1261
|
+
"""Put a terminal settle of an already-healthy generation on the event feed.
|
|
1262
|
+
|
|
1263
|
+
The phase writer emits only a generation's first terminal settle, so a
|
|
1264
|
+
healthy generation failed later (a rebuild on wake) would leave the feed
|
|
1265
|
+
silent. Fixed tokens only, never the failure message.
|
|
1266
|
+
"""
|
|
1267
|
+
if isinstance(prior_ld, dict) and prior_ld.get("phase") == "healthy":
|
|
1268
|
+
await record_job_event(
|
|
1269
|
+
self._c._events,
|
|
1270
|
+
"service.failed",
|
|
1271
|
+
job,
|
|
1272
|
+
reason=reason,
|
|
1273
|
+
data={"error_class": error_class.value},
|
|
1274
|
+
)
|
|
1275
|
+
|
|
1234
1276
|
async def _emit_bypass_settle(self, job: Job, blob: dict) -> None:
|
|
1235
1277
|
"""Emit the terminal deploy event for branches that bypass the phase writer.
|
|
1236
1278
|
|
|
@@ -82,7 +82,14 @@ async def stamp_last_deploy(
|
|
|
82
82
|
and getattr(row.kind, "value", row.kind) == "service"
|
|
83
83
|
):
|
|
84
84
|
new_ld["remediation_code"] = settle_remediation_code(cfg, new_ld)
|
|
85
|
-
|
|
85
|
+
# The version match is re-checked at write time: a redeploy landing between
|
|
86
|
+
# the read and the write owns the row and must not get this stamp.
|
|
87
|
+
expect = ld.get("version") if require_version_match else None
|
|
88
|
+
if not await queries.patch_job_config(
|
|
89
|
+
job_id,
|
|
90
|
+
{"last_deploy": new_ld},
|
|
91
|
+
expect_build_version=expect if isinstance(expect, int) else None,
|
|
92
|
+
):
|
|
86
93
|
return False
|
|
87
94
|
|
|
88
95
|
# Durable settle event — STRICTLY after the real write, past
|
|
@@ -16,7 +16,7 @@ logger = logging.getLogger("nerdit.link.apps")
|
|
|
16
16
|
class ShareLookup(Protocol):
|
|
17
17
|
"""Atomic hosted routing query used by both stream admission and revocation."""
|
|
18
18
|
|
|
19
|
-
def resolve_shared_app(
|
|
19
|
+
def resolve_shared_app( # noqa: PLR0913 - the routing facts of one stream
|
|
20
20
|
self,
|
|
21
21
|
node_id: str,
|
|
22
22
|
service_name: str,
|
|
@@ -24,8 +24,9 @@ class ShareLookup(Protocol):
|
|
|
24
24
|
job_id: str | None,
|
|
25
25
|
access: str | None,
|
|
26
26
|
legacy_host: str | None,
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
nodes_domain: str | None,
|
|
28
|
+
) -> Awaitable[tuple[str, int, str, bool] | None]:
|
|
29
|
+
"""Return the current job, live port, admitted audience and opt-in, or refuse."""
|
|
29
30
|
|
|
30
31
|
|
|
31
32
|
class AppStreamResolver:
|
|
@@ -86,12 +87,14 @@ class AppStreamResolver:
|
|
|
86
87
|
else None
|
|
87
88
|
)
|
|
88
89
|
result = await self._queries.resolve_shared_app(
|
|
89
|
-
self._node_id, service_name, authority, job_id, access, legacy_host
|
|
90
|
+
self._node_id, service_name, authority, job_id, access, legacy_host, self._domain
|
|
90
91
|
)
|
|
91
92
|
if result is None:
|
|
92
93
|
return None
|
|
93
|
-
admitted_job, port, audience = result
|
|
94
|
-
return AppTarget(
|
|
94
|
+
admitted_job, port, audience, forward_authorization = result
|
|
95
|
+
return AppTarget(
|
|
96
|
+
service_name, port, authority, admitted_job, audience, forward_authorization
|
|
97
|
+
)
|
|
95
98
|
|
|
96
99
|
|
|
97
100
|
__all__ = ["AppStreamResolver", "ShareLookup"]
|
|
@@ -670,6 +670,8 @@ class AppTarget:
|
|
|
670
670
|
host: str
|
|
671
671
|
job_id: str | None = None
|
|
672
672
|
access: str | None = None
|
|
673
|
+
#: The app's `[deploy].forward_authorization` opt-in, re-read per stream.
|
|
674
|
+
forward_authorization: bool = False
|
|
673
675
|
|
|
674
676
|
|
|
675
677
|
@dataclass(frozen=True, slots=True)
|
|
@@ -109,6 +109,10 @@ _REQUEST_DROPPED = frozenset({"host", "content-length"})
|
|
|
109
109
|
APP_HEADER = "x-nerdit-app"
|
|
110
110
|
APP_JOB_HEADER = "x-nerdit-app-job"
|
|
111
111
|
APP_ACCESS_HEADER = "x-nerdit-app-access"
|
|
112
|
+
#: The browser's own Authorization, renamed by the cloud because `authorization`
|
|
113
|
+
#: carries the relay capability on node-link/v1. Consumed here and handed to the
|
|
114
|
+
#: app as `authorization` only when it opted in (`[deploy].forward_authorization`).
|
|
115
|
+
APP_AUTHORIZATION_HEADER = "x-nerdit-app-authorization"
|
|
112
116
|
|
|
113
117
|
#: The cloud's control-plane carrier (P32 D-P32-2) — the header that marks a
|
|
114
118
|
#: stream as framed by the cloud's own server-side code rather than by a user,
|
|
@@ -147,6 +151,7 @@ _APP_REQUEST_DROPPED = frozenset(
|
|
|
147
151
|
APP_HEADER,
|
|
148
152
|
APP_JOB_HEADER,
|
|
149
153
|
APP_ACCESS_HEADER,
|
|
154
|
+
APP_AUTHORIZATION_HEADER,
|
|
150
155
|
"x-forwarded-host",
|
|
151
156
|
"forwarded",
|
|
152
157
|
CLOUD_CONTROL_HEADER,
|
|
@@ -217,16 +222,29 @@ _SUBMITTER = "submitter"
|
|
|
217
222
|
|
|
218
223
|
_BEARER_PREFIX = "Bearer "
|
|
219
224
|
|
|
225
|
+
#: `nerdit.daemon.auth.TOKEN_PREFIX`, mirrored because `core` must not import `daemon`.
|
|
226
|
+
_DAEMON_TOKEN_PREFIX = "nrd_"
|
|
227
|
+
|
|
220
228
|
|
|
221
229
|
def _app_request(
|
|
222
|
-
frame: OpenStream,
|
|
230
|
+
frame: OpenStream,
|
|
231
|
+
target: AppTarget,
|
|
232
|
+
admission: HookAdmission | None,
|
|
233
|
+
authorization: str | None = None,
|
|
223
234
|
) -> tuple[str, list[Header]]:
|
|
224
|
-
"""
|
|
235
|
+
"""Build the loopback URL and filtered headers an admitted app stream is dialled with.
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
authorization: The browser credential to hand the app as `authorization`,
|
|
239
|
+
already vetted by `StreamMux._app_authorization`.
|
|
240
|
+
"""
|
|
225
241
|
headers = [
|
|
226
242
|
(name, value)
|
|
227
243
|
for name, value in frame.headers
|
|
228
244
|
if name.lower() not in HOP_BY_HOP and name.lower() not in _APP_REQUEST_DROPPED
|
|
229
245
|
]
|
|
246
|
+
if authorization is not None:
|
|
247
|
+
headers.append(("authorization", authorization))
|
|
230
248
|
# httpx honours an explicit `Host` over the URL authority
|
|
231
249
|
# (`Request._prepare` uses `setdefault`), so the app sees the name
|
|
232
250
|
# the browser typed while the socket still goes to loopback.
|
|
@@ -548,6 +566,25 @@ class StreamMux:
|
|
|
548
566
|
return False
|
|
549
567
|
return bool(self._ctx.validate_capability(token, _SUBMITTER))
|
|
550
568
|
|
|
569
|
+
def _app_authorization(self, frame: OpenStream, target: AppTarget) -> str | None:
|
|
570
|
+
"""Return the browser Authorization an opted-in app may receive, else None.
|
|
571
|
+
|
|
572
|
+
A daemon token or this link's capability is never handed to an app, even
|
|
573
|
+
when the cloud relays one in the carrier.
|
|
574
|
+
"""
|
|
575
|
+
value = frame.header(APP_AUTHORIZATION_HEADER)
|
|
576
|
+
if not target.forward_authorization or value is None:
|
|
577
|
+
return None
|
|
578
|
+
# Whatever the scheme: the bare value and the credential after it.
|
|
579
|
+
candidates = (value.strip(), value.strip().partition(" ")[2].strip())
|
|
580
|
+
if any(
|
|
581
|
+
c.startswith(_DAEMON_TOKEN_PREFIX) or self._ctx.validate_capability(c, _SUBMITTER)
|
|
582
|
+
for c in candidates
|
|
583
|
+
if c
|
|
584
|
+
):
|
|
585
|
+
return None
|
|
586
|
+
return value
|
|
587
|
+
|
|
551
588
|
# -- serving -------------------------------------------------------------
|
|
552
589
|
|
|
553
590
|
async def _serve(self, stream: _Stream) -> None:
|
|
@@ -672,6 +709,7 @@ class StreamMux:
|
|
|
672
709
|
APP_HEADER,
|
|
673
710
|
APP_JOB_HEADER,
|
|
674
711
|
APP_ACCESS_HEADER,
|
|
712
|
+
APP_AUTHORIZATION_HEADER,
|
|
675
713
|
"x-forwarded-host",
|
|
676
714
|
HOOK_HEADER,
|
|
677
715
|
DELIVERY_HEADER,
|
|
@@ -703,7 +741,9 @@ class StreamMux:
|
|
|
703
741
|
await self._send_refusal(frame, *_HOOK_REFUSALS[503 if admission else 404])
|
|
704
742
|
return
|
|
705
743
|
|
|
706
|
-
url, headers = _app_request(
|
|
744
|
+
url, headers = _app_request(
|
|
745
|
+
frame, target, admission, self._app_authorization(frame, target)
|
|
746
|
+
)
|
|
707
747
|
|
|
708
748
|
async def validate_connected_target(event: str, _info: dict[str, object]) -> None:
|
|
709
749
|
# httpcore is pinned: this boundary is AFTER socket acquisition but
|