@openchamber/sdk 2.5.0 → 2.5.1

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 CHANGED
@@ -141,7 +141,7 @@ Access tokens never appear in `ready` or in request results.
141
141
  | Method | Arguments | Returns | Behavior |
142
142
  | ----------------- | --------------------------------- | ------------------------------ | ------------------------------------------------------------------------------------- |
143
143
  | `toast` | `{ kind: 'info' \| 'success' \| 'error', message, copy?, dismiss?, persistent? }` | `Promise<void>` | Show a toast, optionally with host-owned Copy and OK buttons; see Toast buttons below |
144
- | `openUrl` | `url: string` | `Promise<void>` | Open URL in the host |
144
+ | `openUrl` | `url: string` | `Promise<void>` | Open an http(s) URL. In the desktop app with a project open, a local address (`localhost`, `127.0.0.1`, `[::1]`) opens in the browser panel beside the chat, and other links follow the user's setting for chat links; elsewhere the system browser opens it |
145
145
  | `openSurface` | `surfaceId: string` | `Promise<void>` | Show that surface; resolves once it is on screen, rejects `NO_DIRECTORY` or `UNSUPPORTED` when it cannot open |
146
146
  | `writeClipboard` | `text: string` | `Promise<void>` | Copy in the host (1–32000 chars) |
147
147
  | `compose` | `{ text, mode?: 'append' | 'replace' }` | `Promise<void>` |
@@ -519,7 +519,8 @@ Used by the OpenChamber host and by tools that validate packages. Guests rarely
519
519
  | `fileEditors` | Optional, 1–8 editors: unique kebab-case `id`, `title` 1–60 chars, `match` 1–16 file-name patterns (no `/`, not only wildcards), `entry` package `.html` with built scripts, optional `content` `"text"` (default) or `"binary"` (`invalid-file-editors`). See File editors above |
520
520
  | `filesystem` | Optional, 1–16 globs, each 1–256 chars, starting with `/` or `~/`; `**` spans folders, `*` / `?` stay in one segment; no `..`, empty segment, or backslash (`invalid-filesystem`). Declaring it adds the `filesystem` capability and the dialog lists the globs |
521
521
  | `integration` | Optional. Exactly one of `oauth`, `token`, or `host` (`provider: "linear"` only) |
