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
package/.env.template CHANGED
@@ -86,6 +86,26 @@ TEMPORAL_GRACEFUL_SHUTDOWN_SECONDS=30
86
86
  # didn't pre-cache it.
87
87
  TEMPORAL_SERVER_READY_TIMEOUT_SECONDS=120
88
88
 
89
+ # --- Startup resilience (server-ready-before-workers) ---
90
+ # After the gRPC port binds, the client polls the WorkflowService gRPC
91
+ # health check until it reports SERVING before starting the worker /
92
+ # visibility sweep (the documented readiness probe; see
93
+ # https://docs.temporal.io/troubleshooting/deadline-exceeded-error).
94
+ # Bounded — the unbounded layer is main.py's 3s reconnect loop.
95
+ TEMPORAL_HEALTH_CHECK_ATTEMPTS=5
96
+ TEMPORAL_HEALTH_CHECK_DELAY_SECONDS=0.5
97
+ TEMPORAL_HEALTH_CHECK_TIMEOUT_SECONDS=2.0
98
+ # The boot-time terminate-running sweep issues a Visibility query that
99
+ # races shard acquisition ("shard status unknown"). Retry it a few times
100
+ # (linear backoff = attempt x base) before giving up for that boot.
101
+ TEMPORAL_SWEEP_ATTEMPTS=4
102
+ TEMPORAL_SWEEP_BACKOFF_SECONDS=0.5
103
+ # The embedded worker self-restarts on a transient crash (the Temporal
104
+ # worker shuts down on a poll failure rather than auto-retrying). Backoff
105
+ # doubles from base up to max between restarts.
106
+ TEMPORAL_WORKER_RESTART_BACKOFF_SECONDS=1.0
107
+ TEMPORAL_WORKER_RESTART_BACKOFF_MAX_SECONDS=30.0
108
+
89
109
  # Wave 12 event framework. ``true`` (default) routes trigger events
90
110
  # through TriggerListenerWorkflow + Temporal Signal fan-out for the
91
111
  # 8 canary-registered trigger types (webhook, chat, task, telegram,
@@ -98,6 +118,25 @@ EVENT_FRAMEWORK_ENABLED=true
98
118
  # context-window threshold. Per-session overrides via the UI.
99
119
  COMPACTION_ENABLED=true
100
120
 
121
+ # Compaction trigger threshold as a fraction of the model's context
122
+ # window (0.05 - 0.99). 0.8 = compact when cumulative tokens hit 80%.
123
+ # Per-user overrides via the UserSettings row (Settings tab).
124
+ COMPACTION_RATIO=0.8
125
+
126
+ # Agent loop hard step cap. AI Agent + Chat Agent stop the LLM/tool
127
+ # iteration loop after this many rounds and return a truncation note.
128
+ # Per-user overrides via the UserSettings row (Settings tab).
129
+ AGENT_RECURSION_LIMIT=200
130
+
131
+ # Browser automation (agent-browser CLI). Each distinct browser
132
+ # session maps to one Chrome instance. BROWSER_MAX_INSTANCES caps
133
+ # concurrent instances — when a new session would exceed it, the
134
+ # least-recently-used session is closed first. BROWSER_IDLE_TIMEOUT_MS
135
+ # makes the agent-browser daemon close its browser after that many
136
+ # milliseconds without commands (0 disables).
137
+ BROWSER_MAX_INSTANCES=3
138
+ BROWSER_IDLE_TIMEOUT_MS=600000
139
+
101
140
  # Dead-letter queue for failed workflow executions. Off by default.
102
141
  DLQ_ENABLED=false
103
142
 
@@ -603,7 +603,7 @@
603
603
  "aiAgent-177920047601512-ce3047": {
604
604
  "prompt": "{{chattrigger.message}} {{telegramreceive.text}}",
605
605
  "provider": "gemini",
606
- "model": "gemini-flash-latest",
606
+ "model": "gemini-3.5-flash",
607
607
  "system_message": "You are a helpful assistant",
608
608
  "label": "AI Agent"
609
609
  },
