From 043f5e8616f54208b0e590d787f5a1f1f24d4d29 Mon Sep 17 00:00:00 2001 From: Magaav Date: Wed, 29 Apr 2026 13:38:58 +0000 Subject: [PATCH 1/2] feature(customware): add customware bundle interface --- AGENTS.md | 3 +- README.md | 8 + app/AGENTS.md | 9 +- app/L0/_all/mod/_core/documentation/AGENTS.md | 2 +- .../docs/app/customware-bundles.md | 124 ++++++++++ .../docs/app/modules-and-extensions.md | 27 ++- .../docs/app/runtime-and-layers.md | 1 + .../docs/server/api/modules-and-runtime.md | 5 + .../server/customware-layers-and-paths.md | 14 ++ .../ext/skills/documentation/SKILL.md | 2 + app/L0/_all/mod/_core/framework/AGENTS.md | 17 +- .../_all/mod/_core/framework/js/api-client.js | 112 +++++++++ app/L0/_all/mod/_core/framework/js/bundles.js | 150 ++++++++++++ .../_all/mod/_core/framework/js/extensions.js | 18 +- app/L0/_all/mod/_core/framework/js/runtime.js | 4 + .../skillset/ext/skills/development/AGENTS.md | 4 +- .../skillset/ext/skills/development/SKILL.md | 4 + .../development/app-files-apis/SKILL.md | 1 + .../development/backend-reference/SKILL.md | 3 +- .../extensions-components/SKILL.md | 12 +- .../development/frontend-runtime/SKILL.md | 7 +- .../development/layers-ownership/SKILL.md | 1 + app/space-runtime.d.ts | 76 ++++++ server/AGENTS.md | 5 +- server/api/AGENTS.md | 3 + server/api/bundle_info.js | 67 ++++++ server/api/bundle_list.js | 35 +++ server/lib/customware/AGENTS.md | 9 + server/lib/customware/bundles.js | 146 ++++++++++++ server/lib/customware/module_manage.js | 151 ++++++++---- tests/AGENTS.md | 4 + tests/customware_bundle_test.mjs | 219 ++++++++++++++++++ tests/fixtures/AGENTS.md | 24 ++ .../customware_bundle_example/AGENTS.md | 25 ++ .../customware_bundle_example/README.md | 10 + .../space.bundle.yaml | 18 ++ 36 files changed, 1250 insertions(+), 70 deletions(-) create mode 100644 app/L0/_all/mod/_core/documentation/docs/app/customware-bundles.md create mode 100644 app/L0/_all/mod/_core/framework/js/bundles.js create mode 100644 server/api/bundle_info.js create mode 100644 server/api/bundle_list.js create mode 100644 server/lib/customware/bundles.js create mode 100644 tests/customware_bundle_test.mjs create mode 100644 tests/fixtures/AGENTS.md create mode 100644 tests/fixtures/customware_bundle_example/AGENTS.md create mode 100644 tests/fixtures/customware_bundle_example/README.md create mode 100644 tests/fixtures/customware_bundle_example/space.bundle.yaml diff --git a/AGENTS.md b/AGENTS.md index 9f334057..fb7a9e26 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -178,7 +178,7 @@ Top-level structure: - `commands/`: CLI command modules such as `serve`, `help`, `get`, `set`, `version`, and `update` - `app/`: browser runtime, layered customware model, shared frontend modules, and browser test surfaces - `server/`: thin local infrastructure runtime, with page shells, request routing, API hosting, fetch proxying, file-watch indexes, auth/session infrastructure, and Git support code -- `tests/`: repo-level verification harnesses, prepared evaluation fixtures, and saved result artifacts +- `tests/`: repo-level verification harnesses, prepared evaluation fixtures such as `tests/fixtures/`, and saved result artifacts - `packaging/`: optional Electron host and packaging scripts; native hosts should stay thin Project concepts: @@ -188,6 +188,7 @@ Project concepts: - browser modules are namespaced as `mod///...` - frontend extensibility is a core runtime primitive; the framework installs `space.extend` first and the browser runtime grows by loading modules and extension points deterministically - the layered browser model is `app/L0` firmware, `app/L1` group customware, and `app/L2` user customware +- customware bundles are ordinary installed `L1` or `L2` modules that include a `space.bundle.yaml` manifest; the server discovers them through the same module index and permission model, and the browser uses `space.bundles` for metadata reads, removable action handlers, and external bridge-state sync points - `app/L1` and `app/L2` are the logical writable layers; on disk they default to `app/L1` and `app/L2`, but when `CUSTOMWARE_PATH` is set the backend stores them under `CUSTOMWARE_PATH/L1` and `CUSTOMWARE_PATH/L2` - writable layer content is transient runtime state and is gitignored when it lives under the repo; do not treat it as durable repo-owned sample content - `L2//user.yaml` stores user metadata such as `full_name`; auth state lives under `L2//meta/` diff --git a/README.md b/README.md index ec541b69..a3490653 100644 --- a/README.md +++ b/README.md @@ -134,6 +134,14 @@ node space supervise HOST=0.0.0.0 PORT=3000 # zero downtime auto-update Run `node space help` to see the full command surface and built-in help for each from [`commands/params.yaml`](./commands/params.yaml). +## Customware bundles + +Reusable customizations can ship as customware bundles: ordinary `L1` or `L2` modules with a root `space.bundle.yaml` manifest. + +Bundles stay inside Space Agent's existing browser-first model. They add UI through `ext/html`, behavior through `ext/js` and `space.extend(...)`, skills through `ext/skills`, theme or landing-background CSS through `_core/framework/theme/end`, and removable browser actions through `space.bundles.actions`. The manifest exposes metadata such as id, version, capabilities, config defaults, compatibility, extension points, and action descriptions. + +Install, update, or remove a bundle the same way you manage any customware module under `L1//mod///` or `L2//mod///`. Direct runtime injection is discouraged because it depends on private implementation details; when a customization needs a new stable seam, add or propose that seam instead. + ## AI-driven development and documentation Space Agent is developed by AI agents, including its documentation. diff --git a/app/AGENTS.md b/app/AGENTS.md index 2930cdfe..971db6cb 100644 --- a/app/AGENTS.md +++ b/app/AGENTS.md @@ -115,7 +115,7 @@ Current browser entry surfaces are served from `server/pages/`: Current major first-party modules under `app/L0/_all/mod/_core/`: -- `framework/`: frontend bootstrap, runtime primitives, component loader, extension system, shared utilities +- `framework/`: frontend bootstrap, runtime primitives, component loader, extension system, customware-bundle runtime helpers, shared utilities - `agent-chat/`: headless shared chat helpers that belong to first-party agent features rather than the platform layer, currently including repeated-assistant-message evaluation helpers and hook implementations reused by overlay and admin chat - `login_hooks/`: headless authenticated-bootstrap lifecycle hooks for first-login and same-origin `/login` arrival events, with a client-owned `~/meta/login_hooks.json` marker and feature-owned onboarding hooks such as the spaces module's first-login `Big Bang` onboarding-space bootstrap - `visual/`: shared visual language, canvas, chrome, buttons, dialog helpers, conversation rendering primitives, and reusable authenticated-app imagery under `visual/res/`; pre-auth `/login` and `/enter` keep their mirrored astronaut asset under `server/pages/res/` @@ -175,7 +175,7 @@ Current boot flow: 1. A page shell in `server/pages/` loads shared framework CSS and `/mod/_core/framework/js/initFw.js`. 2. The shell exposes one top-level HTML anchor in the body. -3. `initFw.js` installs the runtime, injects the framework-owned `_core/framework/head/end` HTML seam into `document.head`, runs the extensible framework bootstrap step in `_core/framework/js/initializer.js`, including framework-wide navigation interception for same-origin `_blank` app opens and best-effort current-tab cross-origin escape blocking, then installs Alpine helpers and shared bootstrap behavior. +3. `initFw.js` installs the runtime, injects the framework-owned `_core/framework/theme/end` and `_core/framework/head/end` HTML seams into `document.head`, runs the extensible framework bootstrap step in `_core/framework/js/initializer.js`, including framework-wide navigation interception for same-origin `_blank` app opens and best-effort current-tab cross-origin escape blocking, then installs Alpine helpers and shared bootstrap behavior. 4. The first mounted module owns the next seam and exposes more anchors or wrapped functions. 5. Other modules compose into those explicit seams instead of patching private internals. @@ -203,7 +203,7 @@ HTML extension anchors: - HTML callers name only the seam; the runtime loads `` tags from the module's `ext/html/` tree automatically - thin extension files should usually mount the real component from the module root instead of containing the entire feature directly - root page anchors such as `body/start` and `page/admin/body/start` are fixed shell contracts; module-owned anchors should be named after the owning module path, for example `_core/router/shell_start` -- `_core/framework` also creates `_core/framework/head/end` in `document.head` during bootstrap so layers can add declarative head-side tags or inline bootstraps without editing page shells or adding a JS hook +- `_core/framework` also creates `_core/framework/theme/end` and `_core/framework/head/end` in `document.head` during bootstrap so layers can add declarative theme CSS, head-side tags, or inline bootstraps without editing page shells or adding a JS hook - runtime discovery watches the whole document tree, so `x-extension` and `x-component` insertions under `head` are supported the same way as body-mounted seams - `_core/onscreen_menu` owns a reserved centered header bar from `_core/router/shell_start`; it keeps `_core/onscreen_menu/bar_start` on the left and `_core/onscreen_menu/bar_end` on the right for shell-level controls, allows route-owned `x-inject` content to target the existing left-side `[id="_core/onscreen_menu/bar_start"]` container when a feature wants ephemeral controls that disappear with the route but the shell seam may mount later, keeps a persistent Home button that routes to the empty route `#/` so the router default decides the home screen, and exposes `_core/onscreen_menu/items` as the dropdown action seam for non-Home feature buttons, whose modules contribute thin button adapters with numeric `data-order` values while `_core/onscreen_menu` sorts them automatically and keeps only the auth exit action local after the seam; `_core/dashboard` is the current first-party example of a route-owned wrapper that injects into `bar_start` and then exposes ordered dashboard-local seams for dashboard-only topbar actions - `_core/web_browsing` is the current first-party example of a module that uses both `_core/onscreen_menu/items` and `page/router/overlay/end`: it contributes a Browser menu action at `data-order="250"`, opens a new floating browser window each time the action is used, keeps each browser surface on a unique internal `browser-N` id while the public `space.browser` runtime surface exposes numeric ids such as `1`, lets widgets or other screen DOM create registered surfaces with plain `` markup, keeps that public runtime surface top-level and id-based only instead of exposing per-window handles, auto-settles open or state or navigation or inspection helpers internally so they can return fresh browser state snapshots without a separate `sync(...)` step while ref-targeted actions return `{ action, state }` with visible-effect flags such as `reacted`, `noObservedEffect`, `validationTextAdded`, or `domChanged`, requires navigation-capable helpers to wait for an observed browser-side navigation or loading transition before they accept a new snapshot, blocks stale old-guest bridge reads during that handoff, exposes `space.browser.setLogLevel(...)` as the runtime-side override for browser diagnostics while leaving the default app browser log level at `error`, persists each open browser window's URL, geometry, minimized state, and stacking data in browser-local storage so reloads can reopen the same window set, reuses the same viewport-fit pass for both live resize and restore so reopened windows clamp back onto the current screen when the viewport changed, and raises a focused browser window to the top of the browser stack even when the click lands inside the iframe or desktop webview page content. It spawns new windows below the fixed shell bar near the left edge of the centered router-stage column at approximately that column width while still allowing later drag movement all the way to the viewport top edge, renders popup windows through `` with Back, Forward, Reload, and address controls while inline surfaces may omit controls or opt in with the same attribute, keeps its page-side browsing bridge self-contained through `/mod/_core/web_browsing/browser-frame-inject.js` plus matching host helpers, and may also extend the onscreen-agent transient sections seam with a brief `currently open web browsers` status block plus prompt-time `last interacted web browser` content when the remembered interacted browser surface is still open; browser sessions render an iframe fallback inside that element, packaged desktop runs now prefer a DOM `` inside that same element so modal, widget, and other renderer-owned placement clipping stays aligned with the shell, the desktop host installs a tiny document-start browser guest kernel into the top page and all iframe descendants so shadow roots and nested documents stay crawlable later, the renderer then injects the bridge bootstrap plus an extensible guest runtime script list built through `space.extend(...)`, desktop `_blank` and `window.open(...)` requests are routed back into new in-app browser modals instead of separate OS windows, and the injected runtime must stay transport-agnostic so later browser-extension injection can reuse the same page-side request handlers while readable `content(...)` output stays in the lean typed-ref form like `[disabled muted button 18] Continue`, `[checked checkbox 7] Email updates`, `[error button 9] Delete`, or `[input text 30] Search placeholder=Hledat value=Ethereum` and omits obviously non-visible DOM or CSS-hidden content such as `hidden`, `aria-hidden`, `display:none`, `visibility:hidden|collapse`, `content-visibility:hidden`, or `opacity:0` subtrees @@ -215,7 +215,7 @@ JS extension hooks: - wrapped functions become async and should be awaited by callers - JS hook files live at `mod///ext/js//*.js` or `*.mjs` - JS callers name only the seam; the runtime loads hooks from the module's `ext/js/` tree automatically -- framework-backed pages expose `_core/framework/initializer.js/initialize`; use `_core/framework/head/end` when the work can stay as declarative head HTML or inline bootstrap code, and prefer the initializer `/end` hook when the setup must stay imperative instead of editing page shells +- framework-backed pages expose `_core/framework/initializer.js/initialize`; use `_core/framework/theme/end` for declarative theme or background CSS, use `_core/framework/head/end` when the work can stay as other declarative head HTML or inline bootstrap code, and prefer the initializer `/end` hook when the setup must stay imperative instead of editing page shells - framework-backed pages centrally grant `/enter` tab access to same-origin `/` and `/admin` windows opened through normal `target="_blank"` link clicks or `window.open(..., "_blank")`, and they also intercept same-tab cross-origin `http(s)` escapes through the Navigation API `navigate` event when available plus fallback anchor or `window.open(..., "_self")` and `location`-based hooks so web runtime can move those requests into a new tab while packaged desktop runtime blocks them before the Electron main-window origin guard needs to recover; location-bar navigations and manual browser opens such as context-menu, middle-click, or modifier-key opens are not intercepted and still route through `/enter` or the host guard - use `callJsExtensions("name", data)` only when the seam is an explicit event rather than a function lifecycle - `_core/login_hooks` is a first-party example of an explicit event seam: it runs from `_core/framework/initializer.js/initialize/end`, checks `~/meta/login_hooks.json`, then dispatches `_core/login_hooks/first_login` once per user and `_core/login_hooks/any_login` when the authenticated shell was reached from `/login`; `_core/spaces` currently uses that first-login seam to copy or reuse the module-owned `Big Bang` onboarding space and rewrite the main-shell default route before the router falls back to `#/dashboard` @@ -278,6 +278,7 @@ Runtime guidance: - framework-backed pages that boot through `/mod/_core/framework/js/initFw.js` already initialize the runtime before feature modules mount - `globalThis.space` is scoped to the current window or iframe only; do not publish it into other browsing contexts - use `space.api` for authenticated backend calls +- use `space.bundles` for installed customware-bundle metadata, removable browser-side action handlers, and bridge-state sync points owned by external integrations instead of patching framework or feature internals - prefer the shared `space.api` helpers over feature-local request batching; same-tick `fileRead(...)` calls coalesce automatically, preserve per-call missing-file behavior by retrying individually when a combined batch fails, and identical in-flight file, identity, and extension-load requests are deduped by the runtime - use `space.api.folderDownloadUrl(...)` when a folder download should stay as a browser attachment instead of fetching the archive blob into frontend memory - first-party framework, shell, skill-helper, and bundled demo assets required for normal app use must be local `/mod/...` files, server page assets, or inline code; do not load required scripts, styles, fonts, images, or other framework assets from CDNs diff --git a/app/L0/_all/mod/_core/documentation/AGENTS.md b/app/L0/_all/mod/_core/documentation/AGENTS.md index d8bbbdf6..db70b435 100644 --- a/app/L0/_all/mod/_core/documentation/AGENTS.md +++ b/app/L0/_all/mod/_core/documentation/AGENTS.md @@ -16,7 +16,7 @@ Current structure: - `ext/skills/documentation/SKILL.md`: the required entry skill that lists every documentation page by relative path, name, and short description and tells the agent how to read focused docs - `docs/architecture/`: repo-wide runtime, desktop-host packaging, and documentation-system orientation -- `docs/app/`: frontend runtime, admin-agent, browser-side WebLLM and Hugging Face runtime docs, extension, and spaces documentation +- `docs/app/`: frontend runtime, customware bundle, admin-agent, browser-side WebLLM and Hugging Face runtime docs, extension, and spaces documentation - `docs/agent/`: onscreen-agent runtime, prompt, execution, and skill-system documentation - `docs/server/`: router, page, API, auth, layered-filesystem, and writable-layer history documentation, including auth-preserving rollback rules - `docs/cli/`: CLI command and runtime-parameter documentation diff --git a/app/L0/_all/mod/_core/documentation/docs/app/customware-bundles.md b/app/L0/_all/mod/_core/documentation/docs/app/customware-bundles.md new file mode 100644 index 00000000..38f1849c --- /dev/null +++ b/app/L0/_all/mod/_core/documentation/docs/app/customware-bundles.md @@ -0,0 +1,124 @@ +# Customware Bundles + +This doc covers the Customware Bundle Interface for reusable downstream modules. + +## Primary Sources + +- `AGENTS.md` +- `app/AGENTS.md` +- `app/L0/_all/mod/_core/framework/AGENTS.md` +- `server/lib/customware/AGENTS.md` +- `server/api/AGENTS.md` + +## What A Bundle Is + +A customware bundle is an ordinary installed module with one extra manifest: + +```txt +L1//mod///space.bundle.yaml +L2//mod///space.bundle.yaml +``` + +The bundle still uses the normal Space Agent composition model: + +- UI through `ext/html/...` +- behavior through `ext/js/...` and `space.extend(...)` +- skills through `ext/skills/*/SKILL.md` +- browser guest behavior through documented web-browsing guest-runtime seams +- visual/theme changes through `_core/framework/theme/end` +- head-side setup through `_core/framework/head/end` + +The manifest advertises the package. It does not grant permission to monkey patch private runtime internals. + +## Manifest Shape + +The root file is `space.bundle.yaml`. + +The checked-in minimal reference fixture lives at `tests/fixtures/customware_bundle_example/`. + +Example: + +```yaml +id: acme/fleet +name: Fleet Control +version: 1.0.0 +description: Team-owned fleet controls for Space Agent +capabilities: [theme, actions, browser-runtime] +extension_points: + - _core/framework/theme/end + - _core/web_browsing/browser-guest-runtime.js/buildBrowserGuestRuntimeScriptPaths/end +compatibility: + space_agent: ">=0.65" +config_defaults: + accent: teal +actions: + - id: fleet.open + title: Open fleet + capability: actions + description: Open the fleet dashboard +``` + +Stable fields: + +- `id`: lowercase bundle id, usually `/` +- `name`, `version`, and `description`: display metadata +- `capabilities`: high-level advertised capabilities +- `extension_points`: documented seams the bundle expects to use +- `compatibility`: version or runtime compatibility notes +- `config_defaults`: optional structured defaults owned by the bundle +- `actions`: declarative action metadata + +## Runtime Surface + +Installed bundle metadata is available through: + +- `space.api.bundleList(options)` +- `space.api.bundleInfo(pathOrOptions)` +- `space.bundles.list(options)` +- `space.bundles.info(pathOrOptions)` + +Executable browser actions are registered at runtime: + +```js +const dispose = space.bundles.actions.register({ + bundleId: "acme/fleet", + id: "fleet.open", + title: "Open fleet", + async run(payload) { + return payload; + } +}); +``` + +When the module unmounts or disables the feature, call `dispose()` or `space.bundles.actions.unregister("fleet.open")`. + +External integrations can publish bridge state through: + +```js +space.bundles.bridge.registerSync("acme/fleet", async (payload) => payload); +await space.bundles.bridge.syncState("acme/fleet", { online: true }); +``` + +## Install, Update, Remove + +Bundles are installed, updated, and removed as modules. + +Use the normal module locations: + +- `L1//mod///` for group-level customware +- `L2//mod///` for user-level customware + +The existing module APIs and admin module views still own installation and removal. Removing the module removes its bundle metadata, extension files, skills, and action handlers once the owning page reloads or unregisters them. + +## Why Not Runtime Injection + +Direct runtime injection is fragile because it depends on private file names, timing, or implementation details. Bundles should instead turn desired changes into stable extension contracts: + +- add a missing HTML seam +- add a missing JS hook through `space.extend(...)` +- add metadata under `ext/...` +- add a browser-side action through `space.bundles.actions` +- add a bridge sync handler through `space.bundles.bridge` +- propose the missing seam upstream when no stable contract exists + +That keeps downstream customizations rebase-friendly while preserving Space Agent's browser-first customware model. diff --git a/app/L0/_all/mod/_core/documentation/docs/app/modules-and-extensions.md b/app/L0/_all/mod/_core/documentation/docs/app/modules-and-extensions.md index 80202aaa..f202f195 100644 --- a/app/L0/_all/mod/_core/documentation/docs/app/modules-and-extensions.md +++ b/app/L0/_all/mod/_core/documentation/docs/app/modules-and-extensions.md @@ -75,7 +75,7 @@ Resolution rules: - the caller names only the seam - matching files live under `mod///ext/html/some/path/*.html` - extension files should stay thin and normally mount the real component or view -- `_core/framework` also injects `_core/framework/head/end` into `document.head` during bootstrap so layers can add declarative head-side tags or inline bootstraps without editing page shells +- `_core/framework` also injects `_core/framework/theme/end` and `_core/framework/head/end` into `document.head` during bootstrap so layers can add declarative theme CSS, head-side tags, or inline bootstraps without editing page shells - `_core/framework` also registers `x-inject="selector"` during bootstrap; it mirrors Alpine `x-teleport` for `