Build an app

VibeOS SDK

Apps talk to the host through a single global object, window.vos. Every method returns a Promise — filesystem access is directory-scoped by the permissions you declare in your manifest.

Overview

When a .vibe app is opened in VibeOS, its HTML runs in a sandboxed iframe with window.vos injected. All methods return Promises, so you call them with await like any normal async API. Once the SDK is ready, VibeOS dispatches a vos-ready event on window — wait for it before your first call.

html
<script>
  // The SDK is injected as a global `window.vos` object.
  // Wait for the "vos-ready" event before calling any window.vos.* method.
  window.addEventListener("vos-ready", async () => {
    const info = await window.vos.app.getInfo();
    console.log("Running inside VibeOS as", info.name);
  });
</script>

Filesystem — window.vos.fs

Read and write files inside the user's Home directory. Paths are Home-relative strings like "home", "home/Documents" or "home/Documents/note.md".

javascript
// All paths are Home-relative strings: "home", "home/Documents",
// "home/Documents/note.md". Access is gated by the directories you declare
// in your manifest's permissions.

// Requires permissions.read covering the path
const entries = await window.vos.fs.readDir("home");
// → [{ name: "note.md", type: "text" }, { name: "Documents", type: "folder" }]

const text = await window.vos.fs.readFile("home/Documents", "note.md"); // string

// Requires permissions.write covering the path
await window.vos.fs.writeFile("home/Documents", "note.md", "# Hello VibeOS"); // void
await window.vos.fs.deleteFile("home/Documents", "note.md");                   // void

readDir(path) returns an array of { name, type } entries — type is "folder" for subdirectories, otherwise the file's type (e.g. "text") — and requires permissions.read covering path. readFile(path, name) returns the contents as a string and requires permissions.read. writeFile(path, name, content) and deleteFile(path, name) resolve to void and require permissions.write.

Directory-scoping rules

An empty array (e.g. "read": []) grants access only to the app's private sandbox at home/.apps/<appId>/.

Reserved paths are always blocked: .system, .Trash, and other apps' .apps/<id> directories.

Every call is re-checked host-side against the installed grant, so an app can never widen its own scope at runtime. See the .vibe apps page for the manifest format.

Updating, creating folders, moving & renaming

The filesystem API is intentionally small — readDir, readFile, writeFile and deleteFile. Every other operation is composed from these four:

  • Update a file writeFile always overwrites, so writing to an existing name replaces its contents.
  • Create a folder — there is no mkdir. Writing a file into a new path auto-creates every missing parent folder, so a placeholder like .keep materialises an otherwise-empty directory.
  • Rename / move a file — read it, write it under the new name or folder, then delete the original.
javascript
// UPDATE a file — writeFile always overwrites, so "update" is just a re-write.
const current = await window.vos.fs.readFile("home/Documents", "note.md");
await window.vos.fs.writeFile("home/Documents", "note.md", current + "\n\nNew line.");

// CREATE a directory — there is no mkdir(). Writing a file into a new path
// auto-creates every missing parent folder, so drop a placeholder to
// materialise an (otherwise empty) folder.
await window.vos.fs.writeFile("home/Documents/Archive", ".keep", "");
// "Archive" now appears in readDir("home/Documents")

// RENAME a file — copy to the new name, then delete the old one.
async function renameFile(dir, from, to) {
  const data = await window.vos.fs.readFile(dir, from);
  await window.vos.fs.writeFile(dir, to, data ?? "");
  await window.vos.fs.deleteFile(dir, from);
}
await renameFile("home/Documents", "note.md", "note-final.md");

// MOVE a file to another folder — same pattern across directories
// (the destination folder is auto-created if it doesn't exist).
async function moveFile(fromDir, name, toDir) {
  const data = await window.vos.fs.readFile(fromDir, name);
  await window.vos.fs.writeFile(toDir, name, data ?? "");
  await window.vos.fs.deleteFile(fromDir, name);
}
await moveFile("home/Documents", "note-final.md", "home/Documents/Archive");

No native delete-folder or rename-folder

The SDK has no deleteDir / rename for directories. To remove a folder, deleteFile each file inside it (walk it with readDir); to rename a folder, move its files into a new folder one by one. Empty folders created explicitly may still be listed until the host prunes them.

AI — window.vos.ai

