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:

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:

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.