@aiworker/sdk 1.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/API.md +575 -0
- package/DOCUMENTATION.md +120 -0
- package/GUEST_SERVICES.md +166 -0
- package/LICENSE +21 -0
- package/README.md +249 -0
- package/dist/api-version.d.ts +6 -0
- package/dist/api-version.js +5 -0
- package/dist/contract.d.ts +504 -0
- package/dist/contract.js +256 -0
- package/dist/host-version.d.ts +16 -0
- package/dist/host-version.js +50 -0
- package/dist/host.d.ts +107 -0
- package/dist/host.js +639 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +8 -0
- package/dist/manifest.d.ts +303 -0
- package/dist/manifest.js +222 -0
- package/dist/parse.d.ts +293 -0
- package/dist/parse.js +398 -0
- package/dist/protocol.d.ts +1148 -0
- package/dist/protocol.js +473 -0
- package/dist/schemas.d.ts +4 -0
- package/dist/schemas.js +6 -0
- package/dist/scrollbar-style.d.ts +2 -0
- package/dist/scrollbar-style.js +32 -0
- package/dist/ui/badge.d.ts +10 -0
- package/dist/ui/badge.js +26 -0
- package/dist/ui/banner.d.ts +13 -0
- package/dist/ui/banner.js +48 -0
- package/dist/ui/button.d.ts +14 -0
- package/dist/ui/button.js +49 -0
- package/dist/ui/checkbox.d.ts +12 -0
- package/dist/ui/checkbox.js +45 -0
- package/dist/ui/dom.d.ts +15 -0
- package/dist/ui/dom.js +60 -0
- package/dist/ui/empty.d.ts +11 -0
- package/dist/ui/empty.js +44 -0
- package/dist/ui/field.d.ts +18 -0
- package/dist/ui/field.js +49 -0
- package/dist/ui/icons.d.ts +10 -0
- package/dist/ui/icons.js +23 -0
- package/dist/ui/index.d.ts +35 -0
- package/dist/ui/index.js +17 -0
- package/dist/ui/list.d.ts +26 -0
- package/dist/ui/list.js +90 -0
- package/dist/ui/menu.d.ts +20 -0
- package/dist/ui/menu.js +115 -0
- package/dist/ui/navigation.d.ts +18 -0
- package/dist/ui/navigation.js +38 -0
- package/dist/ui/option.d.ts +16 -0
- package/dist/ui/option.js +35 -0
- package/dist/ui/popup.d.ts +5 -0
- package/dist/ui/popup.js +43 -0
- package/dist/ui/progress.d.ts +11 -0
- package/dist/ui/progress.js +44 -0
- package/dist/ui/search.d.ts +11 -0
- package/dist/ui/search.js +62 -0
- package/dist/ui/select.d.ts +22 -0
- package/dist/ui/select.js +168 -0
- package/dist/ui/separator.d.ts +6 -0
- package/dist/ui/separator.js +26 -0
- package/dist/ui/spinner.d.ts +8 -0
- package/dist/ui/spinner.js +29 -0
- package/dist/ui/style.d.ts +1 -0
- package/dist/ui/style.js +202 -0
- package/dist/ui/tabs.d.ts +15 -0
- package/dist/ui/tabs.js +61 -0
- package/dist/ui/text.d.ts +23 -0
- package/dist/ui/text.js +89 -0
- package/dist/ui/theme.d.ts +22 -0
- package/dist/ui/theme.js +79 -0
- package/dist/workspace-schemas.d.ts +136 -0
- package/dist/workspace-schemas.js +44 -0
- package/dist/workspace.d.ts +109 -0
- package/dist/workspace.js +4 -0
- package/package.json +55 -0
- package/scripts/bundle-guest.ts +44 -0
package/DOCUMENTATION.md
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# AiWorker SDK
|
|
2
|
+
|
|
3
|
+
Public author pages live in `packages/docs/content/docs/sdk.mdx`, `sdk/host.mdx`, `sdk/ui.mdx`, and `sdk/example.mdx`; the user page is `extensions.mdx`. Each has a copy under every locale folder.
|
|
4
|
+
|
|
5
|
+
## Purpose
|
|
6
|
+
|
|
7
|
+
`packages/sdk` owns the guest contract. `@aiworker/sdk` is the guest-facing entry: types, limits, `connectHost`, and pure helpers, with no schema library, so a bundled panel stays a few kilobytes. `@aiworker/sdk/schemas` is the host-facing entry: the zod parse of an untrusted guest's manifest (`parseManifest`, `parseManifestJson`) and messages (`guestMessageSchema`, `hostMessageSchema`). Guest-only drawing lives under `@aiworker/sdk/ui`. Host and guest import from here. Do not copy these types into `packages/ui`.
|
|
8
|
+
|
|
9
|
+
## Entrypoints
|
|
10
|
+
|
|
11
|
+
- `src/index.ts`: public exports. Host and guest protocol.
|
|
12
|
+
- `src/parse.ts`: `package.json` / bare-block parse. Fails closed.
|
|
13
|
+
- `src/host-version.ts`: `engines.aiworker` parse and compare. Install uses this against `AIWORKER_VERSION`.
|
|
14
|
+
- `src/protocol.ts`: envelope parse for host and guest messages.
|
|
15
|
+
- `src/host.ts`: iframe client. Posts to `parent`. Ignores messages whose `source` is not `parent`. In-flight RPCs time out at `GUEST_REQUEST_TIMEOUT_MS` (20s) as `HOST_TIMEOUT`. `onDirectory` / `onSession` / `onSessionLifecycle` / `onItem` replay and stay current after later pushes, same as connection and settings. `onResolve` holds one handler for host `resolve` requests and answers each with `resolve-result` (the clamped chip, `null`, or the thrown error's message; no handler answers with an error). `setBadge` posts `badge` with the count clamped to 0..`GUEST_BADGE_MAX`.
|
|
16
|
+
- `scripts/bundle-guest.ts`: official IIFE build, exposed as the `openchamber-guest-bundle` bin. Run `bunx openchamber-guest-bundle panel/main.ts panel/main.js` from the guest folder; paths resolve against the current directory. Needs Bun. Any bundler with an IIFE target is an equivalent substitute.
|
|
17
|
+
- `dist/`: the published output (`bun run --cwd packages/sdk build`, `tsc -p tsconfig.build.json`). `package.json` `exports` point here so Node loads the SDK from `node_modules` (packaged Electron, `aiworker serve`). Inside the monorepo, Vite aliases and tsconfig `paths` resolve `@aiworker/sdk` to `src/` so builds and type-checks never depend on a stale `dist`. The server under Bun and Node uses `dist`; the root `postinstall` builds it. The package ships to npm from the release workflow with `bun publish`, right before `@aiworker/web`; `scripts/bump-version.mjs` bumps it with the app, so the SDK version always equals the AiWorker version it was released with.
|
|
18
|
+
- `GUEST_SERVICES.md`: local service contract (`contributes.service`, `serviceRequest`, grant, loopback).
|
|
19
|
+
- `src/ui/`: guest drawing kit. Building blocks only (`mountButton`, `mountTextField`, `mountSearchField`, `mountSelect`, `mountCheckbox`, `mountSwitch`, `mountTabs`, `mountBadge`, `mountList`, `mountEmpty`, `mountSpinner`, `mountBanner`, `mountSeparator`, `mountProgress`, `mountMenu`, `mountText`); the guest composes screens. See `src/ui/DOCUMENTATION.md`. Import `@aiworker/sdk/ui`. Do not import this from `packages/ui`. Kit changes follow package semver, not `apiVersion`.
|
|
20
|
+
|
|
21
|
+
## Invariants
|
|
22
|
+
|
|
23
|
+
- `scripts/bump-version.mjs` refreshes `bun.lock` after bumping package versions; include both in the release commit. Bun resolves `workspace:*` in published archives from lockfile versions, and a frozen install can succeed with stale versions. `scripts/bump-version.test.mjs` covers the bump, frozen install, and packed dependency versions together.
|
|
24
|
+
|
|
25
|
+
- Extension activity uses `useConfigStore.isConnected` plus live status records and directory `sessionStatusReady` authority. `useUIStore.eventStreamStatus` is an unmaintained diagnostic field, not a connection gate; its default `idle` must not hide known running or idle sessions as `unknown`.
|
|
26
|
+
|
|
27
|
+
- `contributes.page` is `true` or `{ entry, title? }`, requires `panel.entry`, and adds a user-opened full-screen page. `resolvePageEntry` resolves shorthand; the server validates its HTML/scripts and exposes `pageEntry`/`pageTitle`. `PluginPane surface="page"` uses the same sandbox. `useUIStore.openGuestPageId` is transient and mutually exclusive with other main pages. The sidebar menu opens it; `close()` or explicit session navigation closes it. Pages are not context-rail surfaces.
|
|
28
|
+
- `workspace.ts` owns project/worktree/session snapshots and storage types; `workspace-schemas.ts` validates their wire contracts. The existing `sessions` grant covers lists, subscriptions, opening sessions, and targeted creation. `lib/guests/workspace.ts` projects shared project, topology, session, and live-status stores; reads perform no network or git discovery. A registered observer is shared per runtime/extension/query and stops with its last listener. `PluginPane` limits each frame to 32 subscriptions and clears them on unmount, grant withdrawal, and runtime switch. Initial snapshots and updates have explicit loading/error coverage. `HostClient.onProjects/onWorktrees/onSessions` return promises of unsubscribe functions, so approval failures reach the caller.
|
|
29
|
+
- `storage` is instance-owned JSON under the guest's namespace, without a new capability. `storage.js` serializes authorization, read-modify-write, and removal per namespace; writes use unique temporary files and rename. Missing keys differ from stored null; corrupt files and failed writes fail without clearing data. Limits and public result shapes live in `API.md`. For project data, extensions namespace keys with project IDs.
|
|
30
|
+
|
|
31
|
+
- Manifest `apiVersion` is `1`. Wire envelope `v` stays `1`. Any other value is `unsupported-api-version`. Optional `engines.aiworker` is `1.22.0` or `>=1.22.0`. Install refuses with `host-too-old` when this AiWorker build is older. Package installs also need a semver `version` on `package.json` (`1.0.0`); the catalog exposes it and Settings → Extensions shows `v1.0.0` on the card. Already-installed packages without `version` still list; new installs without it fail as `invalid-manifest`.
|
|
32
|
+
- `panel.entry` is optional; `panel.id/name/icon` remain the identity on Settings and approval cards. `hasGuestPage` means a visible panel, not an execution runtime. `background.entry` provides independent package-local `.html` for on-demand code without a rail icon. Without a panel, actions must explicitly use `mode: "background"`; `page` and enabled `attach` fail as `invalid-panel`. A background-only extension can declare commands, services, integrations and capabilities, with the existing grants. Without either entry, only tools are allowed, HTML/JS serving is refused, and `examples/tools-only` remains the reference. Settings shows "No panel" whenever the visible entry is absent.
|
|
33
|
+
- Panel `id` is kebab-case. `icon` is a Remixicon kebab-case name (`RiWindowLine` → `window`, `RiGitMergeLine` → `git-merge`) or a package `.svg` path (`icon.svg`). URLs and absolute paths fail parse. `entry` stays inside the package (no `..`, no absolute, no URL). The rail paints a Remixicon from the host sprite, or a `currentColor` mask for a package SVG. Package SVGs often fill the 24×24 viewBox; the rail scales that mask so it matches Remixicon optical size. A Remixicon name the sprite does not carry falls back to `apps`.
|
|
34
|
+
- Extra manifest keys are dropped, not forwarded.
|
|
35
|
+
- Theme text fields are required on `apiVersion: 1`: `primaryText`, `successText`, `warningText`, `errorText`, `infoText`. Host calculation and UI-kit application are documented in `src/ui/DOCUMENTATION.md`. Keep the wire schema, TypeScript contract and iframe mapping in sync.
|
|
36
|
+
- `connectHost` rejects when `parent === self`. A silent host rejects in-flight RPCs as `HOST_TIMEOUT` after 20s. Dispose rejects the rest as `HOST_UNAVAILABLE`.
|
|
37
|
+
- Failed `result` may carry `code`. Known guest codes are `HOST_UNAVAILABLE`, `HOST_TIMEOUT`, `HOST_REJECTED`, `DISCONNECTED`, `DISABLED`, `BAD_PATH`, `NO_INTEGRATION`, `NO_SESSION`, `SESSION_BUSY`, `NO_SERVICE`, `SERVICE_FAILED`, `NOT_GRANTED`, `NO_DIRECTORY`, `NOT_FOUND`, `FILE_TOO_LARGE`, `DENIED`, `NO_MODEL`, `MODEL_FAILED`. Anything else becomes `HOST_REJECTED`. The `error` string stays.
|
|
38
|
+
- `parseManifest` takes a typed document. File or network bytes go through `parseManifestJson`. Both return a result and do not throw on junk.
|
|
39
|
+
- The host parses iframe `event.data` with `guestMessageSchema` (`@aiworker/sdk/schemas`). The guest reads host pushes with `readHostMessage` in `contract.ts`: envelope and discriminant only, because the sender is the verified parent frame. `protocol.ts` asserts the hand-written `HostMessage` / `GuestMessage` types against the zod schemas at compile time, so the two cannot drift.
|
|
40
|
+
- `ready.item` is `GuestItem | null`: the composer chip (`AttachIssueRequest`, unchanged, `kind` still `issue` / `pull`), or a `GuestMessageItem` / `GuestSessionItem` from a declared action, discriminated by `kind: 'message' | 'session'` and carrying the manifest `action` id. A message item's `text` is what `formatMessageRecordText` renders for the export, capped at `GUEST_ITEM_MESSAGE_TEXT_MAX` (200 000). A session item carries `messages` only when the action declared `payload: ["messages"]` and `conversation` is granted; `buildGuestSessionItem` in `packages/ui/src/lib/guests/actions.ts` drops the oldest messages until the serialized item is under `GUEST_ITEM_SESSION_MAX` (2 000 000) and sets `truncated`. The host schema refuses a larger item. `isGuestAttachItem` / `isGuestMessageItem` / `isGuestSessionItem` narrow it.
|
|
41
|
+
- `contributes.actions` is 1 to 8 entries (`GUEST_ACTIONS_MAX`), each a unique kebab-case `id`, `label` of 1 to `GUEST_ACTION_LABEL_MAX` (40) characters, optional `icon` with the `panel.icon` rules (falls back to the panel icon), and `where`. `roles` is only legal on `where: "message"` (default both roles); `payload` is only legal on `where: "session"` and only knows `"messages"`. Anything else is `invalid-actions`. Declaring a `messages` payload adds the `conversation` capability (`guestActionsNeedConversation`); actions without it need no capability. The public catalog row carries `actions` as declared.
|
|
42
|
+
- `contributes.commands` is 1 to 8 entries (`GUEST_COMMANDS_MAX`) with a unique `name` matching `GUEST_COMMAND_NAME` (`/^[a-z][a-z0-9-]{0,23}$/`) and an optional `description` of 1 to `GUEST_COMMAND_DESCRIPTION_MAX` (80) characters; else `invalid-commands`. The host owns collisions: a name the composer already has (built-in, OpenCode command, skill, or an earlier guest) is dropped with a console warning. The public catalog row carries `commands` as declared.
|
|
43
|
+
- `contributes.tools` is 1 to 16 entries (`GUEST_TOOLS_MAX`) that say how the extension's tool calls look in the chat, without code. `match` is the full tool name as OpenCode reports it, un-normalized (`mcp.jira.search`, `jira_search`), 1 to `GUEST_TOOL_MATCH_MAX` (128) characters of `[A-Za-z0-9_.:-]`, with one `*` allowed at the end as a suffix wildcard (`mcp.jira.*`; `*` alone is refused). Optional `name` (1 to `GUEST_TOOL_NAME_MAX`, 40) is the header title when `title` is absent or renders empty; `icon` is a Remixicon name or a package `.svg` path with the `panel.icon` rules (the chat draws the SVG as a `currentColor` mask at the glyph size through `resolveGuestToolIcon`, fetched as an authenticated asset under `/api/guests/<id>/<path>`; a Remixicon the sprite lacks leaves the host's own tool icon); `title` and `subtitle` are templates of up to `GUEST_TOOL_TEMPLATE_MAX` (200) characters with `{input.path}`, `{output.path}`, and `{metadata.path}` placeholders (dotted paths, array indexes allowed; a missing path is an empty string; a value is stringified and cut at `GUEST_TOOL_TEMPLATE_VALUE_MAX`, 200). `output` is `auto` (the host's own detection, the default), `text`, `json` (the JSON summary/tree/raw views), `markdown` (the assistant markdown renderer), `code` with `language` (1 to 32), or `table` with `columns` (1 to 16 dotted paths; rows are the output array or `output.items`). `language` outside `code` and `columns` outside `table` (or a `table` without `columns`) fail closed; anything else wrong is `invalid-tools`. The public catalog row carries `tools` as declared; the host applies them only for an active guest, an exact `match` beats a wildcard from any extension, and among equals the first extension in the catalog wins. The registry lives in `packages/ui/src/lib/guests/tool-presentation.ts` and is consulted by the existing chat tool renderers; VS Code and mobile keep the catalog empty, so nothing changes there.
|
|
44
|
+
- Host → guest `resolve` is `{ id, payload: { command, args } }`; the guest answers `resolve-result` with the same `id` and `{ item }` (a clamped `AttachIssueRequest` or `null`) or `{ error }` (1 to `GUEST_RESOLVE_ERROR_MAX` characters). The host waits `GUEST_REQUEST_TIMEOUT_MS`, then treats silence as a failure and never sends a `result` for a `resolve-result`. `badge` is `{ count }` with `count` an integer 0..999 or `null`; the host schema drops anything else.
|
|
45
|
+
- `ready` always carries `session` (`{ id, title, busy }` or `null`), `surface` (`panel` on the rail, `dialog` in the attach window), `connection` (`{ connected, account }`), and `settings` (declared keys only). Title falls back to `id` when the host record has no title. `busy` is live `session_status` `busy` or `retry`. Missing status is `busy: false`, not an empty success that clears a previous snapshot. `model` is `providerID/id` when the host session record has both. `agent` is the session agent name when present. `surface` is which host chrome mounted the iframe. It is not `openSurface`. `item` is the attached item the surface was opened for (the user clicked that item's chip on the composer; the same `AttachIssueRequest` shape the guest handed to `attach`, with `text` filled from the chip's context) or `null` from the rail icon and the + menu. The host also pushes `item` when it changes while the frame is up; `onItem` replays the latest value. `ready.theme.tokens` is the host colour bag: surfaces (`background`, `elevated`, `mutedSurface`, `subtle`), text (`foreground`, `elevatedForeground`, `muted`), interaction (`border`, `hover`, `active`, `selection`, `selectionForeground`, `focus`), `primary` / `primaryForeground`, the four status colours, `font`, `mono`, and `radius`. `applyHostReady` writes it onto the iframe. The host also pushes `connection` and `settings` the same way as `session`. Access tokens never appear in `ready` or a result.
|
|
46
|
+
- `writeClipboard` text is 1–32000 characters. `compose` text is 1–16000 after trim. Compose default mode is `append`. Compose writes the chat composer. It does not send.
|
|
47
|
+
- `contributes.capabilities` is a list of `prompt`, `sessions`, `files`, and/or `model` (`DECLARED_GUEST_CAPABILITIES`). `model` gates `generate`: `{ prompt, system?, maxOutputTokens? }` → `{ text }`, posted with a `GUEST_GENERATE_TIMEOUT_MS` (90s) timeout instead of the usual 20s because the model answers slowly; the host answers `NO_MODEL` when the small-model service resolves nothing (or only Claude Code) and `MODEL_FAILED` when the provider call fails. `requestedGuestCapabilities` adds `conversation`, `service`, `network`, and `filesystem` from a session action with `payload: ["messages"]`, `contributes.service`, `contributes.integration`, and `contributes.filesystem`; the user approves that whole list once at install (`PUT /api/guests/:id/capabilities`), and a package that later asks for more is re-approved. `hasGuestCapability` gates `prompt({ send })`, `startSession`, `request`, `serviceRequest`, and the four file calls; a refusal is `NOT_GRANTED` (`NO_SERVICE` for the service).
|
|
48
|
+
- `contributes.filesystem` is 1 to 16 globs, each 1 to 256 characters, starting with `/` or `~/`, with no `..`, empty segment, NUL, or backslash (`isGuestFilesystemPattern`; parse failure is `invalid-filesystem`). `**` spans directories, `*` and `?` stay inside one segment, and a pattern ending in `/**` also matches the directory itself. The public catalog row carries the list as `filesystem` so the approval dialog can show it.
|
|
49
|
+
- `readFile` / `writeFile` / `listDir` / `stat` are `file-read` / `file-write` / `file-list` / `file-stat` on the wire. `guestFileScope` decides the grant: a relative path is `project` (capability `files`, joined to the open project; none open is `NO_DIRECTORY`), `/…` or `~/…` is `filesystem` (must match a declared pattern). The server compares canonical paths, so a symlink out of either tree is `BAD_PATH`, as is any `..` segment. Results are `{ content }`, `{ written: true }`, `{ entries: { name, kind }[] }`, and `{ kind, size, mtime }` where `kind` may be `missing`. `GUEST_FILE_PATH_MAX` (1024), `GUEST_FILE_CONTENT_MAX` (2 000 000 characters, read and write), `GUEST_FILE_LIST_MAX` (2000 entries). The client refuses an empty, oversized, NUL, or backslash path as `BAD_PATH` and oversized content as `FILE_TOO_LARGE` before posting; the host schema accepts only what `isGuestFilePath` does, so a dropped message cannot turn into `HOST_TIMEOUT`. Nothing about the write is logged, not even the path.
|
|
50
|
+
- `contributes.attach` is `true`, `false`, `"panel"`, `"dialog"`, or `{ "mode": "panel" | "dialog", "entry"?: string }`. `true` and `"panel"` add a + menu row that opens the rail. `"dialog"` opens a host window that mounts the same iframe. The object form's `entry` is an HTML page inside the package (same path rules as `panel.entry`, install checks it exists and that its scripts are built) that the dialog loads instead of `panel.entry`; it is only accepted with `mode: "dialog"`, and `resolveAttachEntry(contributes)` returns it or `null`. The public catalog row carries it as `attachEntry`. Clicking the guest's chip on the composer reopens that guest with the chip as `ready.item`: a dialog guest in the attach window, a panel guest on the rail. New Worktree shows the same dialog-mode guests next to GitHub and Linear. Omitted or `false` stays off those menus. Public 1.0 is web and desktop. VS Code and mobile mark the guest catalog `unsupported`. They do not show the row.
|
|
51
|
+
- `contributes.integration` needs either a panel or background entry. Exactly one of `oauth`, `token`, or `host` is required. The public catalog slice is name, description, `auth`, `apiOrigin` (the one origin `request` may call; the approval dialog shows it, it is not a secret), token scheme, and setting field defs. OAuth URLs stay on the server. Client id, secret, and pasted tokens are not in the package. `host.provider: "linear"` binds the first-party Linear connection. `apiOrigin` is `https://api.linear.app` only.
|
|
52
|
+
- `attach` puts `{ providerId, id, title, url }` on the composer as a chip, exclusive with GitHub and Linear. `kind` is `issue` or `pull` and defaults to `issue`. `author` is a login. `branches` is `{ head, base }` on a pull. Avatar URLs stay off this payload. It does not invent a GitHub `number`. `text` is optional model context, 1–16000 after trim. `data` is optional plain JSON the guest chose (`JsonValue`); the host stores it on the chip, the queued context part, and the session snapshot, and hands it back byte-identical as `ready.item.data` / `onItem`. It never reaches the model or the chip text. Serialized size is capped at `GUEST_ATTACH_DATA_MAX` (16 000 characters): `clampAttachRequest` drops a larger value, the host schema refuses it. `connectHost.attach` clamps id/title/url/text/author/branches to the protocol max so the host schema does not drop the message. The rail and the attach window both write `pendingGuestIssue` on the composer store. Send writes `guest-issue` or `guest-pr` and a session snapshot. `close` dismisses the attach window and is a no-op on the rail.
|
|
53
|
+
- `startSession` extends the existing operation with `projectId`, root/existing/new worktree selection, optional new name/base, and `navigation` defaulting to `preserve`. It calls the owning session creation action with that navigation policy, so registration/message initialization/global cache updates still happen while selection stays unchanged. Worktree creation reuses `createWorktreeWithDefaults` and the configured bootstrap wait. It preserves a created worktree on bootstrap/session failure and returns that partial result. It captures send selection before async creation, rechecks runtime and grants before subsequent mutations, and distinguishes session creation from first-message and linked-item persistence outcomes. See `API.md` for the exact result. Web and desktop only.
|
|
54
|
+
- `prompt` is `{ text, send? }`. `send` omitted or `false` replace-composes on the current session. `send: true` sends on that session with the same model path as `startSession`. No session is `NO_SESSION`. A busy session on send is `SESSION_BUSY`. Compose still works while busy. The result is `{ sent }`. The guest does not pick a model or agent.
|
|
55
|
+
- `sessionLink` is the same attach fields on the current session. It does not create a session. No project or no session is a refusal.
|
|
56
|
+
- `onSessionLifecycle` is a host push. Live `session_status` `busy` and `retry` are `started`. `idle` is `completed`. An unknown status type is `failure`. That is not abort or crash. A late listener gets the last phase, from `ready.session.busy` when no dedicated push has arrived.
|
|
57
|
+
- `oauthStart` opens the provider authorize URL, or the first-party Linear authorize URL for a host Linear guest. `oauthDisconnect` drops that guest's tokens, or the Linear connection for a host Linear guest. Token guests have no authorize URL. `request` is `{ method, path, query?, body? }`. `path` must start with `/`, stay on the declared `apiOrigin`, and never include a scheme. The host attaches `Authorization`. Token guests send the pasted token as is (`token.scheme` omitted or `raw`), as `Bearer <token>` (`token.scheme: "bearer"`), or as `Basic base64(username:token)` (`token.scheme: "basic"`; the Integrations card asks for a username, `token.usernameLabel` names that field, and the server stores the encoded pair as the access token so requests never need the username again). OAuth and host Linear send Bearer. The result is `{ status, body }`. A 409 `DISCONNECTED` or 400 `BAD_PATH` / `NO_INTEGRATION` lands on the guest as `HostRequestError.code`. Silence for 20s is `HOST_TIMEOUT`. The guest parses JSON. A host Linear guest may also `GET /api/linear/issues/get` through that same `request` call. The host answers from the first-party Linear route, which already asks Linear for public file URLs. Guest request itself does not know Linear.
|
|
58
|
+
- `contributes.service` names a host-spawned entry on `apiVersion: 1`. `serviceRequest` / `serviceStatus` proxy to that process on loopback. See `GUEST_SERVICES.md`. Permissions are grant UI only in phase 1. No `host.docker`.
|
|
59
|
+
|
|
60
|
+
## Host hole
|
|
61
|
+
|
|
62
|
+
Built-in extension source and registration rules live in `packages/extensions/DOCUMENTATION.md`. The server assigns `source: 'bundled'` only from its app-owned registry and automatically grants the package's declared capabilities. Built-ins use the same SDK, sandbox and enabled-state checks as user extensions; the `openchamber-builtin-` namespace is reserved. Their account cards appear under Built-in integrations, and their persisted disabled state and data survive app updates.
|
|
63
|
+
|
|
64
|
+
Relay panels use `lib/guests/relay-document.ts`: fetch package HTML and its statically referenced scripts, styles, CSS imports/URLs, images, fonts, and nested documents through `runtimeFetch`, then render self-contained `srcDoc` with the same `sandbox="allow-scripts"`. A normal iframe URL bypasses the client-side tunnel and can load the local host UI instead. Local resource paths stay under that guest's package; missing files, import cycles, or resource limits fail the load. The loader allows 500 referenced files, 40 MiB of fetched bytes and 64 MiB of embedded text. Runtime-created relative URLs are not intercepted: bundle those assets into the IIFE or use explicit external URLs/SDK operations. Existing authored CSP is preserved. Direct iframe URLs retain their existing path.
|
|
65
|
+
|
|
66
|
+
`useGuestIconSource` loads package SVGs through authenticated `runtimeFetch` in direct and relay modes, then exposes revocable blob URLs to CSS masks. It uses the package path rather than the original URL token, so cold-start token readiness and token expiry cannot strand an icon. Cache identity follows runtime, package path and extension version; late responses are discarded and old blobs revoked. Unknown icons and SVGs that are loading or failed use the shared `FALLBACK_GUEST_ICON`, the `apps` glyph.
|
|
67
|
+
|
|
68
|
+
Git installation accepts HTTPS, `ssh://` and scp-style SSH addresses. `POST /api/guests` accepts optional `gitIdentityId`; the server stores only that ID in the git origin, and resolves the profile again for clones and update fetches. Global/omitted identity uses the server process's Git/SSH configuration and agent. The Settings picker loads identities from the active runtime, discards stale loads, and never reuses a desktop's cached profiles on a remote server. SSH parsing, public-host validation and pinned connection options belong to `packages/web/server/lib/guests/clone.js`.
|
|
69
|
+
|
|
70
|
+
Catalog loading and installation retain typed request diagnostics in `lib/guests/request-failure.ts`: fixed API route, method, HTTP status, or a transport/invalid-response classification. Settings shows catalog diagnostics and passes install diagnostics to the toast description so Copy includes them. Response bodies, headers, credentials, and arbitrary transport error messages are excluded. Catalog failures preserve a previously loaded list; diagnostics clear on success or runtime switch, and late failures from a retired runtime are ignored.
|
|
71
|
+
|
|
72
|
+
`lib/guests/useGuestFrameUrl.ts` owns iframe URL lifetime. It retains the scoped token's expiry, renews on an expired navigation, and remints after enable/version/runtime changes. Expiry alone never reloads a healthy document. Recovery is bounded until the guest's next `hello`; mint failure leaves the existing load-failed UI rather than mounting an unauthenticated URL. Late mint responses from a retired owner are discarded. Guest capability grants and provider OAuth are separate from this host file-loading authorization.
|
|
73
|
+
|
|
74
|
+
`GET /api/guests` plus `PluginPane` on the desktop/web rail. The iframe URL carries a token minted for `guest:<id>` scope (`resolveGuestFrameUrl`), valid only for `GET /api/guests/<id>/*`; the session-wide URL token never enters a guest frame. Settings → Extensions installs a folder, a local `.zip`, or an https git / zip URL into that AiWorker instance's data dir (`extensions.json`). Settings → Integrations shows a host card for each installed guest that declared `integration`. OAuth and pasted tokens live in `guest-auth.json`. Host Linear uses `linear-auth.json`. VS Code and mobile set the catalog to `unsupported`. That is not an empty success. A guest must ship a classic IIFE (`panel/main.js`). No runtime compiles guest TypeScript. The iframe cannot load ESM.
|
|
75
|
+
|
|
76
|
+
**Folder install** stores the realpath in `extensions.json` with `source: "path"`. Uninstall drops the catalog row and extension-owned storage. The user's folder stays.
|
|
77
|
+
|
|
78
|
+
**Zip / git install** copies into `{dataDir}/extensions/{id}` with `source: "zip"` or `"git"`. Only git installs update: Settings → Extensions compares the installed `package.json` `version` with the origin's and offers Update when the origin is newer, so bump `version` to ship one; a `#tag` or `#branch` on the install URL pins that channel. The archive is not kept open. The host reads it (local file or https download, 20MB cap), extracts with a small allowlist unzip (store/deflate only; no zip64, encryption, or `..` paths; 500 files / 40MB uncompressed), unwraps a single root folder when `package.json` sits inside it, inspects the manifest, then renames staging to `{dataDir}/extensions/{id}`. Runtime serves and spawns from that copy. Uninstall deletes the copy. The original `.zip` on disk is untouched.
|
|
79
|
+
|
|
80
|
+
The iframe is `sandbox="allow-scripts"` with no `allow-same-origin`, so its origin is opaque. Host posts to that frame's `contentWindow` with target `*`. The string `null` is not a legal `postMessage` targetOrigin. Receive still accepts only `event.source === contentWindow`. Guest `connectHost` posts `hello` to `parent` with `*`. The host answers `hello` and also pushes `ready` on iframe `load`, because a cached panel can fire `hello` while React is swapping the listener.
|
|
81
|
+
|
|
82
|
+
File calls go through `POST /api/guests/:id/files` with the `x-opencode-directory` header of the open project (`lib/guests/files.ts`). `PluginPane` answers `NOT_GRANTED` and `NO_DIRECTORY` itself, mirroring the server, so a refused call costs no round trip; the server is still the authority.
|
|
83
|
+
|
|
84
|
+
Clipboard writes run in the host (`copyTextToClipboard`). Compose uses `useInputStore.setPendingInputText`. Attach writes the composer chip. The iframe has no clipboard, no composer, and no chip. The + menu and the attach window chrome are host. Guest JS is not in that React tree.
|
|
85
|
+
|
|
86
|
+
Actions, commands, and the badge are host chrome too. `useGuestActions` (`packages/ui/src/hooks/useGuestSurfaces.ts`) lists active guests' actions for message and session menus, empty on VS Code and mobile. A click builds the item with `buildGuestMessageItem` / `buildGuestSessionItem`; failed conversation loading is a toast, never an empty conversation. `runGuestAction` dispatches the declared mode. In default `open` mode, `openGuestWithItem` routes like a chip click: the attach window for `attach: "dialog"` through `useGuestDialogStore`, otherwise the rail through `useGuestItemStore`. Background mode is documented below.
|
|
87
|
+
|
|
88
|
+
`useGuestCommands` lists commands minus reserved names. The composer routes `/name args` through `submit/guestCommands.ts` to `runGuestCommand`, which asks a mounted pane's registered resolver or mounts a hidden `PluginPane` through `GuestHosts`. The pane registers after hello/load, posts `resolve`, and settles on `resolve-result` or the 20s deadline. A returned chip uses `useInputStore.setPendingGuestIssue`. `badge` writes the memory-only `useGuestBadgeStore`; the rail paints the count and opening the panel clears it. A paused or unapproved guest contributes no actions, commands, tool presentations, or badge.
|
|
89
|
+
|
|
90
|
+
Add a type when the host grows that hole, not before. Any new `connectHost` method ships with a `PluginPane` / `answerGuestMessage` branch in the same change.
|
|
91
|
+
|
|
92
|
+
Rebuild every example bundle after an SDK change (`bunx openchamber-guest-bundle` per entry, see `examples/README.md`); the built files are checked in.
|
|
93
|
+
|
|
94
|
+
## Guest toasts
|
|
95
|
+
|
|
96
|
+
`ToastRequest` adds optional `copy: boolean | { text: string }`, `dismiss: boolean`, and `persistent: boolean`. The client checks message/copy length before sending; the host schema validates the complete payload. `HostBridgeEffects.toast` receives the request intact. `lib/guests/toast.tsx` owns toast creation and per-toast dismissal; `components/layout/GuestToastActions.tsx` renders the buttons and clipboard success/failure state. Copy keeps the toast open. Persistent toasts always include OK, including when the guest passes `dismiss: false`.
|
|
97
|
+
|
|
98
|
+
Toast acknowledgement means displayed, not clicked. Button closures retain only text and toast identity, not the guest's lifetime or runtime. They survive action completion, iframe teardown, and runtime switches. Labels resolve through the host's reactive i18n context. The SDK and schema limits use the existing `GUEST_TOAST_MAX` and `GUEST_CLIPBOARD_TEXT_MAX`; custom copy text is not trimmed. See `API.md` for the author contract.
|
|
99
|
+
|
|
100
|
+
## Background actions
|
|
101
|
+
|
|
102
|
+
`contributes.actions[].mode` is `open` or `background`, defaulting to `open`. The manifest parser and UI catalog parser preserve it; server catalog projection already carries actions unchanged. Existing `onItem` routing stays with open-mode actions. `runGuestAction` revalidates the captured declaration, active grants, target role, and runtime before dispatch. Session actions also discard history loaded across a runtime switch.
|
|
103
|
+
|
|
104
|
+
`lib/guests/run-action.ts` owns temporary invocation state, a 20-second loading/execution deadline, and an eight-frame limit. Each click gets a distinct `PluginPane` from `GuestHosts`, preferring `backgroundEntry` and falling back to `entry`, with `surface: 'background'` and `headless`. It never registers as a slash-command resolver, consumes a parked item, clears a badge, opens a tab, or changes session selection. The pane pins session/directory context to the clicked target. Composer-writing methods retain their existing current-composer behavior.
|
|
105
|
+
|
|
106
|
+
The catalog carries `backgroundEntry` independently of the visible `entry`. Both HTML files and their built scripts are checked at install. The serving route's `hasRuntime` gate permits code when either exists; storage applies that same entry check plus enabled/approval gates. Background-only chips cannot reopen a rail panel, so the composer shows a no-panel notice while retaining the existing browser action.
|
|
107
|
+
|
|
108
|
+
Slash commands use the background entry whenever it is declared. A visible pane then does not register a resolver; only the temporary command host does, with `ready.surface: 'background'`. Panel-only extensions retain resolver reuse. Resolver registrations are retired when the frame URL or entry changes and refuse calls after runtime/grant/version changes. `examples/hello-kit/background` is the reference for a separate entry; omit that example's visible entry and page to make it background-only.
|
|
109
|
+
|
|
110
|
+
Both `MainLayout` and the separate-window `MiniChatLayout` mount `GuestHosts`; a message action must have an execution host in the window that owns its catalog and session state.
|
|
111
|
+
|
|
112
|
+
The SDK adds `HostActionMessage`, `GuestActionResultMessage`, `GuestActionItem`, `ActionResultPayload`, and `HostClient.onAction`. Wire `action` carries one message/session item and an invocation ID; `action-result` carries that ID and a discriminated success/error payload. Error text shares `GUEST_RESOLVE_ERROR_MAX`. `ready.item` stays null; a one-shot `takeMessage` prevents hello/load/context updates from executing an action twice. The handler must be registered synchronously and await its work before returning. Host schema validation plus the frame's source and invocation ID reject unrelated replies.
|
|
113
|
+
|
|
114
|
+
The runner cancels on runtime/catalog invalidation and marks the operation inactive synchronously, so queued messages cannot mutate host state during React teardown. Timeout, completion, frame-load failure, or host unmount clears the invocation and subscriptions. Unmount cleanup yields one microtask for Strict Mode's effect reattachment. Already accepted remote effects are not rolled back; no automatic retry. Web/desktop use the existing direct or relay frame loader; VS Code and mobile remain unsupported. `examples/hello-kit` demonstrates a background message-length toast.
|
|
115
|
+
|
|
116
|
+
## Not here yet
|
|
117
|
+
|
|
118
|
+
`issues.search` / `issues.get` on the host, a public OAuth broker, a second `host.provider`, file watching or globbing from the guest (it gets `listDir` and `stat`, not a search). `attach` is the chip. `startSession` creates a session and writes that snapshot. `sessionLink` writes that snapshot on the current session. `prompt` writes or sends on that session. `onSessionLifecycle` is the live phase. Search and the task list live in the guest. `@aiworker/sdk/ui` is the drawing kit: buttons, fields, select, list, tabs, badges, banners, menu, and `mountText`. No issue page, card, attach picker, or PR window; the guest composes those from the blocks. `mountText` lifts `` and `[label](https://…)` and hands link clicks to `onOpenUrl`. `ready.theme.tokens` carries the host surface and interactive colors. Call `applyHostReady` before mount.
|
|
119
|
+
|
|
120
|
+
Local services ship on `apiVersion: 1` (`contributes.service`, `serviceRequest`, grant UI, platform socket bindings + override). Contract detail: `GUEST_SERVICES.md`. Declared `sockets` / `exec` are shown on the grant card and do not confine the process; there is no sandbox for services. VS Code and mobile do not load guests or services yet.
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# Guest local services
|
|
2
|
+
|
|
3
|
+
Implemented on manifest `apiVersion: 1`. Wire envelope `AIWORKER_SDK_API_VERSION` stays `1`.
|
|
4
|
+
|
|
5
|
+
This is the contract for guests that need a local process (Docker CLI, Engine sockets, kubectl, DB sockets). The iframe stays sandboxed. The host owns spawn and the loopback proxy.
|
|
6
|
+
|
|
7
|
+
## Why services exist
|
|
8
|
+
|
|
9
|
+
HTML guests in a sandboxed iframe reach the network only through `connectHost.request` onto a declared HTTPS `apiOrigin`. That fits cloud trackers. It cannot open `/var/run/docker.sock`, run `docker`, or hold a long-lived local daemon.
|
|
10
|
+
|
|
11
|
+
Services keep the iframe. They add a host-owned child process from the same package. The panel never sees the socket. The service does.
|
|
12
|
+
|
|
13
|
+
Do not put `docker` (or any product name) on `connectHost`. The SDK knows panel, service, and a loopback proxy. The package owns the integration.
|
|
14
|
+
|
|
15
|
+
Declare the AiWorker floor with `engines.aiworker` (`1.22.0` or `>=1.22.0`). Install refuses when this host is older. Put a semver `version` on `package.json` (`1.0.0`); install requires it and Settings → Extensions shows `v1.0.0` on the card.
|
|
16
|
+
|
|
17
|
+
## Model
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
panel (iframe) --serviceRequest--> host --HTTP 127.0.0.1:port--> service process --> socket / CLI
|
|
21
|
+
^
|
|
22
|
+
spawn / kill / grant
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
1. Package still ships `panel/index.html` and a classic IIFE `panel/main.js`.
|
|
26
|
+
2. Optional `contributes.service` names a built entry the host can spawn.
|
|
27
|
+
3. After the user allows the service in Settings → Extensions, the first `serviceRequest` starts that entry with the app runtime (`process.execPath` + `ELECTRON_RUN_AS_NODE` on desktop). A system `node` on PATH is not required.
|
|
28
|
+
4. The host binds `127.0.0.1` on an ephemeral port and passes `AIWORKER_SERVICE_PORT` and `AIWORKER_SERVICE_TOKEN` in the service env. The service does not inherit the host environment: only PATH, HOME, temp, locale, and the Windows system variables are copied. API keys, the UI password, and other host secrets never reach it.
|
|
29
|
+
5. The panel calls `serviceRequest({ method, path, query?, body? })`. The host proxies only to that guest's loopback listener. Same stay-on-origin rule as `request`, but the origin is the service the host started.
|
|
30
|
+
6. The service talks to Docker, kubectl, or anything else. That logic stays in the package.
|
|
31
|
+
|
|
32
|
+
## Permissions: `exec` vs `sockets`
|
|
33
|
+
|
|
34
|
+
| Declare | Means | Service does |
|
|
35
|
+
|---|---|---|
|
|
36
|
+
| `exec` | Named binaries on PATH | `child_process` / CLI |
|
|
37
|
+
| `sockets` | Unix socket or named pipe | Dial Engine API (or similar) itself |
|
|
38
|
+
|
|
39
|
+
Use `exec` when the integration shells out (like a modern Docker CLI panel). Use `sockets` when the service opens the daemon endpoint. Do not list a socket path in Needs for a CLI-only service.
|
|
40
|
+
|
|
41
|
+
## Manifest
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{
|
|
45
|
+
"apiVersion": 1,
|
|
46
|
+
"engines": {
|
|
47
|
+
"aiworker": ">=1.22.0"
|
|
48
|
+
},
|
|
49
|
+
"contributes": {
|
|
50
|
+
"panel": {
|
|
51
|
+
"id": "docker-sock",
|
|
52
|
+
"name": "Docker (socket)",
|
|
53
|
+
"icon": "icon.svg",
|
|
54
|
+
"entry": "panel/index.html"
|
|
55
|
+
},
|
|
56
|
+
"service": {
|
|
57
|
+
"entry": "service/main.js",
|
|
58
|
+
"runtime": "host",
|
|
59
|
+
"permissions": {
|
|
60
|
+
"sockets": [{
|
|
61
|
+
"id": "docker",
|
|
62
|
+
"candidates": {
|
|
63
|
+
"linux": ["/var/run/docker.sock", "/run/user/1000/docker.sock"],
|
|
64
|
+
"darwin": ["~/.docker/run/docker.sock", "~/.colima/default/docker.sock"],
|
|
65
|
+
"win32": ["//./pipe/docker_engine"]
|
|
66
|
+
}
|
|
67
|
+
}]
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
"attach": false
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
A CLI service looks the same except `permissions.exec: ["docker"]` and no `sockets`.
|
|
76
|
+
|
|
77
|
+
Socket entry shapes:
|
|
78
|
+
|
|
79
|
+
- `string` — legacy one path for every platform; public id is that string
|
|
80
|
+
- `{ id, path }` — same path on linux / darwin / win32
|
|
81
|
+
- `{ id, candidates: { linux?, darwin?, win32? } }` — per-OS lists; `~` expands
|
|
82
|
+
|
|
83
|
+
Parse rules:
|
|
84
|
+
|
|
85
|
+
- `apiVersion` is `1`. Wire `v` on postMessage stays `1`.
|
|
86
|
+
- `engines.aiworker` is optional. Values are `1.22.0` or `>=1.22.0` only. Install returns `host-too-old` when this AiWorker build is older.
|
|
87
|
+
- `contributes.panel` stays required. Same id / name / icon / entry rules as any guest.
|
|
88
|
+
- `contributes.service` is optional. A guest without `service` is HTML-only.
|
|
89
|
+
- `service.entry` is a relative path inside the package. Ship compiled JS; the host never compiles TypeScript.
|
|
90
|
+
- `service.runtime` phase 1 accepts only `"host"`.
|
|
91
|
+
- `service.permissions.sockets` and `service.permissions.exec` are shown to the user in the approval dialog. They describe intent and do not confine the process: a service runs with the user's full access. Declaring `contributes.service` adds the `service` capability to the package's request list; the user approves the whole list once when the package is installed (Settings → Extensions), and the first `serviceRequest` is refused with `NO_SERVICE` until then.
|
|
92
|
+
- The catalog adds `service.socketBindings`: `{ id, candidates, resolved, override }` for this host. The user can override a path in Extensions. Override empty clears it and the next spawn re-resolves.
|
|
93
|
+
- `contributes.integration` remains valid next to `service`. Cloud `request` and `serviceRequest` may both exist on one guest.
|
|
94
|
+
|
|
95
|
+
Extra keys still drop, not forward.
|
|
96
|
+
|
|
97
|
+
## Host hole
|
|
98
|
+
|
|
99
|
+
| Method | Role |
|
|
100
|
+
|---|---|
|
|
101
|
+
| `serviceRequest` | `{ method, path, query?, body? }` → `{ status, body }`. Proxy to this guest's service loopback only. `path` starts with `/`, no scheme. |
|
|
102
|
+
| `serviceStatus` | `stopped` \| `starting` \| `ready` \| `failed`. |
|
|
103
|
+
|
|
104
|
+
Do not add: raw unix socket from the panel, arbitrary `spawn`, arbitrary filesystem, `host.docker`.
|
|
105
|
+
|
|
106
|
+
Error codes:
|
|
107
|
+
|
|
108
|
+
- `NO_SERVICE` — no service declared, not granted, not started, or already torn down
|
|
109
|
+
- `DISABLED` — extension paused in Settings → Extensions (service stopped; tokens/grants stay)
|
|
110
|
+
- `SERVICE_FAILED` — process crashed or never became ready
|
|
111
|
+
- Existing: `HOST_TIMEOUT`, `HOST_REJECTED`, `HOST_UNAVAILABLE`, `BAD_PATH`
|
|
112
|
+
|
|
113
|
+
Server routes (authenticated UI session):
|
|
114
|
+
|
|
115
|
+
- `POST /api/guests/:id/service/request`
|
|
116
|
+
- `GET /api/guests/:id/service/status`
|
|
117
|
+
- `PUT /api/guests/:id/capabilities` — `{ granted }`, the full requested list or `[]` to withdraw
|
|
118
|
+
- `PUT /api/guests/:id/service/sockets` — `{ id, path }` (`path` empty or null clears the override). Stops a running service so the next request respawns with the new env.
|
|
119
|
+
|
|
120
|
+
The panel never receives `AIWORKER_SERVICE_TOKEN` and never dials the port itself. Opaque iframe origin stays. Only the host proxy talks to loopback.
|
|
121
|
+
|
|
122
|
+
VS Code and mobile stay `unsupported` for the guest catalog. They do not spawn services.
|
|
123
|
+
|
|
124
|
+
## Service process contract
|
|
125
|
+
|
|
126
|
+
Env the host sets:
|
|
127
|
+
|
|
128
|
+
- `AIWORKER_SERVICE_PORT` — port to bind on `127.0.0.1`
|
|
129
|
+
- `AIWORKER_SERVICE_TOKEN` — shared secret
|
|
130
|
+
- `AIWORKER_SERVICE_SOCKETS` — JSON map `{ [socketId]: absolutePath }` for every binding that resolved (override or first existing candidate)
|
|
131
|
+
|
|
132
|
+
Inbound auth: every request, including ready, must send:
|
|
133
|
+
|
|
134
|
+
```
|
|
135
|
+
Authorization: Bearer <AIWORKER_SERVICE_TOKEN>
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Ready signal: host polls `GET /health` until HTTP 200 (15s timeout), then marks `ready`.
|
|
139
|
+
|
|
140
|
+
Listen only on `127.0.0.1`. Do not bind `0.0.0.0`.
|
|
141
|
+
|
|
142
|
+
Ship `service/main.js` already built. Same packaging rule as `panel/main.js`.
|
|
143
|
+
|
|
144
|
+
## Lifecycle
|
|
145
|
+
|
|
146
|
+
| Event | Host behavior |
|
|
147
|
+
|---|---|
|
|
148
|
+
| Install | Catalog row includes public `service` (`runtime`, `permissions`, `socketBindings`, `granted: false`) and `capabilities.requested` containing `service`. No spawn yet. |
|
|
149
|
+
| Approve | `PUT .../capabilities` writes `capabilityGrants[id]` in `extensions.json`; the list must equal what the package requests. |
|
|
150
|
+
| Socket override | `PUT .../service/sockets` writes `serviceSocketOverrides`. Running service for that guest stops. |
|
|
151
|
+
| First `serviceRequest` | Grant missing → `NO_SERVICE`. Else spawn with resolved sockets, wait for `/health`, proxy. |
|
|
152
|
+
| Panel open | Status via `serviceStatus`. Dead service restarts on the next `serviceRequest`. |
|
|
153
|
+
| Uninstall | SIGTERM, then kill after timeout. Clear grant and socket overrides. Path-install does not delete the user's folder. |
|
|
154
|
+
| Host quit | Kill every guest service. |
|
|
155
|
+
| Crash | Status `failed`. Panel sees `SERVICE_FAILED` / status. Manual retry, not silent loops. |
|
|
156
|
+
|
|
157
|
+
## Security invariants
|
|
158
|
+
|
|
159
|
+
- Panel → host → service loopback only. No panel → socket.
|
|
160
|
+
- `serviceRequest` path must stay on that service (host-allocated port for that guest id).
|
|
161
|
+
- Every service requires an explicit grant before proxy.
|
|
162
|
+
- Permissions text is advisory. Phase 1 does not enforce an OS sandbox around those lists: an allowed service can run any command, use git, and read or write any file the user can. The approval dialog says so in plain words.
|
|
163
|
+
|
|
164
|
+
## Example
|
|
165
|
+
|
|
166
|
+
`examples/service-echo` is a checked-in service extension: a Node HTTP server on loopback that the panel calls through `serviceRequest` and whose status it shows. Bundle the service with `--node`, install the folder from Settings → Extensions, allow the local service in the approval dialog, then open the rail panel. Streaming from a service to the panel (shell into a container, log tails) is deferred; it needs a streaming call on the SDK first.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Bohdan Triapitsyn
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
# @aiworker/sdk
|
|
2
|
+
|
|
3
|
+
Build extensions for [AiWorker](https://openchamber.dev). An extension can show a panel on the right-hand rail or run actions in the background without a panel. It can read the current project and session, show toasts, put text in the chat box, attach a task to a session, and, once the user approves it, start sessions and send prompts. This package is the contract between the extension and the app.
|
|
4
|
+
|
|
5
|
+
Full guide: [Build an extension](https://openchamber.dev/docs/sdk/). Reference: [Host API](https://openchamber.dev/docs/sdk/host/) and [UI kit](https://openchamber.dev/docs/sdk/ui/). Extensions with a local process: [GUEST_SERVICES.md](./GUEST_SERVICES.md).
|
|
6
|
+
|
|
7
|
+
Extensions load in AiWorker web and desktop. VS Code and mobile do not load them yet.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @aiworker/sdk
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The package ships compiled JavaScript with type declarations, so any bundler works. Its version matches the AiWorker release it shipped with, so `@aiworker/sdk@1.24.0` is the contract of AiWorker 1.24.0.
|
|
16
|
+
|
|
17
|
+
## Preview builds
|
|
18
|
+
|
|
19
|
+
Preview packages are for testing unreleased SDK changes against a matching development build of AiWorker. They do not imply compatibility with the published app of the same base version.
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install @aiworker/sdk@preview
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Maintainers can run **Publish SDK preview** in GitHub Actions, select the source branch, and enter a positive preview number. For example, `1` publishes `1.23.2-preview.1` under the `preview` npm tag. Each publication needs an unused number. The workflow uses the existing `NPM_TOKEN` secret and publishes only the SDK. It changes the version in its temporary checkout, without creating commits or release tags or changing `latest`.
|
|
26
|
+
|
|
27
|
+
The workflow must exist on the default branch before GitHub exposes its manual trigger. Once it does, select `bohdan/dev` to publish that branch's SDK. Enable `dry_run` to validate without publishing. Validation installs the packed SDK in an isolated project and checks its imports, TypeScript declarations, and extension bundler before publishing that same archive.
|
|
28
|
+
|
|
29
|
+
## What you ship
|
|
30
|
+
|
|
31
|
+
A folder with three files:
|
|
32
|
+
|
|
33
|
+
- `package.json` with an `aiworker` block (the manifest)
|
|
34
|
+
- `panel/index.html`, the page AiWorker shows
|
|
35
|
+
- `panel/main.js`, your script built into one classic file (an IIFE; the page runs in a sandboxed iframe and cannot load ES modules)
|
|
36
|
+
|
|
37
|
+
AiWorker never compiles your code. Build `panel/main.js` yourself. The package includes a bundler command that runs on Bun; esbuild with `--format=iife --platform=browser` does the same job.
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
bunx openchamber-guest-bundle panel/main.ts panel/main.js
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Then install the folder from Settings → Extensions → Add. Folder installs run from your folder, so edit, rebuild, and reload. A `.zip` or an https git or zip link is copied into AiWorker's data folder instead; ship the built files only. Git installs can update from Settings → Extensions when the repository's `version` is newer than the installed one, so bump `version` to ship an update; `https://…/panel.git#v1` pins a tag or branch.
|
|
44
|
+
|
|
45
|
+
A complete three-file example is on the [Build an extension](https://openchamber.dev/docs/sdk/) page. Six examples are at [roweb.cn/roweb/aihander/tree/main/packages/sdk/examples](https://roweb.cn/roweb/aihander/tree/main/packages/sdk/examples).
|
|
46
|
+
|
|
47
|
+
## Manifest
|
|
48
|
+
|
|
49
|
+
Git installs also accept SSH addresses such as `git@github.com:owner/extension.git` and `ssh://git@github.com/owner/extension.git`. The fingerprint menu in Settings → Extensions selects Global Identity or a Git identity on the active server. That identity is reused for update checks and updates. On a remote instance, SSH keys and any unlocked SSH agent must be available to the server process, not just your desktop. See [Extensions](https://docs.aiworker.dev/extensions/) for details.
|
|
50
|
+
|
|
51
|
+
```json
|
|
52
|
+
{
|
|
53
|
+
"name": "@acme/hello",
|
|
54
|
+
"version": "1.0.0",
|
|
55
|
+
"aiworker": {
|
|
56
|
+
"apiVersion": 1,
|
|
57
|
+
"engines": { "aiworker": ">=1.24.0" },
|
|
58
|
+
"contributes": {
|
|
59
|
+
"panel": {
|
|
60
|
+
"id": "acme-hello",
|
|
61
|
+
"name": "Hello",
|
|
62
|
+
"icon": "window",
|
|
63
|
+
"entry": "panel/index.html"
|
|
64
|
+
},
|
|
65
|
+
"attach": "dialog",
|
|
66
|
+
"capabilities": ["prompt", "sessions"],
|
|
67
|
+
"actions": [
|
|
68
|
+
{ "id": "create-task", "label": "Create task from message", "where": "message", "roles": ["assistant"] },
|
|
69
|
+
{ "id": "summarize", "label": "Summarize session", "where": "session", "payload": ["messages"] }
|
|
70
|
+
],
|
|
71
|
+
"commands": [{ "name": "task", "description": "Attach a task by id" }],
|
|
72
|
+
"tools": [{ "match": "mcp.tasks.*", "name": "Tasks", "icon": "checkbox-circle", "title": "{input.id}", "output": "table", "columns": ["id", "title", "status"] }],
|
|
73
|
+
"integration": {
|
|
74
|
+
"name": "Acme",
|
|
75
|
+
"description": "Tasks from Acme",
|
|
76
|
+
"token": {
|
|
77
|
+
"apiOrigin": "https://api.acme.example",
|
|
78
|
+
"account": { "path": "/me", "name": "login" },
|
|
79
|
+
"scheme": "bearer"
|
|
80
|
+
},
|
|
81
|
+
"settings": [{ "id": "list-id", "label": "List ID" }]
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
- `version` is required semver. Settings → Extensions shows it on the card.
|
|
89
|
+
- `apiVersion` is `1`. Anything else is refused.
|
|
90
|
+
- `engines.aiworker` is optional (`1.24.0` or `>=1.24.0`). Older AiWorker builds refuse the install.
|
|
91
|
+
- `panel.id` is kebab-case and unique. `icon` is a Remixicon name (`RiWindowLine` becomes `window`) or an SVG inside the folder. `entry` is the visible panel's HTML file. Leave `entry` out to have no rail icon or panel. Add `background.entry` for executable actions and commands; with neither entry, the extension can only declare `tools`, as in `examples/tools-only`.
|
|
92
|
+
- `background: { "entry": "background/index.html" }` supplies separate sandboxed HTML for background actions and slash commands. Its scripts must be built. It runs on demand, not continuously or at installation. A panel can coexist with it.
|
|
93
|
+
- `attach` is optional. `"dialog"` opens the page in a window from the + menu next to the chat box; `true` or `"panel"` opens the rail panel instead. `ctx.surface` tells the page which one it is in. The object form `{ "mode": "dialog", "entry": "panel/attach.html" }` gives the window its own page. When the user clicks the attached chip, the page opens again with that item in `ctx.item` (`null` from the + menu), so it can show the item instead of the list.
|
|
94
|
+
- `actions` is optional: menu entries on messages (`where: "message"`, optionally only `roles: ["assistant"]`) and on sessions (`where: "session"`). By default, picking one opens your page with that message or session in `ctx.item` (`kind: "message"` with the text, or `kind: "session"`; add `payload: ["messages"]` to get the conversation too). Set `mode: "background"` to call `onAction` without opening UI, as shown below. Up to 8.
|
|
95
|
+
- `commands` is optional: slash commands for the chat box, up to 8. `/task DEMO-2` calls your `host.onResolve` handler instead of the model; return a chip to attach it, or `null` for nothing. A name the app already has is ignored.
|
|
96
|
+
- `tools` is optional: how your tool calls look in the chat, up to 16, no code. `match` is the tool name OpenCode reports (`mcp.tasks.*` matches every tool under that prefix); `name` and `icon` (a Remixicon name or an SVG inside the folder, like `panel.icon`) set the header, `title` and `subtitle` are templates like `{input.id}` or `{output.total} open`, and `output` picks the body: `text`, `json`, `markdown`, `code` (with `language`), or `table` (with `columns`, rows from the output array or `output.items`). Leave `output` out to keep the app's own detection.
|
|
97
|
+
- `capabilities` lists what needs the user's approval: `prompt` to send messages, `sessions` to create sessions and worktrees, `files` to read and write inside the open project, `model` for one-off text generation with the user's Small Model (`host.generate`, no session involved). An `integration` adds `network`, a `service` adds `service`, `filesystem` patterns (like `["~/.config/opencode/opencode.json"]`) add `filesystem`, which lets `readFile`, `writeFile`, `listDir`, and `stat` reach those paths outside the project, and a session action with `payload: ["messages"]` adds `conversation`. The user approves the whole list once at install. Calls outside it fail with `NOT_GRANTED`.
|
|
98
|
+
- `integration` is optional. It adds a card at Settings → Integrations. `token` takes a pasted API token (`scheme: "bearer"` for `Authorization: Bearer`, `"basic"` for a username and token pair as Jira Cloud wants), `oauth` runs an authorize flow with a pasted client id, and `host: { "provider": "linear" }` reuses the Linear account already connected in AiWorker. The page never sees the token; AiWorker makes the calls through `host.request`.
|
|
99
|
+
- `service` is optional. It declares a local process AiWorker starts next to the extension. It runs with the user's full access and no sandbox, so declare one only when the page cannot do the job. See [GUEST_SERVICES.md](./GUEST_SERVICES.md).
|
|
100
|
+
|
|
101
|
+
## Actions without opening a panel
|
|
102
|
+
|
|
103
|
+
Set `mode: "background"` on a message or session action to run it without opening the rail or a dialog:
|
|
104
|
+
|
|
105
|
+
```json
|
|
106
|
+
{ "id": "message-length", "label": "Show message length", "where": "message", "mode": "background" }
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Register `onAction` immediately after `connectHost` in your background script, or your panel script when no background entry is declared:
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
const host = connectHost();
|
|
113
|
+
host.onAction(async (item) => {
|
|
114
|
+
if (item.kind === 'message' && item.action === 'message-length') {
|
|
115
|
+
await host.toast({
|
|
116
|
+
kind: 'info',
|
|
117
|
+
message: `Message length: ${item.text.length} characters.`,
|
|
118
|
+
copy: true,
|
|
119
|
+
dismiss: true,
|
|
120
|
+
persistent: true,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Each click runs in a fresh hidden iframe. Await all work inside the handler, including the toast. AiWorker removes the iframe when the handler finishes and reports thrown errors as toasts. Loading and execution together have a 20-second limit. A runtime switch, disabling the extension, or withdrawing approval also ends the invocation. Completed side effects are not rolled back or retried.
|
|
127
|
+
|
|
128
|
+
`ctx.surface` is `"background"`; skip drawing your UI in that case. `ctx.item` stays `null`, so the action runs only through `onAction`, without repeated `onItem` snapshots. The session and directory context stay with the clicked target even if the user changes chats. AiWorker loads `background.entry` when declared and falls back to `panel.entry` for existing extensions. Omit `mode`, or use `"open"`, to open a visible panel or dialog. See `examples/hello-kit` for separate panel and background scripts.
|
|
129
|
+
|
|
130
|
+
To remove the panel and its rail icon entirely, use this `contributes` block:
|
|
131
|
+
|
|
132
|
+
```json
|
|
133
|
+
{
|
|
134
|
+
"panel": { "id": "message-tools", "name": "Message Tools", "icon": "apps" },
|
|
135
|
+
"background": { "entry": "background/index.html" },
|
|
136
|
+
"actions": [
|
|
137
|
+
{ "id": "message-length", "label": "Show message length", "where": "message", "mode": "background" }
|
|
138
|
+
]
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
The `panel` object retains the extension's identity for Settings and approval dialogs; only `panel.entry` creates a visible panel. Without it, every action must use `mode: "background"`, and `attach` and `page` cannot open a view. Slash commands call `onResolve` in the background entry, with `ctx.surface` also set to `"background"`. Capabilities, services, integrations, storage, and file access use the same approval checks. An attached chip can still be sent to the model or opened with its browser button; clicking to reopen the extension shows a no-panel notice.
|
|
143
|
+
|
|
144
|
+
This addition requires a matching AiWorker build. For unreleased SDK preview builds, use the app built from the same revision.
|
|
145
|
+
|
|
146
|
+
## Toast buttons
|
|
147
|
+
|
|
148
|
+
`host.toast` accepts three optional fields:
|
|
149
|
+
|
|
150
|
+
- `copy: true` adds Copy for the displayed message. Use `copy: { text: "..." }` to copy a different value, up to 32,000 characters. Whitespace in that value is preserved.
|
|
151
|
+
- `dismiss: true` adds OK to close the toast.
|
|
152
|
+
- `persistent: true` keeps the toast on screen until dismissed and always adds OK, even if `dismiss` is false.
|
|
153
|
+
|
|
154
|
+
Copy keeps the toast open and shows Copied on success. A failed copy shows an error beside the button so the user can retry. AiWorker translates the buttons and handles clicks itself, so they work after a background action's iframe has closed. `await host.toast(...)` waits only for the host to show the toast, not for a button click. Omit these fields for an ordinary timed toast.
|
|
155
|
+
|
|
156
|
+
## In the page
|
|
157
|
+
|
|
158
|
+
For a full-screen board, add `"page": true` under `contributes`, or `"page": { "entry": "panel/page.html", "title": "Board" }` for separate HTML. Users open it from the Extension pages menu above the session list. `ctx.surface` is `"page"`. The extension cannot open the page itself.
|
|
159
|
+
|
|
160
|
+
With `sessions` approved, use `listProjects()`, `listWorktrees(projectId)`, and `listSessions(projectId)`. Subscribe through `await onProjects(listener)`, `await onWorktrees(projectId, listener)`, or `await onSessions(projectId, listener)` and retain the returned unsubscribe function. Snapshots distinguish loading, ready, and error; session activity and observed turn outcomes are separate from your task status.
|
|
161
|
+
|
|
162
|
+
`startSession` accepts `projectId` and `worktree: { kind: "new", name: "fix-login", baseBranch: "main" }` or `{ kind: "existing", directory }`. It preserves the current screen by default. `openSession(sessionId)` explicitly opens the chat. `host.storage.get/set/delete/keys` stores your own JSON on the connected server without a file-access grant. See [API.md](./API.md) for limits and partial results. The `tasks-demo` page exercises these methods together.
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
import { connectHost, HostRequestError } from '@aiworker/sdk';
|
|
166
|
+
|
|
167
|
+
const host = connectHost();
|
|
168
|
+
|
|
169
|
+
host.onReady((ctx) => {
|
|
170
|
+
document.body.dataset.theme = ctx.theme.mode;
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
host.onSession((session) => {
|
|
174
|
+
document.querySelector('#session')!.textContent = session?.title ?? '';
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
const user = await host.request({ method: 'GET', path: '/me' });
|
|
179
|
+
} catch (error) {
|
|
180
|
+
if (error instanceof HostRequestError && error.code === 'DISCONNECTED') {
|
|
181
|
+
await host.oauthStart();
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
await host.toast({ kind: 'info', message: 'Hello' });
|
|
186
|
+
await host.compose({ text: 'Ask about the latest diff' });
|
|
187
|
+
await host.attach({
|
|
188
|
+
providerId: 'acme-hello',
|
|
189
|
+
id: 'TICKET-1',
|
|
190
|
+
title: 'Login is broken',
|
|
191
|
+
url: 'https://example.com/TICKET-1',
|
|
192
|
+
});
|
|
193
|
+
await host.startSession({
|
|
194
|
+
providerId: 'acme-hello',
|
|
195
|
+
id: 'TICKET-1',
|
|
196
|
+
title: 'Login is broken',
|
|
197
|
+
url: 'https://example.com/TICKET-1',
|
|
198
|
+
worktree: true,
|
|
199
|
+
text: 'Optional first message',
|
|
200
|
+
});
|
|
201
|
+
await host.prompt({ text: 'Fix the login', send: true });
|
|
202
|
+
await host.setBadge(3); // number on the rail icon; null clears it
|
|
203
|
+
const { text } = await host.generate({ prompt: task.description, system: 'One-line summary only.' }); // capability model
|
|
204
|
+
|
|
205
|
+
host.onResolve(({ command, args }) => {
|
|
206
|
+
// the user typed /task DEMO-2
|
|
207
|
+
const task = findTask(args.trim());
|
|
208
|
+
return task ? { providerId: 'acme-hello', id: task.id, title: task.title, url: task.url } : null;
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
host.onItem((item) => {
|
|
212
|
+
if (item?.kind === 'message') showMessage(item.text); // "Create task from message"
|
|
213
|
+
if (item?.kind === 'session') showSummary(item.messages); // "Summarize session"
|
|
214
|
+
});
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Every method, its limits, and the error codes are on the [Host API](https://openchamber.dev/docs/sdk/host/) page.
|
|
218
|
+
|
|
219
|
+
## UI kit
|
|
220
|
+
|
|
221
|
+
`@aiworker/sdk/ui` has buttons, fields, a searchable dropdown, checkboxes, tabs, badges, lists, empty states, spinners, banners, separators, progress bars, menus, and safe text, all drawn with the app's colours and fonts. Apply `applyHostReady` on every `onReady`, but mount controls and register listeners once. Repeated snapshots must not erase inputs or drafts. Every mount returns `{ update, dispose }`. Use `update` to pass changed values back to controls, including `tabs.update({ activeId })` and `select.update({ value })` inside `onChange`. See the [UI kit examples](https://docs.aiworker.dev/sdk/ui/) for input state and tab switching.
|
|
222
|
+
|
|
223
|
+
```ts
|
|
224
|
+
import { applyHostReady, mountList } from '@aiworker/sdk/ui';
|
|
225
|
+
|
|
226
|
+
let mounted = false;
|
|
227
|
+
host.onReady((ctx) => {
|
|
228
|
+
applyHostReady(ctx, document.documentElement);
|
|
229
|
+
if (mounted) return;
|
|
230
|
+
mounted = true;
|
|
231
|
+
mountList(document.querySelector('#root')!, {
|
|
232
|
+
items: tasks.map((task) => ({ id: task.id, leading: task.key, title: task.title })),
|
|
233
|
+
onSelect: (id) => {
|
|
234
|
+
const task = tasks.find((item) => item.id === id);
|
|
235
|
+
if (task) void host.attach({ providerId: 'acme-hello', id, title: task.title, url: task.url });
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
AiWorker supplies thin, theme-aware native scrollbars inside extension documents, including nested lists, tabs, and textareas. The UI kit includes the same defaults for development previews. Existing installed bundles get the host stylesheet without rebuilding. Custom rendering hosts can use `GUEST_SCROLLBAR_CSS` from `@aiworker/sdk`. Authors can override these default rules; an extension's CSP still applies.
|
|
242
|
+
|
|
243
|
+
## Schemas
|
|
244
|
+
|
|
245
|
+
`@aiworker/sdk/schemas` exports the zod schemas for the manifest and the messages, for tools that validate extensions. The main entry has no zod dependency, so a page bundle stays small.
|
|
246
|
+
|
|
247
|
+
## Scope
|
|
248
|
+
|
|
249
|
+
This package covers the page, the manifest, the messages, and the UI kit. The page gets no terminal, no git, no files outside what it declared, and no access to AiWorker's React tree. A declared `service` is different: it is a real process with the user's rights, so it can do anything the user can, and the approval dialog says so. `apiVersion` 1 is frozen; new methods arrive with the app's releases and this package's version.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Wire envelope `v`. Guest postMessage and host pushes use this. */
|
|
2
|
+
export declare const AIWORKER_SDK_CHANNEL = "aiworker.sdk";
|
|
3
|
+
export declare const AIWORKER_SDK_API_VERSION = 1;
|
|
4
|
+
/** Manifest `apiVersion` values this host accepts. Service and sockets ship on `1`. */
|
|
5
|
+
export declare const AIWORKER_SDK_MANIFEST_API_VERSIONS: readonly [1];
|
|
6
|
+
export type AiWorkerManifestApiVersion = (typeof AIWORKER_SDK_MANIFEST_API_VERSIONS)[number];
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Wire envelope `v`. Guest postMessage and host pushes use this. */
|
|
2
|
+
export const AIWORKER_SDK_CHANNEL = 'aiworker.sdk';
|
|
3
|
+
export const AIWORKER_SDK_API_VERSION = 1;
|
|
4
|
+
/** Manifest `apiVersion` values this host accepts. Service and sockets ship on `1`. */
|
|
5
|
+
export const AIWORKER_SDK_MANIFEST_API_VERSIONS = [1];
|