# Freestyle Docs Documentation for Freestyle VMs and Freestyle Git. Each section below is a single docs page. The `Source:` line is the canonical URL of the page. --- # Freestyle Source: https://www.freestyle.sh/docs Infrastructure for code your product runs but didn't write. Freestyle gives AI products the infrastructure to run and store code they did not write — fast Linux VMs for execution, and multi-tenant Git for storage. - [Quickstart](https://www.freestyle.sh/docs/quickstart): Create a VM, run your first command, and set up your Freestyle API key. - [Freestyle VMs](https://www.freestyle.sh/docs/vms): Fast Linux VMs for agent workspaces, browser automation, debugging, and interactive user sessions. - [Freestyle Git](https://www.freestyle.sh/docs/git): Multi-tenant Git repositories with scoped access, search, triggers, and GitHub sync. - [Freestyle CLI](https://www.freestyle.sh/docs/cli): Install the CLI and run terminal commands for local development and debugging. ## AI-readable docs Give our Bash API to your agent so it can use our docs: https://www.freestyle.sh/docs/bash For example, give an agent this prompt: ````text Help me create a vm using freestyle, use their docs api with ```bash curl https://www.freestyle.sh/docs/bash ``` ```` --- # Quickstart Source: https://www.freestyle.sh/docs/quickstart Install Freestyle and create your first VM. Freestyle gives AI products the infrastructure to run and store code they did not write: fast Linux VMs for execution, and multi-tenant Git for storage. ## Install ```bash pnpm pnpm add freestyle ``` ```bash bun bun add freestyle ``` ```bash npm npm install freestyle ``` ```bash yarn yarn add freestyle ``` Set your API key before calling the API: ```bash export FREESTYLE_API_KEY="your-api-key" ``` ## Create A VM ```ts import { freestyle } from "freestyle"; const { vm } = await freestyle.vms.create(); const result = await vm.exec("echo 'hello from freestyle'"); console.log(result); ``` ## Next Steps - [Freestyle CLI](https://www.freestyle.sh/docs/cli) for terminal commands. - [Freestyle VMs](https://www.freestyle.sh/docs/vms) for VM lifecycle, files, SSH, and client sessions. - [Freestyle Git](https://www.freestyle.sh/docs/git) for repository storage, API access, search, triggers, and GitHub sync. --- # Freestyle VMs Source: https://www.freestyle.sh/docs/vms Create and control fast Linux virtual machines for agent workspaces and user sessions. Freestyle VMs are full Linux virtual machines designed for long running, complex tasks. They start quickly, can stop when idle, and can be started again by API calls, SSH, or network activity. ## Create A VM ```ts import { freestyle } from "freestyle"; const { vm } = await freestyle.vms.create(); const result = await vm.exec("echo 'hello from freestyle'"); console.log(result); ``` ## Work With Files ```ts await vm.fs.writeTextFile("/tmp/hello.txt", "Hello from Freestyle"); const content = await vm.fs.readTextFile("/tmp/hello.txt"); console.log(content); ``` ## Size A VM VMs start with 4 vCPU, 8 GB RAM, and a 20 GB root filesystem by default. To size a VM for your workload, call `vm.resize()` after creating it. ```ts await vm.resize({ cpu: 8, memory: 16, storage: 80, }); ``` ## Clone A Repository VMs work well with [Freestyle Git](https://www.freestyle.sh/docs/git) repositories. Create or import code into Git, grant access through your application, then clone and run it inside the VM. ```ts const { repoId } = await freestyle.git.repos.create({ name: "workspace", }); const { identity } = await freestyle.identities.create(); await identity.permissions.git.grant({ repoId, permission: "read" }); const { token } = await identity.tokens.create(); const { vm } = await freestyle.vms.create(); await vm.exec("mkdir -p /workspace"); await vm.exec( `git clone https://x-access-token:${token}@git.freestyle.sh/${repoId} /workspace`, ); await vm.exec("ls -la"); ``` ## Common Operations ```ts await vm.exec("pwd"); await vm.start(); await vm.stop(); await freestyle.vms.delete({ vmId }); ``` ## Route Web Traffic Attach a custom domain to a VM service by mapping a public hostname to a VM port. ```ts await freestyle.domains.mappings.create({ domain: "app.example.com", vmId, vmPort: 3000, }); ``` See [VM Domains](https://www.freestyle.sh/docs/vms/domains) for verification, DNS, and mapping setup. --- # VM Lifecycle Source: https://www.freestyle.sh/docs/vms/lifecycle Understand the VM states Freestyle exposes: running, stopped, forked, and deleted. Freestyle VMs are meant to be controlled as durable runtime objects. Your application can start work, stop it, start it again later, fork it for parallel exploration, and delete it when it is no longer needed. ## Running The VM is executing and can accept commands, SSH sessions, and network traffic. ```ts const { vm } = await freestyle.vms.create(); await vm.exec("echo running"); ``` ## Stopped Stopping shuts the VM down. Disk state is preserved, but memory is not. Use `stop()` when you explicitly want a fresh boot. ```ts await vm.stop(); await vm.start(); ``` ## Resize Use `resize()` to size a VM for your workload after it exists. Pass any of `cpu`, `memory`, and `storage` to change the VM's CPU, memory, or root filesystem size. ```ts await vm.resize({ cpu: 8, memory: 16, storage: 80, }); ``` If the VM is running, Freestyle stops it during the resize and starts it again afterward. Disk state is preserved, but in-memory process state is not. `cpu` and `memory` must be powers of two, `storage` can grow the root filesystem but cannot shrink it, and requested sizes are subject to your account limits. ## Forked Forking creates a new VM from the current running state. Use it when an agent needs to explore multiple branches of work from the same environment. ```ts const { forks } = await vm.fork({ count: 1, persistence: { type: "ephemeral" }, }); const [{ vm: forked }] = forks; await forked.exec("echo 'work in parallel'"); ``` ## Idle Timeout Configure an idle timeout to let Freestyle reclaim VMs that have no network activity. ```ts await vm.start({ idleTimeoutSeconds: 600 }); ``` Set `idleTimeoutSeconds` to `null` only for workloads that should stay running until you stop or delete them. ## Delete Delete VMs when the workspace is finished. ```ts await freestyle.vms.delete({ vmId }); ``` Deleting is permanent for the VM. Keep source code and important state in [Freestyle Git](https://www.freestyle.sh/docs/git) or another durable system before deleting the VM. --- # SSH Access Source: https://www.freestyle.sh/docs/vms/ssh SSH into Freestyle VMs with scoped identity tokens. Freestyle VMs accept SSH through `vm-ssh.freestyle.sh`. Access is controlled with Freestyle identities and tokens. ## SSH Format ```bash ssh @vm-ssh.freestyle.sh ssh :@vm-ssh.freestyle.sh ssh +:@vm-ssh.freestyle.sh ``` If you omit the token from the SSH URL, SSH prompts for it as the password. ## Create A Token ```ts import { freestyle } from "freestyle"; const { vm, vmId } = await freestyle.vms.create(); const { identity } = await freestyle.identities.create(); await identity.permissions.vms.grant({ vmId, }); const { token } = await identity.tokens.create(); console.log(`ssh ${vmId}:${token}@vm-ssh.freestyle.sh`); ``` ## SSH As A Linux User Linux users are normal guest OS users. Create them inside the VM, then grant an identity access to the matching username. ```ts const { vm, vmId } = await freestyle.vms.create(); await vm.exec({ command: ` set -e if ! id -u developer >/dev/null 2>&1; then useradd --create-home --shell /bin/bash developer fi if getent group sudo >/dev/null 2>&1; then usermod --append --groups sudo developer fi mkdir -p /home/developer/workspace chown -R developer:developer /home/developer `, }); const { identity } = await freestyle.identities.create(); await identity.permissions.vms.grant({ vmId, allowedUsers: ["developer"], }); const { token } = await identity.tokens.create(); console.log(`ssh ${vmId}+developer:${token}@vm-ssh.freestyle.sh`); ``` Freestyle base images configure `sshd` with an `AuthorizedKeysCommand` that reads the current Freestyle VM SSH public key from VM metadata. Any Linux account you create can use Freestyle SSH as long as that SSH configuration remains in place and the account has a valid login shell. For custom images or heavily modified SSH configs, keep this behavior enabled: ```bash grep -R "AuthorizedKeysCommand /usr/local/bin/fetch-ssh-keys" /etc/ssh/sshd_config /etc/ssh/sshd_config.d test -x /usr/local/bin/fetch-ssh-keys ``` You do not need to copy Freestyle access tokens into the VM. Tokens stay outside the VM and are checked by the SSH proxy before it connects to the Linux account. ## Multiple Developers Create separate identities when different users or agents should have different VM permissions. ```ts const { identity: alice } = await freestyle.identities.create(); await alice.permissions.vms.grant({ vmId, allowedUsers: ["alice"], }); const { identity: bob } = await freestyle.identities.create(); await bob.permissions.vms.grant({ vmId, allowedUsers: ["bob"], }); ``` Keep your Freestyle API key server-side. Send only scoped access tokens to clients or developers. ## Editor Connections Editor connections use the same Freestyle SSH proxy and scoped access tokens as command-line SSH. Create an identity, grant it access to the VM, mint a token, then pass that token in the editor connection URL. For VS Code and Cursor use one of these URL formats: ```text vscode://vscode-remote/ssh-remote+,@.vm-ssh.freestyle.sh?windowId=_blank cursor://vscode-remote/ssh-remote+,@.vm-ssh.freestyle.sh?windowId=_blank ``` For cmux, use its SSH URL format: ```text cmux://ssh?host=.vm-ssh.freestyle.sh&user=,&name= ``` --- # Client Sessions Source: https://www.freestyle.sh/docs/vms/client-sessions Let browser clients operate existing Freestyle VMs with scoped access tokens. Client sessions let a browser or end-user agent operate an existing VM without receiving your Freestyle API key. Your server creates an identity, grants VM permissions, creates a token, and sends that token to the client. ## Issue A Client Token ```ts title="server.ts" import { freestyle } from "freestyle"; const { vm, vmId } = await freestyle.vms.create(); const { identity } = await freestyle.identities.create(); await identity.permissions.vms.grant({ vmId, }); const { token } = await identity.tokens.create(); return { token, vmId }; ``` ## Use The Token In A Client ```ts title="client.ts" import { Freestyle } from "freestyle"; const freestyle = new Freestyle({ accessToken: token, }); const { vm } = await freestyle.vms.get({ vmId }); await vm.exec("pwd"); ``` ## Limits Client session tokens are for operations on existing VMs. They should not be used as a replacement for your server-side API key or for letting users create arbitrary resources. A client token can start a stopped VM when it runs an operation, connects over SSH, or sends traffic to the VM. --- # VM Domains Source: https://www.freestyle.sh/docs/vms/domains Route HTTPS traffic from custom domains to services running inside Freestyle VMs. VM domains route public HTTPS traffic from a hostname you control to a port inside a Freestyle VM. Create a VM first, then create a domain mapping for the VM port that should receive traffic. ## Domain Flow 1. [Verify ownership](https://www.freestyle.sh/docs/vms/domain-verification) of the domain with a TXT record. 2. [Point DNS](https://www.freestyle.sh/docs/vms/domain-dns) at Freestyle. 3. Map the domain to a VM port. 4. Run a service in the VM that listens on that port. ## Create A VM And Map A Domain Create the VM, start a service inside it, then map the public hostname to the service port. ```ts import { freestyle } from "freestyle"; const domain = "app.example.com"; const { vm, vmId } = await freestyle.vms.create(); // Write a small HTTP server into the VM. await vm.fs.writeTextFile( "/root/server.js", ` const http = require("http"); http .createServer((_req, res) => { res.writeHead(200, { "Content-Type": "text/html" }); res.end("

