minutework 0.1.0

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 (203) hide show
  1. package/EXTERNAL_ALPHA.md +74 -0
  2. package/README.md +57 -0
  3. package/assets/claude-local/CLAUDE.md.template +45 -0
  4. package/assets/claude-local/bundle.json +22 -0
  5. package/assets/claude-local/skills/README.md +6 -0
  6. package/assets/claude-local/skills/app-pack-authoring.md +8 -0
  7. package/assets/claude-local/skills/event-bus.md +8 -0
  8. package/assets/claude-local/skills/ontology-mapping.md +8 -0
  9. package/assets/claude-local/skills/openclaw-skill-importer.md +7 -0
  10. package/assets/claude-local/skills/schema-engine.md +8 -0
  11. package/assets/claude-local/skills/secrets-runtime-bridge.md +9 -0
  12. package/assets/claude-local/skills/sidecar-generation.md +9 -0
  13. package/assets/templates/fastapi-sidecar/.env.example +8 -0
  14. package/assets/templates/fastapi-sidecar/README.md +77 -0
  15. package/assets/templates/fastapi-sidecar/poetry.lock +757 -0
  16. package/assets/templates/fastapi-sidecar/pyproject.toml +42 -0
  17. package/assets/templates/fastapi-sidecar/src/fastapi_sidecar/__init__.py +3 -0
  18. package/assets/templates/fastapi-sidecar/src/fastapi_sidecar/auth.py +70 -0
  19. package/assets/templates/fastapi-sidecar/src/fastapi_sidecar/bridge/__init__.py +3 -0
  20. package/assets/templates/fastapi-sidecar/src/fastapi_sidecar/bridge/client.py +71 -0
  21. package/assets/templates/fastapi-sidecar/src/fastapi_sidecar/logging_utils.py +25 -0
  22. package/assets/templates/fastapi-sidecar/src/fastapi_sidecar/main.py +85 -0
  23. package/assets/templates/fastapi-sidecar/src/fastapi_sidecar/receipts.py +24 -0
  24. package/assets/templates/fastapi-sidecar/src/fastapi_sidecar/settings.py +41 -0
  25. package/assets/templates/fastapi-sidecar/src/fastapi_sidecar/template_validation.py +26 -0
  26. package/assets/templates/fastapi-sidecar/src/fastapi_sidecar/worker.py +33 -0
  27. package/assets/templates/fastapi-sidecar/template.json +43 -0
  28. package/assets/templates/fastapi-sidecar/template.schema.json +160 -0
  29. package/assets/templates/fastapi-sidecar/tests/conftest.py +36 -0
  30. package/assets/templates/fastapi-sidecar/tests/test_app.py +39 -0
  31. package/assets/templates/fastapi-sidecar/tests/test_auth.py +32 -0
  32. package/assets/templates/fastapi-sidecar/tests/test_bridge_client.py +31 -0
  33. package/assets/templates/fastapi-sidecar/tests/test_materialization.py +55 -0
  34. package/assets/templates/fastapi-sidecar/tests/test_template_contract.py +49 -0
  35. package/assets/templates/fastapi-sidecar/tests/test_worker.py +7 -0
  36. package/assets/templates/fastapi-sidecar/tools/template/validate_template.py +20 -0
  37. package/assets/templates/next-tenant-app/.env.example +8 -0
  38. package/assets/templates/next-tenant-app/.storybook/main.ts +19 -0
  39. package/assets/templates/next-tenant-app/.storybook/preview.tsx +38 -0
  40. package/assets/templates/next-tenant-app/README.md +115 -0
  41. package/assets/templates/next-tenant-app/components.json +21 -0
  42. package/assets/templates/next-tenant-app/eslint.config.mjs +41 -0
  43. package/assets/templates/next-tenant-app/next-env.d.ts +6 -0
  44. package/assets/templates/next-tenant-app/next.config.ts +8 -0
  45. package/assets/templates/next-tenant-app/package-lock.json +9682 -0
  46. package/assets/templates/next-tenant-app/package.json +59 -0
  47. package/assets/templates/next-tenant-app/pnpm-lock.yaml +6062 -0
  48. package/assets/templates/next-tenant-app/postcss.config.mjs +8 -0
  49. package/assets/templates/next-tenant-app/src/app/api/auth/context/route.test.ts +90 -0
  50. package/assets/templates/next-tenant-app/src/app/api/auth/context/route.ts +78 -0
  51. package/assets/templates/next-tenant-app/src/app/api/auth/login/route.ts +31 -0
  52. package/assets/templates/next-tenant-app/src/app/api/auth/logout/route.ts +16 -0
  53. package/assets/templates/next-tenant-app/src/app/api/auth/password-change/route.test.ts +79 -0
  54. package/assets/templates/next-tenant-app/src/app/api/auth/password-change/route.ts +40 -0
  55. package/assets/templates/next-tenant-app/src/app/api/auth/password-status/route.test.ts +42 -0
  56. package/assets/templates/next-tenant-app/src/app/api/auth/password-status/route.ts +29 -0
  57. package/assets/templates/next-tenant-app/src/app/api/auth/session/route.ts +26 -0
  58. package/assets/templates/next-tenant-app/src/app/api/gateway/commands/[runId]/route.test.ts +40 -0
  59. package/assets/templates/next-tenant-app/src/app/api/gateway/commands/[runId]/route.ts +47 -0
  60. package/assets/templates/next-tenant-app/src/app/api/gateway/commands/route.test.ts +43 -0
  61. package/assets/templates/next-tenant-app/src/app/api/gateway/commands/route.ts +45 -0
  62. package/assets/templates/next-tenant-app/src/app/app/examples/runtime-commands/page.test.ts +83 -0
  63. package/assets/templates/next-tenant-app/src/app/app/examples/runtime-commands/page.tsx +30 -0
  64. package/assets/templates/next-tenant-app/src/app/app/layout.tsx +20 -0
  65. package/assets/templates/next-tenant-app/src/app/app/page.test.ts +62 -0
  66. package/assets/templates/next-tenant-app/src/app/app/page.tsx +24 -0
  67. package/assets/templates/next-tenant-app/src/app/blog/[slug]/page.test.ts +70 -0
  68. package/assets/templates/next-tenant-app/src/app/blog/[slug]/page.tsx +57 -0
  69. package/assets/templates/next-tenant-app/src/app/blog/page.test.ts +42 -0
  70. package/assets/templates/next-tenant-app/src/app/blog/page.tsx +37 -0
  71. package/assets/templates/next-tenant-app/src/app/docs/[...slug]/page.test.ts +70 -0
  72. package/assets/templates/next-tenant-app/src/app/docs/[...slug]/page.tsx +55 -0
  73. package/assets/templates/next-tenant-app/src/app/docs/page.test.ts +42 -0
  74. package/assets/templates/next-tenant-app/src/app/docs/page.tsx +37 -0
  75. package/assets/templates/next-tenant-app/src/app/globals.css +70 -0
  76. package/assets/templates/next-tenant-app/src/app/layout.tsx +69 -0
  77. package/assets/templates/next-tenant-app/src/app/login/page.test.ts +55 -0
  78. package/assets/templates/next-tenant-app/src/app/login/page.tsx +33 -0
  79. package/assets/templates/next-tenant-app/src/app/page.test.ts +56 -0
  80. package/assets/templates/next-tenant-app/src/app/page.tsx +35 -0
  81. package/assets/templates/next-tenant-app/src/app/pricing/page.test.ts +55 -0
  82. package/assets/templates/next-tenant-app/src/app/pricing/page.tsx +35 -0
  83. package/assets/templates/next-tenant-app/src/app/providers.tsx +25 -0
  84. package/assets/templates/next-tenant-app/src/app/robots.test.ts +20 -0
  85. package/assets/templates/next-tenant-app/src/app/robots.ts +18 -0
  86. package/assets/templates/next-tenant-app/src/app/sitemap.test.ts +49 -0
  87. package/assets/templates/next-tenant-app/src/app/sitemap.ts +54 -0
  88. package/assets/templates/next-tenant-app/src/components/ui/button.tsx +59 -0
  89. package/assets/templates/next-tenant-app/src/components/ui/input.tsx +21 -0
  90. package/assets/templates/next-tenant-app/src/design-system/docs/governance.mdx +26 -0
  91. package/assets/templates/next-tenant-app/src/design-system/patterns/panel-frame.stories.tsx +48 -0
  92. package/assets/templates/next-tenant-app/src/design-system/patterns/panel-frame.tsx +26 -0
  93. package/assets/templates/next-tenant-app/src/design-system/patterns/status-badge.stories.tsx +26 -0
  94. package/assets/templates/next-tenant-app/src/design-system/patterns/status-badge.tsx +35 -0
  95. package/assets/templates/next-tenant-app/src/design-system/patterns/theme-mode-toggle.stories.tsx +21 -0
  96. package/assets/templates/next-tenant-app/src/design-system/patterns/theme-mode-toggle.tsx +75 -0
  97. package/assets/templates/next-tenant-app/src/design-system/primitives/button.stories.tsx +37 -0
  98. package/assets/templates/next-tenant-app/src/design-system/primitives/button.ts +1 -0
  99. package/assets/templates/next-tenant-app/src/design-system/primitives/input.stories.tsx +26 -0
  100. package/assets/templates/next-tenant-app/src/design-system/primitives/input.ts +1 -0
  101. package/assets/templates/next-tenant-app/src/design-system/recipes/chrome.ts +28 -0
  102. package/assets/templates/next-tenant-app/src/design-system/tokens/foundation.css +31 -0
  103. package/assets/templates/next-tenant-app/src/design-system/tokens/index.css +3 -0
  104. package/assets/templates/next-tenant-app/src/design-system/tokens/manifest.json +85 -0
  105. package/assets/templates/next-tenant-app/src/design-system/tokens/manifest.ts +87 -0
  106. package/assets/templates/next-tenant-app/src/design-system/tokens/semantic.css +105 -0
  107. package/assets/templates/next-tenant-app/src/design-system/tokens/theme.css +59 -0
  108. package/assets/templates/next-tenant-app/src/design-system/tokens/tokens.stories.tsx +71 -0
  109. package/assets/templates/next-tenant-app/src/features/auth/components/login-screen.tsx +198 -0
  110. package/assets/templates/next-tenant-app/src/features/dashboard/components/tenant-dashboard.tsx +153 -0
  111. package/assets/templates/next-tenant-app/src/features/examples/runtime-command-demo/components/runtime-command-demo.tsx +342 -0
  112. package/assets/templates/next-tenant-app/src/features/public-shell/components/content-article.tsx +66 -0
  113. package/assets/templates/next-tenant-app/src/features/public-shell/components/content-collection.tsx +108 -0
  114. package/assets/templates/next-tenant-app/src/features/public-shell/components/marketing-page-canvas.tsx +111 -0
  115. package/assets/templates/next-tenant-app/src/features/public-shell/components/public-site-shell.tsx +111 -0
  116. package/assets/templates/next-tenant-app/src/features/shell/components/private-app-shell.tsx +624 -0
  117. package/assets/templates/next-tenant-app/src/lib/app-routes.test.ts +20 -0
  118. package/assets/templates/next-tenant-app/src/lib/app-routes.ts +59 -0
  119. package/assets/templates/next-tenant-app/src/lib/content/__fixtures__/public-site-snapshot.ts +189 -0
  120. package/assets/templates/next-tenant-app/src/lib/content/adapter.server.test.ts +318 -0
  121. package/assets/templates/next-tenant-app/src/lib/content/adapter.server.ts +232 -0
  122. package/assets/templates/next-tenant-app/src/lib/content/contracts.ts +339 -0
  123. package/assets/templates/next-tenant-app/src/lib/content/custom-adapter.ts +5 -0
  124. package/assets/templates/next-tenant-app/src/lib/content/empty-state.ts +96 -0
  125. package/assets/templates/next-tenant-app/src/lib/platform/auth.server.test.ts +75 -0
  126. package/assets/templates/next-tenant-app/src/lib/platform/auth.server.ts +25 -0
  127. package/assets/templates/next-tenant-app/src/lib/platform/client.server.test.ts +170 -0
  128. package/assets/templates/next-tenant-app/src/lib/platform/client.server.ts +661 -0
  129. package/assets/templates/next-tenant-app/src/lib/platform/contracts.ts +131 -0
  130. package/assets/templates/next-tenant-app/src/lib/platform/endpoints.server.ts +34 -0
  131. package/assets/templates/next-tenant-app/src/lib/platform/env.server.test.ts +102 -0
  132. package/assets/templates/next-tenant-app/src/lib/platform/env.server.ts +87 -0
  133. package/assets/templates/next-tenant-app/src/lib/platform/route-response.ts +33 -0
  134. package/assets/templates/next-tenant-app/src/lib/platform/session.server.ts +108 -0
  135. package/assets/templates/next-tenant-app/src/lib/public-site.test.ts +20 -0
  136. package/assets/templates/next-tenant-app/src/lib/public-site.ts +49 -0
  137. package/assets/templates/next-tenant-app/src/lib/theme-config.ts +10 -0
  138. package/assets/templates/next-tenant-app/src/lib/theme.tsx +159 -0
  139. package/assets/templates/next-tenant-app/src/lib/utils.ts +6 -0
  140. package/assets/templates/next-tenant-app/template.json +27 -0
  141. package/assets/templates/next-tenant-app/template.schema.json +160 -0
  142. package/assets/templates/next-tenant-app/test/server-only-stub.ts +1 -0
  143. package/assets/templates/next-tenant-app/tools/design-system/build-token-manifest.mjs +3 -0
  144. package/assets/templates/next-tenant-app/tools/design-system/check-imports.mjs +9 -0
  145. package/assets/templates/next-tenant-app/tools/design-system/check-stories.mjs +9 -0
  146. package/assets/templates/next-tenant-app/tools/design-system/check-values.mjs +9 -0
  147. package/assets/templates/next-tenant-app/tools/design-system/checks.mjs +238 -0
  148. package/assets/templates/next-tenant-app/tools/design-system/eslint-plugin-design-system.mjs +184 -0
  149. package/assets/templates/next-tenant-app/tools/design-system/playwright.config.mjs +34 -0
  150. package/assets/templates/next-tenant-app/tools/design-system/run-checks.mjs +22 -0
  151. package/assets/templates/next-tenant-app/tools/design-system/shared.mjs +166 -0
  152. package/assets/templates/next-tenant-app/tools/design-system/visual.spec.ts +41 -0
  153. package/assets/templates/next-tenant-app/tools/template/validate-route-contract.mjs +39 -0
  154. package/assets/templates/next-tenant-app/tools/template/validate-template.mjs +45 -0
  155. package/assets/templates/next-tenant-app/tsconfig.json +42 -0
  156. package/assets/templates/next-tenant-app/vitest.config.ts +25 -0
  157. package/bin/minutework.js +40 -0
  158. package/dist/auth.d.ts +59 -0
  159. package/dist/auth.js +338 -0
  160. package/dist/auth.js.map +1 -0
  161. package/dist/browser.d.ts +1 -0
  162. package/dist/browser.js +26 -0
  163. package/dist/browser.js.map +1 -0
  164. package/dist/cli.d.ts +2 -0
  165. package/dist/cli.js +5 -0
  166. package/dist/cli.js.map +1 -0
  167. package/dist/compile.d.ts +20 -0
  168. package/dist/compile.js +121 -0
  169. package/dist/compile.js.map +1 -0
  170. package/dist/config.d.ts +25 -0
  171. package/dist/config.js +102 -0
  172. package/dist/config.js.map +1 -0
  173. package/dist/deploy-state.d.ts +35 -0
  174. package/dist/deploy-state.js +30 -0
  175. package/dist/deploy-state.js.map +1 -0
  176. package/dist/deploy.d.ts +22 -0
  177. package/dist/deploy.js +308 -0
  178. package/dist/deploy.js.map +1 -0
  179. package/dist/developer-client.d.ts +88 -0
  180. package/dist/developer-client.js +78 -0
  181. package/dist/developer-client.js.map +1 -0
  182. package/dist/index.d.ts +27 -0
  183. package/dist/index.js +290 -0
  184. package/dist/index.js.map +1 -0
  185. package/dist/init.d.ts +22 -0
  186. package/dist/init.js +421 -0
  187. package/dist/init.js.map +1 -0
  188. package/dist/launcher.d.ts +1 -0
  189. package/dist/launcher.js +50 -0
  190. package/dist/launcher.js.map +1 -0
  191. package/dist/paths.d.ts +12 -0
  192. package/dist/paths.js +33 -0
  193. package/dist/paths.js.map +1 -0
  194. package/dist/sandbox.d.ts +30 -0
  195. package/dist/sandbox.js +852 -0
  196. package/dist/sandbox.js.map +1 -0
  197. package/dist/state.d.ts +46 -0
  198. package/dist/state.js +82 -0
  199. package/dist/state.js.map +1 -0
  200. package/dist/tokens.d.ts +14 -0
  201. package/dist/tokens.js +293 -0
  202. package/dist/tokens.js.map +1 -0
  203. package/package.json +43 -0
