machinaos 0.0.88 → 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.
- package/.env.template +29 -0
- package/.machina/workflows/AI Assistant_example_workflow-1779017037684-e2e5da7a.json +1 -1
- package/.machina/workflows/AI Employee_example_workflow-1779102911870-cbc76c82.json +6 -6
- package/README.md +6 -6
- package/bin/cli.js +6 -1
- package/cli/cli.py +87 -0
- package/cli/commands/clean.py +13 -4
- package/cli/commands/deploy/__init__.py +13 -0
- package/cli/commands/deploy/_secrets.py +65 -0
- package/cli/commands/deploy/_state.py +56 -0
- package/cli/commands/deploy/_terraform.py +68 -0
- package/cli/commands/deploy/destroy.py +34 -0
- package/cli/commands/deploy/providers/__init__.py +34 -0
- package/cli/commands/deploy/providers/_base.py +46 -0
- package/cli/commands/deploy/providers/aws.py +45 -0
- package/cli/commands/deploy/providers/gcp.py +89 -0
- package/cli/commands/deploy/status.py +48 -0
- package/cli/commands/deploy/up.py +146 -0
- package/cli/commands/serve.py +98 -0
- package/cli/terraform/gcp/main.tf +116 -0
- package/cli/terraform/gcp/outputs.tf +9 -0
- package/cli/terraform/gcp/startup.sh.tftpl +65 -0
- package/cli/terraform/gcp/variables.tf +54 -0
- package/client/dist/assets/{ActionBar-Bbzjnr1n.js → ActionBar-DqL2u6-G.js} +1 -1
- package/client/dist/assets/{ApiKeyInput-D7odCRvV.js → ApiKeyInput-DmIFsu5L.js} +1 -1
- package/client/dist/assets/ApiKeyPanel-BJGWpSo3.js +1 -0
- package/client/dist/assets/ApiUsageSection-B9FBHYF_.js +1 -0
- package/client/dist/assets/{EmailPanel-DAl9RlbR.js → EmailPanel-DDsRt9xV.js} +1 -1
- package/client/dist/assets/{OAuthPanel-CRwrzCjm.js → OAuthPanel-DQiCai8Z.js} +1 -1
- package/client/dist/assets/{QrPairingPanel-B_0Xi9DV.js → QrPairingPanel-DmVb3FlK.js} +1 -1
- package/client/dist/assets/{RateLimitSection-Btv6LTyd.js → RateLimitSection-CQstfsLJ.js} +1 -1
- package/client/dist/assets/{StatusCard-CxFZD5D4.js → StatusCard-B4JY4t4C.js} +1 -1
- package/client/dist/assets/index-CW3r0-Ob.js +165 -0
- package/client/dist/assets/index-DrhPs19g.css +1 -0
- package/client/dist/assets/{vendor-misc-C4VxKHs5.js → vendor-misc-DBQ988Ev.js} +1 -1
- package/client/dist/assets/{vendor-radix-Dnos29jG.js → vendor-radix-BHmt2CkM.js} +1 -1
- package/client/dist/index.html +4 -4
- package/client/package.json +3 -3
- package/client/src/Dashboard.tsx +3 -4
- package/client/src/components/ConditionalEdge.tsx +2 -2
- package/client/src/components/ParameterRenderer.tsx +209 -809
- package/client/src/components/PricingConfigModal.tsx +27 -66
- package/client/src/components/SquareNode.tsx +1 -1
- package/client/src/components/TriggerNode.tsx +13 -8
- package/client/src/components/credentials/CredentialsModal.tsx +1 -1
- package/client/src/components/credentials/catalogueAdapter.ts +1 -0
- package/client/src/components/credentials/panels/ApiKeyPanel.tsx +9 -1
- package/client/src/components/credentials/sections/ApiUsageSection.tsx +4 -4
- package/client/src/components/credentials/sections/LlmUsageSection.tsx +7 -7
- package/client/src/components/credentials/types.ts +2 -0
- package/client/src/components/onboarding/steps/ApiKeyStep.tsx +1 -1
- package/client/src/components/onboarding/steps/CanvasStep.tsx +12 -13
- package/client/src/components/parameterPanel/MiddleSection.tsx +4 -5
- package/client/src/components/ui/ComponentPalette.tsx +2 -4
- package/client/src/components/ui/ErrorBoundary.tsx +1 -1
- package/client/src/components/ui/ThemeSwitcher.tsx +1 -1
- package/client/src/components/ui/TopToolbar.tsx +6 -6
- package/client/src/components/ui/action-button.tsx +18 -13
- package/client/src/contexts/WebSocketContext.tsx +4 -4
- package/client/src/hooks/useCatalogueQuery.ts +2 -0
- package/client/src/index.css +149 -297
- package/client/src/main.tsx +4 -0
- package/client/src/styles/theme.ts +29 -29
- package/client/src/themes/animations.css +193 -0
- package/client/src/themes/atomic.css +78 -68
- package/client/src/themes/base.css +139 -78
- package/client/src/themes/cyber.css +77 -90
- package/client/src/themes/dark.css +72 -25
- package/client/src/themes/edo.css +71 -65
- package/client/src/themes/greek.css +74 -68
- package/client/src/themes/light.css +98 -0
- package/client/src/themes/plague.css +79 -73
- package/client/src/themes/renaissance.css +81 -92
- package/client/src/themes/rot.css +71 -65
- package/client/src/themes/steampunk.css +77 -71
- package/client/src/themes/surveillance.css +82 -95
- package/client/src/themes/wasteland.css +80 -74
- package/client/tailwind.config.js +53 -50
- package/package.json +12 -4
- package/scripts/install.js +9 -1
- package/server/config/credential_providers.json +2 -1
- package/server/config/llm_defaults.json +51 -41
- package/server/config/model_registry.json +687 -1297
- package/server/core/config.py +57 -0
- package/server/core/database.py +13 -1
- package/server/main.py +80 -0
- package/server/middleware/auth.py +12 -2
- package/server/nodejs/package.json +1 -1
- package/server/nodes/README.md +25 -2
- package/server/nodes/browser/_service.py +100 -0
- package/server/nodes/browser/browser/__init__.py +4 -2
- package/server/nodes/code/monty_executor/__init__.py +267 -0
- package/server/nodes/code/monty_executor/icon.svg +1 -0
- package/server/nodes/code/monty_executor/meta.json +3 -0
- package/server/nodes/filesystem/_backend.py +16 -1
- package/server/nodes/filesystem/file_modify/__init__.py +4 -1
- package/server/nodes/filesystem/file_read/__init__.py +20 -3
- package/server/nodes/filesystem/fs_search/__init__.py +4 -1
- package/server/nodes/tool/write_todos/__init__.py +29 -2
- package/server/nodes/utility/team_monitor/__init__.py +3 -1
- package/server/nodes/visuals.json +3 -0
- package/server/pyproject.toml +13 -1
- package/server/requirements.txt +591 -108
- package/server/services/ai.py +30 -21
- package/server/services/handlers/todo.py +4 -1
- package/server/services/handlers/tools.py +15 -2
- package/server/services/llm/protocol.py +5 -1
- package/server/services/llm/providers/gemini.py +50 -9
- package/server/services/llm/vertex.py +28 -0
- package/server/services/model_registry.py +12 -1
- package/server/services/node_executor.py +1 -0
- package/server/services/plugin/base.py +40 -4
- package/server/services/plugin/tool.py +14 -5
- package/server/services/temporal/_install.py +6 -1
- package/server/services/temporal/_runtime.py +22 -0
- package/server/services/temporal/agent_activities.py +48 -0
- package/server/services/temporal/agent_workflow.py +62 -14
- package/server/services/temporal/client.py +82 -11
- package/server/services/temporal/executor.py +1 -0
- package/server/services/temporal/worker.py +38 -8
- package/server/services/temporal/workflow.py +8 -0
- package/server/services/user_auth.py +3 -2
- package/server/skills/coding_agent/file-modify-skill/SKILL.md +2 -0
- package/server/skills/coding_agent/file-read-skill/SKILL.md +2 -0
- package/server/skills/coding_agent/fs-search-skill/SKILL.md +2 -0
- package/server/skills/coding_agent/monty-skill/SKILL.md +116 -0
- package/server/skills/payments_agent/stripe-skill/SKILL.md +2 -2
- package/server/skills/web_agent/browser-skill/SKILL.md +8 -0
- package/server/tests/llm/test_max_tokens_resolution.py +136 -0
- package/server/tests/llm/test_vertex_key.py +254 -0
- package/server/tests/nodes/_compat.py +2 -1
- package/server/tests/nodes/test_ai_tools.py +37 -0
- package/server/tests/nodes/test_browser_service.py +110 -0
- package/server/tests/nodes/test_code_fs_process.py +61 -2
- package/server/tests/nodes/test_monty_executor.py +289 -0
- package/server/tests/nodes/test_web_automation.py +21 -0
- package/server/tests/temporal/test_agent_workflow.py +198 -0
- package/server/tests/temporal/test_client_readiness.py +114 -0
- package/server/tests/temporal/test_terminate_sweep.py +84 -0
- package/server/tests/temporal/test_worker_restart.py +78 -0
- package/server/tests/test_output_contract.py +203 -0
- package/server/uv.lock +209 -217
- package/client/dist/assets/ApiKeyPanel-DyJo6Qo9.js +0 -1
- package/client/dist/assets/ApiUsageSection-Bp2ZbKvU.js +0 -1
- package/client/dist/assets/index-CdXKM2ZZ.js +0 -165
- 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,
|
|
@@ -108,6 +128,15 @@ COMPACTION_RATIO=0.8
|
|
|
108
128
|
# Per-user overrides via the UserSettings row (Settings tab).
|
|
109
129
|
AGENT_RECURSION_LIMIT=200
|
|
110
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
|
+
|
|
111
140
|
# Dead-letter queue for failed workflow executions. Off by default.
|
|
112
141
|
DLQ_ENABLED=false
|
|
113
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
110
|
-
| Kimi | Kimi K2.5,
|
|
111
|
-
| Mistral | Mistral Large/Small, Codestral
|
|
112
|
-
| Groq | Llama 3
|
|
113
|
-
| Cerebras |
|
|
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(
|
package/cli/commands/clean.py
CHANGED
|
@@ -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.
|
|
48
|
-
#
|
|
49
|
-
#
|
|
50
|
-
|
|
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
|
+
...
|
|
@@ -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 {}
|