Agxos apps — coded (sandboxed bundle)
A coded Agxos app is a contributes.agxos.apps[] entry with "template": "app" plus an entry bundle path. Unlike the no-code templates (chat/note/terminal/list, which are driven entirely by the host's GenericTemplatedApp over the intent bus — no app code at all), a coded app ships a real plain-JS ES module that the host loads and runs isolated inside a child <iframe sandbox="allow-scripts"> — deliberately without allow-same-origin, so the frame gets a null/opaque origin that cannot reach the host DOM, tokens, or any other app's storage (glixo-os/src/apps/SandboxedAppHost.tsx:11-23). The only channel out is a whitelisted MessageChannel, surfaced inside the sandbox as a single global, window.agxos.
Use a coded app when you need a real, stateful UI (a mini text editor, a dashboard, a file browser) rather than a simple log/chat/list view a no-code template already covers.
The end-to-end teaching sample is glixo-extensions-docs/samples/apps/agxos-coded-app. It ships a real dist/app.js, lifecycle process, persistence, notifications, window controls, and tray intent handling. Teaching samples are not marketplace products.
App Creator uses these docs as mandatory context
When App Creator edits or generates a coded Agxos app, it must read the versioned App Creator docs
bundle before changing files: https://extend.glixo.io/artifacts/docs/v0/index.json. That bundle
contains this coded-app guide, the template-app guide, the App Creator workflow guide, the manifest
schema/reference, and compact provider instructions. If the deployed bundle is unavailable, it must
fall back to the checked-in docs source and manifest schema. Treat undocumented request names,
capability names, SDK packages, manifest fields, or build steps as unsupported until they are added
here and to the schema/source that enforces them.
1. The coded app itself (template: "app" + entry)
What it is / when to use it. The base building block for every type below. template: "app" tells the host "this is a real bundle, not a no-code layout"; entry is the manifest-relative path to the ES module the sandbox import()s after the handshake completes.
Manifest fragment (illustrative shape; compare the complete Sticky Counter Board manifest under glixo-extensions-docs/samples/apps/agxos-coded-app):
"contributes": {
"agxos": {
"apps": [
{
"id": "oscontrol.demo",
"name": "OS Control Demo",
"icon": "OC",
"accent": "#34C759",
"template": "app",
"entry": "dist/app.js",
"width": 520,
"height": 420,
"actions": [
{ "id": "ping", "description": "Tray menu intent — opens this app from the manifest tray menu." }
]
}
]
}
}
The top-level manifest also needs a unique extension id, a real lifecycle entry when one is declared, and every capability the coded app calls. requires must list capability.glixo.agxos.apps.register plus one token per OS-control capability used by the app.
The code you write. The bundle is a plain ES module that self-mounts on import — no props, no framework required. App-Creator's own generated template (glixo-code/sources/agxos/userAppGenerator.ts:294-310) shows the real shape:
import { mount, stack, title, text, button } from '@glixo/ui';
let count = 0;
const counter = text('Clicked 0');
mount(stack([
title('My App'),
text('Edit app.js with @glixo/ui components; reach host capabilities via the global window.agxos.', { muted: true }),
button('Click me', () => {
counter.textContent = 'Clicked ' + (++count);
void window.agxos.storage.set('count', count);
}, { variant: 'primary' }),
counter,
]));
@glixo/ui resolves via an import map the host injects (SandboxedAppHost.tsx:87-90) to a runtime served once at /v1/agxos/runtime/ui.js — no bundler needed. The manifest's agxos.apps[].entry declares the bundle path ("dist/app.js"), which the host resolves to GET /v1/extensions/{id}/assets/dist/app.js. The publisher rejects an artifact that does not actually contain this declared entry.
How the host consumes it. glixo-code/sources/agxos/AgxosIframeHost.tsx:2270-2278 — a template === 'app' contribution with a non-empty entry is flagged kind: 'app' and its entry is resolved through resolveExtensionAssetUrl(extensionId, entry) to GET /v1/extensions/{id}/assets/dist/app.js; glixo-os/src/apps/SandboxedAppHost.tsx:149-254 then mounts the sandboxed iframe, transfers the MessageChannel, and import()s that URL inside it.
Sample to study: glixo-extensions-docs/samples/apps/agxos-coded-app.
2. The injected sandbox global — window.agxos (data, storage, notify)
What it is / when to use it. Every coded app talks to the host exclusively through this one global, installed by the host's bootstrap script before your entry module is imported (SandboxedAppHost.tsx:100-125). Use window.agxos.request(...) (or the typed sub-objects) for filesystem/terminal/proxy calls, and window.agxos.storage for small per-app key/value persistence (there is no separate "AgxosDataClient" available inside the sandbox — that class name is a host-side, first-party-app-only API; see the caveat below).
Manifest fragment — declare filesystem and OS-control grants only when your app actually uses them:
"capabilities": ["glixo.agxos.apps.register", "glixo.agxos.workspace.watch"],
"requires": [
"ui",
"capability.glixo.agxos.apps.register",
"capability.glixo.agxos.windows.control",
"capability.glixo.agxos.notifications.create",
"capability.glixo.agxos.tray.contribute",
"filesystem.workspace.read"
]
The code you write. The real global, verbatim from glixo-os/src/apps/SandboxedAppHost.tsx:100-125 (bootstrapHtml):
window.agxos = {
appId: "<app id>",
theme: { /* CSS-variable-keyed color tokens */ },
request(type, payload) { /* raw escape hatch, gated by SANDBOX_ALLOWED_REQUESTS */ },
storage: {
get(key) { /* -> app.storage.get */ },
set(key, value) { /* -> app.storage.set */ },
remove(key) { /* -> app.storage.remove */ },
getDataVersion() { /* -> app.storage.getDataVersion, returns string|null */ },
getPendingUpgrade() { /* -> app.storage.getPendingUpgrade */ },
clearPendingUpgrade() { /* -> app.storage.clearPendingUpgrade */ },
},
notify(n) { /* -> app.notify: {title, body, kind, urgency, category, threadId, collapseId, deepLink, data, actions, target, native, channel} */ },
tray: { setEntries(entries) { /* -> app.tray.set */ } },
window: {
show() { /* -> app.window.control {command:'show'} */ },
hide() { /* -> app.window.control {command:'hide'} */ },
focus() { /* -> app.window.control {command:'bringToFront'} */ },
close() { /* -> app.window.control {command:'close'} */ },
setTitle(title) { /* -> app.window.setTitle */ },
},
onIntent(cb) { /* host intent bus */ },
onReady(cb) { /* fires once the MessagePort handshake completes */ },
};
Example usage (window.agxos.request for filesystem calls):
const listing = await window.agxos.request('fs.list', { pathRef: { path: 'src' } });
const status = await window.agxos.request('fs.gitStatus', {});
const seconds = await window.agxos.storage.get('refreshSeconds');
window.agxos.window.setTitle(`Workspace Watch — ${status?.branch ?? ''}`);
The sandbox API is intentionally narrower than the full Agxos host bridge. Treat public sandbox routes and host-private shell routes as separate API rings:
Public sandbox requests. These are forwarded today from a third-party coded
app iframe through SANDBOX_ALLOWED_REQUESTS:
- Workspace/files:
fs.list,fs.tree,fs.read,fs.search,fs.gitStatus,fs.watch,fs.unwatch,fs.write,fs.delete,fs.rename,media.url - Command/terminal:
terminal.run,command.run,terminal.create,terminal.open,terminal.input,terminal.resize,terminal.kill,terminal.close,pty.start,pty.write,pty.resize,pty.kill - Screen capture/input:
screen.sources,screen.start,screen.stop,screen.takeOver,screen.click,screen.key,screen.scroll,screen.navigate - Media devices:
media.devices,media.camera.start,media.camera.stop,media.mic.start,media.mic.stop,media.audio.record.start,media.audio.record.stop - Native desktop apps:
nativeApps.list,nativeApps.launch,native.apps.list,native.apps.launch - Agent sessions:
model.list,session.spawn,session.converse - Network:
proxy.fetch - App storage:
app.storage.get,app.storage.set,app.storage.remove,app.storage.getDataVersion,app.storage.getPendingUpgrade,app.storage.clearPendingUpgrade - OS app affordances:
app.notify,app.tray.set,app.window.control,app.window.setTitle
Requests outside the allow-list are rejected at the sandbox boundary with
Capability "<type>" is not available to sandboxed apps. Identity
(sourceAppId/sourceWindowId) and focused-window state for screen input are
host-asserted on every forwarded call, so the sandboxed frame cannot spoof a
higher grant or pretend to be focused.
Host-private requests. marketplace.*, app.creator.*, app.fork,
app.ide.import, settings.*, and agent.contributions.* are
owned by the shell, marketplace, settings, and App Creator itself. Do not use
them from third-party app code unless the platform is being changed in the same
patch.
Under the broker, authorization is layered: target-machine capability advertisement, per-extension grants for consent-installed apps, permission mode, host-asserted identity, audit events, and focused-window gates for screen input. That is separate from the sandbox allow-list, which defines what arbitrary third-party app JS may ask for in the first place.
Important — don't confuse two different "data client" names: AgxosDataClient (glixo-os/src/sdk/dataClient.ts:56-64, with .fs/.terminal/.proxy) is the typed client injected as a React prop into first-party/built-in apps only (AppProps { data, windowApi, windowId }, glixo-os/src/sdk/index.ts:14-21) — it is never available inside the sandboxed iframe. A separate published package, @glixo/agxos-app-sdk (AgxosAppProvider, useAgxosStorage, useAgxosData, etc.), exists and is typed but is not rendered anywhere in glixo-os or glixo-code today — treat it as aspirational, not something a coded app can rely on. For a coded app, window.agxos.* is the one real API.
How the host consumes it. glixo-os/src/apps/SandboxedAppHost.tsx:195-223 — the host's port.onmessage handler checks SANDBOX_ALLOWED_REQUESTS.has(req.type), then forwards via osBridge.sendRequest(req.type, { ...payload, sourceAppId, sourceWindowId, initiatedBy: 'user' }).
Sample to study: glixo-extensions-docs/samples/apps/agxos-coded-app for a complete coded UI and host bridge; filesystem calls use the same gated window.agxos.request path shown above.
3. Tray entries — manifest-declared (agxos.trayEntries) vs runtime (tray.setEntries)
What it is / when to use it. Two distinct tray mechanisms exist. contributes.agxos.trayEntries[] in the manifest is a static tray icon + right-click menu, always present once installed and granted. window.agxos.tray.setEntries(...) is a runtime call your app code makes to push/update its own tray entries dynamically (e.g. reflect live state). Use the manifest form for a fixed menu; use the runtime form when the tray content depends on app state.
Manifest fragment (static tray contribution; the Sticky Counter Board example contains the complete working form):
"agxos": {
"trayEntries": [
{
"id": "oscontrol.tray",
"title": "OS Control",
"appId": "oscontrol.demo",
"icon": "⚙",
"tooltip": "OS Control Demo",
"menu": [
{ "id": "open", "label": "Open demo", "action": "oscontrol.demo#ping" },
{ "id": "notify", "label": "Focus + ping intent", "action": "oscontrol.demo#ping" }
]
}
]
}
Requires holding the tray.contribute grant (manifest token "capability.glixo.agxos.tray.contribute").
The code you write. For the runtime form, from inside the sandbox:
await window.agxos.tray.setEntries([
{ id: 'status', label: `Refresh: ${seconds}s`, action: 'workspace.watch#refresh' },
]);
(maps to app.tray.set in SANDBOX_ALLOWED_REQUESTS.)
How the host consumes it. Manifest trayEntries are read by getInstalledAgxosTrayEntries in glixo-code/sources/extensions/store.ts:364-379, which only emits entries for extensions that hold the tray.contribute grant (store.ts:369, gated via extensionHoldsCapability(state, manifest.id, OS_CONTROL_CAPABILITIES.trayContribute)) — an ungranted extension's trayEntries are silently dropped, never shown.
Sample to study: glixo-extensions-docs/samples/apps/agxos-coded-app demonstrates a manifest tray menu and an observable intent in the running sandbox.
4. Window control — toolbar action + in-app window API
What it is / when to use it. Two halves: a manifest contributes.actions[] entry wired to the built-in command glixo.agxos.windowAction lets you add a toolbar button that shows/hides/focuses/closes your app's window (optionally firing a notification); inside the sandbox, window.agxos.window.* lets the app drive its own window directly (e.g. a close button in the app UI).
Manifest fragment (illustrative toolbar action):
"actions": [
{
"id": "com.example.my-app.open",
"slot": "agxos.toolbar",
"title": "OS Control Demo",
"icon": "options-outline",
"command": "glixo.agxos.windowAction",
"priority": 35,
"input": {
"appId": "oscontrol.demo",
"command": "show",
"notify": {
"title": "OS Control Demo",
"body": "Use the buttons to exercise notify, tray, and window APIs.",
"kind": "info"
}
}
}
]
input.command must be one of show | hide | bringToFront | close (glixo-code/sources/extensions/agxosWindowAction.ts:19-21, windowCommand(); anything else falls back to show).
The code you write. Inside the sandbox, to close or retitle the app's own window:
window.agxos.window.setTitle('Workspace Watch — main');
window.agxos.window.close();
How the host consumes it. glixo.agxos.windowAction is registered in glixo-code/sources/extensions/agxosWindowAction.ts:45-56 — it reads context.input.appId/.command/.notify, then calls the host's services.agxos.controlExtensionAppWindow(extensionId, appId, command) and, if notify is present, services.agxos.notifyExtensionApp(extensionId, appId, notification). Requires the "capability.glixo.agxos.windows.control" grant (requires: [...] in the manifest).
Sample to study: glixo-extensions-docs/samples/apps/agxos-coded-app uses the toolbar action plus the in-app window API.
5. File associations (fileAssociations)
What it is / when to use it. fileAssociations declares which workspace file types your coded Agxos app can open from the Glixo Code IDE. The right-rail Changes and Files lists use this field for Open with and for per-file default handlers. The built-in Code editor remains the fallback for every file; third-party Agxos apps only appear for extensions they explicitly declare.
Manifest shape (schema: glixo-platform/catalog/schema/glixo.module.schema.json; SDK type: AgxosAppFileAssociation / ExtensionAgxosAppFileAssociationContribution):
"contributes": {
"agxos": {
"apps": [{
"id": "markdown.preview",
"name": "Markdown Preview",
"template": "app",
"entry": "dist/app.js",
"display": { "surfaces": ["agxos", "mainPanel"], "prefer": "mainPanel" },
"fileAssociations": [
{
"extensions": ["md", "mdx"],
"mimeTypes": ["text/markdown"],
"role": "default"
}
]
}]
}
}
Rules:
extensionsis required and uses bare extension names ("md", not".md").mimeTypesis optional metadata for hosts that know the MIME type.role: "default"means the app may be saved as the default handler for matching files.role: "editor"or"viewer"means the app can appear in Open with, but should not be offered as the saved default.
When the user opens a file with your app, Glixo Code launches the app in its declared/preferred IDE placement and passes launch args equivalent to:
{ "action": "handleFile", "file": "D:\\project\\README.md" }
Inside the sandbox, read the host-provided launch args / intent payload and load the file through granted APIs such as window.agxos.request("fs.read", { path }). If your app opens workspace files, request the matching capability grant, usually filesystem.workspace.read.
Sample to study: App Creator generated image-viewer apps now emit fileAssociations for png, jpg, jpeg, gif, webp, and svg; use that generated manifest as a local reference until a community catalog sample lands.
6. The display/context surface model (display.surfaces, context)
What it is / when to use it. display declares which surface(s) your app may render into — its own Agxos window (agxos), the Glixo Code IDE main content area (mainPanel), or the IDE side rail (sidebar) — plus an optional preferred one. context gates when the app is even offered: machine (always), project (a project must be selected), session (a session must be running), or none (reserved, unused). If you omit display, the app defaults to its Agxos window only; if you omit context, it defaults to machine.
Manifest fragment (shape, from AgxosDisplay/AgxosAppContext in glixo-sdk/packages/nodejs/src/agxosAppSpec.ts:24-46, schema-validated at glixo-platform/catalog/schema/glixo.module.schema.json:432-433 via $defs/uiDisplay / $defs/uiContext):
"app": {
"id": "my-dashboard",
"name": "My Dashboard",
"template": "app",
"entry": "app.js",
"display": { "surfaces": ["agxos", "mainPanel"], "prefer": "mainPanel" },
"context": "project"
}
The code you write. No app-facing code changes — display/context are pure manifest declarations read by the host resolver, not props your bundle consumes.
How the host consumes it — current-state caveat. resolveAgxosAppPlacements()/displayToPlacements()/resolveAgxosAppContext() (glixo-sdk/packages/nodejs/src/agxosAppSpec.ts) are implemented, schema-validated, and unit-tested. Glixo Code consumes display.surfaces/preferredPlacement for IDE app launching (mainPanel/sidebar) and file-association routing. context is still a forward contract for offer-time gating; do not rely on context hiding an app yet.
Sample to study: none of the three set display/context explicitly (all default to agxos/machine) — for the type definitions, read glixo-sdk/packages/nodejs/src/agxosAppSpec.ts:24-46 directly.