Hello from a Freestyle VM

"); }) .listen(3000, "0.0.0.0"); `, ); // Install Node and start the server listening on the mapped port. await vm.exec("apt-get update && apt-get install -y nodejs"); await vm.exec("nohup node /root/server.js >/tmp/server.log 2>&1 &"); await freestyle.domains.mappings.create({ domain, vmId, vmPort: 3000, }); console.log(vmId); ``` ## Map An Existing VM ```ts await freestyle.domains.mappings.create({ domain: "app.example.com", vmId: "your-vm-id", vmPort: 3000, }); ``` ## Unmap A Domain Remove a mapping to stop routing traffic from the domain to the VM. The domain stays verified, so you can map it again later without re-verifying. ```ts await freestyle.domains.mappings.delete({ domain: "app.example.com", }); ``` ## Requirements - The domain must be verified before it can be mapped. - DNS must point at Freestyle before traffic reaches the VM. - HTTPS is provisioned automatically. - The service inside the VM must listen on the mapped `vmPort`. - For HTTP servers, listen on `0.0.0.0`, not only `localhost`. --- # Domain Verification Source: https://www.freestyle.sh/docs/vms/domain-verification Verify that you own a custom domain before routing it to a Freestyle VM. Before a custom domain can route to a Freestyle VM, verify that you own it. Verification creates a TXT record challenge for the domain. You can verify domains in the [Freestyle Dashboard](https://dash.freestyle.sh/dashboard/domain-ownership) or through the SDK. ## Create A Verification ```ts import { freestyle } from "freestyle"; const { verificationId, record, instructions } = await freestyle.domains.verifications.create({ domain: "example.com", }); console.log(verificationId); console.log(record); console.log(instructions); ``` Freestyle returns a TXT record like this: ```ts { type: "TXT", name: "_freestyle_custom_hostname.example.com", value: "", } ``` Add that TXT record in your DNS provider. Some providers automatically append the domain name, so if `_freestyle_custom_hostname.example.com` does not verify, try `_freestyle_custom_hostname` as the record name. ## Complete Verification After adding the TXT record, complete verification by domain: ```ts await freestyle.domains.verifications.complete({ domain: "example.com", }); ``` Or complete verification by ID: ```ts await freestyle.domains.verifications.complete({ verificationId, }); ``` ## List Verifications ```ts const verifications = await freestyle.domains.verifications.list(); for (const verification of verifications) { console.log(verification.domain, verification.verificationCode); } ``` ## Cancel A Verification ```ts await freestyle.domains.verifications.cancel({ domain: "example.com", verificationCode: "", }); ``` ## List Verified Domains ```ts const domains = await freestyle.domains.list({ limit: 50, }); for (const domain of domains) { console.log(domain.domain, domain.verifiedDns); } ``` Verification proves ownership. It does not point the domain at Freestyle or route traffic to a VM. After verification, [configure DNS](https://www.freestyle.sh/docs/vms/domain-dns) and create a [domain mapping](https://www.freestyle.sh/docs/vms/domain-mappings). --- # Domain DNS Source: https://www.freestyle.sh/docs/vms/domain-dns Configure DNS records for custom domains that route to Freestyle VMs. After you [verify ownership](https://www.freestyle.sh/docs/vms/domain-verification), point the domain at Freestyle so public traffic can reach your VM mapping. ## Apex Domain For an apex domain such as `example.com`, add an A record: ```text Type: A Name: @ Value: 35.235.84.134 ``` ## Subdomain For a subdomain such as `app.example.com`, add an A record for the subdomain: ```text Type: A Name: app Value: 35.235.84.134 ``` ## Wildcard Subdomain For all subdomains under a domain, add a wildcard A record: ```text Type: A Name: * Value: 35.235.84.134 ``` ## Check DNS DNS changes can take time to propagate. Use `dig` to verify the record resolves to Freestyle: ```bash dig app.example.com ``` Expected answer: ```text ;; ANSWER SECTION: app.example.com. 60 IN A 35.235.84.134 ``` If the answer shows a name such as `app.example.com.example.com`, your DNS provider probably appended the zone name. Change the record name to the relative name, such as `app`, instead of the full hostname. ## Route Traffic DNS only sends traffic to Freestyle. To send traffic to a VM, create a domain mapping: ```ts await freestyle.domains.mappings.create({ domain: "app.example.com", vmId: "your-vm-id", vmPort: 3000, }); ``` See [Domain Mappings](https://www.freestyle.sh/docs/vms/domain-mappings) for mapping and cleanup examples. --- # Domain Mappings Source: https://www.freestyle.sh/docs/vms/domain-mappings Create, list, and delete mappings from custom domains to VM ports. A domain mapping connects one public hostname to one port inside a VM. Use mappings when you want to attach or move domains after a VM already exists. ## Create A Mapping ```ts import { freestyle } from "freestyle"; await freestyle.domains.mappings.create({ domain: "app.example.com", vmId: "your-vm-id", vmPort: 3000, }); ``` The VM service must listen on `vmPort`. For web servers, bind to `0.0.0.0` so traffic from outside the VM can reach the process. ## List Mappings ```ts const { mappings } = await freestyle.domains.mappings.list({ domain: "app.example.com", limit: 50, }); for (const mapping of mappings) { console.log(mapping.domain, mapping.vmId, mapping.vmPort); } ``` ## Delete A Mapping ```ts await freestyle.domains.mappings.delete({ domain: "app.example.com", }); ``` Deleting a mapping stops new traffic from routing to that VM port. It does not delete the VM, DNS record, or domain verification. ## Multiple Ports Use separate domains for separate VM services: ```ts await freestyle.domains.mappings.create({ domain: "api.example.com", vmId, vmPort: 3000, }); await freestyle.domains.mappings.create({ domain: "terminal.example.com", vmId, vmPort: 8453, }); ``` Each domain gets HTTPS automatically after DNS and mapping are in place. --- # Domains CLI Source: https://www.freestyle.sh/docs/vms/domains-cli Use the Freestyle CLI to verify domains and map them to VM ports. Use the Domains CLI for domain verification and VM domain mappings. ## Prerequisites Set your API key: ```bash export FREESTYLE_API_KEY="your-api-key" ``` Run commands through `npx`: ```bash npx freestyle domains --help ``` ## Verify A Domain Create a verification request and print the TXT record details: ```bash npx freestyle domains verify example.com ``` Complete verification by domain: ```bash npx freestyle domains complete --domain example.com ``` Or complete verification by verification ID: ```bash npx freestyle domains complete --verification-id ``` List pending verification requests: ```bash npx freestyle domains verifications ``` ## List Verified Domains ```bash npx freestyle domains list ``` Use JSON output when scripting: ```bash npx freestyle domains list --json ``` ## Map A Domain To A VM ```bash npx freestyle domains map app.example.com --vm-id --vm-port 3000 ``` List mappings: ```bash npx freestyle domains mappings ``` Filter mappings by domain: ```bash npx freestyle domains mappings --domain app.example.com ``` Delete a mapping: ```bash npx freestyle domains unmap app.example.com ``` For application workflows, use the SDK so your server can verify domains, configure DNS instructions, create VMs, and create domain mappings as separate steps. --- # VPCs Source: https://www.freestyle.sh/docs/vms/network/vpcs Create private networks for Freestyle VMs and attach VM network interfaces to them. VPCs let Freestyle VMs communicate over private IP addresses. Use them when a group of VMs should share an internal network for databases, services, worker pools, or agent environments that should not depend on public VM domains. ## Terms - A VPC is a private network for your VMs. VMs in the same VPC can talk to each other without using public domains. - A private IP is an address that only works inside the VPC, like `192.168.10.10`. - A CIDR is the address range for the VPC. `192.168.10.0/24` means addresses from `192.168.10.1` through `192.168.10.254` are available for VMs. - A NIC is a network interface on a VM. Attaching a NIC to a VPC is how the VM joins the private network. ## Create A VPC ```ts import { freestyle } from "freestyle"; const { vpcId, vpc } = await freestyle.vpc.create({ name: "workspace-network", cidr: "192.168.10.0/24", }); console.log(vpcId); ``` The VPC CIDR is the private address range available to VMs attached to the network. Pick a range that does not overlap with your home, office, or VPN network. ## Attach VMs Attach a VM by passing a routed network interface in `vms.create`: ```ts const { vm: apiVm } = await freestyle.vms.create({ nics: [ { default: true, vpc: vpcId, mode: "routed", ipv4: "192.168.10.10", }, ], }); const { vm: workerVm } = await freestyle.vms.create({ nics: [ { default: true, vpc: vpcId, mode: "routed", ipv4: "192.168.10.11", }, ], }); ``` Use `default: true` when the VPC interface should be the VM's default route. Use `mode: "routed"` for VPC networking. ## Test Private Connectivity VMs in the same VPC can reach each other by private IP: ```ts const result = await apiVm.exec("ping -c 3 192.168.10.11"); console.log(result.stdout); ``` Run services on `0.0.0.0` inside the destination VM when other VMs should connect to them: ```ts await workerVm.exec( "nohup python3 -m http.server 8080 --bind 0.0.0.0 >/tmp/http.log 2>&1 &", ); ``` Then connect from another VM over the VPC address: ```ts const response = await apiVm.exec("curl http://192.168.10.11:8080"); console.log(response.stdout); ``` ## Connect From Your Computer Use [VPNs](https://www.freestyle.sh/docs/vms/network/vpns) to create an ephemeral WireGuard connection from your computer into a VPC. ## Current Limits - VPC VM interfaces use `mode: "routed"`. - A VM can attach one routed VPC interface. - Choose a VPC CIDR that does not overlap with networks your VM or VPN client already uses. --- # VPNs Source: https://www.freestyle.sh/docs/vms/network/vpns Use WireGuard to connect your computer to a Freestyle VPC. VPNs let your computer join a Freestyle VPC with WireGuard. Create an ephemeral WireGuard session for a VPC, bring the generated config up with the WireGuard CLI, then connect to VMs by their private VPC IPs. ## Terms - A VPN is a temporary network connection from your computer into a private network. - WireGuard is the VPN tool Freestyle uses. It creates an encrypted tunnel between your computer and the VPC. - A tunnel is the private path that carries VPC traffic from your computer to Freestyle. - A peer is the other side of the WireGuard connection. Your computer is one peer; Freestyle is the other peer. - A WireGuard config is the small file `wg-quick` uses to create the tunnel. It includes a private key, so treat it like a credential. ## Install WireGuard Tools Install the WireGuard CLI on your computer: ```bash macOS brew install wireguard-tools ``` ```bash Ubuntu/Debian sudo apt-get update sudo apt-get install -y wireguard-tools ``` ```bash Fedora sudo dnf install -y wireguard-tools ``` ## Create A VPN Session Create a VPC, attach a VM, and bring the tunnel up with `wg-quick`: ```ts title="vpn.ts" import { freestyle } from "freestyle"; import { spawn } from "node:child_process"; import { rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; const { vpcId, vpc } = await freestyle.vpc.create({ name: "workspace-network", cidr: "192.168.10.0/24", }); const { vmId } = await freestyle.vms.create({ nics: [ { default: true, vpc: vpcId, mode: "routed", ipv4: "192.168.10.10", }, ], }); const connection = await vpc.wireguard.createEphemeral(); const configPath = join(tmpdir(), `wg-${connection.sessionId}.conf`); await writeFile(configPath, connection.clientConfig, { mode: 0o600 }); await new Promise((resolve, reject) => { const child = spawn("sudo", ["wg-quick", "up", configPath], { stdio: "inherit", }); child.once("error", reject); child.once("exit", (code) => code === 0 ? resolve() : reject(new Error(`wg-quick up failed with ${code}`)), ); }); // Keep the tunnel tied to this script: when the process exits from Ctrl-C or // SIGTERM, bring WireGuard down and release the ephemeral Freestyle session. const close = async (code = 0) => { await new Promise((resolve) => { spawn("sudo", ["-n", "wg-quick", "down", configPath], { stdio: "ignore", }).once("exit", () => resolve()); }); await connection.close().catch(() => undefined); await rm(configPath, { force: true }); process.exit(code); }; process.once("SIGINT", () => void close(130)); process.once("SIGTERM", () => void close(143)); console.log(`Created VM ${vmId} in VPC ${vpcId}`); console.log("Try: ping -c 3 192.168.10.10"); console.log("Press Ctrl-C to disconnect."); await new Promise(() => {}); ``` Run the script: ```bash export FREESTYLE_API_KEY="your-api-key" bun run vpn.ts ``` The generated WireGuard config contains a client private key. This script writes it to your OS temp directory, removes it on shutdown, and closes the ephemeral Freestyle VPN session when you press Ctrl-C. While the script is running, your computer has a route to the VPC CIDR. Traffic to `192.168.10.10` goes through WireGuard; normal internet traffic keeps using your regular network. Verify that WireGuard has a peer and has sent traffic: ```bash sudo wg show ``` Connect to a VM by its VPC IP: ```bash ping 192.168.10.10 ``` When you are done, press Ctrl-C in the script terminal. The script runs `wg-quick down`, removes the temp config file, and closes the ephemeral Freestyle VPN session. ## Use A System Config Path For repeated local testing, you can save `connection.clientConfig` as `freestyle-vpc.conf`, install it under `/etc/wireguard`, and refer to it by name: ```bash sudo mkdir -p /etc/wireguard sudo install -m 600 freestyle-vpc.conf /etc/wireguard/freestyle-vpc.conf sudo wg-quick up freestyle-vpc sudo wg show freestyle-vpc sudo wg-quick down freestyle-vpc ``` ## What The Config Contains `vpc.wireguard.createEphemeral()` returns a standard WireGuard config in `clientConfig`: ```ini [Interface] Address = 100.96.0.2/32 PrivateKey = generated-client-private-key [Peer] PublicKey = freestyle-server-public-key AllowedIPs = 192.168.10.0/24 Endpoint = vpn-endpoint.example.com:51820 PersistentKeepalive = 25 ``` `AllowedIPs` is the VPC CIDR, so only VPC traffic is routed through the tunnel. ## Close The Session Ephemeral sessions are meant to be short lived. Close the session from your app when the user disconnects: ```ts await connection.close(); ``` If your process exits before it can close the session, Freestyle eventually expires the VPN resources. --- # PTY Sessions Source: https://www.freestyle.sh/docs/vms/pty Open persistent interactive shells on a VM that survive WebSocket disconnects, VM suspends, and forks. A PTY (pseudo-terminal) session is a long-lived interactive shell that lives inside the VM and that you can attach to, detach from, and reattach to over a WebSocket. Sessions survive client disconnects, VM suspends, and VM forks, letting agents drive interactive programs (REPLs, editors, package managers, debuggers) without re-spawning every command. Use a PTY when `vm.exec()` is too coarse: the program needs an interactive terminal, you want to send keystrokes mid-run, you need scrollback across reconnects, or you're running a background server (a dev server, a file watcher) whose output you want to read later. ## Open A Session ```ts const session = await vm.pty.open({ cols: 120, rows: 30, onData: (bytes) => process.stdout.write(bytes), onExit: (code) => console.log("program exited with", code), }); session.write("echo hello\n"); console.log(session.sessionId); ``` With no `exec`, the agent spawns the user's login shell (or `/bin/sh`). The session is backed by a real PTY, so the shell is interactive — prompt, line editing, job control — and an explicit `exec` runs interactively too (a REPL like `python3`, a TUI like `htop`). `cols` and `rows` default to 80 x 24. Keep `session.sessionId` if you might reattach later. ## Reattach Across Disconnects Calling `.detach()` (or losing the network) leaves the program running in the VM. Reconnect with `attach({ sessionId })` and you get the current screen replayed plus live output from that point. ```ts session.detach(); // later, or from a different process / machine const rebound = await vm.pty.attach({ sessionId: session.sessionId, onData: (bytes) => process.stdout.write(bytes), }); ``` `detach()` closes only the local handle; the server-side session keeps running. The original `session` object is unusable after detach — `write` / `resize` / `signal` throw, telling you to `vm.pty.attach({ sessionId })` to get a fresh handle. ## Send Input, Resize, Signal ```ts session.write("ls -la\n"); session.write(new Uint8Array([0x03])); // Ctrl-C as raw byte session.resize({ cols: 200, rows: 60 }); session.signal("SIGINT"); ``` `write()` accepts `string` (UTF-8 encoded) or `Uint8Array`. `signal()` accepts `SIGINT` (delivered as Ctrl-C on stdin) and `SIGKILL` (terminates the session's root process). ## Behavior Across VM Lifecycle PTY sessions are first-class with respect to the VM lifecycle: - **VM suspend** (explicit or idle) — session transitions to `Suspended` and stays reattachable; the next attach also wakes the VM. - **VM fork** — every child inherits the parent's non-`Exited` sessions under the same `sessionId`, seeded with the parent's at-fork screen; output diverges per child from there. - **Program exits** — session goes to `Exited`, stays visible in `list()` for ~60s, and `onExit(code)` fires. - **VM stop / kill** — sessions terminate, attached WebSockets are evicted. The key invariant: **an attached PTY does NOT hold the VM open or block a lifecycle operation.** If the VM is suspended (idle or explicit) or torn down, attached clients are evicted with a clean WebSocket close; the session itself stays reattachable as long as the VM is reachable again. If you need a VM to stay running while a PTY session is in use, configure `idleTimeoutSeconds: null` (or a long value) on `vm.start()`. The platform's idle monitor measures only VM network-interface traffic, so PTY input — and even `vm.exec()` — does not reset the timer. To keep the VM alive without disabling idle-suspend entirely, the program in the VM has to generate outbound network traffic (an HTTP request, etc.) on its own. ## Auto-Reconnect The SDK reconnects transparently on transient WebSocket drops, including the eviction triggered when a VM suspends. The reattach also wakes the VM if needed, so callers rarely have to think about transient hiccups. Defaults: 5 attempts, exponential backoff from 500ms doubling to 8s. Writes called during a reconnect attempt are queued and flushed once the new WebSocket opens. `onClose` fires only when the session is *terminally* closed — program exited, caller called `detach()`, or reconnect attempts were exhausted. ```ts const session = await vm.pty.open({ exec: "/bin/sh", reconnect: { maxAttempts: 10, baseDelayMs: 200 }, onReconnecting: (attempt, max) => console.log(`reconnect ${attempt}/${max}`), onReconnect: () => console.log("reconnected"), }); // or disable entirely: const noRetry = await vm.pty.open({ exec: "/bin/sh", reconnect: false }); ``` With `reconnect: false` the original `session` is dead the moment its WebSocket closes — capture `sessionId` ahead of time and call `vm.pty.attach({ sessionId })` to get a new session object. ## Inheritance On Fork ```ts const session = await vm.pty.open({ exec: "/bin/sh" }); const parentSessionId = session.sessionId; session.write("echo parent\n"); const { forks } = await vm.fork({ count: 2 }); for (const { vm: child } of forks) { // Every fork has the parent's session under the same session_id, // seeded with the parent's at-fork screen. const childSession = await child.pty.attach({ sessionId: parentSessionId }); childSession.write("echo from-fork\n"); // diverges from parent here } ``` Writes on a fork's session go to that fork's process only; output never crosses between siblings or parent. Sessions that had already `Exited` in the parent are not inherited. ## List And Close ```ts const { sessions } = await vm.pty.list(); for (const s of sessions) { console.log(s.sessionId, s.exec, s.running, s.suspended, s.attachedCount); } await vm.pty.close({ sessionId }); ``` `Running`, `Suspended`, and recently-`Exited` sessions stay in `list()` for ~60 seconds after exit so reattach can read the final exit code. `close()` kills the underlying program (SIGKILL semantics) and removes the session. ## Multi-User Isolation If the VM is configured with linux users, PTY sessions are scoped per user. A session opened with `vm.user("alice").pty.open()` runs as `alice`. A `vm.user("bob")` caller does not see Alice's sessions in `list()` and gets `403 SessionUserMismatch` if it tries to attach or close one. A plain `vm.pty.list()` (with no `vm.user(...)`) is the VM owner and sees every session on the VM, including user-scoped ones. ```ts const aliceSession = await vm.user("alice").pty.open({ exec: "/bin/sh" }); // from a Bob-authenticated client, list() does NOT show Alice's session. const { sessions: bobView } = await vm.user("bob").pty.list(); // from the VM owner, list() shows everything. const { sessions: ownerView } = await vm.pty.list(); ``` ## Caps And Limits - **Sessions per VM:** 128. Going over the cap returns `SessionCapReached` (HTTP 429). `Exited` sessions count toward the cap until the 60-second retention window expires and the reaper clears them. - **Scrollback:** ~10,000 lines per session, rendered ANSI-aware so the snapshot reproduces colors and cursor position. - **Signals:** `SIGINT` (Ctrl-C on stdin) and `SIGKILL` (terminates the session). No other signals are exposed. - **Errors:** pre-upgrade errors (auth, VM not running, session cap) come back as normal HTTP JSON. Errors after the WebSocket is open come back as RFC 6455 close codes (1008 / 1011). --- # Freestyle Git Source: https://www.freestyle.sh/docs/git Create and manage multi-tenant Git repositories for user- and agent-generated code. Freestyle Git is hosted Git designed for products that manage code on behalf of users or AI agents. You can create repositories by API, grant scoped Git access, inspect repository contents, search code, attach automation triggers, and sync with GitHub. ## Create A Repository ```ts import { freestyle } from "freestyle"; const { repoId, repo } = await freestyle.git.repos.create({ name: "my-repo", }); console.log(repoId); ``` ## Clone A Repository Create an identity, grant repository access, and issue a token. ```ts const { identity } = await freestyle.identities.create(); await identity.permissions.git.grant({ permission: "write", repoId, }); const { token } = await identity.tokens.create(); console.log( `git clone https://x-access-token:${token}@git.freestyle.sh/${repoId}`, ); ``` Use the token with native Git: ```bash git clone https://x-access-token:@git.freestyle.sh/ ``` ## Read Files By API ```ts const ref = freestyle.git.repos.ref({ repoId }); const file = await ref.contents.get({ path: "README.md" }); console.log(file); ``` --- # Repositories Source: https://www.freestyle.sh/docs/git/repositories Create, list, delete, and authenticate Freestyle Git repositories. ## Create An Empty Repository ```ts import { freestyle } from "freestyle"; const { repoId, repo } = await freestyle.git.repos.create({ name: "my-repo", }); ``` ## Create From A Source Repository Fork an existing repository and preserve its history: ```ts await freestyle.git.repos.create({ source: { url: "https://github.com/user/repo.git", rev: "main", }, }); ``` ## List Repositories ```ts const result = await freestyle.git.repos.list({ limit: 20, cursor: "0", }); for (const repo of result.repositories) { console.log(repo); } ``` ## Delete A Repository ```ts await freestyle.git.repos.delete({ repoId: "your-repo-id", }); ``` Deleting a repository permanently removes its Git data. ## Authenticate Native Git Repositories are private by default. Grant access through Freestyle identities. ```ts const { identity } = await freestyle.identities.create(); await identity.permissions.git.grant({ permission: "write", repoId, }); const { token } = await identity.tokens.create(); console.log( `git clone https://x-access-token:${token}@git.freestyle.sh/${repoId}`, ); ``` For local testing with your API key: ```bash git -c http.extraHeader="Authorization: Bearer $FREESTYLE_API_KEY" \ clone https://git.freestyle.sh/ ``` ## Public Repositories ```ts await freestyle.git.repos.create({ public: true, }); ``` Public repositories can be cloned without authentication, but pushes still require write access. --- # Git API Source: https://www.freestyle.sh/docs/git/api Read and modify repository data through Freestyle Git APIs. Use the Git API when your application needs repository data without cloning locally. ## Get File Or Directory Contents ```ts import { freestyle } from "freestyle"; const repo = freestyle.git.repos.ref({ repoId: "your-repo-id", }); const file = await repo.contents.get({ path: "src/index.ts", rev: "main", }); console.log(file); ``` Directory responses include nested entries: ```ts const dir = await repo.contents.get({ path: "src", rev: "main", }); console.log(dir); ``` ## Download Archives ```ts import { writeFile } from "node:fs/promises"; const tarball = await repo.contents.downloadTarball({ rev: "main" }); await writeFile("repo.tar", Buffer.from(tarball)); const zip = await repo.contents.downloadZip({ rev: "main" }); await writeFile("repo.zip", Buffer.from(zip)); ``` ## Branches ```ts const branch = await repo.branches.get({ branchName: "main" }); console.log(branch.sha); const branches = await repo.branches.list(); console.log(branches); ``` Create a branch: ```ts await repo.branches.create({ name: "feature/new-flow", }); await repo.branches.create({ name: "feature/from-commit", sha: "a1b2c3d4e5f6", }); ``` Set the default branch: ```ts await repo.branches.setDefaultBranch({ defaultBranch: "main", }); ``` ## Commits List commits: ```ts const commits = await repo.commits.list(); console.log(commits); ``` Create a commit by writing file changes: ```ts await repo.commits.create({ branch: "main", message: "Update README", files: [{ path: "README.md", content: "# Updated README\n" }], }); ``` ## Tags ```ts const tags = await repo.tags.list(); const tag = await repo.tags.get({ tagName: "v1.0.0", }); console.log(tag); ``` --- # Search Source: https://www.freestyle.sh/docs/git/search Search repository contents, filenames, commit messages, and diffs. Freestyle Git search lets your product inspect code without cloning a repository. ## Content Search Search file contents at a branch, tag, or commit. ```ts TypeScript import { freestyle } from "freestyle"; const repo = freestyle.git.repos.ref({ repoId: "your-repo-id" }); const result = await repo.search({ query: "TODO", rev: "main", pathPattern: "src/**/*.ts", excludePattern: "**/*.test.ts", }); for (const file of result.files) { for (const match of file.matches) { console.log(`${file.path}:${match.lineNumber} ${match.line}`); } } ``` ```bash cURL curl "https://api.freestyle.sh/git/v1/repo/${REPO_ID}/search?query=TODO&pathPattern=src/**/*.ts" \ -H "Authorization: Bearer ${FREESTYLE_API_KEY}" ``` Use regex patterns when you need structural matches: ```ts const result = await repo.search({ query: "^(export\\s+)?(async\\s+)?function\\s+\\w+", isRegex: true, pathPattern: "**/*.ts", }); ``` ## Filename Search ```ts const result = await repo.searchFiles({ query: "index", maxResults: 20, }); for (const file of result.files) { console.log(file.path); } ``` ## Commit Message Search ```ts const result = await repo.searchCommits({ query: "fix", maxResults: 10, }); for (const commit of result.commits) { console.log(`${commit.sha.slice(0, 7)} ${commit.message}`); } ``` ## Diff Search Use diff search when you need to find when text was added or changed. ```ts const result = await repo.searchDiffs({ query: "deprecatedMethod", maxResults: 20, }); for (const commit of result.commits) { for (const file of commit.files) { console.log(commit.sha, file.path); } } ``` ## Pagination Use `maxResults`, `offset`, and `hasMore` to page through large result sets. ```ts let offset = 0; const files = []; while (true) { const result = await repo.search({ query: "TODO", maxResults: 50, offset, }); files.push(...result.files); if (!result.hasMore) break; offset += 50; } ``` --- # Triggers Source: https://www.freestyle.sh/docs/git/triggers Send webhooks when Freestyle Git repositories receive pushes. Triggers automate work when repository events occur, such as pushes to a branch. ## Create A Webhook Trigger ```ts import { freestyle } from "freestyle"; const { repo } = await freestyle.git.repos.create({ name: "automation", }); const { triggerId } = await repo.triggers.create({ trigger: { event: "push", branches: ["main"], globs: ["*.js"], }, action: { action: "webhook", endpoint: "https://your-webhook-url.com", }, }); console.log(triggerId); ``` `branches` and `globs` are optional filters. ## Payload Webhook triggers send a payload like this: ```ts interface GitTriggerPayload { event: "branchUpdate"; repoId: string; branch: string; commit: string; } ``` ## Local Development Use a tunnel such as Tailscale Funnel to expose a local development server. ```bash tailscale funnel 3000 ``` Use the generated public URL as the trigger endpoint while developing. ## Webhook Signing Webhooks include a JWT signature in the `x-freestyle-signature` header. The JWT contains a `body_sha256` claim for the raw request body. ```ts import crypto from "node:crypto"; import { createRemoteJWKSet, jwtVerify } from "jose"; const JWKS = createRemoteJWKSet( new URL("https://git.freestyle.sh/.well-known/jwks.json"), ); async function verifyWebhook(request: Request) { const signature = request.headers.get("x-freestyle-signature"); if (!signature) throw new Error("Missing signature"); const bodyText = await request.text(); const { payload } = await jwtVerify(signature, JWKS, { algorithms: ["EdDSA"], }); const bodyHash = crypto.createHash("sha256").update(bodyText).digest("hex"); if (payload.body_sha256 !== bodyHash) { throw new Error("Payload verification failed"); } return JSON.parse(bodyText); } ``` ## List Triggers ```ts const { triggers } = await repo.triggers.list(); for (const trigger of triggers) { console.log(trigger.id, trigger.trigger.event); } ``` ## Delete A Trigger ```ts await repo.triggers.delete({ triggerId: "trigger-id", }); ``` --- # GitHub Sync Source: https://www.freestyle.sh/docs/git/github-sync Synchronize Freestyle Git repositories with GitHub repositories. Freestyle can synchronize repositories between Freestyle Git and GitHub. Use this when your product stores source code in Freestyle but users still need visibility or workflows in GitHub. ## How Sync Works Freestyle uses GitHub Apps for repository-specific access. When code is pushed to either side, Freestyle syncs changes to the other side while avoiding destructive force pushes. Freestyle syncs: - Branch creation and updates - Commit history - Tags - Branch deletions If branches diverge, Freestyle avoids overwriting data. Resolve conflicts manually in either repository, then sync resumes. ## Create Or Connect A GitHub App 1. Open the [Freestyle Dashboard](https://dash.freestyle.sh). 2. Go to **Git > Sync**. 3. Create a GitHub App or connect an existing app. 4. Install the app on the GitHub repositories you want to sync. For app builders, send users to your GitHub App installation URL so they can authorize repository access. ## Link Repositories In The Dashboard 1. Go to **Git > Repositories**. 2. Select the Freestyle repository. 3. Click **Configure GitHub Sync**. 4. Choose the GitHub repository. 5. Save the configuration. ## Link Repositories By SDK ```ts import { freestyle } from "freestyle"; const { repo } = await freestyle.git.repos.create({ name: "synced-repo", }); await repo.githubSync.enable({ githubRepoName: "owner/repo", }); const syncConfig = await repo.githubSync.get(); console.log(syncConfig?.githubRepoName); await repo.githubSync.disable(); ``` ## Safety Rules - Freestyle does not force-push to resolve conflicts. - GitHub App permissions are scoped to installed repositories. - Repository sync should be configured only after the GitHub App has access to the target repository. --- # Freestyle CLI Source: https://www.freestyle.sh/docs/cli Install and use the Freestyle CLI for local VM and Git workflows. The Freestyle CLI is for one-off operations while developing, debugging, or automating local workflows. Use the SDK for application code, and use the CLI when you want to inspect or operate on VMs and Git repositories from a terminal. ## Install Run the CLI without installing it globally: ```bash npx freestyle --help ``` Or install the package globally: ```bash pnpm pnpm add -g freestyle ``` ```bash bun bun add -g freestyle ``` ```bash npm npm install -g freestyle ``` ```bash yarn yarn global add freestyle ``` Then verify the command is available: ```bash freestyle --help ``` ## Authenticate Authenticate the CLI in one of two ways. Log in through your browser with OAuth. This stores your credentials and sets a default team, so it's the easiest option for local development: ```bash freestyle login ``` Or set an API key directly, which is the better fit for CI and scripts: ```bash export FREESTYLE_API_KEY="your-api-key" ``` If `FREESTYLE_API_KEY` is set, the CLI uses it. Otherwise it falls back to your `freestyle login` session. ## VM Commands Create a VM and run a command: ```bash npx freestyle vm create --exec 'uname -a' ``` Start a temporary SSH session: ```bash npx freestyle vm create --ssh --delete ``` See [VM CLI](https://www.freestyle.sh/docs/vms/cli) for VM list, exec, SSH, and delete commands. ## Git Commands Create a repository: ```bash npx freestyle git create ``` List repositories: ```bash npx freestyle git list --limit 20 ``` See [Git CLI](https://www.freestyle.sh/docs/git/cli) for repository lifecycle commands and native Git usage. --- # VM CLI Source: https://www.freestyle.sh/docs/vms/cli Use the Freestyle CLI for quick VM operations and debugging. The Freestyle CLI is useful for one-off operations while developing or debugging. ## Prerequisites Set your API key: ```bash export FREESTYLE_API_KEY="your-api-key" ``` Run commands through `npx`: ```bash npx freestyle --help ``` ## List VMs ```bash npx freestyle vm list ``` Use JSON output when scripting: ```bash npx freestyle vm list --json ``` ## Create And Exec ```bash npx freestyle vm create --exec 'uname -a' ``` Delete the VM after the command completes: ```bash npx freestyle vm create --exec 'npm test' --delete ``` ## SSH ```bash npx freestyle vm create --ssh ``` For a temporary debug session: ```bash npx freestyle vm create --ssh --delete ``` To create a VM from a snapshot and connect over SSH: ```bash npx freestyle vm create --snapshot --ssh ``` ## Operate On An Existing VM ```bash npx freestyle vm exec 'pwd' npx freestyle vm delete ``` Use the SDK for long-lived application workflows and the CLI for operational tasks. --- # Git CLI Source: https://www.freestyle.sh/docs/git/cli Use Freestyle CLI commands for Git repository lifecycle operations. Use the CLI for quick repository operations. Use native Git for clone, push, and fetch. ## Prerequisites ```bash export FREESTYLE_API_KEY="your-api-key" ``` Run commands with `npx freestyle`. ## Create Repositories ```bash npx freestyle git create ``` Create with options: ```bash npx freestyle git create \ --name my-repo \ --public \ --default-branch main ``` Create from an existing source: ```bash npx freestyle git create \ --source-url https://github.com/owner/repo.git \ --source-rev main ``` ## List Repositories ```bash npx freestyle git list --limit 20 npx freestyle git list --json ``` ## Delete A Repository ```bash npx freestyle git delete ``` ## Use Native Git ```bash git clone https://x-access-token:@git.freestyle.sh/ git push origin main ``` For hooks, GitHub Sync, repository contents, and search, use the SDK/API docs.