@@ -2160,7 +2160,7 @@
2160
2160
  },
2161
2161
  "chatAgent-177920047617122-dbe3b7": {
2162
2162
  "provider": "gemini",
2163
- "model": "gemini-flash-latest",
2163
+ "model": "gemini-3.5-flash",
2164
2164
  "prompt": "{{chattrigger.message}} {{telegramreceive.text}}",
2165
2165
  "system_message": "",
2166
2166
  "label": "Zeenie"
@@ -2233,35 +2233,35 @@
2233
2233
  "coding_agent-177920047617125-dbe3b7": {
2234
2234
  "prompt": "",
2235
2235
  "provider": "gemini",
2236
- "model": "gemini-flash-latest",
2236
+ "model": "gemini-3.5-flash",
2237
2237
  "system_message": "You are a helpful assistant",
2238
2238
  "label": "Coding Agent"
2239
2239
  },
2240
2240
  "productivity_agent-177920047617126-dbe3b7": {
2241
2241
  "prompt": "",
2242
2242
  "provider": "gemini",
2243
- "model": "gemini-flash-latest",
2243
+ "model": "gemini-3.5-flash",
2244
2244
  "system_message": "You are a helpful assistant",
2245
2245
  "label": "Productivity Agent"
2246
2246
  },
2247
2247
  "travel_agent-177920047617127-dbe3b7": {
2248
2248
  "prompt": "",
2249
2249
  "provider": "gemini",
2250
- "model": "gemini-flash-latest",
2250
+ "model": "gemini-3.5-flash",
2251
2251
  "system_message": "You are a helpful assistant",
2252
2252
  "label": "Travel Agent"
2253
2253
  },
2254
2254
  "web_agent-177920047617128-dbe3b7": {
2255
2255
  "prompt": "",
2256
2256
  "provider": "gemini",
2257
- "model": "gemini-flash-latest",
2257
+ "model": "gemini-3.5-flash",
2258
2258
  "system_message": "You are a helpful assistant",
2259
2259
  "label": "Web Agent"
2260
2260
  },
2261
2261
  "social_agent-177920047617129-dbe3b7": {
2262
2262
  "prompt": "",
2263
2263
  "provider": "gemini",
2264
- "model": "gemini-flash-latest",
2264
+ "model": "gemini-3.5-flash",
2265
2265
  "system_message": "You are a helpful assistant",
2266
2266
  "label": "Social Agent"
2267
2267
  },
package/README.md CHANGED
@@ -104,13 +104,13 @@ RAG pipeline out of the box: parse PDFs and HTML, chunk into searchable pieces,
104
104
  | Provider | Notes |
105
105
  |--------------|--------------------------------------------------------------------------|
106
106
  | OpenAI | GPT-5 family, GPT-4.1, o-series reasoning models, GPT-4o |
107
- | Anthropic | Claude Opus 4.x, Sonnet 4.x, Haiku 4.5 — with extended thinking |
107
+ | Anthropic | Claude Fable 5, Opus 4.x, Sonnet 4.6, Haiku 4.5 — with extended thinking |
108
108
  | Google | Gemini 3 Pro/Flash, 2.5 Pro/Flash — with reasoning budgets |
109
- | DeepSeek | DeepSeek V3, DeepSeek Reasoner |
110
- | Kimi | Kimi K2.5, Kimi K2 Thinking |
111
- | Mistral | Mistral Large/Small, Codestral |
112
- | Groq | Llama 3/4, Qwen3, GPT-OSS (ultra-fast inference) |
113
- | Cerebras | Llama 3.1, Qwen-3-235b (custom AI hardware) |
109
+ | DeepSeek | DeepSeek V4 (Flash/Pro); chat/reasoner legacy aliases |
110
+ | Kimi | Kimi K2.6, K2.5, K2.7-Code |
111
+ | Mistral | Mistral Large/Medium/Small, Codestral |
112
+ | Groq | Llama 3.x, Qwen3, GPT-OSS (ultra-fast inference) |
113
+ | Cerebras | GPT-OSS-120b, Qwen-3-235b, GLM-4.7 (custom AI hardware) |
114
114
  | OpenRouter | 200+ models via one unified API |
115
115
  | **Ollama** | Run any local model on your machine — free, private, offline |
116
116
  | **LM Studio**| Run any local model with a desktop app — free, private, offline |
package/bin/cli.js CHANGED
@@ -11,6 +11,8 @@ const PKG = JSON.parse(readFileSync(resolve(ROOT, 'package.json'), 'utf-8'));
11
11
  const COMMANDS = {
12
12
  start: 'Start in production mode',
13
13
  dev: 'Start development server (hot-reload)',
14
+ serve: 'Serve on a single public port (API + WS + SPA; used by deploy)',
15
+ deploy: 'Provision a cloud VM running MachinaOs (Terraform)',
14
16
  stop: 'Stop all running services',
15
17
  build: 'Build the project for production',
16
18
  clean: 'Clean build artifacts',
@@ -175,9 +177,12 @@ if (cmd === 'help' || cmd === '--help' || cmd === '-h') {
175
177
  console.log(`machina v${PKG.version}`);
176
178
  } else if (cmd === 'doctor') {
177
179
  doctor();
178
- } else if (cmd === 'start' || cmd === 'dev' || cmd === 'build') {
180
+ } else if (cmd === 'start' || cmd === 'dev' || cmd === 'build' || cmd === 'serve') {
179
181
  checkDeps();
180
182
  run(cmd, process.argv.slice(3));
183
+ } else if (cmd === 'deploy') {
184
+ // Needs sub-verb + flags forwarded (up/status/destroy --provider ...).
185
+ run(cmd, process.argv.slice(3));
181
186
  } else if (COMMANDS[cmd]) {
182
187
  run(cmd);
183
188
  } else {
package/cli/cli.py CHANGED
@@ -90,6 +90,22 @@ def _build() -> None:
90
90
  build_command()
91
91
 
92
92
 
93
+ @app.command(
94
+ "serve",
95
+ help="Run on a single public port (API + WebSocket + built SPA + sidecar). Used by `machina deploy`.",
96
+ )
97
+ def _serve(
98
+ port: int | None = typer.Option(
99
+ None,
100
+ "--port",
101
+ help="Public port (default: $PORT, else PYTHON_BACKEND_PORT).",
102
+ ),
103
+ ) -> None:
104
+ from cli.commands.serve import serve_command
105
+
106
+ serve_command(port=port)
107
+
108
+
93
109
  # --------------------------------------------------------------- daemon group
94
110
 
95
111
  daemon_app = typer.Typer(
@@ -143,6 +159,77 @@ def _daemon_restart() -> None:
143
159
  app.add_typer(daemon_app, name="daemon")
144
160
 
145
161
 
162
+ # --------------------------------------------------------------- deploy group
163
+
164
+ deploy_app = typer.Typer(
165
+ name="deploy",
166
+ help="Provision a fresh cloud VM running MachinaOs (via Terraform).",
167
+ no_args_is_help=True,
168
+ add_completion=False,
169
+ )
170
+
171
+
172
+ @deploy_app.command(
173
+ "up",
174
+ help="Create the 'machinaos' VM, install MachinaOs behind the login gate, and run it.",
175
+ )
176
+ def _deploy_up(
177
+ provider: str = typer.Option("gcp", "--provider", help="Cloud provider: gcp (aws is a follow-on)."),
178
+ owner_email: str = typer.Option(..., "--owner-email", help="Login email for the owner account."),
179
+ owner_password: str | None = typer.Option(
180
+ None, "--owner-password", help="Login password (>=8 chars). Generated + printed once if omitted."
181
+ ),
182
+ source: str = typer.Option("local", "--source", help="Install source: local (npm pack) or release (npm registry)."),
183
+ version: str = typer.Option("latest", "--version", help="machinaos version when --source release."),
184
+ machine_type: str = typer.Option("e2-standard-2", "--machine-type", help="VM size."),
185
+ port: int = typer.Option(8080, "--port", help="Public port the app binds + the firewall opens."),
186
+ allow_cidr: str = typer.Option("0.0.0.0/0", "--allow-cidr", help="Firewall source range (restrict to your IP/32)."),
187
+ region: str | None = typer.Option(None, "--region", help="Cloud region (provider default if omitted)."),
188
+ zone: str | None = typer.Option(None, "--zone", help="Cloud zone (provider default if omitted)."),
189
+ project: str | None = typer.Option(None, "--project", help="GCP project (defaults to gcloud config)."),
190
+ ) -> None:
191
+ from cli.commands.deploy.up import up_command
192
+
193
+ up_command(
194
+ provider=provider,
195
+ region=region,
196
+ zone=zone,
197
+ machine_type=machine_type,
198
+ port=port,
199
+ owner_email=owner_email,
200
+ owner_password=owner_password,
201
+ source=source,
202
+ version=version,
203
+ allow_cidr=allow_cidr,
204
+ project=project,
205
+ )
206
+
207
+
208
+ @deploy_app.command(
209
+ "status",
210
+ help="Show the 'machinaos' deployment's URL + health.",
211
+ )
212
+ def _deploy_status() -> None:
213
+ from cli.commands.deploy.status import status_command
214
+
215
+ status_command()
216
+
217
+
218
+ @deploy_app.command(
219
+ "destroy",
220
+ help="Terraform-destroy the 'machinaos' deployment and remove its local state.",
221
+ )
222
+ def _deploy_destroy(
223
+ keep_state: bool = typer.Option(False, "--keep-state", help="Keep the local Terraform state dir."),
224
+ ) -> None:
225
+ from cli.commands.deploy.destroy import destroy_command
226
+
227
+ destroy_command(keep_state=keep_state)
228
+
229
+
230
+ app.add_typer(deploy_app, name="deploy")
231
+
232
+
146
233
  # ----------------------------------------------------------------- docs group
147
234
 
148
235
  docs_app = typer.Typer(
@@ -1,8 +1,17 @@
1
1
  """``machina build`` -- replaces ``scripts/build.js``.
2
2
 
3
- Checks toolchain (node, npm, python, uv, temporal-server), then runs
4
- the 4-step build: ``.env`` bootstrap -> ``pnpm install`` -> client
5
- build -> ``uv sync`` -> verify edgymeow binary.
3
+ Checks toolchain (node, npm, python, uv), then runs the 6-step
4
+ build: ``.env`` bootstrap -> ``pnpm install`` -> client build ->
5
+ Node.js sidecar bundle -> ``uv sync`` -> compile Python bytecode
6
+ -> pooch-fetch Temporal binary.
7
+
8
+ Layers ``.env.dev`` (when present in the checkout) BEFORE running
9
+ those steps so the build's ``DATA_DIR`` matches the runtime's
10
+ expected location. Without that, ``machina build`` would pooch
11
+ Temporal under ``~/.machina/`` and ``machina dev`` would re-fetch
12
+ it into ``<repo>/.machina/`` — a redundant download on every fresh
13
+ clone. Production (global-install) operators never have
14
+ ``.env.dev`` in their checkout, so the layering is a no-op there.
6
15
 
7
16
  The ``MACHINAOS_BUILDING`` env var is set so ``scripts/postinstall.js``
8
17
  skips its own ``install.js`` invocation when build is the orchestrator.
@@ -97,6 +106,27 @@ def _ensure_uv(python_cmd: str) -> str:
97
106
  def build_command() -> None:
98
107
  root = project_root()
99
108
 
109
+ # Layer ``.env.dev`` (if present) BEFORE the install steps so the
110
+ # build's ``DATA_DIR`` matches what the runtime (``machina dev``)
111
+ # will see. Without this, ``machina build`` reads
112
+ # ``DATA_DIR=~/.machina`` from ``.env.template`` and lands the
113
+ # Temporal CLI under user home, but ``machina dev`` then reads
114
+ # ``DATA_DIR=.machina`` from ``.env.dev`` and re-downloads into
115
+ # ``<repo>/.machina/`` — a cache-miss the operator pays on every
116
+ # fresh checkout.
117
+ #
118
+ # Safe for global installs: ``.env.dev`` is committed to git for
119
+ # repo-clone contributors but is NOT in the npm ``files`` list, so
120
+ # ``npm install -g machinaos`` doesn't ship it. Without
121
+ # ``.env.dev`` on disk, :func:`load_dev_overrides` is a no-op and
122
+ # the build falls through to ``.env.template`` defaults
123
+ # (``DATA_DIR=~/.machina``) — identical to today's behaviour and
124
+ # matching what ``machina start`` / ``machina daemon`` use at
125
+ # runtime.
126
+ from cli.config import load_dev_overrides
127
+
128
+ load_dev_overrides(root)
129
+
100
130
  # Prevent the postinstall orchestrator from re-running install.js when
101
131
  # we're orchestrating ourselves (matches the existing JS contract).
102
132
  os.environ["MACHINAOS_BUILDING"] = "true"
@@ -44,10 +44,19 @@ _TARGETS = [
44
44
  # ``.machina`` entry can't go in ``_TARGETS`` anymore because the
45
45
  # ``workflows/`` subtree holds shipped example seeds (git-tracked,
46
46
  # imported on first launch by ``services.example_loader``) -- wiping
47
- # it would force the operator to re-clone to recover. Anything else
48
- # under ``.machina/`` (claude state, workspaces, credentials.db, ...)
49
- # is transient runtime state and is fair game.
50
- _MACHINA_KEEP = frozenset({"workflows"})
47
+ # it would force the operator to re-clone to recover. ``deploy/``
48
+ # holds ``machina deploy``'s Terraform working dirs + state files:
49
+ # deleting state for LIVE cloud resources orphans the VM/firewall
50
+ # (``machina deploy destroy`` could no longer find them) -- only
51
+ # ``deploy destroy`` removes that tree. ``packages/`` holds the
52
+ # MachinaOs-managed binaries (Temporal CLI ~114 MB, Stripe CLI, the
53
+ # shared npm tree with claude/agent-browser/edgymeow): all of it is
54
+ # re-fetchable but expensive -- wiping it forced a full Temporal
55
+ # re-download on every clean+build cycle, which hard-fails ``machina
56
+ # build`` on slow links. Anything else under ``.machina/`` (claude
57
+ # state, workspaces, credentials.db, ...) is transient runtime state
58
+ # and is fair game.
59
+ _MACHINA_KEEP = frozenset({"workflows", "deploy", "packages"})
51
60
 
52
61
 
53
62
  def _rmtree_with_retry(path: Path, *, attempts: int = 3, delay: float = 0.1) -> bool:
@@ -0,0 +1,13 @@
1
+ """``machina deploy`` -- provision a fresh cloud VM running MachinaOs.
2
+
3
+ A thin wrapper over **Terraform**: the command generates secrets + a tfvars
4
+ file and drives ``terraform init/apply/destroy``. Terraform owns every cloud
5
+ resource (VM, firewall, optional artifact bucket) declaratively, with its own
6
+ state. Per-provider root modules live under ``cli/terraform/<provider>/`` and
7
+ share one variable interface, so a new provider is one new module + no CLI
8
+ change.
9
+
10
+ The Typer group is assembled in ``cli/cli.py`` (lazy-wrapped leaves), mirroring
11
+ the ``daemon`` group; this package just holds the verb implementations + shared
12
+ helpers (``_config`` / ``_secrets`` / ``_state`` / ``_terraform``).
13
+ """
@@ -0,0 +1,65 @@
1
+ """Secret + environment generation for ``machina deploy``.
2
+
3
+ Generates the cryptographic keys + owner password the login gate needs, and
4
+ assembles the full env-var map (``app_env``) that Terraform renders into the
5
+ VM's ``/etc/machinaos/machina.env`` (a systemd ``EnvironmentFile``). The map
6
+ carries only the deployment overrides + secrets; the VM's ``.env`` (copied
7
+ from ``.env.template`` by the package build) supplies the rest of the required
8
+ settings, and OS env (the EnvironmentFile) wins over ``.env`` in
9
+ pydantic-settings.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import secrets
15
+ import string
16
+
17
+ _PW_ALPHABET = string.ascii_letters + string.digits
18
+
19
+
20
+ def new_key() -> str:
21
+ """48-char hex secret (>= the 32-char minimum the app enforces)."""
22
+ return secrets.token_hex(24)
23
+
24
+
25
+ def new_password(length: int = 20) -> str:
26
+ """Strong alphanumeric password (avoids shell/systemd-quoting pitfalls)."""
27
+ return "".join(secrets.choice(_PW_ALPHABET) for _ in range(length))
28
+
29
+
30
+ def build_app_env(
31
+ *,
32
+ owner_email: str,
33
+ owner_password: str,
34
+ port: int,
35
+ data_dir: str = "/var/lib/machinaos",
36
+ ) -> dict[str, str]:
37
+ """The systemd EnvironmentFile map: gate overrides + freshly minted secrets.
38
+
39
+ JWT/SECRET/ENCRYPTION keys are generated per deploy. ``JWT_COOKIE_SECURE``
40
+ is ``false`` because the VM is reached over plain HTTP on its IP; flip to
41
+ true once a domain + TLS terminator is in front. Temporal/Redis/event
42
+ framework are off (local execution).
43
+ """
44
+ return {
45
+ "HOST": "0.0.0.0",
46
+ "PORT": str(port),
47
+ "DATA_DIR": data_dir,
48
+ "WORKSPACE_BASE_DIR": "workspaces",
49
+ "SERVE_STATIC_CLIENT": "true",
50
+ "VITE_AUTH_ENABLED": "true",
51
+ "AUTH_MODE": "single",
52
+ "JWT_COOKIE_SECURE": "false",
53
+ "JWT_COOKIE_SAMESITE": "lax",
54
+ "TEMPORAL_ENABLED": "false",
55
+ "EVENT_FRAMEWORK_ENABLED": "false",
56
+ "REDIS_ENABLED": "false",
57
+ "NODEJS_EXECUTOR_PORT": "3020",
58
+ "LOG_FORMAT": "text",
59
+ "JWT_SECRET_KEY": new_key(),
60
+ "SECRET_KEY": new_key(),
61
+ "API_KEY_ENCRYPTION_KEY": new_key(),
62
+ "MACHINA_OWNER_EMAIL": owner_email,
63
+ "MACHINA_OWNER_NAME": "Owner",
64
+ "MACHINA_OWNER_PASSWORD": owner_password,
65
+ }
@@ -0,0 +1,56 @@
1
+ """Working dir + metadata for the (single) ``machina deploy`` deployment.
2
+
3
+ The VM instance is always named ``machinaos`` (one deployment per project), so
4
+ the working dir is fixed at ``<user-data>/deploy/machinaos/``. That directory
5
+ is BOTH the Terraform working dir (rendered module + ``terraform.tfvars.json``
6
+ + local state) and the home of a small ``deploy-meta.json`` (provider + port +
7
+ owner email) used by ``status`` / ``destroy``.
8
+
9
+ No module-level side effects -- ``user_data_dir`` (platformdirs) is only
10
+ resolved when a function is called.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ from pathlib import Path
17
+
18
+ from cli.platform_ import user_data_dir
19
+
20
+ #: The fixed instance / deployment name.
21
+ NAME = "machinaos"
22
+
23
+ _META_FILENAME = "deploy-meta.json"
24
+
25
+
26
+ def deploy_root() -> Path:
27
+ return user_data_dir() / "deploy"
28
+
29
+
30
+ def workdir() -> Path:
31
+ """Terraform working dir + state location for the deployment."""
32
+ return deploy_root() / NAME
33
+
34
+
35
+ def meta_file() -> Path:
36
+ return workdir() / _META_FILENAME
37
+
38
+
39
+ def write_meta(meta: dict) -> None:
40
+ wd = workdir()
41
+ wd.mkdir(parents=True, exist_ok=True)
42
+ meta_file().write_text(json.dumps(meta, indent=2), encoding="utf-8")
43
+
44
+
45
+ def read_meta() -> dict | None:
46
+ mf = meta_file()
47
+ if not mf.exists():
48
+ return None
49
+ try:
50
+ return json.loads(mf.read_text(encoding="utf-8"))
51
+ except (ValueError, OSError):
52
+ return None
53
+
54
+
55
+ def exists() -> bool:
56
+ return meta_file().exists()
@@ -0,0 +1,68 @@
1
+ """Terraform driver for ``machina deploy``.
2
+
3
+ Thin helpers over the ``terraform`` CLI (via ``cli.run``): preflight that it
4
+ exists, copy the chosen provider module into the deployment working dir, write
5
+ ``terraform.tfvars.json``, and run init/apply/output/destroy. The CLI never
6
+ talks to a cloud API directly -- Terraform's providers do, using the same
7
+ credentials the cloud CLIs use (gcloud ADC / AWS default chain).
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import shutil
14
+ from pathlib import Path
15
+
16
+ import typer
17
+
18
+ from cli._common import error_block
19
+ from cli.platform_ import project_root
20
+ from cli.run import capture, run
21
+
22
+
23
+ def ensure_terraform() -> None:
24
+ """Abort with guidance if the ``terraform`` binary is not on PATH."""
25
+ if capture(["terraform", "version"]) is None:
26
+ error_block(
27
+ "Terraform is required but was not found on PATH.",
28
+ [
29
+ "Install it: https://developer.hashicorp.com/terraform/install",
30
+ "Then re-run `machina deploy up`.",
31
+ ],
32
+ )
33
+ raise typer.Exit(code=1)
34
+
35
+
36
+ def module_src(provider: str) -> Path:
37
+ """Path to the shipped HCL module for ``provider``."""
38
+ return project_root() / "cli" / "terraform" / provider
39
+
40
+
41
+ def prepare_workdir(workdir: Path, provider: str) -> None:
42
+ """Copy the provider module's files into ``workdir`` (preserving state)."""
43
+ src = module_src(provider)
44
+ if not src.is_dir():
45
+ error_block(
46
+ f"No Terraform module for provider {provider!r}.",
47
+ [f"Expected at {src}", "Supported today: gcp (aws is a follow-on)."],
48
+ )
49
+ raise typer.Exit(code=1)
50
+ workdir.mkdir(parents=True, exist_ok=True)
51
+ # Copy module sources (*.tf, *.tftpl). Never touch tfstate / tfvars / meta.
52
+ for f in src.iterdir():
53
+ if f.is_file() and f.suffix in (".tf", ".tftpl"):
54
+ shutil.copy2(f, workdir / f.name)
55
+
56
+
57
+ def write_tfvars(workdir: Path, variables: dict) -> None:
58
+ (workdir / "terraform.tfvars.json").write_text(
59
+ json.dumps(variables, indent=2), encoding="utf-8"
60
+ )
61
+
62
+
63
+ def tf(workdir: Path, *args: str, check: bool = True) -> int:
64
+ return run(["terraform", *args], cwd=workdir, check=check)
65
+
66
+
67
+ def tf_output(workdir: Path, name: str) -> str | None:
68
+ return capture(["terraform", "output", "-raw", name], cwd=workdir)
@@ -0,0 +1,34 @@
1
+ """``machina deploy destroy`` -- tear down the deployment's cloud resources."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import shutil
6
+
7
+ import typer
8
+
9
+ from cli._common import preflight
10
+ from cli.colors import console
11
+
12
+ from . import _state, _terraform
13
+
14
+
15
+ def destroy_command(*, keep_state: bool = False) -> None:
16
+ # Establish the same DATA_DIR context as `deploy up` so workdir() matches.
17
+ preflight()
18
+ meta = _state.read_meta()
19
+ if meta is None:
20
+ console.print("[yellow]No 'machinaos' deployment found.[/]")
21
+ raise typer.Exit(code=1)
22
+
23
+ _terraform.ensure_terraform()
24
+ wd = _state.workdir()
25
+
26
+ console.log("terraform destroy (machinaos)...")
27
+ _terraform.tf(wd, "destroy", "-auto-approve", "-input=false")
28
+
29
+ if keep_state:
30
+ console.print(f" Destroyed. State kept at {wd}")
31
+ return
32
+
33
+ shutil.rmtree(wd, ignore_errors=True)
34
+ console.print(" [green]Destroyed[/] and removed local state for 'machinaos'.")
@@ -0,0 +1,34 @@
1
+ """Provider CLI adapters -- Stage 1 of ``machina deploy``.
2
+
3
+ Each adapter wraps the operator's already-installed cloud CLI (gcloud / aws)
4
+ to verify it is present + authenticated, resolve the project/region/zone,
5
+ ensure Terraform can authenticate with the same credentials, and enable the
6
+ required cloud APIs. Adapters create NO cloud resources -- Terraform (Stage 2)
7
+ does that, inheriting the CLI's credentials.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import typer
13
+
14
+ from ._base import ProviderCli
15
+
16
+
17
+ def get_provider(name: str) -> ProviderCli:
18
+ """Resolve the CLI adapter for ``name`` (lazy-imports the impl)."""
19
+ if name == "gcp":
20
+ from .gcp import GcpCli
21
+
22
+ return GcpCli()
23
+ if name == "aws":
24
+ from .aws import AwsCli
25
+
26
+ return AwsCli()
27
+
28
+ from cli._common import error_block
29
+
30
+ error_block(f"Unknown provider {name!r}.", ["Supported: gcp (aws is a follow-on)."])
31
+ raise typer.Exit(code=1)
32
+
33
+
34
+ __all__ = ["ProviderCli", "get_provider"]
@@ -0,0 +1,46 @@
1
+ """The provider-CLI adapter contract (Stage 1 of ``machina deploy``)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Protocol
6
+
7
+
8
+ class ProviderCli(Protocol):
9
+ """Auth + context + API-enablement over a cloud provider's own CLI.
10
+
11
+ All methods shell out via ``cli.run``; failures abort with ``typer.Exit``
12
+ and ``error_block`` remediation. None of these create cloud resources --
13
+ Terraform (Stage 2) owns that, using the credentials these methods verify.
14
+ """
15
+
16
+ name: str
17
+
18
+ def check(self) -> None:
19
+ """Abort unless the provider CLI binary is on PATH."""
20
+ ...
21
+
22
+ def authed_account(self) -> str | None:
23
+ """Return the logged-in identity, or ``None`` if not authenticated."""
24
+ ...
25
+
26
+ def resolve_context(
27
+ self, *, region: str | None, zone: str | None, project: str | None
28
+ ) -> dict:
29
+ """Resolve + validate ``{project, region, zone, ...}`` from CLI config + flags.
30
+
31
+ Aborts (with guidance) if the CLI is not logged in or required context
32
+ (e.g. a GCP project) is missing.
33
+ """
34
+ ...
35
+
36
+ def ensure_terraform_auth(self) -> None:
37
+ """Ensure Terraform's provider can authenticate (e.g. gcloud ADC)."""
38
+ ...
39
+
40
+ def enable_apis(self, ctx: dict) -> None:
41
+ """Enable the cloud APIs Terraform will need."""
42
+ ...
43
+
44
+ def tfvars_extra(self, ctx: dict) -> dict:
45
+ """Provider-specific tfvars keys merged into the deployment's tfvars."""
46
+ ...