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.
This commit is contained in:
Tom Boucher
2026-06-28 11:10:59 -04:00
committed by GitHub
parent c642ed0ec5
commit da368311ea
6 changed files with 184 additions and 3 deletions

View File

@@ -0,0 +1,77 @@
/**
* 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);
},
});
}