machinaos 0.0.87 → 0.0.89

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 (188) hide show
  1. package/.env.template +39 -0
  2. package/.machina/workflows/AI Assistant_example_workflow-1779017037684-e2e5da7a.json +1 -1
  3. package/.machina/workflows/AI Employee_example_workflow-1779102911870-cbc76c82.json +6 -6
  4. package/README.md +6 -6
  5. package/bin/cli.js +6 -1
  6. package/cli/cli.py +87 -0
  7. package/cli/commands/build.py +33 -3
  8. package/cli/commands/clean.py +13 -4
  9. package/cli/commands/deploy/__init__.py +13 -0
  10. package/cli/commands/deploy/_secrets.py +65 -0
  11. package/cli/commands/deploy/_state.py +56 -0
  12. package/cli/commands/deploy/_terraform.py +68 -0
  13. package/cli/commands/deploy/destroy.py +34 -0
  14. package/cli/commands/deploy/providers/__init__.py +34 -0
  15. package/cli/commands/deploy/providers/_base.py +46 -0
  16. package/cli/commands/deploy/providers/aws.py +45 -0
  17. package/cli/commands/deploy/providers/gcp.py +89 -0
  18. package/cli/commands/deploy/status.py +48 -0
  19. package/cli/commands/deploy/up.py +146 -0
  20. package/cli/commands/serve.py +98 -0
  21. package/cli/terraform/gcp/main.tf +116 -0
  22. package/cli/terraform/gcp/outputs.tf +9 -0
  23. package/cli/terraform/gcp/startup.sh.tftpl +65 -0
  24. package/cli/terraform/gcp/variables.tf +54 -0
  25. package/client/dist/assets/{ActionBar-Bg1hW3t6.js → ActionBar-DqL2u6-G.js} +1 -1
  26. package/client/dist/assets/{ApiKeyInput-DqeO8LWg.js → ApiKeyInput-DmIFsu5L.js} +1 -1
  27. package/client/dist/assets/ApiKeyPanel-BJGWpSo3.js +1 -0
  28. package/client/dist/assets/ApiUsageSection-B9FBHYF_.js +1 -0
  29. package/client/dist/assets/{EmailPanel-B-Wsn1Cy.js → EmailPanel-DDsRt9xV.js} +1 -1
  30. package/client/dist/assets/{OAuthPanel-C2tVcDmU.js → OAuthPanel-DQiCai8Z.js} +1 -1
  31. package/client/dist/assets/{QrPairingPanel-ChNvq1Zt.js → QrPairingPanel-DmVb3FlK.js} +1 -1
  32. package/client/dist/assets/{RateLimitSection-ByFuEORg.js → RateLimitSection-CQstfsLJ.js} +1 -1
  33. package/client/dist/assets/{StatusCard-Pzhnd_Bf.js → StatusCard-B4JY4t4C.js} +1 -1
  34. package/client/dist/assets/index-CW3r0-Ob.js +165 -0
  35. package/client/dist/assets/index-DrhPs19g.css +1 -0
  36. package/client/dist/assets/{vendor-misc-C4VxKHs5.js → vendor-misc-DBQ988Ev.js} +1 -1
  37. package/client/dist/assets/{vendor-radix-Dnos29jG.js → vendor-radix-BHmt2CkM.js} +1 -1
  38. package/client/dist/index.html +4 -4
  39. package/client/package.json +3 -3
  40. package/client/src/Dashboard.tsx +3 -4
  41. package/client/src/components/ConditionalEdge.tsx +2 -2
  42. package/client/src/components/ParameterRenderer.tsx +209 -809
  43. package/client/src/components/PricingConfigModal.tsx +27 -66
  44. package/client/src/components/SquareNode.tsx +1 -1
  45. package/client/src/components/TriggerNode.tsx +13 -8
  46. package/client/src/components/credentials/CredentialsModal.tsx +1 -1
  47. package/client/src/components/credentials/catalogueAdapter.ts +1 -0
  48. package/client/src/components/credentials/panels/ApiKeyPanel.tsx +9 -1
  49. package/client/src/components/credentials/sections/ApiUsageSection.tsx +4 -4
  50. package/client/src/components/credentials/sections/LlmUsageSection.tsx +7 -7
  51. package/client/src/components/credentials/types.ts +2 -0
  52. package/client/src/components/onboarding/steps/ApiKeyStep.tsx +1 -1
  53. package/client/src/components/onboarding/steps/CanvasStep.tsx +12 -13
  54. package/client/src/components/parameterPanel/MiddleSection.tsx +4 -5
  55. package/client/src/components/ui/ComponentPalette.tsx +2 -4
  56. package/client/src/components/ui/ErrorBoundary.tsx +1 -1
  57. package/client/src/components/ui/SettingsPanel.tsx +33 -5
  58. package/client/src/components/ui/ThemeSwitcher.tsx +1 -1
  59. package/client/src/components/ui/TopToolbar.tsx +6 -6
  60. package/client/src/components/ui/action-button.tsx +18 -13
  61. package/client/src/components/ui/settingsPanel/schema.ts +7 -1
  62. package/client/src/contexts/WebSocketContext.tsx +4 -4
  63. package/client/src/hooks/useCatalogueQuery.ts +2 -0
  64. package/client/src/index.css +149 -297
  65. package/client/src/lib/workflowOps.ts +13 -1
  66. package/client/src/main.tsx +4 -0
  67. package/client/src/styles/theme.ts +29 -29
  68. package/client/src/themes/animations.css +193 -0
  69. package/client/src/themes/atomic.css +78 -68
  70. package/client/src/themes/base.css +139 -78
  71. package/client/src/themes/cyber.css +77 -90
  72. package/client/src/themes/dark.css +72 -25
  73. package/client/src/themes/edo.css +71 -65
  74. package/client/src/themes/greek.css +74 -68
  75. package/client/src/themes/light.css +98 -0
  76. package/client/src/themes/plague.css +79 -73
  77. package/client/src/themes/renaissance.css +81 -92
  78. package/client/src/themes/rot.css +71 -65
  79. package/client/src/themes/steampunk.css +77 -71
  80. package/client/src/themes/surveillance.css +82 -95
  81. package/client/src/themes/wasteland.css +80 -74
  82. package/client/tailwind.config.js +53 -50
  83. package/package.json +11 -4
  84. package/scripts/install.js +9 -1
  85. package/server/config/credential_providers.json +2 -1
  86. package/server/config/llm_defaults.json +57 -45
  87. package/server/config/model_registry.json +687 -1297
  88. package/server/config/node_allowlist.json +2 -1
  89. package/server/core/config.py +65 -3
  90. package/server/core/container.py +36 -1
  91. package/server/core/database.py +44 -48
  92. package/server/core/paths.py +88 -103
  93. package/server/main.py +80 -0
  94. package/server/middleware/auth.py +12 -2
  95. package/server/models/database.py +5 -1
  96. package/server/nodejs/package.json +1 -1
  97. package/server/nodes/README.md +25 -2
  98. package/server/nodes/agent/claude_code_agent/_oauth.py +54 -26
  99. package/server/nodes/browser/_install.py +14 -11
  100. package/server/nodes/browser/_service.py +100 -0
  101. package/server/nodes/browser/browser/__init__.py +4 -2
  102. package/server/nodes/code/monty_executor/__init__.py +267 -0
  103. package/server/nodes/code/monty_executor/icon.svg +1 -0
  104. package/server/nodes/code/monty_executor/meta.json +3 -0
  105. package/server/nodes/filesystem/_backend.py +16 -1
  106. package/server/nodes/filesystem/file_modify/__init__.py +4 -1
  107. package/server/nodes/filesystem/file_read/__init__.py +20 -3
  108. package/server/nodes/filesystem/fs_search/__init__.py +4 -1
  109. package/server/nodes/search/perplexity_search/__init__.py +10 -9
  110. package/server/nodes/stripe/_handlers.py +57 -14
  111. package/server/nodes/stripe/_install.py +15 -8
  112. package/server/nodes/tool/agent_builder/__init__.py +574 -46
  113. package/server/nodes/tool/write_todos/__init__.py +29 -2
  114. package/server/nodes/utility/team_monitor/__init__.py +3 -1
  115. package/server/nodes/visuals.json +3 -0
  116. package/server/nodes/whatsapp/_install.py +71 -0
  117. package/server/nodes/whatsapp/_runtime.py +34 -5
  118. package/server/pyproject.toml +19 -1
  119. package/server/requirements.txt +591 -108
  120. package/server/services/ai.py +320 -159
  121. package/server/services/events/cli.py +17 -0
  122. package/server/services/events/daemon.py +10 -3
  123. package/server/services/handlers/todo.py +4 -1
  124. package/server/services/handlers/tools.py +15 -2
  125. package/server/services/llm/__init__.py +22 -1
  126. package/server/services/llm/protocol.py +5 -1
  127. package/server/services/llm/providers/__init__.py +26 -1
  128. package/server/services/llm/providers/_compat.py +109 -0
  129. package/server/services/llm/providers/anthropic.py +21 -0
  130. package/server/services/llm/providers/gemini.py +72 -9
  131. package/server/services/llm/providers/openai.py +24 -0
  132. package/server/services/llm/providers/openrouter.py +19 -0
  133. package/server/services/llm/registry.py +111 -0
  134. package/server/services/llm/unifier.py +132 -0
  135. package/server/services/llm/vertex.py +28 -0
  136. package/server/services/model_registry.py +37 -8
  137. package/server/services/node_executor.py +1 -0
  138. package/server/services/plugin/__init__.py +2 -0
  139. package/server/services/plugin/base.py +83 -9
  140. package/server/services/plugin/tool.py +25 -6
  141. package/server/services/process_service.py +19 -5
  142. package/server/services/temporal/_install.py +36 -10
  143. package/server/services/temporal/_runtime.py +22 -0
  144. package/server/services/temporal/agent_activities.py +179 -3
  145. package/server/services/temporal/agent_workflow.py +170 -26
  146. package/server/services/temporal/client.py +82 -11
  147. package/server/services/temporal/executor.py +1 -0
  148. package/server/services/temporal/worker.py +38 -8
  149. package/server/services/temporal/workflow.py +8 -0
  150. package/server/services/user_auth.py +3 -2
  151. package/server/services/workflow.py +10 -1
  152. package/server/services/workflow_ops.py +4 -0
  153. package/server/skills/assistant/agent-builder-skill/SKILL.md +107 -80
  154. package/server/skills/coding_agent/file-modify-skill/SKILL.md +2 -0
  155. package/server/skills/coding_agent/file-read-skill/SKILL.md +2 -0
  156. package/server/skills/coding_agent/fs-search-skill/SKILL.md +2 -0
  157. package/server/skills/coding_agent/monty-skill/SKILL.md +116 -0
  158. package/server/skills/payments_agent/stripe-skill/SKILL.md +2 -2
  159. package/server/skills/web_agent/browser-skill/SKILL.md +8 -0
  160. package/server/tests/conftest.py +3 -5
  161. package/server/tests/llm/test_live_providers.py +289 -0
  162. package/server/tests/llm/test_max_tokens_resolution.py +136 -0
  163. package/server/tests/llm/test_plugin_shape.py +168 -0
  164. package/server/tests/llm/test_provider_self_registration.py +82 -0
  165. package/server/tests/llm/test_unifier_incompatible_models_filter.py +106 -0
  166. package/server/tests/llm/test_unifier_typed_errors.py +145 -0
  167. package/server/tests/llm/test_vertex_key.py +254 -0
  168. package/server/tests/llm/test_wiring.py +65 -56
  169. package/server/tests/nodes/_compat.py +2 -1
  170. package/server/tests/nodes/test_agent_builder.py +587 -3
  171. package/server/tests/nodes/test_ai_tools.py +37 -0
  172. package/server/tests/nodes/test_browser_service.py +110 -0
  173. package/server/tests/nodes/test_code_fs_process.py +61 -2
  174. package/server/tests/nodes/test_monty_executor.py +289 -0
  175. package/server/tests/nodes/test_stripe_plugin.py +3 -2
  176. package/server/tests/nodes/test_web_automation.py +21 -0
  177. package/server/tests/services/test_agent_loop_rebind.py +355 -0
  178. package/server/tests/temporal/test_agent_workflow.py +338 -3
  179. package/server/tests/temporal/test_client_readiness.py +114 -0
  180. package/server/tests/temporal/test_terminate_sweep.py +84 -0
  181. package/server/tests/temporal/test_worker_restart.py +78 -0
  182. package/server/tests/test_output_contract.py +203 -0
  183. package/server/tests/test_plugin_contract.py +124 -0
  184. package/server/uv.lock +346 -350
  185. package/client/dist/assets/ApiKeyPanel-BaGAyu8Z.js +0 -1
  186. package/client/dist/assets/ApiUsageSection-C_pELnWe.js +0 -1
  187. package/client/dist/assets/index-D-LxTbwD.js +0 -165
  188. package/client/dist/assets/index-DxmbVskS.css +0 -1
