Make dev workspaces serve a working browser IDE end to end
The workspace feature shipped its routes, agent tools and docs, but the editor was never reachable: the project page had no entry point, the ppy image had no code-server binary, no certificate was ever requested for a tunnel, and both nginx and the proxy dropped what the editor needs. - Add a VS Code button to the project action row and a Workspace item to the overflow menu, gated by can_open_workspace plus a running instance (viewer_can_workspace and workspace_editor_url on ProjectDetailOut). - Install a pinned code-server in ppy.Dockerfile before USER pravda and assert it in the build smoke test, so an image that cannot run the editor no longer builds green. - Run the editor with --auth password and a per workspace 8 character pronounceable secret, minted once at the ensure_editor_password choke point and injected as PASSWORD. Keep it off WorkspaceViewOut, which the admin listing shares. - Publish the editor tunnel when a workspace is created and issue its certificate from a new WorkspaceService phase against the molohttp admin API, then notify the owner with the live URL and the password. Only pending rows are retried, so a broken host cannot burn the ACME failure rate limit. Renewal stays molohttp's job. - Forward the original Host on proxied requests and the client cookie on proxied websockets, so code-server scopes its session cookie to the public hostname and authenticates the workbench socket. - Recreate a container stuck in the created state instead of retrying docker start forever against an image it can no longer run. - Return a JSON string from WorkspaceController.dispatch; raw dicts landed in a tool message and aborted the turn at the model endpoint. - Let the nginx catch-all carry websocket upgrades, keeping upstream keepalive, so tunnelled apps and the editor both connect.
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
import asyncio
|
||||
import json
|
||||
import re
|
||||
import secrets
|
||||
import socket
|
||||
from pathlib import Path
|
||||
|
||||
@@ -487,6 +488,7 @@ def workspace_env(instance: dict, base_url: str) -> dict:
|
||||
"DEVPLACE_TUNNEL_MANIFEST": f"{WORKSPACE_MOUNT}/.devplace/tunnels.json",
|
||||
"DEVPLACE_TUNNEL_MAX": str(limits.max_tunnels),
|
||||
"VSCODE_PROXY_URI": naming.proxy_uri_template(name),
|
||||
"PASSWORD": instance.get("editor_password") or "",
|
||||
"DEVPLACE_EDITOR": "code-server",
|
||||
"DEVPLACE_EDITOR_PORT": str(editor_port),
|
||||
"DEVPLACE_EDITOR_URL": (
|
||||
@@ -509,6 +511,28 @@ def workspace_env(instance: dict, base_url: str) -> dict:
|
||||
|
||||
|
||||
EDITOR_DEFAULT_PORT = 8443
|
||||
EDITOR_PASSWORD_LENGTH = 8
|
||||
EDITOR_PASSWORD_CONSONANTS = "bdfgkmnprstvz"
|
||||
EDITOR_PASSWORD_VOWELS = "aeiou"
|
||||
|
||||
|
||||
def generate_editor_password() -> str:
|
||||
pairs = EDITOR_PASSWORD_LENGTH // 2
|
||||
return "".join(
|
||||
secrets.choice(EDITOR_PASSWORD_CONSONANTS)
|
||||
+ secrets.choice(EDITOR_PASSWORD_VOWELS)
|
||||
for _ in range(pairs)
|
||||
)
|
||||
|
||||
|
||||
def ensure_editor_password(instance: dict) -> str:
|
||||
password = (instance.get("editor_password") or "").strip()
|
||||
if password:
|
||||
return password
|
||||
password = generate_editor_password()
|
||||
store.update_instance(instance["uid"], {"editor_password": password})
|
||||
instance["editor_password"] = password
|
||||
return password
|
||||
|
||||
|
||||
def editor_command(instance: dict) -> list[str]:
|
||||
@@ -518,7 +542,7 @@ def editor_command(instance: dict) -> list[str]:
|
||||
"--bind-addr",
|
||||
f"0.0.0.0:{port}",
|
||||
"--auth",
|
||||
"none",
|
||||
"password",
|
||||
"--disable-telemetry",
|
||||
"--disable-update-check",
|
||||
"--user-data-dir",
|
||||
@@ -530,6 +554,8 @@ def editor_command(instance: dict) -> list[str]:
|
||||
|
||||
|
||||
def run_spec_for(instance: dict, image_tag: str) -> RunSpec:
|
||||
if instance.get("is_workspace"):
|
||||
ensure_editor_password(instance)
|
||||
env = {**json.loads(instance.get("env_json") or "{}"), **pravda_env(instance)}
|
||||
ports = [
|
||||
PortMapping(p["host"], p["container"], p.get("proto", "tcp"))
|
||||
|
||||
Reference in New Issue
Block a user