@@ -0,0 +1,42 @@
1
+ [build-system]
2
+ requires = ["poetry-core>=2.0.0,<3.0.0"]
3
+ build-backend = "poetry.core.masonry.api"
4
+
5
+ [tool.poetry]
6
+ packages = [{ include = "fastapi_sidecar", from = "src" }]
7
+
8
+ [project]
9
+ name = "fastapi-sidecar-template"
10
+ version = "0.1.0"
11
+ description = "Canonical FastAPI internal sidecar scaffold for MinuteWork Builder."
12
+ readme = "README.md"
13
+ requires-python = ">=3.12,<4.0"
14
+ dependencies = [
15
+ "fastapi>=0.115,<1.0",
16
+ "httpx>=0.27,<1.0",
17
+ "pydantic-settings>=2.6,<3.0",
18
+ "uvicorn>=0.32,<1.0",
19
+ ]
20
+
21
+ [project.optional-dependencies]
22
+ dev = [
23
+ "jsonschema>=4.23,<5.0",
24
+ "pytest>=8.3,<9.0",
25
+ "ruff>=0.11,<0.12",
26
+ ]
27
+
28
+ [project.scripts]
29
+ fastapi-sidecar-worker = "fastapi_sidecar.worker:main"
30
+ fastapi-sidecar-template-validate = "fastapi_sidecar.template_validation:main"
31
+
32
+ [tool.pytest.ini_options]
33
+ pythonpath = ["src"]
34
+ testpaths = ["tests"]
35
+
36
+ [tool.ruff]
37
+ line-length = 100
38
+ target-version = "py312"
39
+ extend-exclude = ["build"]
40
+
41
+ [tool.ruff.lint]
42
+ select = ["E", "F", "I", "UP"]
@@ -0,0 +1,3 @@
1
+ from fastapi_sidecar.main import app, create_app
2
+
3
+ __all__ = ["app", "create_app"]
@@ -0,0 +1,70 @@
1
+ from __future__ import annotations
2
+
3
+ import secrets
4
+ from dataclasses import dataclass
5
+ from typing import Annotated
6
+ from uuid import uuid4
7
+
8
+ from fastapi import Depends, HTTPException, Request, status
9
+
10
+ from fastapi_sidecar.settings import Settings, get_settings
11
+
12
+
13
+ @dataclass(frozen=True, slots=True)
14
+ class AuthContext:
15
+ request_id: str
16
+ tenant_id: str
17
+ auth_mode: str
18
+ principal_kind: str
19
+
20
+
21
+ def _request_id(request: Request) -> str:
22
+ return request.headers.get("X-Request-Id", "").strip() or str(uuid4())
23
+
24
+
25
+ def require_internal_only(
26
+ request: Request,
27
+ settings: Annotated[Settings, Depends(get_settings)],
28
+ ) -> AuthContext:
29
+ token = request.headers.get("X-Platform-Dispatch-Token", "").strip()
30
+
31
+ if not token or not secrets.compare_digest(token, settings.platform_dispatch_token):
32
+ raise HTTPException(
33
+ status_code=status.HTTP_401_UNAUTHORIZED,
34
+ detail="Platform dispatch authentication required.",
35
+ )
36
+
37
+ return AuthContext(
38
+ request_id=_request_id(request),
39
+ tenant_id=settings.tenant_id,
40
+ auth_mode="internal_only",
41
+ principal_kind="platform_dispatch",
42
+ )
43
+
44
+
45
+ def require_runtime_token(
46
+ request: Request,
47
+ settings: Annotated[Settings, Depends(get_settings)],
48
+ ) -> AuthContext:
49
+ runtime_id = request.headers.get("X-Runtime-Id", "").strip()
50
+ runtime_key = request.headers.get("X-Runtime-Key", "").strip()
51
+ expected_runtime_key = settings.read_runtime_key()
52
+
53
+ if runtime_id != settings.runtime_id or not runtime_key:
54
+ raise HTTPException(
55
+ status_code=status.HTTP_401_UNAUTHORIZED,
56
+ detail="Runtime token authentication required.",
57
+ )
58
+
59
+ if not secrets.compare_digest(runtime_key, expected_runtime_key):
60
+ raise HTTPException(
61
+ status_code=status.HTTP_401_UNAUTHORIZED,
62
+ detail="Runtime token authentication required.",
63
+ )
64
+
65
+ return AuthContext(
66
+ request_id=_request_id(request),
67
+ tenant_id=settings.tenant_id,
68
+ auth_mode="runtime_token",
69
+ principal_kind="runtime_machine",
70
+ )
@@ -0,0 +1,3 @@
1
+ from fastapi_sidecar.bridge.client import BridgeClient, BridgeClientProtocol
2
+
3
+ __all__ = ["BridgeClient", "BridgeClientProtocol"]
@@ -0,0 +1,71 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Protocol
4
+
5
+ import httpx
6
+
7
+ from fastapi_sidecar.settings import Settings, get_settings
8
+
9
+
10
+ class BridgeClientProtocol(Protocol):
11
+ def runtime_headers(self) -> dict[str, str]:
12
+ ...
13
+
14
+ def request_json(
15
+ self,
16
+ method: str,
17
+ path: str,
18
+ *,
19
+ payload: dict[str, Any] | None = None,
20
+ headers: dict[str, str] | None = None,
21
+ ) -> dict[str, Any]:
22
+ ...
23
+
24
+ def publish_receipt(self, *, path: str, receipt: dict[str, Any]) -> dict[str, Any]:
25
+ ...
26
+
27
+
28
+ class BridgeClient:
29
+ def __init__(
30
+ self,
31
+ *,
32
+ settings: Settings | None = None,
33
+ client: httpx.Client | None = None,
34
+ ) -> None:
35
+ self.settings = settings or get_settings()
36
+ self._client = client or httpx.Client(base_url=self.settings.api_base_url.rstrip("/"))
37
+ self._owns_client = client is None
38
+
39
+ def close(self) -> None:
40
+ if self._owns_client:
41
+ self._client.close()
42
+
43
+ def runtime_headers(self) -> dict[str, str]:
44
+ return {
45
+ "X-Runtime-Id": self.settings.runtime_id,
46
+ "X-Runtime-Key": self.settings.read_runtime_key(),
47
+ }
48
+
49
+ def request_json(
50
+ self,
51
+ method: str,
52
+ path: str,
53
+ *,
54
+ payload: dict[str, Any] | None = None,
55
+ headers: dict[str, str] | None = None,
56
+ ) -> dict[str, Any]:
57
+ request_headers = self.runtime_headers()
58
+ if headers:
59
+ request_headers.update(headers)
60
+
61
+ response = self._client.request(method, path, json=payload, headers=request_headers)
62
+ response.raise_for_status()
63
+ if not response.content:
64
+ return {}
65
+ parsed = response.json()
66
+ if not isinstance(parsed, dict):
67
+ raise RuntimeError("Bridge client expected an object JSON response.")
68
+ return parsed
69
+
70
+ def publish_receipt(self, *, path: str, receipt: dict[str, Any]) -> dict[str, Any]:
71
+ return self.request_json("POST", path, payload=receipt)
@@ -0,0 +1,25 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import logging
5
+ from datetime import UTC, datetime
6
+ from typing import Any
7
+
8
+
9
+ def configure_logging(level: str) -> None:
10
+ logging.basicConfig(level=getattr(logging, level.upper(), logging.INFO))
11
+
12
+
13
+ def log_structured_event(
14
+ logger: logging.Logger,
15
+ *,
16
+ event: str,
17
+ level: int = logging.INFO,
18
+ **fields: Any,
19
+ ) -> None:
20
+ payload = {
21
+ "event": event,
22
+ "timestamp": datetime.now(UTC).isoformat(),
23
+ **fields,
24
+ }
25
+ logger.log(level, json.dumps(payload, sort_keys=True, default=str))
@@ -0,0 +1,85 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from contextlib import asynccontextmanager
5
+ from typing import Any
6
+
7
+ from fastapi import Depends, FastAPI
8
+ from pydantic import BaseModel, Field
9
+
10
+ from fastapi_sidecar.auth import AuthContext, require_internal_only
11
+ from fastapi_sidecar.logging_utils import configure_logging, log_structured_event
12
+ from fastapi_sidecar.receipts import build_receipt
13
+ from fastapi_sidecar.settings import get_settings
14
+
15
+ logger = logging.getLogger("fastapi_sidecar.api")
16
+
17
+
18
+ class InternalWorkRequest(BaseModel):
19
+ operation: str = Field(default="noop")
20
+ payload: dict[str, Any] = Field(default_factory=dict)
21
+
22
+
23
+ @asynccontextmanager
24
+ async def lifespan(_app: FastAPI):
25
+ try:
26
+ settings = get_settings()
27
+ configure_logging(settings.log_level)
28
+ log_structured_event(
29
+ logger,
30
+ event="fastapi_sidecar.startup",
31
+ route_name="startup",
32
+ tenant_id=settings.tenant_id,
33
+ auth_mode="system",
34
+ outcome="ready",
35
+ )
36
+ except Exception as exc: # pragma: no cover - defensive scaffolding path
37
+ configure_logging("INFO")
38
+ logger.warning("Sidecar startup logging skipped: %s", exc)
39
+ yield
40
+
41
+
42
+ def create_app() -> FastAPI:
43
+ app = FastAPI(
44
+ title="MinuteWork FastAPI Sidecar",
45
+ lifespan=lifespan,
46
+ docs_url=None,
47
+ redoc_url=None,
48
+ openapi_url=None,
49
+ )
50
+
51
+ @app.get("/healthz")
52
+ def healthz() -> dict[str, str]:
53
+ return {"status": "ok"}
54
+
55
+ @app.post("/internal/v1/dispatch")
56
+ def dispatch_internal_work(
57
+ body: InternalWorkRequest,
58
+ auth: AuthContext = Depends(require_internal_only),
59
+ ) -> dict[str, Any]:
60
+ receipt = build_receipt(
61
+ request_id=auth.request_id,
62
+ tenant_id=auth.tenant_id,
63
+ handler_name="internal_dispatch",
64
+ auth_mode=auth.auth_mode,
65
+ outcome="accepted",
66
+ extra={"operation": body.operation},
67
+ )
68
+ log_structured_event(
69
+ logger,
70
+ event="fastapi_sidecar.internal_dispatch",
71
+ request_id=auth.request_id,
72
+ tenant_id=auth.tenant_id,
73
+ route_name="internal_dispatch",
74
+ auth_mode=auth.auth_mode,
75
+ outcome="accepted",
76
+ )
77
+ return {
78
+ "accepted": True,
79
+ "receipt": receipt,
80
+ }
81
+
82
+ return app
83
+
84
+
85
+ app = create_app()
@@ -0,0 +1,24 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+
6
+ def build_receipt(
7
+ *,
8
+ request_id: str,
9
+ tenant_id: str,
10
+ handler_name: str,
11
+ auth_mode: str,
12
+ outcome: str,
13
+ detail: str = "",
14
+ extra: dict[str, Any] | None = None,
15
+ ) -> dict[str, Any]:
16
+ return {
17
+ "request_id": request_id,
18
+ "tenant_id": tenant_id,
19
+ "handler_name": handler_name,
20
+ "auth_mode": auth_mode,
21
+ "outcome": outcome,
22
+ "detail": detail,
23
+ "extra": extra or {},
24
+ }
@@ -0,0 +1,41 @@
1
+ from __future__ import annotations
2
+
3
+ from functools import lru_cache
4
+ from pathlib import Path
5
+
6
+ from pydantic import Field
7
+ from pydantic_settings import BaseSettings, SettingsConfigDict
8
+
9
+
10
+ class Settings(BaseSettings):
11
+ model_config = SettingsConfigDict(
12
+ env_file=(".env", ".env.local"),
13
+ env_file_encoding="utf-8",
14
+ extra="ignore",
15
+ )
16
+
17
+ api_base_url: str = Field(validation_alias="MW_API_BASE_URL")
18
+ runtime_id: str = Field(validation_alias="MW_RUNTIME_ID")
19
+ tenant_id: str = Field(validation_alias="MW_TENANT_ID")
20
+ runtime_key_path: Path = Field(validation_alias="MW_RUNTIME_KEY_PATH")
21
+ platform_dispatch_token: str = Field(validation_alias="MW_PLATFORM_DISPATCH_TOKEN")
22
+ log_level: str = Field(default="INFO", validation_alias="MW_LOG_LEVEL")
23
+ pubsub_subscription: str | None = Field(
24
+ default=None,
25
+ validation_alias="MW_PUBSUB_SUBSCRIPTION",
26
+ )
27
+ worker_poll_interval_seconds: float = Field(
28
+ default=5.0,
29
+ validation_alias="MW_WORKER_POLL_INTERVAL_SECONDS",
30
+ )
31
+
32
+ def read_runtime_key(self) -> str:
33
+ value = self.runtime_key_path.read_text(encoding="utf-8").strip()
34
+ if not value:
35
+ raise RuntimeError("Runtime key file is blank.")
36
+ return value
37
+
38
+
39
+ @lru_cache(maxsize=1)
40
+ def get_settings() -> Settings:
41
+ return Settings()
@@ -0,0 +1,26 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+ from jsonschema import Draft202012Validator
7
+
8
+
9
+ def validate_template_manifest(template_root: Path | None = None) -> None:
10
+ resolved_root = template_root or Path(__file__).resolve().parents[2]
11
+ schema = json.loads((resolved_root / "template.schema.json").read_text(encoding="utf-8"))
12
+ manifest = json.loads((resolved_root / "template.json").read_text(encoding="utf-8"))
13
+ validator = Draft202012Validator(schema)
14
+ errors = sorted(validator.iter_errors(manifest), key=lambda error: error.path)
15
+ if errors:
16
+ details = "\n".join(
17
+ f"{'/'.join(str(part) for part in error.path) or '/'} {error.message}"
18
+ for error in errors
19
+ )
20
+ raise RuntimeError(f"Template manifest validation failed.\n{details}")
21
+
22
+
23
+ def main() -> int:
24
+ validate_template_manifest()
25
+ print("template.json is valid")
26
+ return 0
@@ -0,0 +1,33 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from uuid import uuid4
5
+
6
+ from fastapi_sidecar.logging_utils import configure_logging, log_structured_event
7
+ from fastapi_sidecar.receipts import build_receipt
8
+ from fastapi_sidecar.settings import get_settings
9
+
10
+ logger = logging.getLogger("fastapi_sidecar.worker")
11
+
12
+
13
+ def main() -> int:
14
+ settings = get_settings()
15
+ configure_logging(settings.log_level)
16
+ receipt = build_receipt(
17
+ request_id=str(uuid4()),
18
+ tenant_id=settings.tenant_id,
19
+ handler_name="worker_bootstrap",
20
+ auth_mode="worker_bootstrap",
21
+ outcome="started",
22
+ extra={"pubsub_subscription": settings.pubsub_subscription or ""},
23
+ )
24
+ log_structured_event(
25
+ logger,
26
+ event="fastapi_sidecar.worker_bootstrap",
27
+ request_id=receipt["request_id"],
28
+ tenant_id=settings.tenant_id,
29
+ worker_name="worker_bootstrap",
30
+ auth_mode=receipt["auth_mode"],
31
+ outcome=receipt["outcome"],
32
+ )
33
+ return 0
@@ -0,0 +1,43 @@
1
+ {
2
+ "template_id": "fastapi-sidecar",
3
+ "template_kind": "sidecar_fastapi_internal",
4
+ "template_profile": "bridge_internal",
5
+ "template_bundle_ref": "runtime/builder/templates/fastapi-sidecar",
6
+ "template_version": "0.1.0",
7
+ "materialize": {
8
+ "destination": "app"
9
+ },
10
+ "builder_edit_mode": "workspace_copy_only",
11
+ "seed_source": "runtime/builder/templates/fastapi-sidecar",
12
+ "required_bootstrap_steps": [
13
+ "python_env_sync",
14
+ "python_import_smoke"
15
+ ],
16
+ "runtime_contract_refs": [
17
+ "reference/mwv3-dj6-docs/auth_and_credential_contract.md",
18
+ "reference/mwv3-dj6-docs/runtime_app_pack_contract.md",
19
+ "reference/mwv3-dj6-docs/runtime_compute_isolation_and_sandboxing_contract.md"
20
+ ],
21
+ "sidecar_processes": [
22
+ {
23
+ "name": "api",
24
+ "process_type": "fastapi",
25
+ "entrypoint": "fastapi_sidecar.main:app",
26
+ "health_check_path": "/healthz",
27
+ "composition_profiles": [
28
+ "internal_api_only",
29
+ "worker_plus_internal_api"
30
+ ]
31
+ },
32
+ {
33
+ "name": "worker",
34
+ "process_type": "worker",
35
+ "entrypoint": "fastapi_sidecar.worker:main",
36
+ "composition_profiles": [
37
+ "worker_only",
38
+ "worker_plus_internal_api"
39
+ ]
40
+ }
41
+ ],
42
+ "example_features": {}
43
+ }
@@ -0,0 +1,160 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://minutework.dev/schemas/runtime/builder/template.schema.json",
4
+ "title": "Runtime Builder Template Manifest",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": [
8
+ "template_id",
9
+ "template_kind",
10
+ "template_profile",
11
+ "template_version",
12
+ "materialize",
13
+ "builder_edit_mode",
14
+ "seed_source",
15
+ "required_bootstrap_steps",
16
+ "example_features"
17
+ ],
18
+ "properties": {
19
+ "template_id": {
20
+ "type": "string",
21
+ "minLength": 1
22
+ },
23
+ "template_kind": {
24
+ "type": "string",
25
+ "enum": [
26
+ "sidecar_nextjs_private",
27
+ "sidecar_fastapi_internal"
28
+ ]
29
+ },
30
+ "template_profile": {
31
+ "type": "string",
32
+ "enum": [
33
+ "platform_session_bff",
34
+ "bridge_internal"
35
+ ]
36
+ },
37
+ "template_version": {
38
+ "type": "string",
39
+ "pattern": "^\\d+\\.\\d+\\.\\d+$"
40
+ },
41
+ "materialize": {
42
+ "type": "object",
43
+ "additionalProperties": false,
44
+ "required": [
45
+ "destination"
46
+ ],
47
+ "properties": {
48
+ "destination": {
49
+ "type": "string",
50
+ "const": "app"
51
+ }
52
+ }
53
+ },
54
+ "builder_edit_mode": {
55
+ "type": "string",
56
+ "const": "workspace_copy_only"
57
+ },
58
+ "seed_source": {
59
+ "type": "string",
60
+ "minLength": 1
61
+ },
62
+ "template_bundle_ref": {
63
+ "type": "string",
64
+ "minLength": 1
65
+ },
66
+ "required_bootstrap_steps": {
67
+ "type": "array",
68
+ "minItems": 1,
69
+ "uniqueItems": true,
70
+ "items": {
71
+ "type": "string",
72
+ "enum": [
73
+ "next_typegen",
74
+ "design_system_tokens",
75
+ "python_env_sync",
76
+ "python_import_smoke"
77
+ ]
78
+ }
79
+ },
80
+ "runtime_contract_refs": {
81
+ "type": "array",
82
+ "items": {
83
+ "type": "string",
84
+ "minLength": 1
85
+ }
86
+ },
87
+ "sidecar_processes": {
88
+ "type": "array",
89
+ "minItems": 1,
90
+ "items": {
91
+ "$ref": "#/$defs/sidecarProcess"
92
+ }
93
+ },
94
+ "example_features": {
95
+ "type": "object",
96
+ "additionalProperties": {
97
+ "$ref": "#/$defs/exampleFeature"
98
+ }
99
+ }
100
+ },
101
+ "$defs": {
102
+ "exampleFeature": {
103
+ "type": "object",
104
+ "additionalProperties": false,
105
+ "required": [
106
+ "default_enabled"
107
+ ],
108
+ "properties": {
109
+ "default_enabled": {
110
+ "type": "boolean"
111
+ }
112
+ }
113
+ },
114
+ "sidecarProcess": {
115
+ "type": "object",
116
+ "additionalProperties": false,
117
+ "required": [
118
+ "name",
119
+ "process_type",
120
+ "entrypoint",
121
+ "composition_profiles"
122
+ ],
123
+ "properties": {
124
+ "name": {
125
+ "type": "string",
126
+ "minLength": 1
127
+ },
128
+ "process_type": {
129
+ "type": "string",
130
+ "enum": [
131
+ "fastapi",
132
+ "worker",
133
+ "cron"
134
+ ]
135
+ },
136
+ "entrypoint": {
137
+ "type": "string",
138
+ "minLength": 1
139
+ },
140
+ "health_check_path": {
141
+ "type": "string",
142
+ "minLength": 1
143
+ },
144
+ "composition_profiles": {
145
+ "type": "array",
146
+ "minItems": 1,
147
+ "uniqueItems": true,
148
+ "items": {
149
+ "type": "string",
150
+ "enum": [
151
+ "worker_only",
152
+ "internal_api_only",
153
+ "worker_plus_internal_api"
154
+ ]
155
+ }
156
+ }
157
+ }
158
+ }
159
+ }
160
+ }
@@ -0,0 +1,36 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ import pytest
6
+
7
+ from fastapi_sidecar.settings import get_settings
8
+
9
+ TEMPLATE_ROOT = Path(__file__).resolve().parents[1]
10
+ REPO_ROOT = TEMPLATE_ROOT.parents[3]
11
+ NEXT_TEMPLATE_ROOT = REPO_ROOT / "runtime" / "builder" / "templates" / "next-tenant-app"
12
+ SHARED_SCHEMA_PATH = REPO_ROOT / "runtime" / "builder" / "templates" / "template.schema.json"
13
+
14
+
15
+ @pytest.fixture
16
+ def configured_env(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> dict[str, str]:
17
+ runtime_key_path = tmp_path / "runtime-key"
18
+ runtime_key_path.write_text("runtime-secret", encoding="utf-8")
19
+
20
+ env = {
21
+ "MW_API_BASE_URL": "https://platform.example.test",
22
+ "MW_RUNTIME_ID": "rt_test_sidecar",
23
+ "MW_TENANT_ID": "tenant_test_sidecar",
24
+ "MW_RUNTIME_KEY_PATH": str(runtime_key_path),
25
+ "MW_PLATFORM_DISPATCH_TOKEN": "dispatch-secret",
26
+ "MW_LOG_LEVEL": "INFO",
27
+ "MW_PUBSUB_SUBSCRIPTION": "projects/test/subscriptions/sidecar-worker",
28
+ "MW_WORKER_POLL_INTERVAL_SECONDS": "1.5",
29
+ }
30
+
31
+ for key, value in env.items():
32
+ monkeypatch.setenv(key, value)
33
+
34
+ get_settings.cache_clear()
35
+ yield env
36
+ get_settings.cache_clear()