Repository navigation
Cloud create response carries the attach route; attach route timing and probe skip #13309
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
3edb1b3
17b6f05
ff8668b
994d08d
f8e932b
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 |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| import { CMUX_TUI_SESSION } from "./drivers/cmuxTuiDaemon"; | ||
| import { freestyleCmuxRemoteRoute } from "./drivers/freestyle"; | ||
| import { | ||
| GUEST_TOOLS_BAKED_EPOCH, | ||
| TRUSTED_CARRIER_EPOCH, | ||
| imageEpochAtLeast, | ||
| vmImageEntryEpoch, | ||
| type VmImageManifestEntry, | ||
| } from "./images/resolver"; | ||
|
|
||
| /** | ||
| * What a client needs to dial a machine's daemon straight from the create | ||
| * response, before any attach call: the transport and route, whether the | ||
| * daemon's cloud listener grants carrier authentication, the daemon build the | ||
| * image carries, and whether the guest tools are baked (no heal execs on | ||
| * attach). `readiness` is always `"dial"`: the daemon may still be starting, | ||
| * and the Noise handshake is the readiness proof. | ||
| * | ||
| * `attach-endpoint` keeps its own shape as the repair and reconnect path. | ||
| */ | ||
| export type VmAttachBlock = { | ||
| readonly transport: "cmux-remote"; | ||
| readonly route: string; | ||
| readonly session: string; | ||
| readonly trustedCarrier: boolean; | ||
| readonly daemonBuild: { | ||
| readonly commit: string | null; | ||
| readonly remoteProtocol: null; | ||
| readonly version: null; | ||
| }; | ||
| readonly guestToolsBaked: boolean; | ||
| readonly readiness: "dial"; | ||
| }; | ||
|
|
||
| export type VmAttachEntry = { | ||
| readonly addressIpv4: string | null; | ||
| readonly addressIpv6: string | null; | ||
| /** The epoch stamped on the row at create; null on older rows (the manifest entry answers then). */ | ||
| readonly imageEpoch?: string | null; | ||
| readonly providerVmId?: string; | ||
| }; | ||
|
|
||
| /** | ||
| * The attach block for a created machine, derived from the row and the | ||
| * checked-in manifest alone (no provider round trip). Null when the row holds | ||
| * no private address (the daemon is unreachable by address) or the image is | ||
| * not a manifest entry (nothing is known about its daemon). | ||
| * | ||
| * The route follows the driver's rule: IPv4 first because only the v4 path is | ||
| * reliable over the WireGuard tunnel, `[ipv6]` bracketed otherwise. | ||
| */ | ||
| export function createAttachBlock(input: { | ||
| readonly entry: VmAttachEntry; | ||
| readonly manifestEntry: VmImageManifestEntry | null; | ||
| }): VmAttachBlock | null { | ||
| const { entry, manifestEntry } = input; | ||
| const ipv4 = entry.addressIpv4?.trim() || undefined; | ||
| const ipv6 = entry.addressIpv6?.trim() || undefined; | ||
| if (!manifestEntry || (!ipv4 && !ipv6)) return null; | ||
| const epoch = entry.imageEpoch ?? vmImageEntryEpoch(manifestEntry); | ||
| return { | ||
| transport: "cmux-remote", | ||
| route: freestyleCmuxRemoteRoute({ vpcs: [{ ipv4, ipv6 }] }, entry.providerVmId ?? "unknown"), | ||
| session: CMUX_TUI_SESSION, | ||
| trustedCarrier: imageEpochAtLeast(epoch, TRUSTED_CARRIER_EPOCH), | ||
| daemonBuild: { commit: manifestEntry.cmuxTuiCommit ?? null, remoteProtocol: null, version: null }, | ||
| guestToolsBaked: imageEpochAtLeast(epoch, GUEST_TOOLS_BAKED_EPOCH), | ||
| readiness: "dial", | ||
| }; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| /** | ||
| * Work a route may run once its response has left (usage-event rows, | ||
| * metadata backfills): the route supplies `runAfterResponse`, so the writes | ||
| * still happen but the client does not wait for them. Workflows given no | ||
| * sink run the work inline, as they always did. | ||
| */ | ||
| export type VmDeferSink = (work: () => Promise<void>) => void; | ||
|
|
||
| /** | ||
| * A sink whose units run in the order they were handed in, each after the | ||
| * previous one settles, so deferred ledger rows keep their lifecycle order | ||
| * (`vm.create.requested` before `vm.created`) whatever the scheduler does | ||
| * with the callbacks it is given (Next's `after()` queue is concurrent; the | ||
| * detached fallback starts everything at once). A unit still starts only | ||
| * when the scheduler invokes it, and a failed unit never blocks the next. | ||
| */ | ||
| export function orderedDeferSink(schedule: VmDeferSink): VmDeferSink { | ||
| let tail: Promise<void> = Promise.resolve(); | ||
| return (work) => { | ||
| const previous = tail; | ||
| let start: () => void = () => undefined; | ||
| const started = new Promise<void>((resolve) => { | ||
| start = resolve; | ||
| }); | ||
| const run = started.then(() => previous).then(work); | ||
| tail = run.catch(() => undefined); | ||
| schedule(() => { | ||
| start(); | ||
| return run; | ||
| }); | ||
| }; | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -1750,8 +1750,7 @@ export class FreestyleProvider implements VMProvider { | |||||
| private async installGuestCliFiles(vm: Vm, vmId: string, promptIdentity?: GuestPromptIdentity): Promise<void> { | ||||||
| const temporaryPath = `${GUEST_CMUX_SHIM_PATH}.tmp-${randomBytes(12).toString("hex")}`; | ||||||
| try { | ||||||
| await this.execOrThrow(vm, vmId, "mkdir -p /usr/local/libexec", 5_000); | ||||||
| await vm.fs.writeTextFile(temporaryPath, GUEST_CMUX_SHIM, { mode: 0o755 }); | ||||||
| await this.uploadGuestShim(vm, vmId, temporaryPath); | ||||||
| const result = await vm.exec({ | ||||||
| command: `${guestBrowserInstallCommand()} && chmod 0755 '${temporaryPath}' && mv -f '${temporaryPath}' '${GUEST_CMUX_SHIM_PATH}' && ${guestCliDistributionCommand()}` | ||||||
| + (promptIdentity ? ` && ${guestPromptInstallCommand(promptIdentity)}` : ""), | ||||||
|
|
@@ -1768,6 +1767,22 @@ export class FreestyleProvider implements VMProvider { | |||||
| } | ||||||
| } | ||||||
|
|
||||||
| /** | ||||||
| * The bake creates the shim's directory (`/usr/local/libexec`, the | ||||||
| * opencode launcher step), so the upload goes straight in; an image from | ||||||
| * before that step rejects it once, and the directory is created and the | ||||||
| * upload retried instead of paying a mkdir round trip on every create. | ||||||
| */ | ||||||
| private async uploadGuestShim(vm: Vm, vmId: string, temporaryPath: string): Promise<void> { | ||||||
| try { | ||||||
| await vm.fs.writeTextFile(temporaryPath, GUEST_CMUX_SHIM, { mode: 0o755 }); | ||||||
| } catch (error) { | ||||||
| if (!isMissingGuestDirectoryError(error)) throw error; | ||||||
| await this.execOrThrow(vm, vmId, "mkdir -p /usr/local/libexec", 5_000); | ||||||
| await vm.fs.writeTextFile(temporaryPath, GUEST_CMUX_SHIM, { mode: 0o755 }); | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| private async execResult(vm: Vm, command: string, timeoutMs = EXEC_DEFAULT_TIMEOUT_MS): Promise<ExecResult | null> { | ||||||
| try { | ||||||
| const r = await vm.exec({ command, timeoutMs, linuxUser: GUEST_LINUX_USER }); | ||||||
|
|
@@ -1794,6 +1809,14 @@ export class FreestyleProvider implements VMProvider { | |||||
| } | ||||||
| } | ||||||
|
|
||||||
| /** A filesystem write refused because the target directory does not exist in the guest. */ | ||||||
| function isMissingGuestDirectoryError(error: unknown): boolean { | ||||||
| if (error instanceof FreestyleApiError) { | ||||||
| return error.status === 404 || /no such file|not found|enoent/i.test(error.message); | ||||||
|
Contributor
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. This treats every Freestyle 404 as a missing parent directory. A 404 for a missing VM or another provider resource will therefore run an unrelated
Suggested change
|
||||||
| } | ||||||
| return error instanceof Error && /no such file|enoent/i.test(error.message); | ||||||
| } | ||||||
|
|
||||||
| /** The resources a machine of `memoryMb` is sold with (see entitlements.ts). */ | ||||||
| export function freestyleTargetResources( | ||||||
| memoryMb: number, | ||||||
|
|
||||||
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.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
Repository: manaflow-ai/cmux
Length of output: 20607
🏁 Script executed:
Repository: manaflow-ai/cmux
Length of output: 29101
🏁 Script executed:
Repository: manaflow-ai/cmux
Length of output: 24197
🏁 Script executed:
Repository: manaflow-ai/cmux
Length of output: 31559
Derive replay metadata from the created row.
On an idempotent replay,
created.imagecan differ from the current request'simageSelection.image. The response then emits the current request'simageSelection.sizewith the created row's image. Use the manifest entry resolved fromcreated.image, and omitsizewhen that image is not manifest-backed.Proposed fix
📝 Committable suggestion
🤖 Prompt for AI Agents