feat(#2103): drive VS Code through the Embeddable Orchestration System (ADR-1239)

VS Code is a net-new EoS runtime that — unlike every prior migration — is NOT
CLI-installed (Marketplace/VSIX extension). It has zero runtime==='vscode'
branches in bin/install.js and stays that way (regression-guarded); it is driven
entirely through the negotiated imperative Host-Integration adapter.

Registry + validator (the hard part):
- capabilities/vscode/capability.json (role:runtime): full hostIntegration block
  (imperative / palette / active vscode.lm model / engine hook bus /
  sandboxed-storage / mcp transport / sandboxed-web runtime; dispatch nested,
  maxDepth 5 per VS Code's documented subagent depth).
- capability-validator.cjs extended so a role:runtime capability can legitimately
  declare "extension-distributed, no config directory": new configHome.kind:'none'
  + installSurface:'none' (+ GATE-A pairing + the parity maps), with localConfigDir
  and configHome.name made conditional on kind!=='none'. All 18 runtimes still
  validate; getDirName returns a distinct sentinel (not '.claude') for a no-config
  runtime.
- The add-a-registry-runtime tax: NON_INSTALLABLE_RUNTIMES exemption in the
  runtime-flags drift guard, vscode added to global-config-home SPECIAL_CASED,
  EXPECTED_PROFILES.vscode='ide', and the config-adapter/derivation/pin-count
  guards updated. No golden-install fixture, model-catalog, or CONFIGURATION rows
  (vscode never enters allRuntimes).

Dispatch + extension surface:
- Fixed vscode/extension.js's createHub()-no-args bug (every dispatch was
  UnknownCommand, masked by a vacuous reachability test) — now reuses the shared
  dispatchGsdCommand subprocess-shim (Node/desktop); the reachability test is
  tightened to assert real dispatch.
- Promoted the #1933 host binding to a shipped vscode/host-binding.js; activate()
  now composes the model/hookBus/stateIO seams through it. Corrected the model
  seam to VS Code's real API (vscode.lm.selectChatModels() -> model.sendRequest();
  vscode.lm.sendRequest does not exist) so the binding actually composes on real
  desktop VS Code instead of throwing.
- New vscode/browser.js Web Extension entry with ZERO Node APIs (the engine's
  config/capability loading is Node-bound, so the web entry registers the surface
  and directs full dispatch to the native MCP server — honestly documented).
- UPGRADE 1: GSD skills as native Language Model Tools (contributes.languageModelTools
  + vscode.lm.registerTool), invoke() dispatching through the hub.
- UPGRADE 2: native subagent dispatch wired onto #runSubagent /
  chat.subagents.allowInvocationsFromSubagents (fail-soft on API availability,
  maxDepth 5 enforced).
- vscode/package.json: browser entry, engines.vscode ^1.105, chatParticipants +
  languageModelTools contributions; fixed a stale activationPoints->activationEvents
  manifest key. Added "vscode" to the package files array.

Docs (## vscode matrix section) + changeset (Added).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-07-11 22:35:18 -04:00
parent 97297d7e3f
commit a0fafedfa0
28 changed files with 1973 additions and 157 deletions

View File

@@ -1,69 +1,12 @@
'use strict';
/**
* Reference VS Code IDE host binding for GSD (ADR-1239 Phase D / #1933).
* Thin re-export of the SHIPPED VS Code host binding (#2103).
*
* VS Code is the IDE-profile reference host. It composes the Phase-3 engine
* seams for the negotiated `ide` profile (host-integration.cts PROFILE_BASELINES):
*
* - modelMode: 'active' → createModelAdapter({modelMode:'active'}, {sendRequest})
* backed by `vscode.lm` (LanguageModelChat). VS Code rejects
* system-role messages, so the request mapper uses User role only.
* - hookBus: 'engine' → createHookBus({bus:'engine'}) — VS Code has NO host event bus,
* so GSD owns the bus in-process (full subscribe + emit).
* - stateIO: 'sandboxed-storage' → createStateIO({io:'sandboxed-storage'}, {backend}) bound to a
* host-supplied storage (no arbitrary FS — web/no-child_process safe).
* - embeddingMode: 'imperative' → createImperativeAdapter({runtime:'vscode'}) — engine-as-library.
*
* Distribution: VS Code is shipped as an EXTENSION (Marketplace), NOT file-projected onto a
* config dir, so it intentionally has NO runtime descriptor / `--vscode` installer entry — the
* extension IS the host. This module is the binding the extension's activate() runs.
*
* Mock-friendly: takes `vscode` (with `vscode.lm`) + `hostStorage` ({read,write}) so it is
* behaviorally testable without a live VS Code host.
*
* @param {{ lm: { sendRequest: (req: unknown) => unknown } }} vscode VS Code namespace (vscode.lm)
* @param {{ read: (path: string) => string, write: (path: string, content: string) => void }} hostStorage
* sandboxed-storage backend (e.g. globalState/workspaceState/secrets).
* @returns {object} the composed IDE host surface: { runtime, model, hookBus, stateIO, adapter, commands }
* `bindGsdToVscode` moved to `vscode/host-binding.js` (the module the real
* extension's `activate()` requires) so it ships with the extension instead of
* living only under tests/. This fixture re-exports it so every existing test
* that requires `./fixtures/vscode-host-binding.cjs` keeps resolving without
* edits (tests/vscode-ide-reference.test.cjs and any other consumer).
*/
module.exports = function bindGsdToVscode(vscode, hostStorage) {
if (!vscode || !vscode.lm || typeof vscode.lm.sendRequest !== 'function') {
throw new TypeError('bindGsdToVscode: vscode.lm.sendRequest is required (active model provider)');
}
if (!hostStorage || typeof hostStorage.read !== 'function' || typeof hostStorage.write !== 'function') {
throw new TypeError('bindGsdToVscode: hostStorage {read,write} is required (sandboxed-storage backend)');
}
const { createImperativeAdapter } = require('../../gsd-core/bin/lib/adapter-imperative.cjs');
const { createModelAdapter } = require('../../gsd-core/bin/lib/model-adapter.cjs');
const { createHookBus } = require('../../gsd-core/bin/lib/hook-bus.cjs');
const { createStateIO } = require('../../gsd-core/bin/lib/state-io.cjs');
// Active model: GSD model calls route through vscode.lm. (No system-role
// messages — VS Code rejects them; a full extension builds LanguageModelChatMessages
// with User role only and selects a model via vscode.lm.selectChatModels.)
const model = createModelAdapter({ modelMode: 'active' }, {
sendRequest(req) {
return vscode.lm.sendRequest(req);
},
});
// Engine-owned hook bus: VS Code has no host bus, so GSD owns it in-process.
const hookBus = createHookBus({ bus: 'engine' });
// Sandboxed-storage stateIO bound to the host storage backend (no fs / no child_process).
const stateIO = createStateIO({ io: 'sandboxed-storage' }, { backend: hostStorage });
// Imperative adapter: the engine-as-library for the VS Code runtime.
const adapter = createImperativeAdapter({ runtime: 'vscode' });
// Command surface: Command Palette + Chat participant entries bound to the
// GSD command-routing hub via the imperative adapter (interface point 1).
const commands = Object.freeze({
'gsd.invoke': Object.freeze({ description: 'Invoke a GSD command via the embedded engine (palette/chat).' }),
'gsd.help': Object.freeze({ description: 'List GSD commands available in the IDE host.' }),
});
return Object.freeze({ runtime: 'vscode', model, hookBus, stateIO, adapter, commands });
};
module.exports = require('../../vscode/host-binding.js');