Use the built-in AI gateway for chat. Pass an array of { role, content } messages where role is "system", "user" or "assistant"; it resolves to the assistant's reply string. No API keys required — usage runs through VibeOS.

javascript
// window.vos.ai.chat(messages) → string
const reply = await window.vos.ai.chat([
  { role: "system", content: "You are a concise writing assistant." },
  { role: "user", content: "Summarise this note in one sentence." },
]);

AI requires the ai permission

window.vos.ai is gated by the "ai" capability. Declare "permissions": { "ai": true } in your manifest, otherwise the host rejects every window.vos.ai.chat call. The user approves AI access at install time alongside any filesystem scopes. See the manifest permissions reference.

Checking AI access — window.vos.ai.canUse

Before you show an AI option, call window.vos.ai.canUse("chat") to check whether the current user can actually use it. It resolves to { ok, code? } without sending a model request, and the result is cached for ~30s so it's cheap to call on load and on window focus. When ok is false, the code tells you exactly what the user needs to do — so you can show the right call to action instead of a generic error.

javascript
// window.vos.ai.canUse(feature) → { ok: boolean, code?: string }
// Use it for an upfront entitlement check before showing AI controls — it's
// cheap (result cached ~30s) and never sends a model request.
const access = await window.vos.ai.canUse("chat");
if (!access.ok) {
  // access.code tells you WHY, so you can show the right call to action:
  //   "auth_required"   → user is signed out         → prompt sign in
  //   "upgrade_required"→ plan doesn't include AI     → prompt upgrade
  //   "quota_exceeded"  → out of AI credits           → prompt top up
  //   "rate_limited"    → too many requests, slow down
  showBanner(access.code);
  disableAiOption();
}

ai.chat is the authoritative enforcement point: it re-checks access at call time and rejects with the same codes. Handle them and re-throw — never silently fall back to a local engine when the user explicitly chose AI, or the host's sign-in / upgrade / top-up modal won't fire.

javascript
// ai.chat is the authoritative check — it enforces access at call time and
// rejects with the same machine-readable codes when the user lacks access.
try {
  const reply = await window.vos.ai.chat(messages);
} catch (err) {
  switch (err.code) {
    case "auth_required":    /* prompt sign in */ break;
    case "upgrade_required": /* prompt upgrade */ break;
    case "quota_exceeded":   /* prompt top up  */ break;
    case "rate_limited":     /* back off + retry */ break;
    default:                 /* show generic AI error */ break;
  }
  // Do NOT silently fall back to a local engine when the user explicitly
  // chose AI — surface the error so the host's sign-in / upgrade / top-up
  // modal can fire.
  throw err;
}

Access codes

  • auth_required — the user is signed out. Prompt them to sign in.
  • upgrade_required — their plan doesn't include AI. Prompt an upgrade.
  • quota_exceeded — they're out of AI credits. Prompt a top up.
  • rate_limited — too many requests in a short window. Back off and retry.

Network — window.vos.net

Apps cannot reach the network directly: the host injects a Content-Security-Policy built from your permissions.network list, and fetch/XHR/WebSocket to any other origin is blocked by the browser. Use window.vos.net.fetch for outbound HTTPS calls — it is proxied through VibeOS, re-validates the URL against your declared prefixes, and bypasses CORS.

javascript
// window.vos.net.fetch(url, init?) → { status, statusText, headers, body | bodyBase64 }
// URL must match one of the prefixes declared in permissions.network.
const res = await window.vos.net.fetch(
  "https://api.example.com/v1/books?q=tolkien",
  { method: "GET", headers: { Accept: "application/json" } }
);

if (res.status >= 200 && res.status < 300) {
  const data = JSON.parse(res.body);          // text responses
  // const bytes = atob(res.bodyBase64);      // binary responses
}

Network requires the network permission

Every URL you pass to window.vos.net.fetch must be covered by one of the https:// prefixes in permissions.network. Requests to other origins are rejected by the host. Users approve the list at install time and can revoke individual origins from Web Apps → Permissions. See the manifest permissions reference.

App info — window.vos.app

Read metadata about the running app instance: its name, id and closeBehavior.

javascript
// window.vos.app.getInfo() → { name, id, closeBehavior }
const { name, id, closeBehavior } = await window.vos.app.getInfo();

Permissions are user-gated

There is no SDK surface for changing permissions, manifest, or install state. The only mutation path is the user-gated install flow.

Ship it

Once your app uses the SDK the way you want, move on to submission & review.