522
- | `service` | Optional. `entry` must be a built `.js` file on disk. `provides: ["browser"]` makes it the agent's browser when the user selects it; `surface: true` gives it a host-drawn live panel the user can take over (no `panel.entry` then). Neither needs a panel or background entry. See [GUEST_SERVICES.md](https://github.com/openchamber/openchamber/blob/main/packages/sdk/GUEST_SERVICES.md) |
522
+ | `service` | Optional. `entry` must be a built `.js` file on disk. `provides: ["browser"]` makes it the agent's browser when the user selects it; `provides: ["source-control"]` makes it a Git host and needs `sourceControl`; `surface: true` gives it a host-drawn live panel the user can take over (no `panel.entry` then). Neither needs a panel or background entry. See [GUEST_SERVICES.md](https://github.com/openchamber/openchamber/blob/main/packages/sdk/GUEST_SERVICES.md) |
523
+ | `sourceControl` | Required with `provides: ["source-control"]`, refused without it. The Git host the service implements: `wire: 1`, `label`, `terminology`, `referencePrefix`, `repositoryPath`, `instances`, `capabilities`, `token`, `transport`, `limits`, `trackedItems`, `presentation`. Strict; URL templates use only `{owner}`, `{namespace+}`, `{repo}`, `{number}`. `panel.id` is at most 28 characters (the host id is `ext-<panel.id>`). Change-based hosts may add the `change` term, `capabilities.reviewModel` and `capabilities.revisions`, the `submit` merge method (alone), `presentation.features.submitRequirements`, and `token.scheme: "basic"` with `token.username: true`; older hosts refuse these, so set an `engines.openchamber` floor. Errors are `invalid-service`. See [GUEST_SERVICES.md](https://github.com/openchamber/openchamber/blob/main/packages/sdk/GUEST_SERVICES.md#source-control-provider-provides-source-control) |
523
524
 
524
525
 
525
526
  Extra keys are dropped, not forwarded.
@@ -535,7 +536,8 @@ Extra keys are dropped, not forwarded.
535
536
  | `resolveAttachEntry(contributes)` | Dialog page from the object form, or `null` when the dialog reuses `panel.entry` |
536
537
  | `hasGuestPage(contributes)` | `true` when `panel.entry` is set; background-only and tools-only packages return `false` |
537
538
  | `resolveIntegrationAuth` / `resolveIntegrationApi` | Auth kind and API origin |
538
- | `toPublicIntegration` / `toPublicService` | Catalog-safe public slices |
539
+ | `toPublicIntegration` / `toPublicService` / `toPublicSourceControl` | Catalog-safe public slices |
540
+ | `sourceControlProviderIdOf(guestId)` | The provider id a Git host extension gets (`ext-<guestId>`) |
539
541
  | `isGuestPackageSvgIcon` | Whether icon is a package SVG path |
540
542
  | `compareOpenChamberVersions` | Semver compare |
541
543
  | `hostMeetsOpenChamberEngine` | Host vs `engines.openchamber` floor |
@@ -581,6 +583,8 @@ Manifest sketch: `service.entry` (path to **built** JS, e.g. `service/main.js`),
581
583
 
582
584
  A service with `provides: ["browser"]` answers the agent's `browser.*` actions at `POST /browser-control` instead of the desktop app's browser panel, once the user selects it in Settings → OpenChamber Tools. The host starts it on the first action and stops it when idle. Parse the body with `readBrowserProviderRequest`; request and answer types (`BrowserProviderRequest`, `BrowserProviderResult`, per-action `Browser*Parameters` / `Browser*Data`) and the limits (`BROWSER_PROVIDER_*`) are exported from `@openchamber/sdk`. Full contract in [GUEST_SERVICES.md](https://github.com/openchamber/openchamber/blob/main/packages/sdk/GUEST_SERVICES.md#browser-provider-provides-browser).
583
585
 
586
+ A service with `provides: ["source-control"]` is a Git host next to GitHub, GitLab, Gitea and Gerrit, described by `contributes.sourceControl`. OpenChamber keeps the accounts and tokens and forwards reads and mutation steps to `POST /source-control` (`SOURCE_CONTROL_PROVIDER_PATH`); parse the body with `readSourceControlProviderRequest` and answer `{ ok, data }` or `{ ok: false, error }` (`SourceControlProviderResult`). The service reaches the forge only through `createSourceControlForge({ env: process.env }).request(handle, request)`, which goes through OpenChamber's outbound proxy (`OPENCHAMBER_HOST_CALLBACK_URL`); OpenChamber adds the token and keeps the request on the instance. Methods (`SOURCE_CONTROL_PROVIDER_METHODS`), inputs (`SourceControlProviderInputs`), deadlines (`sourceControlProviderTimeoutMs`) and caps (`SOURCE_CONTROL_*`) are exported from `@openchamber/sdk`; the host's answer schemas are `sourceControlProviderSchemasV1` in `@openchamber/sdk/schemas`. Full contract in [GUEST_SERVICES.md](https://github.com/openchamber/openchamber/blob/main/packages/sdk/GUEST_SERVICES.md#source-control-provider-provides-source-control); reference: `examples/source-control-provider`.
587
+
584
588
  A service with `surface: true` shows a live picture in the extension's rail panel: the host pulls frames from `GET /surface/frame`, draws them, sends the user's input to `POST /surface/input`, and owns who is in control (nobody, the agent, the user). Paths, event types, and the `readSurface*` parsers are exported from `@openchamber/sdk`; contract in [GUEST_SERVICES.md](https://github.com/openchamber/openchamber/blob/main/packages/sdk/GUEST_SERVICES.md#shared-surface-surface-true).
585
589
 
586
590
  Bundle the service with the Node target:
package/DOCUMENTATION.md CHANGED
@@ -17,7 +17,7 @@ Public author pages live in `packages/docs/content/docs/sdk.mdx`, `sdk/host.mdx`
17
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, `openchamber serve`). Inside the monorepo, Vite aliases and tsconfig `paths` resolve `@openchamber/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 `@openchamber/web`; `scripts/bump-version.mjs` bumps it with the app, so the SDK version always equals the OpenChamber version it was released with.
18
18
  - `src/file-editor.ts`: the `contributes.fileEditors` contract: limits, file-name pattern check and match, the document/snapshot/change types, and `createFileSaveTracker`. Zod-free, so a guest editor bundles it.
19
19
  - `GUEST_SERVICES.md`: local service contract (`contributes.service`, `serviceRequest`, grant, loopback).
20
- - `src/service-providers.ts`: contracts a service fulfils for a host role (`contributes.service.provides`). Today only `browser`: the ten `browser.*` actions, their parameter and answer types, the limits, `BrowserProviderContext` (the calling project and chat, filled by the host from the tool call; `null` fields mean unknown), and `readBrowserProviderRequest` (a zod-free boundary parse for the service side). `src/service-provider-schemas.ts` is the host's zod parse of the answer, exported from `@openchamber/sdk/schemas`. A providing service needs no panel or background entry (`runtimeContributions` in `parse.ts`). The host side is `packages/web/server/lib/browser-control/provider.js`.
20
+ - `src/service-providers.ts`: contracts a service fulfils for a host role (`contributes.service.provides`). `source-control` is described under Source-control role below. `browser`: the twelve `browser.*` actions, their parameter and answer types, the limits, `BrowserProviderContext` (the calling project and chat, filled by the host from the tool call; `null` fields mean unknown), and `readBrowserProviderRequest` (a zod-free boundary parse for the service side). `src/service-provider-schemas.ts` is the host's zod parse of the answer, exported from `@openchamber/sdk/schemas`. A providing service needs no panel or background entry (`runtimeContributions` in `parse.ts`). The host side is `packages/web/server/lib/browser-control/provider.js`.
21
21
  - `src/service-surface.ts`: the shared-surface contract (`contributes.service.surface`): loopback paths and headers, input event types, limits, and the zod-free `readSurfaceInputBatch` / `readSurfaceControlNotice` / `readSurfaceResizeRequest` for the service side. `src/service-surface-schemas.ts` (host entry) holds the viewer↔host socket messages (`surfaceViewerMessageSchema`, `surfaceHostMessageSchema`) shared by `packages/ui/src/lib/guests/surface-client.ts` and `packages/web/server/lib/guests/surface.js`, plus the service answer schemas. A surface service needs no entry of its own; with `panel.entry` that page is docked to one edge of the picture, `panel.dock` (`GUEST_SURFACE_DOCKS`) and `panel.size` px (`GUEST_SURFACE_DOCK_SIZE_*`), only valid with both. The rail panel is `packages/ui/src/components/layout/GuestSurfacePane.tsx` (host-drawn canvas, not an iframe); `ContextPanel` lays the page iframe (kept mounted) beside the pane (mounted only while shown) per `DOCK_LAYOUT`, and `lib/guests/surfaces.ts#guestSurfaceDocking` decides. Viewer identity is host-set: input carries `x-surface-viewer` and `x-surface-frame-seq` (the service's own number of the frame that viewer last drew; a 409 refuses it as stale), control notices carry `viewer`, and page `serviceRequest`s from a window with a live viewer (`packages/ui/src/lib/guests/surface-viewers.ts`) also carry `x-surface-viewer-controls`.
22
22
  - `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 `@openchamber/sdk/ui`. Do not import this from `packages/ui`. Kit changes follow package semver, not `apiVersion`.
23
23
 
@@ -79,17 +79,17 @@ Git installation accepts HTTPS, `ssh://` and scp-style SSH addresses. `POST /api
79
79
 
80
80
  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.
81
81
 
82
- `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.
82
+ `lib/guests/useGuestFrameUrl.ts` owns iframe URL lifetime. A direct frame loads by a grant in the path (`/api/guest-files/<grant>/…`, see `packages/web/server/lib/guests/DOCUMENTATION.md`), or by the guest-scoped query token from a server without grants. It retains the credential'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.
83
83
 
84
- `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 OpenChamber 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.
84
+ `GET /api/guests` plus `PluginPane` on the desktop/web rail. The iframe loads from `/api/guest-files/<grant>/<entry>` with a grant minted for that extension (`resolveGuestFrameUrl`); against a server without grants it falls back to a token minted for `guest:<id>` scope, 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 OpenChamber 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.
85
85
 
86
86
  **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.
87
87
 
88
88
  **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.
89
89
 
90
- 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`. Every guest document also runs under `guestFramePolicy` (`src/frame-policy.ts`): the server sends it with `sandbox allow-scripts` on each served file, the relay loader as a `<meta>`. It keeps the guest off the network: scripts, styles, images, fonts, media, workers and nested frames load only from the package (`'self'`) or `data:`/`blob:`, and `connect-src` is the guest's own `/api/guests/<id>/` path (named by the request's `Host`, since a sandboxed document has no origin for `'self'`; `'none'` in relay). `'self'` for connections would reach the whole OpenChamber API. External data goes through the host (`request` to the declared, approved `apiOrigin`) or to `contributes.origins`: 1 to `GUEST_ORIGINS_MAX` (8) plain https origins (literal hostnames, so no `*` or other CSP syntax can widen them), granted as the `origins` capability. Its scope is the sorted list (`grant-scope.js`), so an update that adds an origin is unapproved until the dialog runs again. Approved origins join `connect-src`, `img-src`, `font-src`, `style-src` and `media-src`, never `script-src`/`worker-src`/`frame-src`: code from elsewhere could change after approval. The server reads them from the effective grants when it serves a file; relay frames get them from `PluginPane` (`approvedOrigins`, part of the frame key so an approval reloads the frame). External CDN assets not declared there do not load; bundle them. Guest files are served with `Access-Control-Allow-Origin: null`, because fonts and fetch from the opaque-origin frame are CORS requests. A guest navigating its own frame elsewhere is refused by the desktop shell (`packages/electron/guest-frame-navigation.mjs`, `will-frame-navigate`); the web runtime has no such hook. 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.
90
+ 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`. Every guest document also runs under `guestFramePolicy` (`src/frame-policy.ts`): the server sends it with `sandbox allow-scripts` on each served file, the relay loader as a `<meta>`. It keeps the guest off the network: scripts, styles, images, fonts, media, workers and nested frames load only from the package (`'self'`) or `data:`/`blob:`, and `connect-src` is the guest's own package path, `/api/guest-files/<grant>/` (or `/api/guests/<id>/` on the query-token route), named by the request's `Host`, since a sandboxed document has no origin for `'self'`; `'none'` in relay. `'self'` for connections would reach the whole OpenChamber API. External data goes through the host (`request` to the declared, approved `apiOrigin`) or to `contributes.origins`: 1 to `GUEST_ORIGINS_MAX` (8) plain https origins (literal hostnames, so no `*` or other CSP syntax can widen them), granted as the `origins` capability. Its scope is the sorted list (`grant-scope.js`), so an update that adds an origin is unapproved until the dialog runs again. Approved origins join `connect-src`, `img-src`, `font-src`, `style-src` and `media-src`, never `script-src`/`worker-src`/`frame-src`: code from elsewhere could change after approval. The server reads them from the effective grants when it serves a file; relay frames get them from `PluginPane` (`approvedOrigins`, part of the frame key so an approval reloads the frame). External CDN assets not declared there do not load; bundle them. Guest files are served with `Access-Control-Allow-Origin: null`, because fonts and fetch from the opaque-origin frame are CORS requests. A guest navigating its own frame elsewhere is refused by the desktop shell (`packages/electron/guest-frame-navigation.mjs`, `will-frame-navigate`); the web runtime has no such hook. 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.
91
91
 
92
- 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.
92
+ File calls go through `POST /api/guests/:id/files` with the `x-opencode-directory` header of the open project (`lib/guests/files.ts`). The header is handed over as plain entries so the runtime can carry a project path outside Latin-1: such a value is sent percent-encoded with `x-opencode-directory-encoding: uri` and decoded again by `resolveOptionalProjectDirectory`. `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.
93
93
 
94
94
  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.
95
95
 
@@ -133,6 +133,39 @@ The SDK adds `HostActionMessage`, `GuestActionResultMessage`, `GuestActionItem`,
133
133
 
134
134
  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.
135
135
 
136
+ ## Source-control role
137
+
138
+ `provides: ["source-control"]` plus `contributes.sourceControl` (`SourceControlContribution`, `manifest.ts`) make an extension a Git host. The SDK owns the manifest parse, the wire (`service-providers.ts`: `SOURCE_CONTROL_PROVIDER_PATH`, `SOURCE_CONTROL_PROVIDER_METHODS`, deadlines and caps, `readSourceControlProviderRequest`, the outbound types, `createSourceControlForge`) and the host's answer schemas (`sourceControlProviderSchemasV1` in `service-provider-schemas.ts`, from `@openchamber/sdk/schemas`). The host adapter that turns the contribution into a provider descriptor, the callback listener and the outbound proxy live in `packages/web/server/lib/source-control` and `lib/guests`. The service-side contract is in `GUEST_SERVICES.md`; `examples/source-control-provider` and its fake forge are the reference, run by `scripts/source-control-example.test.ts`.
139
+
140
+ - The role and the contribution come together: either alone fails as `invalid-service`, as does a `panel.id` longer than `SOURCE_CONTROL_GUEST_ID_MAX` (28), because the host id is `ext-<panel.id>` within 32 characters. Contribution field errors are `invalid-service` too, and the message starts with the failing field (`contributes.sourceControl.instances.defaultInstances[0]`), refined values included.
141
+ - Every contribution object is `.strict()`. Capabilities and `presentation.features` default to `false`; review threads, uploads, stacks, the reference picker, multiple accounts, `createVia` and `branchHeads` are not declarable.
142
+ - The change-based fields (`terminology.changeRequest: "change"`, `capabilities.reviewModel`, `capabilities.revisions`, the `submit` merge method, `presentation.features.submitRequirements`, `token.scheme: "basic"` with `token.username`) are optional with no default, so a manifest without them parses to the same object as before and the host builds the same descriptor. Two refines hold: `submit` is the only merge method when present, and `basic` comes with `username: true` and the other way round.
143
+ - The status schema accepts `localRevision` and `stack`; the vote labels, submit requirements and revisions ride on the context, which stays a pass-through read.
144
+ - URL templates accept literal segments and the `SOURCE_CONTROL_URL_TOKENS` placeholders only, each a whole segment, with `{repo}` and `{number}` once and the owner token matching `repositoryPath`. No template can carry regex syntax, a `.` or `..` segment (a normalized URL never matches one), or appear in both `changeRequest` and `issue` (change request patterns match first, so every issue link would read as a change request).
145
+ - `readSourceControlProviderRequest` checks the envelope (`v`, `requestId`, `method`, `provider`, `session`, plain-object `input`), not each method's input: the host built and validated it, and the service bundle stays free of zod.
146
+ - The answer schemas check deeply only what the host acts on (account, probe, head, source, mutation target/result/reconcile, live summaries, status). DTO reads the UI parses itself pass as any JSON object or array within `SOURCE_CONTROL_PROVIDER_RESPONSE_MAX`. `mutationTarget` and `mutationResult` hold exactly the keys `mutation-storage.js` accepts; keep them in step.
147
+ - `SOURCE_CONTROL_PROVIDER_METHODS` is one literal array. Spreading sub-lists into it keeps the arrays in every panel bundle, because Bun cannot drop an array spread.
148
+
149
+ ### Why the Git host is declared in the manifest and not answered by the service
150
+
151
+ Descriptors are read synchronously by bindings, the Git planner and tracked items, and listing providers must never start a third-party process. In the manifest the description is parsed at install, shown in the approval dialog, and pinned by the grant scope.
152
+
153
+ ### Why OpenChamber names the provider `ext-<panel.id>` and not the extension
154
+
155
+ Guest ids are already unique and stable, and bindings, the auth section and mutation records persist the provider id. A derived id cannot collide with a built-in or another extension, and cannot change between versions.
156
+
157
+ ### Why tokens stay in OpenChamber and not in the service
158
+
159
+ Git transport needs the raw secret inside OpenChamber anyway, the auth store already handles accounts, revisions and invalidation, and the outbound proxy is the one place origin pinning, enterprise policy and audit can act. The service reaches the forge through a per-call handle. This is least exposure, not a sandbox.
160
+
161
+ ### Why change-based fields are additive on wire 1 and not a wire 2
162
+
163
+ The envelope and the methods are unchanged; `revision` on three reads and `votes` in a review's input are new optional fields. The host sends them only to a host whose manifest declares the matching capability, and a service built before them cannot declare it, so no service ever receives a field it would ignore and answer wrongly. A new wire would make every existing extension rebuild for nothing. The cost is that older hosts refuse a manifest that uses the fields, which `engines.openchamber` makes an install-time message.
164
+
165
+ ### Why OpenChamber owns mutation kinds and digests and not the service
166
+
167
+ The mutation store's kinds are a closed list and the digest decides whether a retry is a replay. A digest an extension computed could change between its versions and turn retained records into conflicts, or make two different writes look like one. The service resolves, performs and reconciles; the host decides what a failure means from what its proxy observed.
168
+
136
169
  ## Late generate answers
137
170
 
138
171
  `LateGenerateResult`, `ready.features.lateGenerateResults`, the guest notice `generate-late-listen` and the host push `generate-late` live here; `HostClient.onLateGenerateResult` sends the notice on registration and again on every `ready` while a listener exists. The UI host owns keeping: `packages/ui/src/lib/guests/generate-results.ts` holds the outcomes, `PluginPane` decides when a `generate` reply has no frame to go to and registers a listening frame.
package/GUEST_SERVICES.md CHANGED
@@ -88,7 +88,7 @@ Parse rules:
88
88
  - `contributes.service` is optional. A guest without `service` is HTML-only.
89
89
  - `service.entry` is a relative path inside the package. Ship compiled JS; the host never compiles TypeScript.
90
90
  - `service.runtime` phase 1 accepts only `"host"`.
91
- - `service.provides` is optional: roles the service stands in for on the host. Only `"browser"` exists today (below). A service that provides a role needs no `panel.entry` or `background.entry`; the host starts it itself.
91
+ - `service.provides` is optional: roles the service stands in for on the host, `"browser"` and `"source-control"` (below). A service that provides a role needs no `panel.entry` or `background.entry`; the host starts it itself.
92
92
  - `service.surface` is optional (`true`): the service shows a live picture the host draws in the extension's rail panel and takes the user's input back (below). With `panel.entry` too, that page is docked to one edge of the picture (`panel.dock`: `top`, `bottom`, `left`, or `right`, default `top`; `panel.size` in CSS px across that edge, 24 to 480, default 40); without it, the picture is the whole panel.
93
93
  - `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.
94
94
  - 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.
@@ -132,6 +132,7 @@ Env the host sets:
132
132
  - `OPENCHAMBER_SERVICE_PORT` — port to bind on `127.0.0.1`
133
133
  - `OPENCHAMBER_SERVICE_TOKEN` — shared secret
134
134
  - `OPENCHAMBER_SERVICE_SOCKETS` — JSON map `{ [socketId]: absolutePath }` for every binding that resolved (override or first existing candidate)
135
+ - `OPENCHAMBER_HOST_CALLBACK_URL` — only for a service that provides `source-control`: OpenChamber's callback listener for forge requests (see below)
135
136
 
136
137
  Inbound auth: every request, including ready, must send:
137
138
 
@@ -172,7 +173,7 @@ Ship `service/main.js` already built. Same packaging rule as `panel/main.js`.
172
173
 
173
174
  ## Browser provider (`provides: ["browser"]`)
174
175
 
175
- The host's `openchamber_web` tool gives agents eleven `browser.*` actions. By default a connected desktop app answers them with its browser panel; on a server with no desktop client the agent gets nothing. A service that declares `provides: ["browser"]` can answer instead, from a browser it runs itself (headless Chrome over CDP, for instance). The agent sees one tool either way.
176
+ The host's `openchamber_web` tool gives agents twelve `browser.*` actions. By default a connected desktop app answers them with its browser panel; on a server with no desktop client the agent gets nothing. A service that declares `provides: ["browser"]` can answer instead, from a browser it runs itself (headless Chrome over CDP, for instance). The agent sees one tool either way.
176
177
 
177
178
  The user picks the provider in Settings → General → OpenChamber Tools → Browser provider. The dropdown lists installed, enabled, fully approved extensions with the role; with none it shows only "OpenChamber Web", disabled. The choice is `browserProvider` in the instance's `settings.json` (`builtin` or the extension id) and is read on every action, so it applies to the next action without a restart. Pausing, removing, or withdrawing approval from the selected extension puts `builtin` back and every open client shows a toast saying so; the same happens on the next action if the extension became unusable any other way.
178
179
 
@@ -214,6 +215,7 @@ Parameters the host sends and `data` the service answers; types are exported fro
214
215
  | `browser.snapshot` | `selector?` (scope) | `url`, `title`, `scope`, `scrollY`, `maxScrollY`, `text`, `elements[]`, `viewport`, optional `textTruncated`/`textTotalChars`, `elementsTruncated`/`interactiveElementsOnPage`, `consoleProblems[]` |
215
216
  | `browser.click` | `selector?` or `text?` (visible label) | `clicked` (selector), `label`, `url` |
216
217
  | `browser.type` | `selector`, `value`, `submit` | `selector`, `url` |
218
+ | `browser.select` | `selector`, exactly one of `label` or `value` | `selector`, selected `label`, selected `value`, `url` |
217
219
  | `browser.scroll` | `direction?` (`up`/`down`/`top`/`bottom`) or `selector?` | `scrollY`, `maxScrollY`, `atTop`, `atBottom`, optional `scrolledTo`/`direction` |
218
220
  | `browser.back` / `browser.forward` | none | `url`, `title` |
219
221
  | `browser.inspect` | `selector` | `selector`, `tag`, `label`, `bounds`, `inViewport`, `styles` (computed, as strings) |
@@ -223,10 +225,134 @@ Parameters the host sends and `data` the service answers; types are exported fro
223
225
 
224
226
  Every action may carry `tabId` (`BrowserTabTarget`): an id from the `tabs` your snapshot listed (`[{ id, title, url, active }]`, `active` being the tab the user sees), passed through from the agent untouched. Without it, act on the tab the user sees, except `browser.open`: without `tabId` it opens a new background tab and answers its id as `tabId` (`BrowserOpenData`), so the agent never replaces the user's page. Refuse an id you did not issue with `ok: false`; never act on another tab instead. A provider with one page lists no tabs and refuses every id. `browser.close` is the exception to "the tab the user sees": it closes a tab the agent opened, so without `tabId` it closes the one this caller opened most recently and refuses a tab it did not open.
225
227
 
226
- `viewport` in answers is `{ mode, width, height }` (`mode` may be `custom`; `fill` has `null` sizes). Snapshot `elements` carry `selector`, `tag`, `bounds`, and only the fields that apply (`inViewport`, `type`, `role`, `label`, `disabled`, `missingAccessibleName`). Keep `text` and `elements` bounded yourself; report what was dropped with the truncation fields.
228
+ `viewport` in answers is `{ mode, width, height }` (`mode` may be `custom`; `fill` has `null` sizes). Snapshot `elements` carry `selector`, `tag`, `bounds`, and only the fields that apply (`inViewport`, `type`, `role`, `label`, `disabled`, `missingAccessibleName`). Native `select` elements additionally carry `multiple` and an `options[]` list in DOM order. Each option has its live `label`, `value`, `selected`, and `disabled` state; the list is capped at 32 entries and each label/value at 80 characters. `optionsTruncated: true` means the list or one of its strings was capped. A selected option outside the cap is not moved into the returned prefix. Keep `text`, `elements`, and select options bounded yourself; report what was dropped with the truncation fields.
229
+
230
+ `browser.select` uses exact string matching. It targets native single-value HTML `<select>` elements, rejects custom controls and multiple selects, and refuses disabled selects or matching options. A missing or ambiguous label/value is an error, so the service must leave the page unchanged.
227
231
 
228
232
  `examples/browser-provider-stub` is a checked-in provider with no browser: one in-memory page that answers every action. Install it to see the dropdown, the routing, and the idle stop before writing a real one.
229
233
 
234
+ ## Source-control provider (`provides: ["source-control"]`)
235
+
236
+ A service with this role is a Git host next to GitHub, GitLab, Gitea and Gerrit: its own card in Settings → Integrations, its pull requests and issues on the board, its badges on branches, worktrees from its pull requests. The role needs `contributes.sourceControl` in the same manifest, and the manifest needs it only with the role; either alone fails as `invalid-service`. Hosts older than the role refuse the manifest the same way, so declare an `engines.openchamber` floor.
237
+
238
+ OpenChamber names the host `ext-<panel.id>` (provider ids are at most 32 characters, so `panel.id` is at most `SOURCE_CONTROL_GUEST_ID_MAX`, 28). The id stays the same across versions; accounts, bindings and mutation records are stored under it. `sourceControlProviderIdOf(guestId)` spells it.
239
+
240
+ ### `contributes.sourceControl`
241
+
242
+ Declared in the manifest, not answered by the service: OpenChamber reads it to draw the card, route Git transport and recognise remotes without starting a process, and the approval dialog shows it. Every object is strict; an unknown key fails.
243
+
244
+ | Field | Contents |
245
+ |---|---|
246
+ | `wire` | `1` |
247
+ | `label` | Product name, 1–64 characters, shown untranslated |
248
+ | `terminology` | `{ changeRequest: "pullRequest" \| "mergeRequest" \| "change", ci: "checks" \| "pipeline" }` |
249
+ | `referencePrefix` | `{ changeRequest, issue }`, 1–4 characters each (`#`, `!`) |
250
+ | `repositoryPath` | `"owner/name"` or `"namespace/name"` (nested groups) |
251
+ | `instances` | `{ defaultInstances: up to 4 https origins, custom, allowLoopbackHttp, allowPort, placeholder }`. With `custom: false` the user can add only the default instances, and there must be at least one |
252
+ | `capabilities` | `projects`, `issues`, `changeRequests`, `draftChangeRequests`, `merge` (`false` or `{ methods: ["merge" \| "squash" \| "rebase" \| "submit"] }`, with `submit` the only method when present), `ci`, `comments`, `reviews`, `state`, `labels`, `reviewers`, `changeRequestPatch`, `liveSummaries`, `reviewModel?` (`"verdict"` by default, or `"votes"`), `revisions?`. One left out is off. Review threads, uploads, stacks, the reference picker, multiple accounts and how a change request is created are fixed by OpenChamber |
253
+ | `token` | `{ header: "authorization" \| "private-token", scheme: "bearer" \| "token" \| "basic" \| "none", username?, scopes, createPath? }`. `private-token` takes the bare token (`none`); `authorization` takes `Bearer`, `token` or `basic`. `basic` needs `username: true`, and `username: true` is refused with any other scheme |
254
+ | `transport` | `{ httpsUsername: { kind: "login" } \| { kind: "fixed", value }, appendDotGitToHttpsClone }`: how Git over HTTPS signs in with the stored token |
255
+ | `limits` | `{ commentMaxLength (≤ 1 000 000), reviewerId: "numeric" \| "login", labelsMax (≤ 100), reviewersMax (≤ 50) }` |
256
+ | `trackedItems` | `{ batchLimit }`, 1–50 items per `liveSummaries` call |
257
+ | `presentation` | `{ abbreviation (1–8), promptNoun, sharedNumberSpace, iconPath? (24×24 SVG path data), urls: { changeRequest[], issue[] }, features: { authorCanReview, issueCloseReason, submitRequirements? }, changeRequestTemplates[] }` |
258
+
259
+ `presentation.urls` are web path templates such as `/{owner}/{repo}/pulls/{number}`: `/`, then segments that are each a literal (`[A-Za-z0-9._~-]`) or one placeholder (`SOURCE_CONTROL_URL_TOKENS`). `{repo}` and `{number}` appear once, and the owner once as `{owner}` for `owner/name` paths or `{namespace+}` (one or more segments) for `namespace/name` paths. OpenChamber compiles them into the pattern that recognises a pasted link; an extension never supplies a regular expression. `changeRequestTemplates` are repository file paths (`.gitea/pull_request_template.md`), first match wins.
260
+
261
+ ### Change-based hosts
262
+
263
+ A host that reviews one change at a time, with numbered revisions and per-label votes (Gerrit is the model), declares the fields below. Each is optional, and a manifest without them parses, installs and publishes exactly as before. Hosts older than these fields refuse a manifest that uses them as `invalid-service`, because every object is strict. Declare an `engines.openchamber` floor of the first release that has them, newer than 2.5.0.
264
+
265
+ | Field | Effect |
266
+ |---|---|
267
+ | `terminology.changeRequest: "change"` | The UI says "change" where other hosts say pull request or merge request |
268
+ | `capabilities.reviewModel: "votes"` | `/pr/review` takes `{ votes, body? }` instead of a verdict. `votes` maps a label name to an integer score. The labels, their ranges and the caller's permitted scores come with each change's context as `voteLabels`. A verdict sent to a votes host, or votes sent to a verdict host, is refused with a 400 before the service sees it |
269
+ | `capabilities.revisions: true` | `changeRequestContext`, `readChangeRequestPatch` and `readChangeRequestFile` may carry `revision`, a 40-hex commit sha, to read that revision. The context lists them as `revisions`, oldest first. Without the capability a read with `revision` is a 400 and never reaches the service |
270
+ | `capabilities.merge: { methods: ["submit"] }` | The merge button reads Submit. `/pr/merge` carries `{ method: "submit" }` |
271
+ | `presentation.features.submitRequirements: true` | The context carries `submitRequirements`, `[{ name, status, description? }]`, and the UI lists the unmet ones by the Submit button |
272
+ | `token: { header: "authorization", scheme: "basic", username: true }` | Settings asks for a username beside the token. The proxy sends `Authorization: Basic base64(<login>:<token>)` with the account's stored login. `auth.verify` goes out with the username the user typed; a login with a `:` or a control character is refused |
273
+
274
+ `changeRequestStatus` may add `localRevision: { sha, changeId, uploadedAs }` (the local tip, the change id in its commit message or `null`, the revision number it was uploaded as or `null`) and `stack: { number: null, base, layers }` with layers bottom first.
275
+
276
+ Creating a change by pushing to `refs/for/<branch>` stays host-only. An extension's host creates through `/pr/create`, and its change request heads are branches, so declare `changeRequests` only if the forge's API can open one. `auth.probe` never carries a token, with `basic` too: answer it from an anonymous request, or answer `{ supported: true }` when the instance refuses anonymous reads and let `auth.verify` decide.
277
+
278
+ The catalog row carries `toPublicSourceControl(contribution)`: `{ label, defaultInstances, custom }`, what the approval dialog shows.
279
+
280
+ ### Lifecycle
281
+
282
+ OpenChamber registers the host when the extension is installed, enabled, approved for `service`, and declares the role; pausing, withdrawing approval, or an update without the role unregisters it, and its routes answer 404 until it comes back. Tokens stay through a pause; uninstalling forgets them. The service starts on the first call, not at registration, and stops after `SOURCE_CONTROL_PROVIDER_IDLE_MS` (ten minutes) without one. Every read starts a cold service at worst, so do not cache across calls: OpenChamber keeps its own status cache and drops it after each write.
283
+
284
+ ### Calls
285
+
286
+ `POST /source-control` on the service loopback (`SOURCE_CONTROL_PROVIDER_PATH`), same bearer as every request:
287
+
288
+ ```json
289
+ {
290
+ "v": 1,
291
+ "requestId": "scm-…",
292
+ "method": "listIssues",
293
+ "provider": "ext-teahouse",
294
+ "session": { "instance": "https://git.example.com", "handle": "…", "user": { "id": 7, "login": "mona" } },
295
+ "input": { "context": { … }, "directory": "/Users/me/app", "remote": "origin", "page": 1, "state": "open", "people": "any" }
296
+ }
297
+ ```
298
+
299
+ `readSourceControlProviderRequest(body)` parses it (`null` → answer HTTP 400). `provider` is the id OpenChamber gave the host: put it in every DTO's `provider` field. `session.user` is `null` in `auth.verify` and `auth.probe`, before an account exists. `input` per method is `SourceControlProviderInputs`; the reads take the route shell's own input, with `directory` and `remote` as OpenChamber resolved them and `context` the binding it validated.
300
+
301
+ Answer HTTP 200 with `{ "ok": true, "data": … }` or `{ "ok": false, "error": { "message", "status"?, "code"? } }` (`message` ≤ 2000 characters, `status` 400–599, `code` `[A-Z_]{1,64}`). Any other status or shape is a malformed answer, never an empty one.
302
+
303
+ | Method | Answer OpenChamber checks |
304
+ |---|---|
305
+ | `auth.verify` | `{ user: { id, login, name?, email?, avatarUrl? } }`: who the candidate token belongs to; OpenChamber stores the account |
306
+ | `auth.probe` | `{ supported, version?, message? }`: whether the instance is this forge; the handle carries no token |
307
+ | `changeRequestStatus` | The branch's status as the badge shows it (`ChangeRequestStatus` in the UI): its open pull request, else its most recent closed or merged one, so a merged branch is not offered a new pull request |
308
+ | `listChangeRequests`, `changeRequestContext`, `listIssues`, `getIssue`, `issueComments`, `projectUpstream`, `projectBranches`, `listLabels`, `listReviewerCandidates` | A JSON object or array within `SOURCE_CONTROL_PROVIDER_RESPONSE_MAX`; the UI parses these DTOs itself |
309
+ | `readChangeRequestPatch` | `{ patch, meta: { owner, repo, number } }`: the change request's whole unified diff (`diff --git` per file) and the repository it was read from. Served to the Changes view and walkthroughs only when `capabilities.changeRequestPatch` is on. `sourceRepo`, when given, names the repository the change request lives in; read only within the bound repository's network |
310
+ | `readChangeRequestFile` | `{ original, modified }`: one file at the diff's base (the merge base) and at the head, for `path` (`previousPath` for the old side of a rename). `status` `A` has an empty `original`, `D` an empty `modified`. Same capability and network rule as the patch |
311
+ | `liveSummaries` | `{ summaries, issueSummaries }` for the refs asked; an item the forge cannot answer is left out, a 401 fails the batch |
312
+ | `readChangeRequestHead` | `{ project: { id, owner, name }, headSha (40 hex), headBranch, headOwner }` |
313
+ | `resolveChangeRequestSource` | `{ sourceProject, targetProject, classification, headSha, headRef, endpoint }`; OpenChamber checks the endpoint is on the instance and names `sourceProject`, and that the sha is the one it expected |
314
+ | `mutation.resolveTarget` | `{ providerTarget (≤ 4 KB JSON), target }`; `target` has exactly the keys OpenChamber stores and must match the context's target. On a replay, return `replayOf.target` unchanged |
315
+ | `mutation.perform` | Only the stored result keys (`number`, `merged`, `state`, `ready`, …) |
316
+ | `mutation.reconcile` | `{ state: "succeeded" \| "failed" \| "outcome-unknown", result? }`: after a restart, whether the write recorded as running landed |
317
+
318
+ A change request is in reach when the repository it targets is in the context's repository network (the primary remote's repository and its fork parent), as the built-in hosts do: refuse one whose target is outside it, and do not check where its head comes from. A maintainer's upstream clone reads, comments on, reviews and merges a contributor's fork pull request through the target repository, which the forge already authorises for the token. Creating one is the other direction: its `headRemote` must be in the network.
319
+
320
+ A capability opens the mutation routes below; one that is off answers 501 there and never reaches the service. `mutation.*` calls carry the route's `kind`, and its normalized `input` (without the digest). A service that cannot perform a kind its capabilities open should answer `UNSUPPORTED` (501), but the user then sees an action fail that the board offered, so implement every kind of every capability you turn on.
321
+
322
+ | Capability | Route | `kind` | Normalized `input` |
323
+ |---|---|---|---|
324
+ | `changeRequests` | `/pr/create` | `change-request-create` | `{ title, draft, body?, remote?, headRemote? }`. `remote` is where the pull request opens and `headRemote` where its head branch lives; either left out means the context's `primaryRemote`. A head on another remote is usually a fork: name its branch the way the forge expects (Gitea and GitHub take `owner:branch`) and refuse a remote that is not in the target's network |
325
+ | `changeRequests` | `/pr/update` | `change-request-update` | `{ title, body? }`: a new title, and a new body when given |
326
+ | `merge` | `/pr/merge` | `change-request-merge` | `{ method }`, one of the declared `merge.methods` |
327
+ | `draftChangeRequests` | `/pr/ready` | `change-request-ready` | `{}` |
328
+ | `comments` | `/pr/comment`, `/issues/comment` | `change-request-comment`, `issue-comment` | `{ body }` |
329
+ | `state` | `/pr/state`, `/issues/state` | `change-request-state`, `issue-state` | `{ state }`: `open` or `closed` |
330
+ | `labels` | `/pr/labels`, `/issues/labels` | `change-request-labels`, `issue-labels` | `{ names }`: the whole set |
331
+ | `reviewers` | `/pr/reviewers` | `change-request-reviewers` | `{ names }`: the whole set, as `limits.reviewerId` spells them |
332
+ | `reviews` | `/pr/review` | `change-request-review` | `{ verdict: "approve" \| "request-changes", body? }`, for the `headSha` in the target. With `reviewModel: "votes"`, `{ votes, body? }` instead |
333
+
334
+ Review threads have routes too, but they stay off for an extension's Git host.
335
+
336
+ Deadlines: `changeRequestStatus` 12 s (past it the last known status shows), other reads 20 s, mutation steps 30 s, `auth.*` 15 s (`sourceControlProviderTimeoutMs`).
337
+
338
+ OpenChamber decides, not the service, what a failure means. A mutation counts as definitely failed only when the service was never reached or every write the proxy saw got a 4xx; anything else is an unknown outcome to reconcile. A `status: 401` writes off the account only when OpenChamber's proxy saw the forge answer 401 on that call; a 401 the service reports on its own is a malformed answer.
339
+
340
+ ### Reaching the forge
341
+
342
+ The service never holds a token. `createSourceControlForge({ env: process.env })` returns `{ request(handle, req) }`, which posts to `${OPENCHAMBER_HOST_CALLBACK_URL}/source-control/outbound` with `Authorization: Bearer <handle>`. `OPENCHAMBER_HOST_CALLBACK_URL` is a separate loopback listener, set only for services with this role.
343
+
344
+ - Request (`SourceControlOutboundRequest`): `{ method: GET|POST|PUT|PATCH|DELETE, path, query?, headers?, body? }`. `path` starts with one `/` and stays on the call's instance origin. `headers` (lower-case names) may be `accept`, `content-type`, `if-none-match` and `range`; OpenChamber sets the rest, the token included, per `token.header` and `token.scheme` (with `basic`, the account's login too). `body` is text up to `SOURCE_CONTROL_OUTBOUND_BODY_MAX` (1 MB).
345
+ - Answer: HTTP 200 with `{ status, headers, body }`, the forge's status (any, 401 included), the headers in `SOURCE_CONTROL_OUTBOUND_RESPONSE_HEADERS` plus `x-ratelimit-*`, and the body as text up to `SOURCE_CONTROL_OUTBOUND_RESPONSE_MAX` (8 MB). Redirects are not followed. `request` resolves with it.
346
+ - Refusal: any other status with `{ error: { message, code } }` (an expired or unknown handle, a path off the instance, a body too large). `request` throws `SourceControlForgeError` with that `code` and `status`; `HOST_UNAVAILABLE` when there was no usable answer.
347
+
348
+ A handle is minted for one call, carries `auth.verify`'s candidate token or the bound account's token (none for `auth.probe`), and is revoked when the call ends. Never store it.
349
+
350
+ This is least exposure, not a sandbox. OpenChamber keeps tokens because Git transport needs them anyway and the proxy is the one place origin pinning, enterprise policy and audit can act. An approved service still runs as the user and could read OpenChamber's files itself.
351
+
352
+ VS Code lists no Git hosts from extensions: the guest catalog is unsupported there.
353
+
354
+ `examples/source-control-provider` is a checked-in provider for a subset of Gitea's REST v1, with a fake forge its tests run against.
355
+
230
356
  ## Shared surface (`surface: true`)
231
357
 
232
358
  A surface lets a person watch what an agent is working in and step in for a manual step. It is not about browsers: a service that drives a browser, a simulator, or a desktop app can show one. The host draws it in the extension's rail panel (a canvas; the extension runs no code there), sends the user's pointer, keyboard, and paste back, and decides who is in control.
package/dist/index.d.ts CHANGED
@@ -9,8 +9,8 @@ export { GUEST_STATUS_CONTROLS_MAX, GUEST_STATUS_CONTROL_ID, GUEST_STATUS_CONTRO
9
9
  export type { GuestStatusControl, GuestStatusControlEvent, GuestStatusControlOption } from './status-controls.ts';
10
10
  export type { OpenChamberManifestApiVersion } from './api-version.ts';
11
11
  export { compareOpenChamberVersions, hostMeetsOpenChamberEngine, openChamberEngineMinimum, OPENCHAMBER_ENGINE_PATTERN, parseOpenChamberVersion, } from './host-version.ts';
12
- export { DECLARED_GUEST_CAPABILITIES, GUEST_ACTIONS_MAX, GUEST_ACTION_LABEL_MAX, GUEST_CAPABILITIES, GUEST_COMMANDS_MAX, GUEST_COMMAND_DESCRIPTION_MAX, GUEST_COMMAND_NAME, GUEST_TOOLS_MAX, GUEST_TOOL_COLUMNS_MAX, GUEST_TOOL_COLUMN_MAX, GUEST_TOOL_LANGUAGE_MAX, GUEST_TOOL_MATCH, GUEST_TOOL_MATCH_MAX, GUEST_TOOL_NAME_MAX, GUEST_TOOL_OUTPUTS, GUEST_TOOL_TEMPLATE_MAX, GUEST_TOOL_TEMPLATE_VALUE_MAX, guestActionsNeedConversation, GUEST_FILESYSTEM_PATTERN_MAX, GUEST_FILESYSTEM_PATTERNS_MAX, GUEST_ORIGINS_MAX, GUEST_SERVICE_PROVIDES, GUEST_SURFACE_DOCKS, GUEST_SURFACE_DOCK_DEFAULT, GUEST_SURFACE_DOCK_SIZE_DEFAULT, GUEST_SURFACE_DOCK_SIZE_MAX, GUEST_SURFACE_DOCK_SIZE_MIN, GUEST_STATUS_SECTION_HEIGHT_DEFAULT, GUEST_STATUS_SECTION_HEIGHT_MAX, GUEST_STATUS_SECTION_HEIGHT_MIN, GUEST_STATUS_SECTION_TITLE_MAX, clampStatusSectionHeight, HOST_LINEAR_API_ORIGIN, isGuestFilesystemPattern, hasGuestCapability, hasGuestPage, isGuestApproved, isGuestPackageSvgIcon, requestedGuestCapabilities, resolveAttachEntry, resolvePageEntry, resolveStatusSectionEntry, resolveAttachMode, resolveIntegrationApi, resolveIntegrationAuth, serviceProvides, toPublicService, toPublicIntegration, } from './manifest.ts';
13
- export type { GuestServiceProvides, GuestSurfaceDock, ServiceContribution, ServicePermissions, GuestActionContribution, GuestActionPayload, GuestActionRole, GuestActionWhere, GuestCommandContribution, GuestToolContribution, GuestToolOutput, AttachContribution, AttachContributionObject, AttachMode, DeclaredGuestCapability, GuestAuthorization, GuestCapability, IntegrationAuth, IntegrationContribution, IntegrationHost, IntegrationHostProvider, IntegrationOAuth, IntegrationOAuthAccount, IntegrationSettingField, IntegrationToken, IntegrationTokenScheme, OpenChamberContributes, OpenChamberEngines, OpenChamberManifest, PanelContribution, BackgroundContribution, PageContribution, StatusSectionContribution, ParseManifestErrorCode, ParseManifestFailure, ParseManifestResult, ParseManifestSuccess, PublicService, PublicServicePermissions, PublicGuestCapabilities, PublicIntegration, PublicIntegrationToken, PublicSocketBinding, ResolvedGuestApi, SocketBinding, SocketPlatform, } from './manifest.ts';
12
+ export { DECLARED_GUEST_CAPABILITIES, GUEST_ACTIONS_MAX, GUEST_ACTION_LABEL_MAX, GUEST_CAPABILITIES, GUEST_COMMANDS_MAX, GUEST_COMMAND_DESCRIPTION_MAX, GUEST_COMMAND_NAME, GUEST_TOOLS_MAX, GUEST_TOOL_COLUMNS_MAX, GUEST_TOOL_COLUMN_MAX, GUEST_TOOL_LANGUAGE_MAX, GUEST_TOOL_MATCH, GUEST_TOOL_MATCH_MAX, GUEST_TOOL_NAME_MAX, GUEST_TOOL_OUTPUTS, GUEST_TOOL_TEMPLATE_MAX, GUEST_TOOL_TEMPLATE_VALUE_MAX, guestActionsNeedConversation, GUEST_FILESYSTEM_PATTERN_MAX, GUEST_FILESYSTEM_PATTERNS_MAX, GUEST_ORIGINS_MAX, GUEST_SERVICE_PROVIDES, GUEST_SURFACE_DOCKS, GUEST_SURFACE_DOCK_DEFAULT, GUEST_SURFACE_DOCK_SIZE_DEFAULT, GUEST_SURFACE_DOCK_SIZE_MAX, GUEST_SURFACE_DOCK_SIZE_MIN, GUEST_STATUS_SECTION_HEIGHT_DEFAULT, GUEST_STATUS_SECTION_HEIGHT_MAX, GUEST_STATUS_SECTION_HEIGHT_MIN, GUEST_STATUS_SECTION_TITLE_MAX, clampStatusSectionHeight, HOST_LINEAR_API_ORIGIN, isGuestFilesystemPattern, hasGuestCapability, hasGuestPage, isGuestApproved, isGuestPackageSvgIcon, requestedGuestCapabilities, resolveAttachEntry, resolvePageEntry, resolveStatusSectionEntry, resolveAttachMode, resolveIntegrationApi, resolveIntegrationAuth, serviceProvides, SOURCE_CONTROL_DEFAULT_INSTANCES_MAX, SOURCE_CONTROL_GUEST_ID_MAX, SOURCE_CONTROL_URL_TEMPLATES_MAX, SOURCE_CONTROL_URL_TEMPLATE_MAX, SOURCE_CONTROL_URL_TOKENS, sourceControlProviderIdOf, toPublicService, toPublicSourceControl, toPublicIntegration, } from './manifest.ts';
13
+ export type { GuestServiceProvides, GuestSurfaceDock, ServiceContribution, ServicePermissions, GuestActionContribution, GuestActionPayload, GuestActionRole, GuestActionWhere, GuestCommandContribution, GuestToolContribution, GuestToolOutput, AttachContribution, AttachContributionObject, AttachMode, DeclaredGuestCapability, GuestAuthorization, GuestCapability, IntegrationAuth, IntegrationContribution, IntegrationHost, IntegrationHostProvider, IntegrationOAuth, IntegrationOAuthAccount, IntegrationSettingField, IntegrationToken, IntegrationTokenScheme, OpenChamberContributes, OpenChamberEngines, OpenChamberManifest, PanelContribution, BackgroundContribution, PageContribution, StatusSectionContribution, ParseManifestErrorCode, ParseManifestFailure, ParseManifestResult, ParseManifestSuccess, PublicService, PublicServicePermissions, PublicSourceControl, SourceControlCapabilities, SourceControlContribution, SourceControlMergeMethod, PublicGuestCapabilities, PublicIntegration, PublicIntegrationToken, PublicSocketBinding, ResolvedGuestApi, SocketBinding, SocketPlatform, } from './manifest.ts';
14
14
  export { GUEST_FILE_EDITORS_MAX, GUEST_FILE_EDITOR_CONTENT_MAX, GUEST_FILE_EDITOR_PATTERNS_MAX, GUEST_FILE_EDITOR_PATTERN_MAX, GUEST_FILE_EDITOR_TITLE_MAX, GUEST_FILE_EDITOR_VERSION_MAX, createFileSaveTracker, fileEditorPayloadSize, isFileEditorPattern, matchesFileEditorPattern, } from './file-editor.ts';
15
15
  export type { FileEditorChange, FileEditorContentKind, FileEditorContribution, FileEditorDocument, FileEditorSnapshot, FileSnapshotPurpose, FileSnapshotRequest, FileSnapshotResultPayload, } from './file-editor.ts';
16
16
  export { connectHost, HostRequestError } from './host.ts';
@@ -20,7 +20,7 @@ export type { GuestPopoverAnchor, GuestPopoverClosedEvent, GuestPopoverContext,
20
20
  export { clampAttachRequest, clampBadgeCount, clampFrameHeight, isGuestCommitSha, GUEST_COMMIT_SHA, clampPromptRequest, clampStartSessionRequest, guestFileScope, isAttachData, isJsonValue, isFileListResult, isFileReadResult, isFileStatResult, isGenerateResult, isFileWriteResult, isGuestFilePath, isGuestAttachItem, isGuestMessageItem, isGuestSessionItem, isServiceStatusResult, isGuestRequestPath, isGuestRequestResult, isHostRequestErrorCode, isPromptResult, isStartSessionResult, readHostMessage, resolveHostRequestErrorCode, } from './contract.ts';
21
21
  export type { BadgeRequest, ActionResultPayload, GuestActionItem, GuestActionResultMessage, HostActionMessage, HostFileOpenMessage, HostFileSnapshotMessage, HostFileSavedMessage, HostGenerateLateMessage, GuestFileSnapshotResultMessage, GuestFileChangeMessage, GuestFileSaveMessage, GuestFileUnsupportedMessage, GuestGenerateLateListenMessage, GuestBadgeMessage, GuestResizeMessage, GuestOpenCommitMessage, GuestStatusControlsMessage, GuestPopoverAnchorMessage, GuestPopoverCloseMessage, GuestPopoverOpenMessage, OpenCommitRequest, ResizeRequest, GuestItem, GuestItemRole, GuestMessageItem, GuestResolveResultMessage, GuestSessionItem, GuestSessionItemMessage, HostResolveMessage, ResolveRequest, ResolveResultPayload, FileListEntry, FileListRequest, FileListResult, FileReadRequest, FileReadResult, FileStatRequest, FileStatResult, FileWriteRequest, FileWriteResult, GenerateRequest, GenerateResult, LateGenerateResult, GuestFileEntryKind, GuestFileListMessage, GuestFileReadMessage, GuestFileScope, GuestFileStatKind, GuestFileStatMessage, GuestGenerateMessage, GuestFileWriteMessage, ServiceStatus, ServiceStatusResult, HostRequestErrorCode, AttachBranches, AttachIssueRequest, AttachThreadKind, JsonValue, HostResultPayload, PromptRequest, PromptResult, SessionLifecycleEvent, SessionLifecyclePhase, StartSessionRequest, StartSessionResult, StartSessionSent, ComposeRequest, GuestServiceRequestMessage, GuestServiceStatusMessage, GuestShellOutputMessage, GuestShellsSubscribeMessage, GuestShellsUnsubscribeMessage, GuestShellStopMessage, GuestAttachMessage, GuestClipboardWriteMessage, GuestCloseMessage, GuestComposeMessage, GuestConnection, GuestHelloMessage, GuestMessage, GuestOauthDisconnectMessage, GuestOauthStartMessage, GuestOpenSurfaceMessage, GuestOpenUrlMessage, GuestRequest, GuestRequestMessage, GuestRequestMethod, GuestPromptMessage, GuestSessionLinkMessage, GuestStartSessionMessage, GuestRequestResult, GuestSettings, GuestToastMessage, GuestHostSurface, HostConnectionMessage, HostDirectoryMessage, HostItemMessage, HostStatusControlEventMessage, HostPopoverClosedMessage, HostMessage, HostReadyContext, HostReadyMessage, HostResultMessage, HostSessionLifecycleMessage, HostSessionMessage, HostSettingsMessage, HostShellsMessage, HostTheme, HostThemeMode, HostThemeTokens, SessionSnapshot, ToastKind, ToastRequest, } from './contract.ts';
22
22
  export { EMPTY_GUEST_CONNECTION, GUEST_BADGE_MAX, GUEST_FRAME_HEIGHT_MAX, GUEST_ITEM_MESSAGE_TEXT_MAX, GUEST_ITEM_SESSION_MAX, GUEST_RESOLVE_ERROR_MAX, GUEST_FILE_CONTENT_MAX, GUEST_FILE_ENTRY_KINDS, GUEST_FILE_LIST_MAX, GUEST_FILE_PATH_MAX, GUEST_GENERATE_OUTPUT_TOKENS_MAX, GUEST_GENERATE_PROMPT_MAX, GUEST_GENERATE_SYSTEM_MAX, GUEST_GENERATE_TEXT_MAX, GUEST_GENERATE_TIMEOUT_MS, GUEST_GENERATE_LATE_KEEP_MS, GUEST_GENERATE_LATE_MAX, GUEST_FILE_STAT_KINDS, SERVICE_STATUS_VALUES, HOST_REQUEST_ERROR_CODES, SESSION_LIFECYCLE_PHASES, START_SESSION_SENT, GUEST_ACCOUNT_MAX, GUEST_SESSION_AGENT_MAX, GUEST_SESSION_MODEL_MAX, GUEST_ATTACH_AUTHOR_MAX, GUEST_ATTACH_BRANCH_MAX, GUEST_ATTACH_DATA_MAX, GUEST_ATTACH_ID_MAX, GUEST_ATTACH_TEXT_MAX, GUEST_ATTACH_TITLE_MAX, GUEST_ATTACH_URL_MAX, GUEST_CLIPBOARD_TEXT_MAX, GUEST_TOAST_MAX, GUEST_COMPOSE_TEXT_MAX, GUEST_REQUEST_BODY_MAX, GUEST_REQUEST_PATH_MAX, GUEST_REQUEST_RESPONSE_MAX, GUEST_REQUEST_TIMEOUT_MS, GUEST_SETTING_VALUE_MAX, } from './contract.ts';
23
- export { BROWSER_CONTROL_ACTIONS, BROWSER_PROVIDER_ACTION_TIMEOUT_MS, BROWSER_PROVIDER_IDLE_MS, BROWSER_PROVIDER_OPEN_TIMEOUT_MS, BROWSER_PROVIDER_PATH, BROWSER_PROVIDER_RESPONSE_MAX, BROWSER_SCROLL_DIRECTIONS, BROWSER_VIEWPORT_MODES, isBrowserControlAction, readBrowserProviderRequest, } from './service-providers.ts';
24
- export type { BrowserCaptureData, BrowserCaptureParameters, BrowserClickData, BrowserClickParameters, BrowserCloseData, BrowserCloseParameters, BrowserConsoleProblem, BrowserControlAction, BrowserElementBounds, BrowserInspectData, BrowserInspectParameters, BrowserNavigationData, BrowserOpenData, BrowserOpenParameters, BrowserProviderContext, BrowserProviderData, BrowserProviderRequest, BrowserProviderResult, BrowserResizeData, BrowserResizeParameters, BrowserScrollData, BrowserScrollDirection, BrowserScrollParameters, BrowserSnapshotData, BrowserSnapshotElement, BrowserSnapshotParameters, BrowserTab, BrowserTabTarget, BrowserTypeData, BrowserTypeParameters, BrowserViewportMode, BrowserViewportSummary, } from './service-providers.ts';
23
+ export { BROWSER_CONTROL_ACTIONS, BROWSER_PROVIDER_ACTION_TIMEOUT_MS, BROWSER_PROVIDER_IDLE_MS, BROWSER_PROVIDER_OPEN_TIMEOUT_MS, BROWSER_PROVIDER_PATH, BROWSER_PROVIDER_RESPONSE_MAX, BROWSER_SCROLL_DIRECTIONS, BROWSER_VIEWPORT_MODES, isBrowserControlAction, readBrowserProviderRequest, OPENCHAMBER_HOST_CALLBACK_URL, SOURCE_CONTROL_OUTBOUND_BODY_MAX, SOURCE_CONTROL_OUTBOUND_METHODS, SOURCE_CONTROL_OUTBOUND_PATH, SOURCE_CONTROL_OUTBOUND_REQUEST_HEADERS, SOURCE_CONTROL_OUTBOUND_RESPONSE_HEADERS, SOURCE_CONTROL_OUTBOUND_RESPONSE_HEADER_PREFIX, SOURCE_CONTROL_OUTBOUND_RESPONSE_MAX, SOURCE_CONTROL_OUTBOUND_TIMEOUT_MS, SOURCE_CONTROL_PROVIDER_AUTH_TIMEOUT_MS, SOURCE_CONTROL_PROVIDER_ERROR_MESSAGE_MAX, SOURCE_CONTROL_PROVIDER_IDLE_MS, SOURCE_CONTROL_PROVIDER_METHODS, SOURCE_CONTROL_PROVIDER_MUTATION_TIMEOUT_MS, SOURCE_CONTROL_PROVIDER_PATH, SOURCE_CONTROL_PROVIDER_READ_TIMEOUT_MS, SOURCE_CONTROL_PROVIDER_RESPONSE_MAX, SOURCE_CONTROL_PROVIDER_STATUS_TIMEOUT_MS, SOURCE_CONTROL_PROVIDER_TARGET_MAX, SOURCE_CONTROL_PROVIDER_WIRE_VERSION, SourceControlForgeError, createSourceControlForge, isSourceControlProviderMethod, readSourceControlProviderRequest, sourceControlProviderTimeoutMs, } from './service-providers.ts';
24
+ export type { BrowserCaptureData, BrowserCaptureParameters, BrowserClickData, BrowserClickParameters, BrowserCloseData, BrowserCloseParameters, BrowserConsoleProblem, BrowserControlAction, BrowserElementBounds, BrowserInspectData, BrowserInspectParameters, BrowserNavigationData, BrowserOpenData, BrowserOpenParameters, BrowserProviderContext, BrowserProviderData, BrowserProviderRequest, BrowserProviderResult, BrowserResizeData, BrowserResizeParameters, BrowserScrollData, BrowserScrollDirection, BrowserScrollParameters, BrowserSnapshotData, BrowserSnapshotElement, BrowserSnapshotOption, BrowserSnapshotParameters, BrowserSelectData, BrowserSelectParameters, BrowserTab, BrowserTabTarget, BrowserTypeData, BrowserTypeParameters, BrowserViewportMode, BrowserViewportSummary, SourceControlChangeRequestHead, SourceControlChangeRequestSource, SourceControlForge, SourceControlForgeUser, SourceControlJson, SourceControlJsonObject, SourceControlMutationMethod, SourceControlMutationResolution, SourceControlMutationResult, SourceControlMutationTarget, SourceControlOutboundMethod, SourceControlOutboundRequest, SourceControlOutboundResponse, SourceControlProbeData, SourceControlProjectRef, SourceControlProjectSelector, SourceControlProviderError, SourceControlProviderInputs, SourceControlProviderMethod, SourceControlProviderRequest, SourceControlProviderResult, SourceControlProviderSession, SourceControlProviderUser, SourceControlReconcileData, SourceControlResourceMethod, SourceControlVerifyData, } from './service-providers.ts';
25
25
  export { SURFACE_AGENT_ACTIVE_HEADER, SURFACE_AGENT_HOLD_MS, SURFACE_CLIPBOARD_PATH, SURFACE_CONTROL_PATH, SURFACE_CONTROLLERS, SURFACE_DIMENSION_MAX, SURFACE_FRAME_MAX_BYTES, SURFACE_FRAME_MIMES, SURFACE_FRAME_PATH, SURFACE_FRAME_SEQ_HEADER, SURFACE_FRAME_WAIT_MS, SURFACE_HEIGHT_HEADER, SURFACE_INPUT_BATCH_MAX, SURFACE_INPUT_PATH, SURFACE_RESIZE_PATH, SURFACE_SEQ_HEADER, SURFACE_TEXT_MAX, SURFACE_TITLE_HEADER, SURFACE_TITLE_MAX, SURFACE_VIEWER_CONTROLS_HEADER, SURFACE_VIEWER_HEADER, SURFACE_WIDTH_HEADER, readSurfaceControlNotice, readSurfaceInputBatch, readSurfaceResizeRequest, } from './service-surface.ts';
26
26
  export type { SurfaceClipboardAnswer, SurfaceControlNotice, SurfaceController, SurfaceFrameMime, SurfaceInputBatch, SurfaceInputEvent, SurfaceKeyEvent, SurfaceModifiers, SurfacePointerEvent, SurfaceResizeAnswer, SurfaceResizeRequest, SurfaceTextEvent, SurfaceWheelEvent, } from './service-surface.ts';
package/dist/index.js CHANGED
@@ -5,11 +5,11 @@ export { GUEST_STORAGE_KEY_MAX, GUEST_STORAGE_KEYS_MAX, GUEST_STORAGE_VALUE_BYTE
5
5
  export { GUEST_SHELL_END_STATUSES, GUEST_SHELL_ID_MAX, GUEST_SHELLS_MAX, GUEST_SHELL_OUTPUT_TAIL_MAX, } from './shells.js';
6
6
  export { GUEST_STATUS_CONTROLS_MAX, GUEST_STATUS_CONTROL_ID, GUEST_STATUS_CONTROL_LABEL_MAX, GUEST_STATUS_CONTROL_OPTIONS_MAX, GUEST_STATUS_CONTROL_VALUE_MAX } from './status-controls.js';
7
7
  export { compareOpenChamberVersions, hostMeetsOpenChamberEngine, openChamberEngineMinimum, OPENCHAMBER_ENGINE_PATTERN, parseOpenChamberVersion, } from './host-version.js';
8
- export { DECLARED_GUEST_CAPABILITIES, GUEST_ACTIONS_MAX, GUEST_ACTION_LABEL_MAX, GUEST_CAPABILITIES, GUEST_COMMANDS_MAX, GUEST_COMMAND_DESCRIPTION_MAX, GUEST_COMMAND_NAME, GUEST_TOOLS_MAX, GUEST_TOOL_COLUMNS_MAX, GUEST_TOOL_COLUMN_MAX, GUEST_TOOL_LANGUAGE_MAX, GUEST_TOOL_MATCH, GUEST_TOOL_MATCH_MAX, GUEST_TOOL_NAME_MAX, GUEST_TOOL_OUTPUTS, GUEST_TOOL_TEMPLATE_MAX, GUEST_TOOL_TEMPLATE_VALUE_MAX, guestActionsNeedConversation, GUEST_FILESYSTEM_PATTERN_MAX, GUEST_FILESYSTEM_PATTERNS_MAX, GUEST_ORIGINS_MAX, GUEST_SERVICE_PROVIDES, GUEST_SURFACE_DOCKS, GUEST_SURFACE_DOCK_DEFAULT, GUEST_SURFACE_DOCK_SIZE_DEFAULT, GUEST_SURFACE_DOCK_SIZE_MAX, GUEST_SURFACE_DOCK_SIZE_MIN, GUEST_STATUS_SECTION_HEIGHT_DEFAULT, GUEST_STATUS_SECTION_HEIGHT_MAX, GUEST_STATUS_SECTION_HEIGHT_MIN, GUEST_STATUS_SECTION_TITLE_MAX, clampStatusSectionHeight, HOST_LINEAR_API_ORIGIN, isGuestFilesystemPattern, hasGuestCapability, hasGuestPage, isGuestApproved, isGuestPackageSvgIcon, requestedGuestCapabilities, resolveAttachEntry, resolvePageEntry, resolveStatusSectionEntry, resolveAttachMode, resolveIntegrationApi, resolveIntegrationAuth, serviceProvides, toPublicService, toPublicIntegration, } from './manifest.js';
8
+ export { DECLARED_GUEST_CAPABILITIES, GUEST_ACTIONS_MAX, GUEST_ACTION_LABEL_MAX, GUEST_CAPABILITIES, GUEST_COMMANDS_MAX, GUEST_COMMAND_DESCRIPTION_MAX, GUEST_COMMAND_NAME, GUEST_TOOLS_MAX, GUEST_TOOL_COLUMNS_MAX, GUEST_TOOL_COLUMN_MAX, GUEST_TOOL_LANGUAGE_MAX, GUEST_TOOL_MATCH, GUEST_TOOL_MATCH_MAX, GUEST_TOOL_NAME_MAX, GUEST_TOOL_OUTPUTS, GUEST_TOOL_TEMPLATE_MAX, GUEST_TOOL_TEMPLATE_VALUE_MAX, guestActionsNeedConversation, GUEST_FILESYSTEM_PATTERN_MAX, GUEST_FILESYSTEM_PATTERNS_MAX, GUEST_ORIGINS_MAX, GUEST_SERVICE_PROVIDES, GUEST_SURFACE_DOCKS, GUEST_SURFACE_DOCK_DEFAULT, GUEST_SURFACE_DOCK_SIZE_DEFAULT, GUEST_SURFACE_DOCK_SIZE_MAX, GUEST_SURFACE_DOCK_SIZE_MIN, GUEST_STATUS_SECTION_HEIGHT_DEFAULT, GUEST_STATUS_SECTION_HEIGHT_MAX, GUEST_STATUS_SECTION_HEIGHT_MIN, GUEST_STATUS_SECTION_TITLE_MAX, clampStatusSectionHeight, HOST_LINEAR_API_ORIGIN, isGuestFilesystemPattern, hasGuestCapability, hasGuestPage, isGuestApproved, isGuestPackageSvgIcon, requestedGuestCapabilities, resolveAttachEntry, resolvePageEntry, resolveStatusSectionEntry, resolveAttachMode, resolveIntegrationApi, resolveIntegrationAuth, serviceProvides, SOURCE_CONTROL_DEFAULT_INSTANCES_MAX, SOURCE_CONTROL_GUEST_ID_MAX, SOURCE_CONTROL_URL_TEMPLATES_MAX, SOURCE_CONTROL_URL_TEMPLATE_MAX, SOURCE_CONTROL_URL_TOKENS, sourceControlProviderIdOf, toPublicService, toPublicSourceControl, toPublicIntegration, } from './manifest.js';
9
9
  export { GUEST_FILE_EDITORS_MAX, GUEST_FILE_EDITOR_CONTENT_MAX, GUEST_FILE_EDITOR_PATTERNS_MAX, GUEST_FILE_EDITOR_PATTERN_MAX, GUEST_FILE_EDITOR_TITLE_MAX, GUEST_FILE_EDITOR_VERSION_MAX, createFileSaveTracker, fileEditorPayloadSize, isFileEditorPattern, matchesFileEditorPattern, } from './file-editor.js';
10
10
  export { connectHost, HostRequestError } from './host.js';
11
11
  export { GUEST_POPOVER_COORDINATE_MAX, GUEST_POPOVER_DATA_DEPTH_MAX, GUEST_POPOVER_DATA_MAX, GUEST_POPOVER_HEIGHT_MAX, GUEST_POPOVER_HEIGHT_MIN, GUEST_POPOVER_ID, GUEST_POPOVER_SIDES, GUEST_POPOVER_WIDTH_MAX, GUEST_POPOVER_WIDTH_MIN, GUEST_POPOVER_CLOSE_REASONS, isGuestPopoverRequest, } from './popover.js';
12
12
  export { clampAttachRequest, clampBadgeCount, clampFrameHeight, isGuestCommitSha, GUEST_COMMIT_SHA, clampPromptRequest, clampStartSessionRequest, guestFileScope, isAttachData, isJsonValue, isFileListResult, isFileReadResult, isFileStatResult, isGenerateResult, isFileWriteResult, isGuestFilePath, isGuestAttachItem, isGuestMessageItem, isGuestSessionItem, isServiceStatusResult, isGuestRequestPath, isGuestRequestResult, isHostRequestErrorCode, isPromptResult, isStartSessionResult, readHostMessage, resolveHostRequestErrorCode, } from './contract.js';
13
13
  export { EMPTY_GUEST_CONNECTION, GUEST_BADGE_MAX, GUEST_FRAME_HEIGHT_MAX, GUEST_ITEM_MESSAGE_TEXT_MAX, GUEST_ITEM_SESSION_MAX, GUEST_RESOLVE_ERROR_MAX, GUEST_FILE_CONTENT_MAX, GUEST_FILE_ENTRY_KINDS, GUEST_FILE_LIST_MAX, GUEST_FILE_PATH_MAX, GUEST_GENERATE_OUTPUT_TOKENS_MAX, GUEST_GENERATE_PROMPT_MAX, GUEST_GENERATE_SYSTEM_MAX, GUEST_GENERATE_TEXT_MAX, GUEST_GENERATE_TIMEOUT_MS, GUEST_GENERATE_LATE_KEEP_MS, GUEST_GENERATE_LATE_MAX, GUEST_FILE_STAT_KINDS, SERVICE_STATUS_VALUES, HOST_REQUEST_ERROR_CODES, SESSION_LIFECYCLE_PHASES, START_SESSION_SENT, GUEST_ACCOUNT_MAX, GUEST_SESSION_AGENT_MAX, GUEST_SESSION_MODEL_MAX, GUEST_ATTACH_AUTHOR_MAX, GUEST_ATTACH_BRANCH_MAX, GUEST_ATTACH_DATA_MAX, GUEST_ATTACH_ID_MAX, GUEST_ATTACH_TEXT_MAX, GUEST_ATTACH_TITLE_MAX, GUEST_ATTACH_URL_MAX, GUEST_CLIPBOARD_TEXT_MAX, GUEST_TOAST_MAX, GUEST_COMPOSE_TEXT_MAX, GUEST_REQUEST_BODY_MAX, GUEST_REQUEST_PATH_MAX, GUEST_REQUEST_RESPONSE_MAX, GUEST_REQUEST_TIMEOUT_MS, GUEST_SETTING_VALUE_MAX, } from './contract.js';
14
- export { BROWSER_CONTROL_ACTIONS, BROWSER_PROVIDER_ACTION_TIMEOUT_MS, BROWSER_PROVIDER_IDLE_MS, BROWSER_PROVIDER_OPEN_TIMEOUT_MS, BROWSER_PROVIDER_PATH, BROWSER_PROVIDER_RESPONSE_MAX, BROWSER_SCROLL_DIRECTIONS, BROWSER_VIEWPORT_MODES, isBrowserControlAction, readBrowserProviderRequest, } from './service-providers.js';
14
+ export { BROWSER_CONTROL_ACTIONS, BROWSER_PROVIDER_ACTION_TIMEOUT_MS, BROWSER_PROVIDER_IDLE_MS, BROWSER_PROVIDER_OPEN_TIMEOUT_MS, BROWSER_PROVIDER_PATH, BROWSER_PROVIDER_RESPONSE_MAX, BROWSER_SCROLL_DIRECTIONS, BROWSER_VIEWPORT_MODES, isBrowserControlAction, readBrowserProviderRequest, OPENCHAMBER_HOST_CALLBACK_URL, SOURCE_CONTROL_OUTBOUND_BODY_MAX, SOURCE_CONTROL_OUTBOUND_METHODS, SOURCE_CONTROL_OUTBOUND_PATH, SOURCE_CONTROL_OUTBOUND_REQUEST_HEADERS, SOURCE_CONTROL_OUTBOUND_RESPONSE_HEADERS, SOURCE_CONTROL_OUTBOUND_RESPONSE_HEADER_PREFIX, SOURCE_CONTROL_OUTBOUND_RESPONSE_MAX, SOURCE_CONTROL_OUTBOUND_TIMEOUT_MS, SOURCE_CONTROL_PROVIDER_AUTH_TIMEOUT_MS, SOURCE_CONTROL_PROVIDER_ERROR_MESSAGE_MAX, SOURCE_CONTROL_PROVIDER_IDLE_MS, SOURCE_CONTROL_PROVIDER_METHODS, SOURCE_CONTROL_PROVIDER_MUTATION_TIMEOUT_MS, SOURCE_CONTROL_PROVIDER_PATH, SOURCE_CONTROL_PROVIDER_READ_TIMEOUT_MS, SOURCE_CONTROL_PROVIDER_RESPONSE_MAX, SOURCE_CONTROL_PROVIDER_STATUS_TIMEOUT_MS, SOURCE_CONTROL_PROVIDER_TARGET_MAX, SOURCE_CONTROL_PROVIDER_WIRE_VERSION, SourceControlForgeError, createSourceControlForge, isSourceControlProviderMethod, readSourceControlProviderRequest, sourceControlProviderTimeoutMs, } from './service-providers.js';
15
15
  export { SURFACE_AGENT_ACTIVE_HEADER, SURFACE_AGENT_HOLD_MS, SURFACE_CLIPBOARD_PATH, SURFACE_CONTROL_PATH, SURFACE_CONTROLLERS, SURFACE_DIMENSION_MAX, SURFACE_FRAME_MAX_BYTES, SURFACE_FRAME_MIMES, SURFACE_FRAME_PATH, SURFACE_FRAME_SEQ_HEADER, SURFACE_FRAME_WAIT_MS, SURFACE_HEIGHT_HEADER, SURFACE_INPUT_BATCH_MAX, SURFACE_INPUT_PATH, SURFACE_RESIZE_PATH, SURFACE_SEQ_HEADER, SURFACE_TEXT_MAX, SURFACE_TITLE_HEADER, SURFACE_TITLE_MAX, SURFACE_VIEWER_CONTROLS_HEADER, SURFACE_VIEWER_HEADER, SURFACE_WIDTH_HEADER, readSurfaceControlNotice, readSurfaceInputBatch, readSurfaceResizeRequest, } from './service-surface.js';
@@ -241,12 +241,184 @@ export type PublicSocketBinding = {
241
241
  };
242
242
  /**
243
243
  * Host roles a service can stand in for. `browser` answers the agent's
244
- * `browser.*` actions in place of the in-app browser view; the contract is in
245
- * `service-providers.ts`. A service that provides a role needs no panel or
246
- * background entry: the host starts it on the first action.
244
+ * `browser.*` actions in place of the in-app browser view. `source-control`
245
+ * makes the extension a Git host next to GitHub, GitLab and Gitea, described
246
+ * by `contributes.sourceControl`. Both contracts are in `service-providers.ts`.
247
+ * A service that provides a role needs no panel or background entry: the host
248
+ * starts it on the first call.
247
249
  */
248
- export declare const GUEST_SERVICE_PROVIDES: readonly ['browser'];
250
+ export declare const GUEST_SERVICE_PROVIDES: readonly ['browser', 'source-control'];
249
251
  export type GuestServiceProvides = (typeof GUEST_SERVICE_PROVIDES)[number];
252
+ /**
253
+ * The host names an extension's Git host `ext-<panel.id>`, and provider ids
254
+ * are at most 32 characters, so a guest taking the role has an id of at most
255
+ * this many.
256
+ */
257
+ export declare const SOURCE_CONTROL_GUEST_ID_MAX = 28;
258
+ /** The provider id OpenChamber gives the Git host an extension provides. */
259
+ export declare const sourceControlProviderIdOf: (guestId: string) => string;
260
+ /** Default instances a Git host extension may name. */
261
+ export declare const SOURCE_CONTROL_DEFAULT_INSTANCES_MAX = 4;
262
+ /** URL templates per kind (`presentation.urls.changeRequest` / `.issue`). */
263
+ export declare const SOURCE_CONTROL_URL_TEMPLATES_MAX = 8;
264
+ /** Characters in one URL template. */
265
+ export declare const SOURCE_CONTROL_URL_TEMPLATE_MAX = 256;
266
+ /**
267
+ * The placeholders a URL template may use, each a whole path segment.
268
+ * `{namespace+}` is one or more segments (GitLab groups); `{owner}` exactly one.
269
+ */
270
+ export declare const SOURCE_CONTROL_URL_TOKENS: readonly ['{owner}', '{namespace+}', '{repo}', '{number}'];
271
+ /** `submit` is a change-based host's own merge (Gerrit's): when declared it is the only method. */
272
+ export type SourceControlMergeMethod = 'merge' | 'squash' | 'rebase' | 'submit';
273
+ /**
274
+ * What the Git host can do. A capability left out is off. `merge` lists the
275
+ * methods it offers, or is `false`. Review threads, uploads, stacks, the
276
+ * reference picker and multiple accounts are not declared: the host fixes them
277
+ * for every extension.
278
+ */
279
+ export type SourceControlCapabilities = {
280
+ projects: boolean;
281
+ issues: boolean;
282
+ changeRequests: boolean;
283
+ draftChangeRequests: boolean;
284
+ merge: false | {
285
+ methods: SourceControlMergeMethod[];
286
+ };
287
+ ci: boolean;
288
+ comments: boolean;
289
+ reviews: boolean;
290
+ state: boolean;
291
+ labels: boolean;
292
+ reviewers: boolean;
293
+ changeRequestPatch: boolean;
294
+ liveSummaries: boolean;
295
+ /**
296
+ * How a review is given. `verdict` (the default when left out): approve or
297
+ * request changes. `votes`: per-label scores, such as Code-Review -2..+2;
298
+ * the labels and their ranges come with each change request's context.
299
+ */
300
+ reviewModel?: 'verdict' | 'votes';
301
+ /**
302
+ * Whether a change request has numbered revisions (patchsets) that can be
303
+ * read one at a time: reads then may carry `revision`, a commit sha.
304
+ */
305
+ revisions?: boolean;
306
+ };
307
+ /**
308
+ * `contributes.sourceControl`: the Git host an extension with
309
+ * `service.provides: ["source-control"]` adds, declared in the manifest so
310
+ * OpenChamber can draw and route it without starting the service. The
311
+ * provider id is not declared: OpenChamber names it `ext-<panel.id>`.
312
+ */
313
+ export type SourceControlContribution = {
314
+ /** Wire version of the role's service contract; `1`. */
315
+ wire: 1;
316
+ /** The product name, shown untranslated. 1 to 64 characters. */
317
+ label: string;
318
+ /** `change` names a change-based host's unit of review (Gerrit's change). */
319
+ terminology: {
320
+ changeRequest: 'pullRequest' | 'mergeRequest' | 'change';
321
+ ci: 'checks' | 'pipeline';
322
+ };
323
+ /** `#`, `!`: what precedes a number. 1 to 4 characters. */
324
+ referencePrefix: {
325
+ changeRequest: string;
326
+ issue: string;
327
+ };
328
+ /** `owner/name`: two segments. `namespace/name`: nested groups before the name. */
329
+ repositoryPath: 'owner/name' | 'namespace/name';
330
+ instances: {
331
+ /** Up to 4 https origins offered without typing one. */
332
+ defaultInstances: string[];
333
+ /** Whether the user may add an instance outside `defaultInstances`. */
334
+ custom: boolean;
335
+ /** Whether a typed instance may be `http://` on a loopback address. */
336
+ allowLoopbackHttp: boolean;
337
+ /** Whether a typed instance may carry a port. */
338
+ allowPort: boolean;
339
+ /** Example text in the instance field. */
340
+ placeholder: string;
341
+ };
342
+ capabilities: SourceControlCapabilities;
343
+ /** How OpenChamber sends the token: which header, with which prefix. */
344
+ token: {
345
+ header: 'authorization' | 'private-token';
346
+ /**
347
+ * `bearer` → `Bearer <token>`, `token` → `token <token>`, `basic` →
348
+ * `Basic base64(<login>:<token>)`, `none` → the bare token (only with `private-token`).
349
+ */
350
+ scheme: 'bearer' | 'token' | 'basic' | 'none';
351
+ /** Settings asks for the account's username beside the token. Required with, and only with, `basic`. */
352
+ username?: boolean;
353
+ /** Scopes the Settings card asks the user to grant. */
354
+ scopes: string[];
355
+ /** Path on the instance where tokens are created, linked from Settings. */
356
+ createPath?: string;
357
+ };
358
+ /** How Git over HTTPS signs in with the stored token. */
359
+ transport: {
360
+ httpsUsername: {
361
+ kind: 'login';
362
+ } | {
363
+ kind: 'fixed';
364
+ value: string;
365
+ };
366
+ appendDotGitToHttpsClone: boolean;
367
+ };
368
+ limits: {
369
+ commentMaxLength: number;
370
+ /** How a reviewer is named in a reviewer mutation: a numeric user id or a login. */
371
+ reviewerId: 'numeric' | 'login';
372
+ labelsMax: number;
373
+ reviewersMax: number;
374
+ };
375
+ /** Pull requests and issues read in one `liveSummaries` call. */
376
+ trackedItems: {
377
+ batchLimit: number;
378
+ };
379
+ presentation: {
380
+ /** `PR`, `MR`: shown untranslated. 1 to 8 characters. */
381
+ abbreviation: string;
382
+ /** English noun for generation prompts, such as `pull request`. */
383
+ promptNoun: string;
384
+ /** Whether issues and change requests share one number sequence. */
385
+ sharedNumberSpace: boolean;
386
+ /** A 24×24 SVG path `d`, the mark drawn for the Git host. */
387
+ iconPath?: string;
388
+ /**
389
+ * Web paths of a change request and an issue on the instance, made of
390
+ * literal segments and the `SOURCE_CONTROL_URL_TOKENS` placeholders, such
391
+ * as `/{owner}/{repo}/pulls/{number}`. OpenChamber compiles them; an
392
+ * extension never supplies a regular expression.
393
+ */
394
+ urls: {
395
+ changeRequest: string[];
396
+ issue: string[];
397
+ };
398
+ /**
399
+ * Review threads are not part of the role: OpenChamber keeps starting one
400
+ * off for an extension's Git host, so there is no `startThread` here.
401
+ */
402
+ /**
403
+ * `submitRequirements`: each change request's context lists what must
404
+ * hold before it can be submitted, and the UI shows them by the Submit button.
405
+ */
406
+ features: {
407
+ authorCanReview: boolean;
408
+ issueCloseReason: boolean;
409
+ submitRequirements?: boolean;
410
+ };
411
+ /** Repository paths read as the change request template, first match wins. */
412
+ changeRequestTemplates: string[];
413
+ };
414
+ };
415
+ /** What the extension catalog and approval dialog show of the role; never a token. */
416
+ export type PublicSourceControl = {
417
+ label: string;
418
+ defaultInstances: string[];
419
+ custom: boolean;
420
+ };
421
+ export declare const toPublicSourceControl: (contribution: SourceControlContribution) => PublicSourceControl;
250
422
  export type ServiceContribution = {
251
423
  entry: string;
252
424
  runtime: 'host';
@@ -321,6 +493,8 @@ export type OpenChamberContributes = {
321
493
  tools?: GuestToolContribution[];
322
494
  /** Editors the Files view opens matching files in. */
323
495
  fileEditors?: FileEditorContribution[];
496
+ /** The Git host a `source-control` service provides; present exactly when it does. */
497
+ sourceControl?: SourceControlContribution;
324
498
  };
325
499
  /** Whether any declared action asks for a session's messages, which needs `conversation`. */
326
500
  export declare const guestActionsNeedConversation: (actions: readonly GuestActionContribution[] | undefined) => boolean;