@@ -0,0 +1,45 @@
1
+ """aws-cli adapter -- placeholder (the AWS Terraform module is a follow-on).
2
+
3
+ Implements the ``ProviderCli`` shape so the resolver/wiring is uniform, but
4
+ aborts with a clear message until the ``cli/terraform/aws/`` module lands. The
5
+ real adapter will mirror gcp.py: ``aws sts get-caller-identity`` for auth, the
6
+ default credential chain for Terraform, region from ``aws configure get region``
7
+ / ``$AWS_REGION``, and a security-group + EC2 instance module.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import typer
13
+
14
+ from cli._common import error_block
15
+
16
+
17
+ class AwsCli:
18
+ name = "aws"
19
+
20
+ def _not_yet(self) -> None:
21
+ error_block(
22
+ "The aws provider is not implemented yet.",
23
+ ["Use --provider gcp for now; the AWS module is a planned follow-on."],
24
+ )
25
+ raise typer.Exit(code=1)
26
+
27
+ def check(self) -> None:
28
+ self._not_yet()
29
+
30
+ def authed_account(self) -> str | None:
31
+ return None
32
+
33
+ def resolve_context(self, *, region, zone, project) -> dict:
34
+ self._not_yet()
35
+ return {}
36
+
37
+ def ensure_terraform_auth(self) -> None:
38
+ self._not_yet()
39
+
40
+ def enable_apis(self, ctx: dict) -> None:
41
+ self._not_yet()
42
+
43
+ def tfvars_extra(self, ctx: dict) -> dict:
44
+ self._not_yet()
45
+ return {}
@@ -0,0 +1,89 @@
1
+ """gcloud adapter -- Stage 1 of ``machina deploy --provider gcp``.
2
+
3
+ Uses the operator's installed + authenticated ``gcloud`` CLI for auth/context/
4
+ API-enablement; Terraform's ``google`` provider then reuses the same
5
+ Application Default Credentials to create the resources.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import typer
11
+
12
+ from cli._common import error_block
13
+ from cli.colors import console
14
+ from cli.run import capture, run
15
+
16
+ _REQUIRED_APIS = [
17
+ "compute.googleapis.com",
18
+ "storage.googleapis.com",
19
+ "iam.googleapis.com",
20
+ ]
21
+
22
+
23
+ def _clean(value: str | None) -> str | None:
24
+ """Normalise gcloud config reads (``(unset)`` / empty -> None)."""
25
+ if not value:
26
+ return None
27
+ v = value.strip()
28
+ return None if v in ("", "(unset)") else v
29
+
30
+
31
+ class GcpCli:
32
+ name = "gcp"
33
+
34
+ def check(self) -> None:
35
+ if capture(["gcloud", "version"]) is None:
36
+ error_block(
37
+ "The gcloud CLI was not found on PATH.",
38
+ ["Install the Google Cloud SDK: https://cloud.google.com/sdk/docs/install"],
39
+ )
40
+ raise typer.Exit(code=1)
41
+
42
+ def authed_account(self) -> str | None:
43
+ return _clean(
44
+ capture(
45
+ ["gcloud", "auth", "list", "--filter=status:ACTIVE", "--format=value(account)"]
46
+ )
47
+ )
48
+
49
+ def resolve_context(self, *, region, zone, project) -> dict:
50
+ account = self.authed_account()
51
+ if not account:
52
+ error_block(
53
+ "gcloud is not logged in.",
54
+ ["Run: gcloud auth login"],
55
+ )
56
+ raise typer.Exit(code=1)
57
+
58
+ proj = project or _clean(capture(["gcloud", "config", "get-value", "project"]))
59
+ if not proj:
60
+ error_block(
61
+ "No GCP project is set.",
62
+ ["Pass --project <id>, or run: gcloud config set project <id>"],
63
+ )
64
+ raise typer.Exit(code=1)
65
+
66
+ reg = region or _clean(capture(["gcloud", "config", "get-value", "compute/region"])) or "us-central1"
67
+ zn = zone or _clean(capture(["gcloud", "config", "get-value", "compute/zone"])) or "us-central1-a"
68
+
69
+ console.print(f" gcloud account: {account}")
70
+ console.print(f" project={proj} region={reg} zone={zn}")
71
+ return {"project": proj, "region": reg, "zone": zn}
72
+
73
+ def ensure_terraform_auth(self) -> None:
74
+ # Terraform's google provider authenticates via Application Default
75
+ # Credentials. print-access-token exits non-zero (capture -> None) when
76
+ # ADC is not configured.
77
+ if capture(["gcloud", "auth", "application-default", "print-access-token"]) is None:
78
+ error_block(
79
+ "Terraform needs Application Default Credentials.",
80
+ ["Run: gcloud auth application-default login"],
81
+ )
82
+ raise typer.Exit(code=1)
83
+
84
+ def enable_apis(self, ctx: dict) -> None:
85
+ console.log("Enabling required GCP APIs (compute, storage, iam)...")
86
+ run(["gcloud", "services", "enable", *_REQUIRED_APIS, "--project", ctx["project"]])
87
+
88
+ def tfvars_extra(self, ctx: dict) -> dict:
89
+ return {"project": ctx["project"], "region": ctx["region"], "zone": ctx["zone"]}
@@ -0,0 +1,48 @@
1
+ """``machina deploy status`` -- show the deployment's URL + health."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import urllib.error
6
+ import urllib.request
7
+
8
+ import typer
9
+
10
+ from cli._common import preflight
11
+ from cli.colors import console
12
+
13
+ from . import _state, _terraform
14
+
15
+
16
+ def status_command() -> None:
17
+ # Establish the same DATA_DIR context as `deploy up` (load_config sets it)
18
+ # so workdir() resolves to the directory `up` created.
19
+ preflight()
20
+ meta = _state.read_meta()
21
+ if meta is None:
22
+ console.print("[yellow]No 'machinaos' deployment found.[/]")
23
+ raise typer.Exit(code=1)
24
+
25
+ wd = _state.workdir()
26
+ ip = _terraform.tf_output(wd, "external_ip")
27
+ url = _terraform.tf_output(wd, "url") or (
28
+ f"http://{ip}:{meta.get('port', 8080)}" if ip else None
29
+ )
30
+
31
+ console.print(" Deployment: machinaos")
32
+ console.print(f" Provider: {meta.get('provider', '?')}")
33
+ console.print(f" External IP: {ip or '(unknown / destroyed)'}")
34
+ console.print(f" URL: {url or '(unknown)'}")
35
+ console.print(f" Login email: {meta.get('owner_email', '?')}")
36
+
37
+ if not url:
38
+ raise typer.Exit(code=1)
39
+
40
+ healthy = False
41
+ try:
42
+ with urllib.request.urlopen(url.rstrip("/") + "/health", timeout=5) as resp: # noqa: S310
43
+ healthy = resp.status == 200
44
+ except (urllib.error.URLError, OSError):
45
+ healthy = False
46
+ console.print(f" Health: {'[green]OK[/]' if healthy else '[yellow]not ready[/]'}")
47
+ if not healthy:
48
+ raise typer.Exit(code=1)
@@ -0,0 +1,146 @@
1
+ """``machina deploy up`` -- provision + run MachinaOs on a fresh cloud VM.
2
+
3
+ Two stages:
4
+ STAGE 1 (cloud CLI): the provider adapter verifies the CLI is installed +
5
+ authenticated, resolves project/region/zone, ensures Terraform auth, and
6
+ enables the required cloud APIs.
7
+ STAGE 2 (Terraform): generate secrets + owner creds -> (local source) npm
8
+ pack -> write tfvars -> ``terraform init`` + ``apply`` -> read outputs ->
9
+ poll ``/health`` -> print URL + credentials.
10
+
11
+ The VM instance is always named ``machinaos`` (one deployment per project).
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import time
17
+ import urllib.error
18
+ import urllib.request
19
+ from pathlib import Path
20
+
21
+ import typer
22
+
23
+ from cli._common import error_block, preflight
24
+ from cli.colors import console
25
+ from cli.run import capture
26
+
27
+ from . import _secrets, _state, _terraform
28
+ from .providers import get_provider
29
+
30
+
31
+ def _npm_pack(root: Path) -> str:
32
+ """Run ``npm pack`` in the repo root; return the abs path to the tarball."""
33
+ console.log("Packaging local build (npm pack)...")
34
+ out = capture(["npm", "pack"], cwd=root)
35
+ if not out:
36
+ error_block(
37
+ "`npm pack` produced no output.",
38
+ ["Ensure Node/npm are installed and you are in a MachinaOs checkout."],
39
+ )
40
+ raise typer.Exit(code=1)
41
+ tarball = out.strip().splitlines()[-1].strip()
42
+ path = (root / tarball).resolve()
43
+ if not path.is_file():
44
+ error_block(f"npm pack reported {tarball!r} but it is not on disk.", [str(path)])
45
+ raise typer.Exit(code=1)
46
+ return str(path)
47
+
48
+
49
+ def _poll_health(url: str, *, attempts: int = 40, delay: float = 15.0) -> bool:
50
+ """Poll ``<url>/health`` until 200 or attempts exhausted (~10 min)."""
51
+ health = url.rstrip("/") + "/health"
52
+ for i in range(1, attempts + 1):
53
+ try:
54
+ with urllib.request.urlopen(health, timeout=5) as resp: # noqa: S310
55
+ if resp.status == 200:
56
+ return True
57
+ except (urllib.error.URLError, OSError):
58
+ pass
59
+ console.log(f"waiting for the VM to finish provisioning... ({i}/{attempts})")
60
+ time.sleep(delay)
61
+ return False
62
+
63
+
64
+ def up_command(
65
+ *,
66
+ provider: str,
67
+ region: str | None,
68
+ zone: str | None,
69
+ machine_type: str,
70
+ port: int,
71
+ owner_email: str,
72
+ owner_password: str | None,
73
+ source: str,
74
+ version: str,
75
+ allow_cidr: str,
76
+ project: str | None,
77
+ ) -> None:
78
+ _, root = preflight()
79
+
80
+ # --- STAGE 1: cloud CLI (auth + context + APIs) ------------------------
81
+ cli = get_provider(provider)
82
+ cli.check()
83
+ _terraform.ensure_terraform()
84
+ ctx = cli.resolve_context(region=region, zone=zone, project=project)
85
+ cli.ensure_terraform_auth()
86
+ cli.enable_apis(ctx)
87
+
88
+ if _state.exists() and (_state.workdir() / "terraform.tfstate").exists():
89
+ console.print(
90
+ "[yellow]A 'machinaos' deployment already exists. Re-running will "
91
+ "re-apply Terraform (safe), or run `machina deploy destroy` first.[/]"
92
+ )
93
+
94
+ # --- STAGE 2: secrets + source + Terraform -----------------------------
95
+ pw = owner_password or _secrets.new_password()
96
+ pw_generated = owner_password is None
97
+ app_env = _secrets.build_app_env(owner_email=owner_email, owner_password=pw, port=port)
98
+
99
+ pack_tarball = _npm_pack(root) if source == "local" else ""
100
+
101
+ tfvars: dict = {
102
+ "machine_type": machine_type,
103
+ "port": port,
104
+ "allow_cidr": allow_cidr,
105
+ "app_env": app_env,
106
+ "source_mode": source,
107
+ "machinaos_version": version,
108
+ "pack_tarball": pack_tarball,
109
+ }
110
+ tfvars.update(cli.tfvars_extra(ctx))
111
+
112
+ wd = _state.workdir()
113
+ _terraform.prepare_workdir(wd, provider)
114
+ _terraform.write_tfvars(wd, tfvars)
115
+ _state.write_meta({"provider": provider, "port": port, "owner_email": owner_email})
116
+
117
+ console.log("terraform init...")
118
+ _terraform.tf(wd, "init", "-input=false")
119
+ console.log("terraform apply (creating cloud resources)...")
120
+ _terraform.tf(wd, "apply", "-auto-approve", "-input=false")
121
+
122
+ ip = _terraform.tf_output(wd, "external_ip")
123
+ url = _terraform.tf_output(wd, "url") or (f"http://{ip}:{port}" if ip else None)
124
+
125
+ console.print()
126
+ console.print(" [bold green]VM 'machinaos' provisioned.[/]")
127
+ if ip:
128
+ console.print(f" External IP: {ip}")
129
+ if url:
130
+ console.print(f" URL: {url}")
131
+ console.print(f" Login email: {owner_email}")
132
+ if pw_generated:
133
+ console.print(f" Login password (save this now): [bold]{pw}[/]")
134
+ else:
135
+ console.print(" Login password: (the one you provided)")
136
+ console.print()
137
+
138
+ if url:
139
+ console.log("The VM is installing MachinaOs (Node + npm + build); this takes a few minutes.")
140
+ if _poll_health(url):
141
+ console.print(f" [bold green]Ready.[/] Open {url} and log in.")
142
+ else:
143
+ console.print(
144
+ " [yellow]Still provisioning.[/] Check again with "
145
+ "`machina deploy status` in a few minutes."
146
+ )
@@ -0,0 +1,98 @@
1
+ """``machina serve`` -- single-port production runtime.
2
+
3
+ Runs the app on ONE public port: uvicorn serves the REST API + WebSocket +
4
+ the built React SPA (via the ``SERVE_STATIC_CLIENT`` block in
5
+ ``server/main.py``), plus the Node.js code-exec sidecar on its own internal
6
+ port. Used locally for a production-shaped run AND as the systemd
7
+ ``ExecStart`` on a VM provisioned by ``machina deploy``.
8
+
9
+ Unlike ``machina start`` (which runs a separate static-client server + the
10
+ backend + temporal on multiple ports), ``serve`` is single-port and serves
11
+ the client from the backend itself. The Node sidecar is NOT launched by
12
+ ``start``/``dev`` today, so ``serve`` adds it (the JS/TS executor nodes need
13
+ it).
14
+
15
+ The long-running uvicorn is invoked via the server venv's interpreter
16
+ directly (not ``uv run``) so the systemd service has no runtime dependency
17
+ on ``uv`` being on PATH.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import asyncio
23
+ import os
24
+ from pathlib import Path
25
+
26
+ import typer
27
+
28
+ from cli._common import preflight
29
+ from cli.buildenv import validate_build
30
+ from cli.colors import console
31
+ from cli.platform_ import IS_WINDOWS, server_dir, server_venv
32
+
33
+
34
+ def _venv_python(root: Path | None = None) -> str:
35
+ """Absolute path to the server venv's Python interpreter."""
36
+ venv = server_venv(root)
37
+ rel = "Scripts/python.exe" if IS_WINDOWS else "bin/python"
38
+ return str(venv / rel)
39
+
40
+
41
+ def serve_command(port: int | None = None) -> None:
42
+ from cli.supervisor import Manager, ServiceSpec
43
+
44
+ cfg, root = preflight()
45
+ os.environ.setdefault("PYTHONUTF8", "1")
46
+ validate_build(root, require_client_dist=True)
47
+
48
+ # Public port: --port flag > $PORT (Cloud Run / systemd convention) >
49
+ # PYTHON_BACKEND_PORT from the env files.
50
+ bind_port = port or int(os.environ.get("PORT") or cfg.backend_port)
51
+
52
+ # Free the ports we will bind (clears stale orphans; idempotent).
53
+ from cli.ports import kill_port
54
+
55
+ for p in {bind_port, cfg.nodejs_port}:
56
+ kill_port(p)
57
+
58
+ console.print()
59
+ console.print(" [bold]MachinaOS[/] serve (single-port)")
60
+ console.print(f" App: http://0.0.0.0:{bind_port} (API + WebSocket + SPA)")
61
+ console.print(f" Sidecar: 127.0.0.1:{cfg.nodejs_port} (JS/TS executor)")
62
+ console.print()
63
+
64
+ sidecar = server_dir(root) / "nodejs" / "dist" / "index.js"
65
+
66
+ specs = [
67
+ ServiceSpec(
68
+ name="server",
69
+ argv=[
70
+ _venv_python(root),
71
+ "-m",
72
+ "uvicorn",
73
+ "main:app",
74
+ "--host",
75
+ "0.0.0.0",
76
+ "--port",
77
+ str(bind_port),
78
+ "--log-level",
79
+ "warning",
80
+ ],
81
+ cwd=server_dir(root),
82
+ env={"SERVE_STATIC_CLIENT": "1", "PORT": str(bind_port)},
83
+ ready_port=bind_port,
84
+ ),
85
+ ServiceSpec(
86
+ name="nodejs",
87
+ argv=["node", str(sidecar)],
88
+ cwd=server_dir(root) / "nodejs",
89
+ env={"NODEJS_EXECUTOR_PORT": str(cfg.nodejs_port)},
90
+ ready_port=cfg.nodejs_port,
91
+ ),
92
+ ]
93
+
94
+ manager = Manager()
95
+ manager.add_all(specs)
96
+ rc = asyncio.run(manager.run())
97
+ if rc != 0:
98
+ raise typer.Exit(code=rc)
@@ -0,0 +1,116 @@
1
+ terraform {
2
+ required_version = ">= 1.3"
3
+ required_providers {
4
+ google = {
5
+ source = "hashicorp/google"
6
+ version = "~> 5.0"
7
+ }
8
+ }
9
+ }
10
+
11
+ provider "google" {
12
+ project = var.project
13
+ region = var.region
14
+ }
15
+
16
+ locals {
17
+ # The instance is always named "machinaos" (one deployment per project).
18
+ use_local = var.source_mode == "local"
19
+ res_name = "machinaos"
20
+ bucket_name = "${var.project}-machinaos"
21
+ object_name = "machinaos.tgz"
22
+ }
23
+
24
+ # --- Artifact bucket + object (local source only) --------------------------
25
+
26
+ resource "google_storage_bucket" "artifact" {
27
+ count = local.use_local ? 1 : 0
28
+ name = local.bucket_name
29
+ location = var.region
30
+ force_destroy = true
31
+ uniform_bucket_level_access = true
32
+ }
33
+
34
+ resource "google_storage_bucket_object" "pkg" {
35
+ count = local.use_local ? 1 : 0
36
+ name = local.object_name
37
+ bucket = google_storage_bucket.artifact[0].name
38
+ source = var.pack_tarball
39
+ }
40
+
41
+ # --- VM service account (reads the artifact bucket) ------------------------
42
+
43
+ resource "google_service_account" "vm" {
44
+ account_id = local.res_name
45
+ display_name = "MachinaOs VM"
46
+ }
47
+
48
+ resource "google_storage_bucket_iam_member" "vm_read" {
49
+ count = local.use_local ? 1 : 0
50
+ bucket = google_storage_bucket.artifact[0].name
51
+ role = "roles/storage.objectViewer"
52
+ member = "serviceAccount:${google_service_account.vm.email}"
53
+ }
54
+
55
+ # --- Firewall: app port + SSH ----------------------------------------------
56
+
57
+ resource "google_compute_firewall" "app" {
58
+ name = "${local.res_name}-app"
59
+ network = "default"
60
+
61
+ allow {
62
+ # CF-proxied front door (80 http-redirected, 443 TLS via nginx) + SSH +
63
+ # the app port for direct-IP access.
64
+ protocol = "tcp"
65
+ ports = ["80", "443", tostring(var.port), "22"]
66
+ }
67
+
68
+ source_ranges = [var.allow_cidr]
69
+ target_tags = [local.res_name]
70
+ }
71
+
72
+ # --- Instance --------------------------------------------------------------
73
+
74
+ resource "google_compute_instance" "vm" {
75
+ name = local.res_name
76
+ machine_type = var.machine_type
77
+ zone = var.zone
78
+ tags = [local.res_name]
79
+
80
+ boot_disk {
81
+ initialize_params {
82
+ # Ubuntu 24.04 ships Python 3.12 natively — required by the package's
83
+ # install (server needs >=3.11,<3.13; the CLI needs >=3.12). 22.04
84
+ # ships 3.10 and the npm postinstall hard-fails on it.
85
+ image = "ubuntu-os-cloud/ubuntu-2404-lts-amd64"
86
+ size = 40
87
+ type = "pd-balanced"
88
+ }
89
+ }
90
+
91
+ network_interface {
92
+ network = "default"
93
+ access_config {} # ephemeral external IP
94
+ }
95
+
96
+ service_account {
97
+ email = google_service_account.vm.email
98
+ scopes = ["cloud-platform"]
99
+ }
100
+
101
+ metadata = {
102
+ startup-script = templatefile("${path.module}/startup.sh.tftpl", {
103
+ app_env = var.app_env
104
+ source_mode = var.source_mode
105
+ version = var.machinaos_version
106
+ bucket = local.use_local ? google_storage_bucket.artifact[0].name : ""
107
+ object = local.object_name
108
+ })
109
+ }
110
+
111
+ # The startup script reads the bucket object; ensure it exists + is readable first.
112
+ depends_on = [
113
+ google_storage_bucket_object.pkg,
114
+ google_storage_bucket_iam_member.vm_read,
115
+ ]
116
+ }
@@ -0,0 +1,9 @@
1
+ output "external_ip" {
2
+ value = google_compute_instance.vm.network_interface[0].access_config[0].nat_ip
3
+ description = "The VM's public IP."
4
+ }
5
+
6
+ output "url" {
7
+ value = "http://${google_compute_instance.vm.network_interface[0].access_config[0].nat_ip}:${var.port}"
8
+ description = "The app URL (login gate)."
9
+ }
@@ -0,0 +1,65 @@
1
+ #!/bin/bash
2
+ # MachinaOs VM startup script (rendered by Terraform `templatefile`).
3
+ # Installs the toolchain, installs MachinaOs (npm), writes the login-gate env
4
+ # file, and runs it under systemd via `machina serve`.
5
+ #
6
+ # NOTE on syntax: Terraform renders this file with templatefile(), so any
7
+ # dollar-brace sequence is a Terraform interpolation. Bash variables are
8
+ # therefore written UNBRACED ($TOKEN, $MACHINA_BIN) so Terraform leaves them
9
+ # alone. `set -x` is intentionally NOT used so secrets in the env file are
10
+ # never echoed to the serial console.
11
+ set -euo pipefail
12
+ export DEBIAN_FRONTEND=noninteractive
13
+
14
+ echo "[machinaos] installing toolchain..."
15
+ apt-get update
16
+ apt-get install -y curl ca-certificates git build-essential pkg-config libffi-dev libssl-dev
17
+ curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
18
+ apt-get install -y nodejs
19
+ corepack enable || true
20
+ # uv on a stable system path (machina build uses it to sync the Python venv).
21
+ curl -LsSf https://astral.sh/uv/install.sh | env UV_INSTALL_DIR=/usr/local/bin sh
22
+
23
+ echo "[machinaos] installing MachinaOs package..."
24
+ %{ if source_mode == "local" ~}
25
+ TOKEN=$(curl -s -H "Metadata-Flavor: Google" "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token" | python3 -c 'import sys, json; print(json.load(sys.stdin)["access_token"])')
26
+ curl -fsS -H "Authorization: Bearer $TOKEN" -o /tmp/machinaos.tgz "https://storage.googleapis.com/storage/v1/b/${bucket}/o/${object}?alt=media"
27
+ npm install -g /tmp/machinaos.tgz
28
+ %{ else ~}
29
+ npm install -g machinaos@${version}
30
+ %{ endif ~}
31
+
32
+ echo "[machinaos] writing login-gate env..."
33
+ mkdir -p /etc/machinaos
34
+ cat > /etc/machinaos/machina.env <<'MACHINA_ENV_EOF'
35
+ %{ for k, v in app_env ~}
36
+ ${k}=${v}
37
+ %{ endfor ~}
38
+ MACHINA_ENV_EOF
39
+ chmod 600 /etc/machinaos/machina.env
40
+
41
+ mkdir -p /var/lib/machinaos
42
+
43
+ echo "[machinaos] installing systemd service..."
44
+ MACHINA_BIN=$(command -v machina)
45
+ cat > /etc/systemd/system/machinaos.service <<SERVICE_EOF
46
+ [Unit]
47
+ Description=MachinaOs (single login-gated instance)
48
+ After=network-online.target
49
+ Wants=network-online.target
50
+
51
+ [Service]
52
+ Type=simple
53
+ EnvironmentFile=/etc/machinaos/machina.env
54
+ ExecStart=$MACHINA_BIN serve
55
+ Restart=on-failure
56
+ RestartSec=5
57
+ TimeoutStopSec=20
58
+
59
+ [Install]
60
+ WantedBy=multi-user.target
61
+ SERVICE_EOF
62
+
63
+ systemctl daemon-reload
64
+ systemctl enable --now machinaos
65
+ echo "[machinaos] done."
@@ -0,0 +1,54 @@
1
+ # Shared deployment variable interface (every provider module declares the
2
+ # same set, so `machina deploy` writes one tfvars shape regardless of provider).
3
+
4
+ variable "project" {
5
+ type = string
6
+ description = "GCP project id."
7
+ }
8
+
9
+ variable "region" {
10
+ type = string
11
+ description = "GCP region."
12
+ }
13
+
14
+ variable "zone" {
15
+ type = string
16
+ description = "GCP zone for the instance."
17
+ }
18
+
19
+ variable "machine_type" {
20
+ type = string
21
+ description = "Compute Engine machine type."
22
+ }
23
+
24
+ variable "port" {
25
+ type = number
26
+ description = "Public port the app binds and the firewall opens."
27
+ }
28
+
29
+ variable "allow_cidr" {
30
+ type = string
31
+ description = "Firewall source range (e.g. 0.0.0.0/0 or <your-ip>/32)."
32
+ }
33
+
34
+ variable "source_mode" {
35
+ type = string
36
+ description = "Install source: 'local' (npm pack tarball via bucket) or 'release' (npm registry)."
37
+ }
38
+
39
+ variable "machinaos_version" {
40
+ type = string
41
+ description = "machinaos version to install when source_mode = 'release'."
42
+ }
43
+
44
+ variable "pack_tarball" {
45
+ type = string
46
+ default = ""
47
+ description = "Absolute path to the npm pack tarball (source_mode = 'local')."
48
+ }
49
+
50
+ variable "app_env" {
51
+ type = map(string)
52
+ sensitive = true
53
+ description = "KEY=VALUE map rendered into the VM's /etc/machinaos/machina.env."
54
+ }
@@ -1 +1 @@
1
- import{f as u,n as d,o as p,p as m,q as f,r as g,k as x}from"./index-D-LxTbwD.js";import{j as l}from"./vendor-query-SzWcOU0G.js";import{q as b}from"./vendor-icons-CVrPjN2Q.js";function h(r){const t=u.c(7),a=d(),n=p(),s=m(),i=f(),o=g();if(!r)return null;let e;return t[0]!==n||t[1]!==i||t[2]!==r||t[3]!==s||t[4]!==o||t[5]!==a?(e={whatsapp:a,android:n,twitter:s,google:i,telegram:o}[r]??null,t[0]=n,t[1]=i,t[2]=r,t[3]=s,t[4]=o,t[5]=a,t[6]=e):e=t[6],e}const y=r=>{const t=u.c(7),{actions:a,loading:n}=r;let s;if(t[0]!==a||t[1]!==n){let o;t[3]!==n?(o=e=>{const c=n===e.key;return l.jsxs(x,{intent:e.intent,onClick:e.onClick,disabled:e.disabled||c,children:[c?l.jsx(b,{className:"h-4 w-4 animate-spin"}):e.icon,e.label]},e.key)},t[3]=n,t[4]=o):o=t[4],s=a.filter(j).map(o),t[0]=a,t[1]=n,t[2]=s}else s=t[2];let i;return t[5]!==s?(i=l.jsx("div",{className:"flex justify-center gap-2 border-t border-border pt-3",children:s}),t[5]=s,t[6]=i):i=t[6],i};function j(r){return!r.hidden}export{y as A,h as u};
1
+ import{f as u,n as d,o as p,p as m,q as f,r as g,k as x}from"./index-CW3r0-Ob.js";import{j as l}from"./vendor-query-SzWcOU0G.js";import{q as b}from"./vendor-icons-CVrPjN2Q.js";function h(r){const t=u.c(7),a=d(),n=p(),s=m(),i=f(),o=g();if(!r)return null;let e;return t[0]!==n||t[1]!==i||t[2]!==r||t[3]!==s||t[4]!==o||t[5]!==a?(e={whatsapp:a,android:n,twitter:s,google:i,telegram:o}[r]??null,t[0]=n,t[1]=i,t[2]=r,t[3]=s,t[4]=o,t[5]=a,t[6]=e):e=t[6],e}const y=r=>{const t=u.c(7),{actions:a,loading:n}=r;let s;if(t[0]!==a||t[1]!==n){let o;t[3]!==n?(o=e=>{const c=n===e.key;return l.jsxs(x,{intent:e.intent,onClick:e.onClick,disabled:e.disabled||c,children:[c?l.jsx(b,{className:"h-4 w-4 animate-spin"}):e.icon,e.label]},e.key)},t[3]=n,t[4]=o):o=t[4],s=a.filter(j).map(o),t[0]=a,t[1]=n,t[2]=s}else s=t[2];let i;return t[5]!==s?(i=l.jsx("div",{className:"flex justify-center gap-2 border-t border-border pt-3",children:s}),t[5]=s,t[6]=i):i=t[6],i};function j(r){return!r.hidden}export{y as A,h as u};