Repository navigation
Make Cloud Bash prompts follow VM names #12502
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
c63ab4f
3231137
8ec7e68
d04d973
eac0f94
3e36085
98b0071
edf1489
eeed39b
0dcda69
a741e72
4c1a20c
6de0700
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,4 @@ | ||
| import { NextRequest } from "next/server"; | ||
| import { NextRequest, type NextResponse } from "next/server"; | ||
| import { directDevBackendOrigin } from "./direct-dev-backend-origin"; | ||
|
|
||
| /** | ||
|
|
@@ -36,3 +36,22 @@ export function requestWithOrigin( | |
| method: request.method, | ||
| }); | ||
| } | ||
|
|
||
| /** Public URLs belong in redirects; local rewrites must stay inside Next. | ||
| * Rewriting through Tailscale Serve runs middleware again and loops when | ||
| * next-intl removes the default locale prefix from its own rewrite. */ | ||
| export function responseWithInternalRewrite( | ||
| response: NextResponse, | ||
| incomingRequest: NextRequest, | ||
| environment: Record<string, string | undefined> = process.env, | ||
| ): NextResponse { | ||
| const publicOrigin = directDevBackendOrigin(environment)?.origin; | ||
| const rewrite = response.headers.get("x-middleware-rewrite"); | ||
| if (!publicOrigin || !rewrite) return response; | ||
| const target = new URL(rewrite); | ||
| if (target.origin !== publicOrigin) return response; | ||
| target.protocol = incomingRequest.nextUrl.protocol; | ||
| target.host = incomingRequest.nextUrl.host; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🛡️ Analyzed with Security Review | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- request-origin.ts ---'
cat -n web/app/lib/request-origin.ts
printf '%s\n' '--- direct-dev-backend-host.test.ts ---'
cat -n web/tests/direct-dev-backend-host.test.ts
printf '%s\n' '--- proxy.ts ---'
cat -n web/proxy.ts
printf '%s\n' '--- bounded host references ---'
rg -n -C 3 'nextUrl\\.host|x-forwarded-host|Host|host' web/app/lib/request-origin.ts web/tests/direct-dev-backend-host.test.ts web/proxy.tsRepository: manaflow-ai/cmux Length of output: 42540 Security Misconfiguration Reachability: External Validate the incoming authority before setting the rewrite target. 🤖 Prompt for AI Agents |
||
| response.headers.set("x-middleware-rewrite", target.toString()); | ||
| return response; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| import { readFileSync } from "node:fs"; | ||
| import { shellQuote } from "./drivers/cmuxTuiDaemon"; | ||
|
|
||
| export type GuestPromptIdentity = { | ||
| readonly machineId: string; | ||
| readonly name: string; | ||
| readonly revision: number; | ||
| }; | ||
|
|
||
| /** A display label becomes a prompt slug; the machine's routing id stays stable. */ | ||
| export function vmPromptIdentity(row: { | ||
| readonly id: string; | ||
| readonly slug: string | null; | ||
| readonly displayName: string | null; | ||
| readonly updatedAt: Date; | ||
| }): GuestPromptIdentity { | ||
| const slug = (value: string) => value.normalize("NFKD").toLowerCase() | ||
| .replace(/[\u0300-\u036f]/g, "") | ||
| .replace(/[^a-z0-9]+/g, "-").slice(0, 63).replace(/^-+|-+$/g, ""); | ||
| return { | ||
| machineId: row.id, | ||
| name: slug(row.displayName ?? "") || slug(row.slug ?? "") || "cmux", | ||
| revision: row.updatedAt.getTime(), | ||
| }; | ||
| } | ||
|
|
||
| const bashrc = readFileSync(new URL("./images/devbox/cmux-bashrc", import.meta.url), "utf8"); | ||
| const prompt = readFileSync(new URL("./images/devbox/cmux-prompt.bash", import.meta.url), "utf8"); | ||
|
|
||
| // Runs on lifecycle operations, never during shell startup or prompt drawing. | ||
| // A lock serializes competing attaches/renames. Atomic replacement gives every | ||
| // shell either the old complete name or the new one. The machine id prevents a | ||
| // fork from inheriting its source's revision. User rc files are never touched. | ||
| const install = String.raw` | ||
| import fcntl, json, os, pathlib, sys, tempfile | ||
| directory = pathlib.Path(sys.argv[1]) | ||
| payload = json.loads(sys.argv[2]) | ||
| directory.mkdir(parents=True, exist_ok=True) | ||
| def replace(name, content): | ||
| target = directory / name | ||
| if target.is_file() and not target.is_symlink() and target.read_text() == content: | ||
| return | ||
| fd, temporary = tempfile.mkstemp(prefix=".prompt-", dir=directory) | ||
| try: | ||
| with os.fdopen(fd, "w") as stream: | ||
| stream.write(content) | ||
| os.fchmod(stream.fileno(), 0o644) | ||
| os.replace(temporary, target) | ||
| finally: | ||
| if os.path.exists(temporary): os.unlink(temporary) | ||
| with (directory / ".prompt-lock").open("a") as lock: | ||
| fcntl.flock(lock, fcntl.LOCK_EX) | ||
| try: | ||
| current = json.loads((directory / ".prompt-identity").read_text()) | ||
| except (FileNotFoundError, ValueError): | ||
| current = {} | ||
| if not isinstance(current, dict) or not isinstance(current.get("revision", -1), int): | ||
| current = {} | ||
| incoming = payload["identity"] | ||
| if current.get("machineId") != incoming["machineId"] or current.get("revision", -1) <= incoming["revision"]: | ||
| replace("vm-name", incoming["name"] + "\n") | ||
| replace(".prompt-identity", json.dumps(incoming)) | ||
|
Comment on lines
+61
to
+62
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win Persist
Write 🤖 Prompt for AI Agents |
||
| for name, content in payload["files"].items(): | ||
| replace(name, content) | ||
| `; | ||
|
|
||
| export function guestPromptInstallCommand(identity: GuestPromptIdentity, directory = "/etc/cmux"): string { | ||
| if (!/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(identity.name) || !Number.isSafeInteger(identity.revision)) { | ||
| throw new Error("Invalid Cloud prompt identity"); | ||
| } | ||
| const files = { | ||
| "prompt.bash": prompt.replaceAll("/etc/cmux", directory), | ||
| bashrc: bashrc.replaceAll("/etc/cmux", directory), | ||
| }; | ||
| return `python3 -c ${shellQuote(install)} ${shellQuote(directory)} ${shellQuote(JSON.stringify({ identity, files }))}`; | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🚀 Performance & Scalability | 🔵 Trivial | 🏗️ Heavy lift
Decode terminal responses off
MainActor.CloudMachineLinkis an actor, so connection acquisition andrunMeasuredprocess I/O do not execute onCmuxTuiSurfaceProvider'sMainActor. However,readScreen,waitForScreen,currentWorkingDirectory,waitForExit, andreadOutputcallJSONSerialization.jsonObject(with:)after the await.readOutputaccepts up to 4 MiB, so decoding a large response can delay rendering and pane lifecycle work.Move response decoding to a nonisolated
@concurrenttransport helper and return immutableSendableresults to the provider. Keep UI and catalog state onCmuxTuiSurfaceProvider. The scoped rule requires this parsing hop; it does not require movingCloudMachineLink's connection lookup or process execution to a new transport owner.🤖 Prompt for AI Agents