Files
msd-core/src/adapter-imperative.cts
Tom Boucher da368311ea feat(#1680): ADR-1239 Phase C-1 — imperative embedding adapter (composes loadRegistry) [AC2] (#1803)
* feat(#1680): ADR-1239 Phase C-1 — imperative embedding adapter (composes loadRegistry) [AC2]

Phase 3 slice 2 (AC2). The engine-as-library path: createImperativeAdapter
composes loadRegistry({includeInstalled:true}) — first-party-wins + consent +
fail-closed gates, identical trust semantics to the CLI — and binds the engine
surface behind the SAME HostIntegrationInterface the declarative adapter (AC1)
satisfies, plus a registry accessor for the composed capability set.

- src/adapter-imperative.cts: createImperativeAdapter({runtime}, {loadOptions})
  → ImperativeAdapter (kind:'imperative' + .registry + install/uninstall
  delegating to install-engine). Thin: delegates the loop, does not reimplement.
- tests/adapter-imperative.test.cjs: kind (16 runtimes), registry composition
  (loadRegistry called with includeInstalled:true), loadOptions pass-through,
  install/uninstall delegation, fail-closed construction.
- eslint.config.mjs + docs/INVENTORY-MANIFEST.json: ADR-457 ignores entry +
  cli_modules entry for the new emitted .cjs (the two drift gates that bit AC1,
  applied proactively here).

Concrete host binding (OpenCode/VS Code/pi) deferred to Phase 5 (#1682).

* test: remove dead readStateMd helper from bug-1760 test

readStateMd was defined but never called (writeStateMd is the only state-md
helper this test uses). Clears the lone no-unused-vars warning so the repo
lints fully clean (0 problems). No behavior change — test still passes 2/2.

* chore(changeset): add Changed fragment for imperative embedding adapter (#1680)

* fix(adapter-imperative): reword comment to avoid injection-scan substring match

The prompt-injection scan regex 'act\s+as\s+(?:a|an|the)' was matching the
'act as the' substring inside 'contract as the declarative adapter' (contrACT
AS THE). Reword 'contract as' -> 'shape as' — no 'act' substring, identical
meaning. Clears the 'lib source files are clean' + 'codebase prompt injection
scan' security-gate failures.
2026-06-28 11:10:59 -04:00

78 lines
3.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Imperative embedding adapter (ADR-1239 Phase C-1, AC2 / #1680).
*
* The engine-as-library path: an in-process host plugin calls
* `createImperativeAdapter({runtime})`, which composes the capability registry
* via `loadRegistry({includeInstalled:true})` (first-party-wins + consent +
* fail-closed gates) and binds the engine surface behind the SAME
* `HostIntegrationInterface` the declarative adapter satisfies. The adapter
* stays thin — it does NOT reimplement the loop resolver; it delegates to the
* engine + exposes the composed registry so a host (Phase 5) can bind its
* primitives (command/dispatch/model/hooks/state/artifact) to the registry's
* declared capability set.
*
* Concrete host binding (OpenCode/VS Code/pi) is deferred to Phase 5 (#1682,
* D15/D18). This slice ships the adapter + the composed-registry seam.
*
* Minimal interface (per ADR-1239 open wire-shape question): satisfies the
* same `{kind, runtime, install, uninstall}` shape as the declarative adapter,
* plus an imperative-specific `registry` accessor (the composed loadRegistry
* result). The full 6-point binding surface grows when a real host consumer
* fixes the shape.
*/
'use strict';
// eslint-disable-next-line @typescript-eslint/no-require-imports
import installEngine = require('./install-engine.cjs');
// eslint-disable-next-line @typescript-eslint/no-require-imports
import capabilityLoader = require('./capability-loader.cjs');
import type { HostIntegrationInterface, AdapterInstallIntent, AdapterUninstallIntent } from './embedding-adapter.cjs';
/**
* The imperative adapter: the shared contract PLUS the composed capability
* registry an in-process host binds its primitives to.
*/
export interface ImperativeAdapter extends HostIntegrationInterface {
readonly kind: 'imperative';
/** The composed capability registry (loadRegistry({includeInstalled:true})). */
readonly registry: ReturnType<typeof capabilityLoader.loadRegistry>;
}
export interface CreateImperativeAdapterOptions {
/** Optional overrides forwarded to loadRegistry (cwd, gsdHome, hostVersion). */
loadOptions?: Record<string, unknown>;
}
export function createImperativeAdapter(
{ runtime }: { runtime: string },
options: CreateImperativeAdapterOptions = {},
): ImperativeAdapter {
if (!runtime || typeof runtime !== 'string') {
throw new TypeError('createImperativeAdapter: runtime is required (non-empty string)');
}
// Compose first-party ∪ installed capability overlays with the SAME
// precedence, consent, and fail-closed-gate guarantees the CLI enforces —
// an in-process host gets identical trust semantics, not a parallel path.
const registry = capabilityLoader.loadRegistry({
includeInstalled: true,
...(options.loadOptions ?? {}),
});
return Object.freeze({
kind: 'imperative' as const,
runtime,
registry,
install(intent: AdapterInstallIntent): void {
installEngine.installRuntimeArtifacts(
runtime,
intent.configDir,
intent.scope,
intent.resolvedProfile,
intent.resolveAttribution,
);
},
uninstall(intent: AdapterUninstallIntent): void {
installEngine.uninstallRuntimeArtifacts(runtime, intent.configDir, intent.scope);
},
});
}