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.
- package/EXTERNAL_ALPHA.md +74 -0
- package/README.md +57 -0
- package/assets/claude-local/CLAUDE.md.template +45 -0
- package/assets/claude-local/bundle.json +22 -0
- package/assets/claude-local/skills/README.md +6 -0
- package/assets/claude-local/skills/app-pack-authoring.md +8 -0
- package/assets/claude-local/skills/event-bus.md +8 -0
- package/assets/claude-local/skills/ontology-mapping.md +8 -0
- package/assets/claude-local/skills/openclaw-skill-importer.md +7 -0
- package/assets/claude-local/skills/schema-engine.md +8 -0
- package/assets/claude-local/skills/secrets-runtime-bridge.md +9 -0
- package/assets/claude-local/skills/sidecar-generation.md +9 -0
- package/assets/templates/fastapi-sidecar/.env.example +8 -0
- package/assets/templates/fastapi-sidecar/README.md +77 -0
- package/assets/templates/fastapi-sidecar/poetry.lock +757 -0
- package/assets/templates/fastapi-sidecar/pyproject.toml +42 -0
- package/assets/templates/fastapi-sidecar/src/fastapi_sidecar/__init__.py +3 -0
- package/assets/templates/fastapi-sidecar/src/fastapi_sidecar/auth.py +70 -0
- package/assets/templates/fastapi-sidecar/src/fastapi_sidecar/bridge/__init__.py +3 -0
- package/assets/templates/fastapi-sidecar/src/fastapi_sidecar/bridge/client.py +71 -0
- package/assets/templates/fastapi-sidecar/src/fastapi_sidecar/logging_utils.py +25 -0
- package/assets/templates/fastapi-sidecar/src/fastapi_sidecar/main.py +85 -0
- package/assets/templates/fastapi-sidecar/src/fastapi_sidecar/receipts.py +24 -0
- package/assets/templates/fastapi-sidecar/src/fastapi_sidecar/settings.py +41 -0
- package/assets/templates/fastapi-sidecar/src/fastapi_sidecar/template_validation.py +26 -0
- package/assets/templates/fastapi-sidecar/src/fastapi_sidecar/worker.py +33 -0
- package/assets/templates/fastapi-sidecar/template.json +43 -0
- package/assets/templates/fastapi-sidecar/template.schema.json +160 -0
- package/assets/templates/fastapi-sidecar/tests/conftest.py +36 -0
- package/assets/templates/fastapi-sidecar/tests/test_app.py +39 -0
- package/assets/templates/fastapi-sidecar/tests/test_auth.py +32 -0
- package/assets/templates/fastapi-sidecar/tests/test_bridge_client.py +31 -0
- package/assets/templates/fastapi-sidecar/tests/test_materialization.py +55 -0
- package/assets/templates/fastapi-sidecar/tests/test_template_contract.py +49 -0
- package/assets/templates/fastapi-sidecar/tests/test_worker.py +7 -0
- package/assets/templates/fastapi-sidecar/tools/template/validate_template.py +20 -0
- package/assets/templates/next-tenant-app/.env.example +8 -0
- package/assets/templates/next-tenant-app/.storybook/main.ts +19 -0
- package/assets/templates/next-tenant-app/.storybook/preview.tsx +38 -0
- package/assets/templates/next-tenant-app/README.md +115 -0
- package/assets/templates/next-tenant-app/components.json +21 -0
- package/assets/templates/next-tenant-app/eslint.config.mjs +41 -0
- package/assets/templates/next-tenant-app/next-env.d.ts +6 -0
- package/assets/templates/next-tenant-app/next.config.ts +8 -0
- package/assets/templates/next-tenant-app/package-lock.json +9682 -0
- package/assets/templates/next-tenant-app/package.json +59 -0
- package/assets/templates/next-tenant-app/pnpm-lock.yaml +6062 -0
- package/assets/templates/next-tenant-app/postcss.config.mjs +8 -0
- package/assets/templates/next-tenant-app/src/app/api/auth/context/route.test.ts +90 -0
- package/assets/templates/next-tenant-app/src/app/api/auth/context/route.ts +78 -0
- package/assets/templates/next-tenant-app/src/app/api/auth/login/route.ts +31 -0
- package/assets/templates/next-tenant-app/src/app/api/auth/logout/route.ts +16 -0
- package/assets/templates/next-tenant-app/src/app/api/auth/password-change/route.test.ts +79 -0
- package/assets/templates/next-tenant-app/src/app/api/auth/password-change/route.ts +40 -0
- package/assets/templates/next-tenant-app/src/app/api/auth/password-status/route.test.ts +42 -0
- package/assets/templates/next-tenant-app/src/app/api/auth/password-status/route.ts +29 -0
- package/assets/templates/next-tenant-app/src/app/api/auth/session/route.ts +26 -0
- package/assets/templates/next-tenant-app/src/app/api/gateway/commands/[runId]/route.test.ts +40 -0
- package/assets/templates/next-tenant-app/src/app/api/gateway/commands/[runId]/route.ts +47 -0
- package/assets/templates/next-tenant-app/src/app/api/gateway/commands/route.test.ts +43 -0
- package/assets/templates/next-tenant-app/src/app/api/gateway/commands/route.ts +45 -0
- package/assets/templates/next-tenant-app/src/app/app/examples/runtime-commands/page.test.ts +83 -0
- package/assets/templates/next-tenant-app/src/app/app/examples/runtime-commands/page.tsx +30 -0
- package/assets/templates/next-tenant-app/src/app/app/layout.tsx +20 -0
- package/assets/templates/next-tenant-app/src/app/app/page.test.ts +62 -0
- package/assets/templates/next-tenant-app/src/app/app/page.tsx +24 -0
- package/assets/templates/next-tenant-app/src/app/blog/[slug]/page.test.ts +70 -0
- package/assets/templates/next-tenant-app/src/app/blog/[slug]/page.tsx +57 -0
- package/assets/templates/next-tenant-app/src/app/blog/page.test.ts +42 -0
- package/assets/templates/next-tenant-app/src/app/blog/page.tsx +37 -0
- package/assets/templates/next-tenant-app/src/app/docs/[...slug]/page.test.ts +70 -0
- package/assets/templates/next-tenant-app/src/app/docs/[...slug]/page.tsx +55 -0
- package/assets/templates/next-tenant-app/src/app/docs/page.test.ts +42 -0
- package/assets/templates/next-tenant-app/src/app/docs/page.tsx +37 -0
- package/assets/templates/next-tenant-app/src/app/globals.css +70 -0
- package/assets/templates/next-tenant-app/src/app/layout.tsx +69 -0
- package/assets/templates/next-tenant-app/src/app/login/page.test.ts +55 -0
- package/assets/templates/next-tenant-app/src/app/login/page.tsx +33 -0
- package/assets/templates/next-tenant-app/src/app/page.test.ts +56 -0
- package/assets/templates/next-tenant-app/src/app/page.tsx +35 -0
- package/assets/templates/next-tenant-app/src/app/pricing/page.test.ts +55 -0
- package/assets/templates/next-tenant-app/src/app/pricing/page.tsx +35 -0
- package/assets/templates/next-tenant-app/src/app/providers.tsx +25 -0
- package/assets/templates/next-tenant-app/src/app/robots.test.ts +20 -0
- package/assets/templates/next-tenant-app/src/app/robots.ts +18 -0
- package/assets/templates/next-tenant-app/src/app/sitemap.test.ts +49 -0
- package/assets/templates/next-tenant-app/src/app/sitemap.ts +54 -0
- package/assets/templates/next-tenant-app/src/components/ui/button.tsx +59 -0
- package/assets/templates/next-tenant-app/src/components/ui/input.tsx +21 -0
- package/assets/templates/next-tenant-app/src/design-system/docs/governance.mdx +26 -0
- package/assets/templates/next-tenant-app/src/design-system/patterns/panel-frame.stories.tsx +48 -0
- package/assets/templates/next-tenant-app/src/design-system/patterns/panel-frame.tsx +26 -0
- package/assets/templates/next-tenant-app/src/design-system/patterns/status-badge.stories.tsx +26 -0
- package/assets/templates/next-tenant-app/src/design-system/patterns/status-badge.tsx +35 -0
- package/assets/templates/next-tenant-app/src/design-system/patterns/theme-mode-toggle.stories.tsx +21 -0
- package/assets/templates/next-tenant-app/src/design-system/patterns/theme-mode-toggle.tsx +75 -0
- package/assets/templates/next-tenant-app/src/design-system/primitives/button.stories.tsx +37 -0
- package/assets/templates/next-tenant-app/src/design-system/primitives/button.ts +1 -0
- package/assets/templates/next-tenant-app/src/design-system/primitives/input.stories.tsx +26 -0
- package/assets/templates/next-tenant-app/src/design-system/primitives/input.ts +1 -0
- package/assets/templates/next-tenant-app/src/design-system/recipes/chrome.ts +28 -0
- package/assets/templates/next-tenant-app/src/design-system/tokens/foundation.css +31 -0
- package/assets/templates/next-tenant-app/src/design-system/tokens/index.css +3 -0
- package/assets/templates/next-tenant-app/src/design-system/tokens/manifest.json +85 -0
- package/assets/templates/next-tenant-app/src/design-system/tokens/manifest.ts +87 -0
- package/assets/templates/next-tenant-app/src/design-system/tokens/semantic.css +105 -0
- package/assets/templates/next-tenant-app/src/design-system/tokens/theme.css +59 -0
- package/assets/templates/next-tenant-app/src/design-system/tokens/tokens.stories.tsx +71 -0
- package/assets/templates/next-tenant-app/src/features/auth/components/login-screen.tsx +198 -0
- package/assets/templates/next-tenant-app/src/features/dashboard/components/tenant-dashboard.tsx +153 -0
- package/assets/templates/next-tenant-app/src/features/examples/runtime-command-demo/components/runtime-command-demo.tsx +342 -0
- package/assets/templates/next-tenant-app/src/features/public-shell/components/content-article.tsx +66 -0
- package/assets/templates/next-tenant-app/src/features/public-shell/components/content-collection.tsx +108 -0
- package/assets/templates/next-tenant-app/src/features/public-shell/components/marketing-page-canvas.tsx +111 -0
- package/assets/templates/next-tenant-app/src/features/public-shell/components/public-site-shell.tsx +111 -0
- package/assets/templates/next-tenant-app/src/features/shell/components/private-app-shell.tsx +624 -0
- package/assets/templates/next-tenant-app/src/lib/app-routes.test.ts +20 -0
- package/assets/templates/next-tenant-app/src/lib/app-routes.ts +59 -0
- package/assets/templates/next-tenant-app/src/lib/content/__fixtures__/public-site-snapshot.ts +189 -0
- package/assets/templates/next-tenant-app/src/lib/content/adapter.server.test.ts +318 -0
- package/assets/templates/next-tenant-app/src/lib/content/adapter.server.ts +232 -0
- package/assets/templates/next-tenant-app/src/lib/content/contracts.ts +339 -0
- package/assets/templates/next-tenant-app/src/lib/content/custom-adapter.ts +5 -0
- package/assets/templates/next-tenant-app/src/lib/content/empty-state.ts +96 -0
- package/assets/templates/next-tenant-app/src/lib/platform/auth.server.test.ts +75 -0
- package/assets/templates/next-tenant-app/src/lib/platform/auth.server.ts +25 -0
- package/assets/templates/next-tenant-app/src/lib/platform/client.server.test.ts +170 -0
- package/assets/templates/next-tenant-app/src/lib/platform/client.server.ts +661 -0
- package/assets/templates/next-tenant-app/src/lib/platform/contracts.ts +131 -0
- package/assets/templates/next-tenant-app/src/lib/platform/endpoints.server.ts +34 -0
- package/assets/templates/next-tenant-app/src/lib/platform/env.server.test.ts +102 -0
- package/assets/templates/next-tenant-app/src/lib/platform/env.server.ts +87 -0
- package/assets/templates/next-tenant-app/src/lib/platform/route-response.ts +33 -0
- package/assets/templates/next-tenant-app/src/lib/platform/session.server.ts +108 -0
- package/assets/templates/next-tenant-app/src/lib/public-site.test.ts +20 -0
- package/assets/templates/next-tenant-app/src/lib/public-site.ts +49 -0
- package/assets/templates/next-tenant-app/src/lib/theme-config.ts +10 -0
- package/assets/templates/next-tenant-app/src/lib/theme.tsx +159 -0
- package/assets/templates/next-tenant-app/src/lib/utils.ts +6 -0
- package/assets/templates/next-tenant-app/template.json +27 -0
- package/assets/templates/next-tenant-app/template.schema.json +160 -0
- package/assets/templates/next-tenant-app/test/server-only-stub.ts +1 -0
- package/assets/templates/next-tenant-app/tools/design-system/build-token-manifest.mjs +3 -0
- package/assets/templates/next-tenant-app/tools/design-system/check-imports.mjs +9 -0
- package/assets/templates/next-tenant-app/tools/design-system/check-stories.mjs +9 -0
- package/assets/templates/next-tenant-app/tools/design-system/check-values.mjs +9 -0
- package/assets/templates/next-tenant-app/tools/design-system/checks.mjs +238 -0
- package/assets/templates/next-tenant-app/tools/design-system/eslint-plugin-design-system.mjs +184 -0
- package/assets/templates/next-tenant-app/tools/design-system/playwright.config.mjs +34 -0
- package/assets/templates/next-tenant-app/tools/design-system/run-checks.mjs +22 -0
- package/assets/templates/next-tenant-app/tools/design-system/shared.mjs +166 -0
- package/assets/templates/next-tenant-app/tools/design-system/visual.spec.ts +41 -0
- package/assets/templates/next-tenant-app/tools/template/validate-route-contract.mjs +39 -0
- package/assets/templates/next-tenant-app/tools/template/validate-template.mjs +45 -0
- package/assets/templates/next-tenant-app/tsconfig.json +42 -0
- package/assets/templates/next-tenant-app/vitest.config.ts +25 -0
- package/bin/minutework.js +40 -0
- package/dist/auth.d.ts +59 -0
- package/dist/auth.js +338 -0
- package/dist/auth.js.map +1 -0
- package/dist/browser.d.ts +1 -0
- package/dist/browser.js +26 -0
- package/dist/browser.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +5 -0
- package/dist/cli.js.map +1 -0
- package/dist/compile.d.ts +20 -0
- package/dist/compile.js +121 -0
- package/dist/compile.js.map +1 -0
- package/dist/config.d.ts +25 -0
- package/dist/config.js +102 -0
- package/dist/config.js.map +1 -0
- package/dist/deploy-state.d.ts +35 -0
- package/dist/deploy-state.js +30 -0
- package/dist/deploy-state.js.map +1 -0
- package/dist/deploy.d.ts +22 -0
- package/dist/deploy.js +308 -0
- package/dist/deploy.js.map +1 -0
- package/dist/developer-client.d.ts +88 -0
- package/dist/developer-client.js +78 -0
- package/dist/developer-client.js.map +1 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +290 -0
- package/dist/index.js.map +1 -0
- package/dist/init.d.ts +22 -0
- package/dist/init.js +421 -0
- package/dist/init.js.map +1 -0
- package/dist/launcher.d.ts +1 -0
- package/dist/launcher.js +50 -0
- package/dist/launcher.js.map +1 -0
- package/dist/paths.d.ts +12 -0
- package/dist/paths.js +33 -0
- package/dist/paths.js.map +1 -0
- package/dist/sandbox.d.ts +30 -0
- package/dist/sandbox.js +852 -0
- package/dist/sandbox.js.map +1 -0
- package/dist/state.d.ts +46 -0
- package/dist/state.js +82 -0
- package/dist/state.js.map +1 -0
- package/dist/tokens.d.ts +14 -0
- package/dist/tokens.js +293 -0
- package/dist/tokens.js.map +1 -0
- 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,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,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()
|