From cca6aaf94764735f8bd9dc62a22fb0f7eb2d9ff9 Mon Sep 17 00:00:00 2001 From: "jason.woltje" Date: Mon, 13 Jul 2026 13:14:43 +0000 Subject: [PATCH] feat(agent): add Hermes transitional capability matrix (#738) --- .../agent/src/hermes-runtime-provider.test.ts | 22 +++++++++++ packages/agent/src/hermes-runtime-provider.ts | 39 ++++++++++++++++++- .../types/src/agent/agent-runtime-provider.ts | 18 +++++++++ 3 files changed, 78 insertions(+), 1 deletion(-) diff --git a/packages/agent/src/hermes-runtime-provider.test.ts b/packages/agent/src/hermes-runtime-provider.test.ts index e94b822..e5577ee 100644 --- a/packages/agent/src/hermes-runtime-provider.test.ts +++ b/packages/agent/src/hermes-runtime-provider.test.ts @@ -30,6 +30,28 @@ const transport = (capabilities = ['session.list', 'session.tree']): HermesRunti terminate: vi.fn(), }); describe('HermesRuntimeProvider normalization boundary', () => { + it('returns an exhaustive fail-closed transitional capability matrix', async () => { + const provider = new HermesRuntimeProvider(transport()); + + await expect(provider.transitionalCapabilityMatrix(scope)).resolves.toEqual([ + { capability: 'kanban', status: 'unsupported' }, + { capability: 'skills', status: 'unsupported' }, + { capability: 'memory', status: 'unsupported' }, + { capability: 'tools', status: 'unsupported' }, + { capability: 'cron', status: 'unsupported' }, + ]); + }); + + it('denies unsupported transitional capabilities without calling Hermes', async () => { + const hermes = transport(); + const provider = new HermesRuntimeProvider(hermes); + + await expect(provider.assertTransitionalCapability('memory', scope)).rejects.toMatchObject({ + code: 'capability_unsupported', + }); + expect(hermes.capabilities).not.toHaveBeenCalled(); + }); + it('normalizes legacy sessions without exposing legacy fields', async () => { const provider = new HermesRuntimeProvider(transport()); await expect(provider.listSessions(scope)).resolves.toEqual( diff --git a/packages/agent/src/hermes-runtime-provider.ts b/packages/agent/src/hermes-runtime-provider.ts index 90c7df9..8e4bb51 100644 --- a/packages/agent/src/hermes-runtime-provider.ts +++ b/packages/agent/src/hermes-runtime-provider.ts @@ -11,9 +11,19 @@ import type { RuntimeSessionState, RuntimeSessionTree, RuntimeStreamEvent, + TransitionalCapabilityInventoryEntry, + TransitionalCapabilityInventoryProvider, + TransitionalRuntimeCapability, } from '@mosaicstack/types'; const HERMES_PROVIDER_ID = 'runtime.hermes'; +const TRANSITIONAL_CAPABILITIES: readonly TransitionalRuntimeCapability[] = [ + 'kanban', + 'skills', + 'memory', + 'tools', + 'cron', +]; const RUNTIME_CAPABILITIES: readonly RuntimeCapability[] = [ 'session.list', 'session.tree', @@ -65,11 +75,38 @@ export class HermesRuntimeProviderError extends Error { * Transitional Hermes adapter. Legacy identifiers and schemas do not cross this * boundary: callers only observe Mosaic AgentRuntimeProvider contracts. */ -export class HermesRuntimeProvider implements AgentRuntimeProvider { +export class HermesRuntimeProvider + implements AgentRuntimeProvider, TransitionalCapabilityInventoryProvider +{ readonly id = HERMES_PROVIDER_ID; constructor(private readonly transport: HermesRuntimeTransport) {} + /** + * Full AC-TESS-05 migration inventory. These operations are deliberately + * unsupported until their Mosaic-owned plugin contracts exist. + */ + async transitionalCapabilityMatrix( + _scope: RuntimeScope, + ): Promise { + return TRANSITIONAL_CAPABILITIES.map((capability) => ({ capability, status: 'unsupported' })); + } + + async assertTransitionalCapability( + capability: TransitionalRuntimeCapability, + scope: RuntimeScope, + ): Promise { + const entry = (await this.transitionalCapabilityMatrix(scope)).find( + (candidate) => candidate.capability === capability, + ); + if (!entry || entry.status !== 'supported') { + throw new HermesRuntimeProviderError( + 'capability_unsupported', + `Hermes transitional capability is unsupported: ${capability}`, + ); + } + } + async capabilities(scope: RuntimeScope): Promise { const legacyCapabilities = await this.transport.capabilities(scope); return { diff --git a/packages/types/src/agent/agent-runtime-provider.ts b/packages/types/src/agent/agent-runtime-provider.ts index 4a5fb56..c5b343a 100644 --- a/packages/types/src/agent/agent-runtime-provider.ts +++ b/packages/types/src/agent/agent-runtime-provider.ts @@ -6,6 +6,24 @@ export type RuntimeCapability = | 'session.attach' | 'session.terminate'; export type RuntimeSessionState = 'starting' | 'active' | 'idle' | 'stopped' | 'failed'; + +/** Transitional capability inventory is normalized; provider legacy vocabularies never enter core. */ +export type TransitionalRuntimeCapability = 'kanban' | 'skills' | 'memory' | 'tools' | 'cron'; +export type TransitionalCapabilityStatus = 'supported' | 'unsupported'; +export interface TransitionalCapabilityInventoryEntry { + capability: TransitionalRuntimeCapability; + status: TransitionalCapabilityStatus; +} +/** Optional extension for transitional adapters; not every runtime has Hermes inventory. */ +export interface TransitionalCapabilityInventoryProvider { + transitionalCapabilityMatrix( + scope: RuntimeScope, + ): Promise; + assertTransitionalCapability( + capability: TransitionalRuntimeCapability, + scope: RuntimeScope, + ): Promise; +} export type RuntimeAttachMode = 'read' | 'control'; /** Server-derived immutable authority context. Client identity fields are intentionally absent. */