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.
Files changed (388) hide show
  1. {nerdit-0.9.0 → nerdit-0.9.2}/PKG-INFO +6 -6
  2. {nerdit-0.9.0 → nerdit-0.9.2}/README.md +5 -5
  3. {nerdit-0.9.0 → nerdit-0.9.2}/pyproject.toml +1 -1
  4. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/__init__.py +1 -1
  5. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/commands/apply.py +4 -1
  6. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/commands/deploy.py +13 -1
  7. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/upload.py +21 -2
  8. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/config/app_config.py +5 -1
  9. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/config/build.py +4 -1
  10. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/config/defaults.py +1 -1
  11. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/config/project.py +43 -0
  12. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/app_build.py +43 -1
  13. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/deploy_state.py +8 -1
  14. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/link/apps.py +9 -6
  15. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/link/client.py +2 -0
  16. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/link/mux.py +43 -3
  17. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/services.py +120 -30
  18. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/deploy_pipeline.py +359 -19
  19. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/remediation.py +12 -2
  20. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/routes/app_config.py +11 -0
  21. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/routes/deploy.py +15 -2
  22. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/routes/projects.py +7 -2
  23. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/routes/services.py +9 -2
  24. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/routes/share.py +2 -0
  25. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/routes/system.py +3 -0
  26. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/schemas/exposure.py +10 -0
  27. nerdit-0.9.2/src/nerdit/daemon/web/dist/assets/index-Bo4WRmzm.js +265 -0
  28. 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
  29. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/dist/index.html +2 -2
  30. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/components/AuthGate.test.tsx +4 -1
  31. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/components/AuthGate.tsx +19 -12
  32. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/components/CommandPalette.tsx +4 -3
  33. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/components/DaemonStatusBanner.tsx +11 -4
  34. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/components/SecretsPanel.tsx +2 -3
  35. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/components/ServiceShareCard.test.tsx +1 -1
  36. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/components/ServiceShareCard.tsx +3 -7
  37. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/components/bindings/GenericBindingsPanel.tsx +1 -3
  38. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/components/bindings/sections.tsx +21 -9
  39. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/components/settings/DoctorCard.tsx +33 -2
  40. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/components/settings/RoutesCard.tsx +4 -11
  41. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/components/ui.tsx +9 -18
  42. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/lib/auth.ts +3 -0
  43. nerdit-0.9.2/src/nerdit/daemon/web/src/lib/button.ts +20 -0
  44. nerdit-0.9.2/src/nerdit/daemon/web/src/pages/AppStates.tsx +40 -0
  45. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/pages/Audit.tsx +9 -2
  46. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/pages/Databases.tsx +13 -5
  47. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/pages/Models.tsx +13 -5
  48. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/pages/ProjectDetail.tsx +34 -35
  49. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/pages/ProjectPage.test.tsx +1 -1
  50. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/pages/ProjectPage.tsx +20 -10
  51. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/pages/Projects.tsx +4 -2
  52. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/pages/ServiceRedirect.tsx +10 -2
  53. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/pages/Settings.tsx +3 -3
  54. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/pages/Tokens.tsx +9 -2
  55. nerdit-0.9.2/src/nerdit/daemon/web/src/shell/AppShell.test.tsx +42 -0
  56. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/shell/AppShell.tsx +5 -4
  57. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/shell/navItems.ts +9 -2
  58. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/tests/e2e/01-login-and-overview.spec.ts +5 -4
  59. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/tests/e2e/07-audit.spec.ts +1 -1
  60. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/tests/e2e/10-settings.spec.ts +8 -1
  61. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/tests/e2e/19-project-page.spec.ts +1 -1
  62. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/tsconfig.tsbuildinfo +1 -1
  63. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/db/queries/shares.py +32 -7
  64. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/db/queries/workloads.py +14 -0
  65. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/mcp/tools/_shared.py +5 -4
  66. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/mcp/tools/config.py +6 -4
  67. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/mcp/tools/deploy.py +48 -11
  68. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/mcp/tools/secrets.py +1 -1
  69. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/mcp/tools/workspaces.py +14 -9
  70. nerdit-0.9.0/src/nerdit/daemon/web/dist/assets/index-DI2mvcfQ.js +0 -265
  71. {nerdit-0.9.0 → nerdit-0.9.2}/.gitignore +0 -0
  72. {nerdit-0.9.0 → nerdit-0.9.2}/LICENSE +0 -0
  73. {nerdit-0.9.0 → nerdit-0.9.2}/NOTICE +0 -0
  74. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/__main__.py +0 -0
  75. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/__init__.py +0 -0
  76. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/app.py +0 -0
  77. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/checks.py +0 -0
  78. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/client.py +0 -0
  79. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/commands/__init__.py +0 -0
  80. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/commands/ai.py +0 -0
  81. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/commands/backup.py +0 -0
  82. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/commands/capabilities.py +0 -0
  83. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/commands/check_deps.py +0 -0
  84. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/commands/config.py +0 -0
  85. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/commands/connect.py +0 -0
  86. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/commands/daemon.py +0 -0
  87. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/commands/db.py +0 -0
  88. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/commands/dev.py +0 -0
  89. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/commands/diagnose.py +0 -0
  90. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/commands/doctor.py +0 -0
  91. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/commands/domains.py +0 -0
  92. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/commands/events.py +0 -0
  93. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/commands/exit.py +0 -0
  94. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/commands/hooks.py +0 -0
  95. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/commands/init.py +0 -0
  96. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/commands/license.py +0 -0
  97. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/commands/link.py +0 -0
  98. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/commands/logs.py +0 -0
  99. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/commands/mcp.py +0 -0
  100. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/commands/models.py +0 -0
  101. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/commands/projects.py +0 -0
  102. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/commands/proxy.py +0 -0
  103. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/commands/routes.py +0 -0
  104. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/commands/secrets.py +0 -0
  105. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/commands/serve.py +0 -0
  106. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/commands/services.py +0 -0
  107. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/commands/share.py +0 -0
  108. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/commands/store.py +0 -0
  109. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/commands/system.py +0 -0
  110. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/commands/token.py +0 -0
  111. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/commands/trust.py +0 -0
  112. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/commands/uninstall.py +0 -0
  113. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/commands/update.py +0 -0
  114. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/commands/vars.py +0 -0
  115. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/cli/display.py +0 -0
  116. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/config/__init__.py +0 -0
  117. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/config/app_templates/__init__.py +0 -0
  118. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/config/app_templates/builtin.json +0 -0
  119. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/config/app_templates/loader.py +0 -0
  120. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/config/redaction.py +0 -0
  121. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/config/settings.py +0 -0
  122. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/config/store.py +0 -0
  123. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/__init__.py +0 -0
  124. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/ai_gateway/__init__.py +0 -0
  125. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/ai_gateway/app.py +0 -0
  126. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/ai_gateway/keys.py +0 -0
  127. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/ai_gateway/proxy.py +0 -0
  128. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/backup.py +0 -0
  129. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/bindings/__init__.py +0 -0
  130. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/bindings/registry.py +0 -0
  131. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/bindings/secretref.py +0 -0
  132. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/builder.py +0 -0
  133. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/cutover.py +0 -0
  134. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/data/__init__.py +0 -0
  135. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/data/backend.py +0 -0
  136. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/data/binding.py +0 -0
  137. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/data/controller.py +0 -0
  138. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/discovery.py +0 -0
  139. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/dns.py +0 -0
  140. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/eventlog.py +0 -0
  141. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/events.py +0 -0
  142. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/gitsource.py +0 -0
  143. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/gitwatch.py +0 -0
  144. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/health.py +0 -0
  145. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/hooks.py +0 -0
  146. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/jobconfig.py +0 -0
  147. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/launch.py +0 -0
  148. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/license.py +0 -0
  149. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/link/__init__.py +0 -0
  150. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/link/capability.py +0 -0
  151. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/link/frames.py +0 -0
  152. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/link/hosted.py +0 -0
  153. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/link/identity.py +0 -0
  154. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/link/manager.py +0 -0
  155. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/models/__init__.py +0 -0
  156. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/models/backend.py +0 -0
  157. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/models/binding.py +0 -0
  158. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/models/controller.py +0 -0
  159. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/monitor.py +0 -0
  160. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/node_packages.py +0 -0
  161. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/node_runtime.py +0 -0
  162. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/notify.py +0 -0
  163. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/project_identity.py +0 -0
  164. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/proxy/__init__.py +0 -0
  165. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/proxy/admin.py +0 -0
  166. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/proxy/certs.py +0 -0
  167. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/proxy/domains.py +0 -0
  168. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/proxy/edgeauth.py +0 -0
  169. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/proxy/manager.py +0 -0
  170. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/proxy/routing.py +0 -0
  171. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/proxy/supervisor.py +0 -0
  172. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/proxy/tls.py +0 -0
  173. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/remediation_settle.py +0 -0
  174. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/resources/__init__.py +0 -0
  175. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/resources/controller.py +0 -0
  176. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/retention.py +0 -0
  177. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/runtime/__init__.py +0 -0
  178. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/runtime/container.py +0 -0
  179. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/runtime/docker.py +0 -0
  180. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/runtime/protocol.py +0 -0
  181. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/runtime/stub.py +0 -0
  182. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/sandbox.py +0 -0
  183. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/secrets.py +0 -0
  184. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/sweeper.py +0 -0
  185. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/variables.py +0 -0
  186. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/volumes.py +0 -0
  187. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/workload.py +0 -0
  188. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/core/workspaces.py +0 -0
  189. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/__init__.py +0 -0
  190. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/appfactory.py +0 -0
  191. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/audit.py +0 -0
  192. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/auth.py +0 -0
  193. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/bodylimit.py +0 -0
  194. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/bootstrap.py +0 -0
  195. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/errors.py +0 -0
  196. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/idempotency.py +0 -0
  197. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/imagegc.py +0 -0
  198. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/lifecycle.py +0 -0
  199. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/limits.py +0 -0
  200. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/middleware.py +0 -0
  201. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/project_delegation.py +0 -0
  202. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/routes/__init__.py +0 -0
  203. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/routes/_resources.py +0 -0
  204. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/routes/ai_gateway.py +0 -0
  205. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/routes/app_templates.py +0 -0
  206. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/routes/audit.py +0 -0
  207. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/routes/auth.py +0 -0
  208. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/routes/cluster.py +0 -0
  209. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/routes/config.py +0 -0
  210. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/routes/databases.py +0 -0
  211. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/routes/domains.py +0 -0
  212. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/routes/events.py +0 -0
  213. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/routes/gpus.py +0 -0
  214. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/routes/health.py +0 -0
  215. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/routes/hooks.py +0 -0
  216. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/routes/images.py +0 -0
  217. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/routes/license.py +0 -0
  218. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/routes/link.py +0 -0
  219. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/routes/models.py +0 -0
  220. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/routes/proxy.py +0 -0
  221. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/routes/secrets.py +0 -0
  222. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/routes/service_diagnose.py +0 -0
  223. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/routes/service_logs_stream.py +0 -0
  224. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/routes/service_run.py +0 -0
  225. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/routes/service_stats.py +0 -0
  226. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/routes/service_wait.py +0 -0
  227. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/routes/tokens.py +0 -0
  228. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/routes/workspaces.py +0 -0
  229. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/schemas/__init__.py +0 -0
  230. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/schemas/_base.py +0 -0
  231. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/schemas/audit.py +0 -0
  232. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/schemas/cluster.py +0 -0
  233. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/schemas/config.py +0 -0
  234. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/schemas/databases.py +0 -0
  235. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/schemas/deploy.py +0 -0
  236. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/schemas/events.py +0 -0
  237. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/schemas/gpus.py +0 -0
  238. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/schemas/health.py +0 -0
  239. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/schemas/license.py +0 -0
  240. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/schemas/link.py +0 -0
  241. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/schemas/models.py +0 -0
  242. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/schemas/proxy.py +0 -0
  243. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/schemas/service_diagnose.py +0 -0
  244. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/schemas/service_purge.py +0 -0
  245. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/schemas/service_run.py +0 -0
  246. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/schemas/service_stats.py +0 -0
  247. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/schemas/service_wait.py +0 -0
  248. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/schemas/services.py +0 -0
  249. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/schemas/system.py +0 -0
  250. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/schemas/tokens.py +0 -0
  251. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/schemas/workspaces.py +0 -0
  252. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/secret_scope.py +0 -0
  253. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/server.py +0 -0
  254. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/service_purge.py +0 -0
  255. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/sse.py +0 -0
  256. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/sweeps.py +0 -0
  257. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/uploads.py +0 -0
  258. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/views/__init__.py +0 -0
  259. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/views/hosted.py +0 -0
  260. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/views/service.py +0 -0
  261. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/dist/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
  262. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/dist/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
  263. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/dist/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
  264. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/dist/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
  265. {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
  266. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/dist/assets/geist-mono-cyrillic-wght-normal-DiZS0aHC.woff2 +0 -0
  267. {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
  268. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/dist/assets/geist-mono-latin-wght-normal-XN7g48iV.woff2 +0 -0
  269. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/dist/assets/geist-mono-symbols2-wght-normal-CO5SzqOn.woff2 +0 -0
  270. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/dist/assets/geist-mono-vietnamese-wght-normal-DadHysG0.woff2 +0 -0
  271. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/dist/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
  272. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/eslint.config.js +0 -0
  273. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/index.html +0 -0
  274. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/package-lock.json +0 -0
  275. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/package.json +0 -0
  276. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/playwright.config.ts +0 -0
  277. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/postcss.config.js +0 -0
  278. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/api/auditStream.ts +0 -0
  279. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/api/client.test.ts +0 -0
  280. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/api/client.ts +0 -0
  281. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/api/clusterEvents.ts +0 -0
  282. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/api/queries.test.ts +0 -0
  283. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/api/queries.ts +0 -0
  284. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/api/types.ts +0 -0
  285. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/components/ConfigurePanel.tsx +0 -0
  286. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/components/DeployDialog.tsx +0 -0
  287. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/components/DeployProgress.tsx +0 -0
  288. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/components/DiagnosePanel.tsx +0 -0
  289. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/components/Dropzone.tsx +0 -0
  290. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/components/ErrorBoundary.tsx +0 -0
  291. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/components/ServiceLogsPanel.tsx +0 -0
  292. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/components/ServicePublicUrl.tsx +0 -0
  293. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/components/TemplateDeployDialog.tsx +0 -0
  294. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/components/ToastViewport.tsx +0 -0
  295. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/components/UnlinkedBanner.test.tsx +0 -0
  296. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/components/UnlinkedBanner.tsx +0 -0
  297. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/components/bindings/AiEndpointFields.tsx +0 -0
  298. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/components/bindings/BindingSwitch.tsx +0 -0
  299. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/components/bindings/KeyFlow.tsx +0 -0
  300. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/components/bindings/useBindingDraft.ts +0 -0
  301. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/components/settings/AppearanceCard.tsx +0 -0
  302. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/components/settings/DiskCard.tsx +0 -0
  303. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/components/settings/GpuCard.tsx +0 -0
  304. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/components/settings/HostnameCard.tsx +0 -0
  305. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/components/settings/ProxyStatusCard.tsx +0 -0
  306. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/components/ui.test.tsx +0 -0
  307. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/lib/aiProviders.ts +0 -0
  308. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/lib/analytics.ts +0 -0
  309. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/lib/apiErrors.test.ts +0 -0
  310. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/lib/apiErrors.ts +0 -0
  311. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/lib/bindings.test.ts +0 -0
  312. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/lib/bindings.ts +0 -0
  313. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/lib/deployPhase.ts +0 -0
  314. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/lib/format.test.ts +0 -0
  315. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/lib/format.ts +0 -0
  316. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/lib/projects.test.ts +0 -0
  317. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/lib/projects.ts +0 -0
  318. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/lib/sse.test.ts +0 -0
  319. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/lib/sse.ts +0 -0
  320. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/lib/status.test.ts +0 -0
  321. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/lib/status.ts +0 -0
  322. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/lib/theme.test.ts +0 -0
  323. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/lib/theme.ts +0 -0
  324. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/main.tsx +0 -0
  325. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/pages/Login.test.tsx +0 -0
  326. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/pages/Login.tsx +0 -0
  327. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/pages/ProjectPolling.test.tsx +0 -0
  328. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/state/toastStore.ts +0 -0
  329. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/src/styles/globals.css +0 -0
  330. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/tailwind.config.js +0 -0
  331. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/tests/e2e/05-services.spec.ts +0 -0
  332. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/tests/e2e/06-models.spec.ts +0 -0
  333. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/tests/e2e/08-secrets.spec.ts +0 -0
  334. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/tests/e2e/11-configure.spec.ts +0 -0
  335. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/tests/e2e/12-deploy.spec.ts +0 -0
  336. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/tests/e2e/13-bindings.spec.ts +0 -0
  337. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/tests/e2e/14-responsive-shell.spec.ts +0 -0
  338. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/tests/e2e/17-projects.spec.ts +0 -0
  339. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/tests/e2e/18-navigation.spec.ts +0 -0
  340. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/tests/e2e/20-command-palette.spec.ts +0 -0
  341. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/tests/e2e/21-tokens.spec.ts +0 -0
  342. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/tests/e2e/22-system-cards.spec.ts +0 -0
  343. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/tests/e2e/fixtures.ts +0 -0
  344. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/tsconfig.json +0 -0
  345. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/daemon/web/vite.config.ts +0 -0
  346. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/db/__init__.py +0 -0
  347. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/db/database.py +0 -0
  348. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/db/enums.py +0 -0
  349. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/db/models.py +0 -0
  350. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/db/queries/__init__.py +0 -0
  351. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/db/queries/_base.py +0 -0
  352. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/db/queries/ai_gateway.py +0 -0
  353. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/db/queries/audit.py +0 -0
  354. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/db/queries/domains.py +0 -0
  355. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/db/queries/endpoints.py +0 -0
  356. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/db/queries/events.py +0 -0
  357. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/db/queries/gpus.py +0 -0
  358. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/db/queries/hooks.py +0 -0
  359. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/db/queries/idempotency.py +0 -0
  360. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/db/queries/logs.py +0 -0
  361. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/db/queries/projects.py +0 -0
  362. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/db/queries/secret_claims.py +0 -0
  363. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/db/queries/service_config.py +0 -0
  364. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/db/queries/services.py +0 -0
  365. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/db/queries/tokens.py +0 -0
  366. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/db/rows.py +0 -0
  367. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/mcp/__init__.py +0 -0
  368. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/mcp/errors.py +0 -0
  369. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/mcp/server.py +0 -0
  370. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/mcp/tools/__init__.py +0 -0
  371. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/mcp/tools/ai_gateway.py +0 -0
  372. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/mcp/tools/cluster.py +0 -0
  373. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/mcp/tools/databases.py +0 -0
  374. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/mcp/tools/exposure.py +0 -0
  375. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/mcp/tools/models.py +0 -0
  376. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/mcp/tools/projects.py +0 -0
  377. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/mcp/tools/services.py +0 -0
  378. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/mcp/tools/system.py +0 -0
  379. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/mcp/transport.py +0 -0
  380. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/utils/__init__.py +0 -0
  381. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/utils/certs.py +0 -0
  382. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/utils/disk.py +0 -0
  383. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/utils/frozen.py +0 -0
  384. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/utils/fs.py +0 -0
  385. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/utils/ids.py +0 -0
  386. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/utils/install_layout.py +0 -0
  387. {nerdit-0.9.0 → nerdit-0.9.2}/src/nerdit/utils/logging.py +0 -0
  388. {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.0
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 **Connect** in
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
- assistant can reach. No token leaves your machine. See the
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.0/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.0/NOTICE).
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.0/CONTRIBUTING.md)
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 **Connect** in
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
- assistant can reach. No token leaves your machine. See the
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.0/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.0/NOTICE).
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.0/CONTRIBUTING.md)
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.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "nerdit"
7
- version = "0.9.0"
7
+ version = "0.9.2"
8
8
  description = "The local AI PaaS — deploy and operate apps on your own hardware, with first-class local AI"
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -1,3 +1,3 @@
1
1
  """Nerdit — Le moteur de l'IA locale."""
2
2
 
3
- __version__ = "0.9.0"
3
+ __version__ = "0.9.2"
@@ -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
- zip_bytes = create_dir_zip(directory, skipped_secrets)
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
- zip_bytes = create_dir_zip(directory, skipped_secrets)
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 create_dir_zip(directory: str | Path, skipped_secrets: list[str] | None = None) -> bytes:
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. Symlinks
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
- if "${secrets." in item or "${vars." in item or "${github." in item:
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", "data/", "*.pyc", "__pycache__", ".venv", "node_modules"}
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
- await self.cleanup_context(job, cleanup_dir)
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
- if not await queries.patch_job_config(job_id, {"last_deploy": new_ld}):
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
- ) -> Awaitable[tuple[str, int, str] | None]:
28
- """Return the current job, live port and admitted audience, or refuse."""
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(service_name, port, authority, admitted_job, audience)
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, target: AppTarget, admission: HookAdmission | None
230
+ frame: OpenStream,
231
+ target: AppTarget,
232
+ admission: HookAdmission | None,
233
+ authorization: str | None = None,
223
234
  ) -> tuple[str, list[Header]]:
224
- """The loopback URL and filtered headers an admitted app stream is dialled with."""
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(frame, target, admission)
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