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.
<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".
// 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"); // voidreadDir(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 —
writeFilealways 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.keepmaterialises an otherwise-empty directory. - Rename / move a file — read it, write it under the new name or folder, then delete the original.
// 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.
// 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.
// 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.
// 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.
// 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.
// window.vos.app.getInfo() → { name, id, closeBehavior }
const { name, id, closeBehavior } = await window.vos.app.getInfo();Permissions are user-gated
Ship it