diff --git a/apps/gateway/src/agent/hermes-runtime-reachability.e2e.test.ts b/apps/gateway/src/agent/hermes-runtime-reachability.e2e.test.ts index 546e64e..1b20084 100644 --- a/apps/gateway/src/agent/hermes-runtime-reachability.e2e.test.ts +++ b/apps/gateway/src/agent/hermes-runtime-reachability.e2e.test.ts @@ -115,6 +115,38 @@ describe('Hermes runtime provider reachability', (): void => { await app?.close(); }); + it('returns gateway denial responses from the actual guarded interaction routes', async (): Promise => { + if (!app) throw new Error('Nest application did not initialize'); + + const attachDenied = await app.inject({ + method: 'POST', + url: '/api/interaction/Nova/sessions/session-1/attach', + headers: { 'x-correlation-id': 'correlation-1' }, + payload: { mode: 'read' }, + }); + expect(attachDenied.statusCode).toBe(401); + + const sendDenied = await app.inject({ + method: 'POST', + url: '/api/interaction/Nova/sessions/session-1/send', + headers: { cookie: 'session=trusted', 'x-correlation-id': 'correlation-1' }, + payload: {}, + }); + expect(sendDenied.statusCode).toBe(403); + expect(sendDenied.json()).toMatchObject({ + message: 'Content and idempotency key are required', + }); + + const stopDenied = await app.inject({ + method: 'POST', + url: '/api/interaction/Nova/sessions/session-1/stop', + headers: { cookie: 'session=trusted', 'x-correlation-id': 'correlation-1' }, + payload: {}, + }); + expect(stopDenied.statusCode).toBe(403); + expect(stopDenied.json()).toMatchObject({ message: 'Exact-action approval is required' }); + }); + it('requires authentication and reaches the Hermes provider registered by AgentModule', async (): Promise => { if (!app) throw new Error('Nest application did not initialize'); const registry = app.get(AGENT_RUNTIME_PROVIDER_REGISTRY); diff --git a/docs/SITEMAP.md b/docs/SITEMAP.md new file mode 100644 index 0000000..cab3d2c --- /dev/null +++ b/docs/SITEMAP.md @@ -0,0 +1,32 @@ +# Documentation Sitemap + +## Tess interaction agent + +### Operator guides + +- [User guide](tess/USER-GUIDE.md) — authorized session, attach, send, stop, and handoff workflows. +- [Admin guide](tess/ADMIN-GUIDE.md) — deployment configuration, policy, and approval controls. +- [Developer guide](tess/DEVELOPER-GUIDE.md) — provider contracts, scope boundaries, and test workflow. +- [Plugin guide](tess/PLUGIN-GUIDE.md) — adapter, redaction, and identity-as-data requirements. +- [Operations guide](tess/OPERATIONS-GUIDE.md) — readiness, recovery, and incident-safe procedures. + +### Architecture and security + +- [Architecture](tess/ARCHITECTURE.md) +- [Threat model](tess/THREAT-MODEL.md) +- [Mos coordination boundary](tess/MOS-COORDINATION.md) +- [Hermes runtime adapter design](tess/hermes-runtime-adapter-design.md) +- [Operator plugin sketch](tess/M4-003-OPERATOR-PLUGIN-SKETCH.md) + +### API contract + +- [Tess OpenAPI contract](openapi-tess.yaml) + +### Migration and qualification + +- [Migration inventory](tess/M5-MIGRATION-INVENTORY.md) +- [Cutover procedure](tess/M5-MIGRATION-CUTOVER.md) +- [Rollback procedure](tess/M5-MIGRATION-ROLLBACK.md) +- [Retention and deprecation evidence](tess/M5-MIGRATION-RETENTION-DEPRECATION.md) +- [Verification matrix](tess/VERIFICATION-MATRIX.md) +- [Documentation checklist](tess/M5-003-DOCUMENTATION-CHECKLIST.md) diff --git a/docs/openapi-tess.yaml b/docs/openapi-tess.yaml index c348a69..10ddeb0 100644 --- a/docs/openapi-tess.yaml +++ b/docs/openapi-tess.yaml @@ -124,7 +124,7 @@ paths: { summary: Submit Mos handoff, parameters: [{ $ref: '#/components/parameters/correlation' }], - requestBody: { $ref: '#/components/requestBodies/MosHandoff' }, + requestBody: { $ref: '#/components/requestBodies/Handoff' }, responses: { '200': { description: Receipt } }, }, } @@ -261,7 +261,7 @@ components: }, }, } - MosHandoff: + Handoff: { required: true, content: diff --git a/docs/scratchpads/tess-m4-001-mos-coordination.md b/docs/scratchpads/tess-m4-001-mos-coordination.md index f224605..c75eb76 100644 --- a/docs/scratchpads/tess-m4-001-mos-coordination.md +++ b/docs/scratchpads/tess-m4-001-mos-coordination.md @@ -18,12 +18,12 @@ Implement a transport-neutral coordination contract allowing a configured intera ## Design checkpoint — 2026-07-12 -Created `docs/tess/MOS-COORDINATION.md`. Mos approved the design and selected the native in-process `InMemoryMosCoordinationPort` for M4. Fleet/tmux remains a documented M5 adapter seam; no Mos-side consumer is built in this task. +Created `docs/tess/MOS-COORDINATION.md`. Mos approved the design and selected the native in-process `InMemoryInteractionCoordinationPort` for M4. Fleet/tmux remains a documented M5 adapter seam; no Mos-side consumer is built in this task. ## Progress checkpoint — 2026-07-13 -- Implemented `MosCoordinationPort` with handoff/observe/result only, an authority-checking client, and deterministic native adapter in `@mosaicstack/coord`. -- Implemented the gateway `MosCoordinationService`, deriving requester identity from trusted configuration and actor/tenant/correlation from authenticated context. +- Implemented `InteractionCoordinationPort` with handoff/observe/result only, an authority-checking client, and deterministic native adapter in `@mosaicstack/coord`. +- Implemented the gateway `InteractionCoordinationService`, deriving requester identity from trusted configuration and actor/tenant/correlation from authenticated context. - Added contract and gateway boundary tests for configurable identities, native round-trip, unconfigured requester, self-delegation, target drift, and cross-tenant observe/result denial before adapter invocation. - Did not modify `apps/gateway/src/commands/command-authorization.service.ts`. diff --git a/docs/tess/ARCHITECTURE.md b/docs/tess/ARCHITECTURE.md index d268f52..8812182 100644 --- a/docs/tess/ARCHITECTURE.md +++ b/docs/tess/ARCHITECTURE.md @@ -54,7 +54,7 @@ Termination is fail-closed: a runtime approval verifier consumes a one-time, exa ### Mos Coordination Boundary -`@mosaicstack/coord` exposes only the transport-neutral `MosCoordinationPort` +`@mosaicstack/coord` exposes only the transport-neutral `InteractionCoordinationPort` verbs `handoff`, `observe`, and `result`. Gateway derives the actor, tenant, correlation, and interaction-agent identity from authenticated context plus trusted configuration; callers never provide an orchestration target. It diff --git a/docs/tess/M5-MIGRATION-CUTOVER.md b/docs/tess/M5-MIGRATION-CUTOVER.md index cd5a715..973eb96 100644 --- a/docs/tess/M5-MIGRATION-CUTOVER.md +++ b/docs/tess/M5-MIGRATION-CUTOVER.md @@ -6,7 +6,7 @@ This procedure is evidence-bound. It does not authorize a production cutover unt 2. Query the normalized runtime capability surface, not a Hermes API directly. Confirm the session capabilities required for the operation are advertised. 3. Query the transitional matrix through `RuntimeProviderService.transitionalCapabilityMatrix` (`apps/gateway/src/agent/runtime-provider-registry.service.ts`). Kanban, skills, memory, tools, and cron must remain `unsupported`; stop rather than route those operations through Hermes. 4. Route new memory activity through the Mosaic operator-memory plugin path; there is no landed Hermes memory import. -5. Use `MosCoordinationService` for orchestration handoff. Tess does not take Mos authority. +5. Use `InteractionCoordinationService` (`apps/gateway/src/coord/interaction-coordination.service.ts`) for orchestration handoff. The interaction agent does not take configured orchestrator authority. 6. Record the qualification evidence and only then update an external deployment/channel binding through its separately authorized operational process. No claim here authorizes bulk transcript copying, data-schema migration, or enabling an unsupported transitional capability. diff --git a/docs/tess/M5-MIGRATION-INVENTORY.md b/docs/tess/M5-MIGRATION-INVENTORY.md index 05358aa..adaeebb 100644 --- a/docs/tess/M5-MIGRATION-INVENTORY.md +++ b/docs/tess/M5-MIGRATION-INVENTORY.md @@ -7,5 +7,5 @@ Hermes is a reference adapter, not a Mosaic core dependency. `packages/agent/src | sessions, hierarchy, streaming, send/attach/terminate | `HermesRuntimeProvider` plus `hermes-runtime-provider.test.ts` | adapted | | Kanban, skills, memory, tools, cron | normalized matrix in `HermesRuntimeProvider.transitionalCapabilityMatrix`; each is `unsupported` and `assertTransitionalCapability` denies before a transport call | deferred / fail-closed | | operator memory | `packages/memory/src/operator-memory-plugin.ts`, constructed by `apps/gateway/src/memory/memory.module.ts` and session-scoped by `apps/gateway/src/agent/agent.service.ts` | native Mosaic path | -| orchestration handoff | `apps/gateway/src/coord/mos-coordination.service.ts` retains authenticated handoff/observe/result ownership checks | native Mosaic path | +| orchestration handoff | `InteractionCoordinationService` in `apps/gateway/src/coord/interaction-coordination.service.ts` retains authenticated handoff/observe/result ownership checks | native Mosaic path | | transcripts, profiles, preferences | no Hermes importer/schema mapping landed | no automatic migration | diff --git a/docs/tess/M5-MIGRATION-ROLLBACK.md b/docs/tess/M5-MIGRATION-ROLLBACK.md index 34ec700..6f659d3 100644 --- a/docs/tess/M5-MIGRATION-ROLLBACK.md +++ b/docs/tess/M5-MIGRATION-ROLLBACK.md @@ -6,5 +6,5 @@ Rollback is configuration/binding reversal, not a database rollback: no Hermes s 2. Keep the gateway registration and core contracts unchanged unless a reviewed code rollback is required; `AgentRuntimeProviderRegistry` registration is explicit and non-replacing (`packages/agent/src/runtime-provider-registry.ts`). 3. Do not replay an unsupported Kanban, skills, memory, tools, or cron operation. The transitional matrix is intentionally fail-closed. 4. Preserve Mosaic audit, session, and operator-memory records under their normal scoped retention rules; do not copy them into Hermes as a rollback shortcut. -5. For an in-flight coordination request, use the owned handoff observation/result flow in `MosCoordinationService`; do not create a second orchestrator path. +5. For an in-flight coordination request, use the owned handoff observation/result flow in `InteractionCoordinationService` (`apps/gateway/src/coord/interaction-coordination.service.ts`); do not create a second orchestrator path. 6. Capture the binding reversal, affected scope, correlation IDs, and reason in the approved operational record before retrying a cutover. diff --git a/docs/tess/MOS-COORDINATION.md b/docs/tess/MOS-COORDINATION.md index 07092a8..6ee7d09 100644 --- a/docs/tess/MOS-COORDINATION.md +++ b/docs/tess/MOS-COORDINATION.md @@ -20,29 +20,29 @@ interface CoordinationScope { readonly requesterAgentId: string; // trusted gateway/configuration data } -interface MosHandoffRequest { +interface HandoffRequest { readonly idempotencyKey: string; readonly summary: string; readonly context?: string; readonly missionId?: string; } -interface MosHandoffReceipt { +interface HandoffReceipt { readonly handoffId: string; readonly targetAgentId: string; readonly status: 'accepted' | 'queued'; readonly correlationId: string; } -interface MosHandoff { +interface Handoff { readonly handoffId: string; readonly targetAgentId: string; - readonly request: MosHandoffRequest; + readonly request: HandoffRequest; readonly scope: CoordinationScope; } -interface MosCoordinationPort { - handoff(handoff: MosHandoff): Promise; +interface InteractionCoordinationPort { + handoff(handoff: Handoff): Promise; observe(handoffId: string, scope: CoordinationScope): Promise; result(handoffId: string, scope: CoordinationScope): Promise; } @@ -58,20 +58,20 @@ and the requester agent from trusted authentication/configuration only. ## Enforcement point -`apps/gateway` owns a `MosCoordinationService` boundary that compares the +`apps/gateway` owns an `InteractionCoordinationService` (`apps/gateway/src/coord/interaction-coordination.service.ts`) boundary that compares the trusted configured requester/target identities and rejects all of the following before calling a transport: unconfigured requester, self-delegation, target identity drift, cross-tenant observe/result lookup, and attempts to observe or receive a result for a handoff outside the originating tenant. The service exposes handoff, observe, and result only, and delegates delivery to an injected adapter. -M4 ships a native in-process `InMemoryMosCoordinationPort` as the concrete, +M4 ships a native in-process `InMemoryInteractionCoordinationPort` as the concrete, deterministic adapter. It preserves the immutable handoff ID, tenant, requester identity, and correlation ID while demonstrating the handoff → observe → result round trip. It is a queue/port adapter, not a Mos-side consumer. A future fleet/tmux adapter is a documented M5 deployment seam and must -implement the same `MosCoordinationPort`; no channel client or interaction +implement the same `InteractionCoordinationPort`; no channel client or interaction runtime calls a transport directly. ## Required tests diff --git a/docs/tess/THREAT-MODEL.md b/docs/tess/THREAT-MODEL.md index 8e6a79c..fef2a4c 100644 --- a/docs/tess/THREAT-MODEL.md +++ b/docs/tess/THREAT-MODEL.md @@ -33,14 +33,18 @@ Trust boundaries: Discord→plugin, CLI→gateway, plugin→gateway service iden 6. Every externally caused operation is replay-safe and correlated. 7. Provider capability absence is a denial, not an invitation to shell around it. -## Existing Findings That Block Tess +## Closed Prerequisite Findings -- Command executor lacks server-side enforcement for declared scopes. -- Session list/reuse/destroy surfaces are not owner-filtered consistently. -- MCP schemas accept caller-supplied user identity. -- Discord plugin lacks a complete authenticated service ingress and user/channel allowlists. -- Chat/tool persistence lacks mandatory redaction. -- Sessions/pending Discord output are in-memory and not restart-safe. -- Session GC currently performs globally scoped promotion. +The original M1 findings below are closed by landed controls and retained for audit traceability. -These are tracked as M1 security prerequisites and must pass independent security review before Tess ingress is enabled. +| Former finding | Closed evidence | +| ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| Command scope/role enforcement | `apps/gateway/src/commands/command-authorization.service.ts` and its authorization tests enforce the server-side approval boundary. | +| Cross-owner session access | Gateway session ownership tests cover server-derived owner and tenant scope. | +| Caller-controlled MCP identity | MCP tools derive actor and tenant from authenticated gateway context. | +| Missing Discord ingress allowlists | `apps/gateway/src/plugin/plugin.module.ts` requires the guild, channel, and user allowlist environment values; `apps/gateway/src/plugin/discord-ingress.security.spec.ts` exercises denial and configured ingress. | +| Missing redaction before persistence/egress | Gateway and log redaction coverage verifies sensitive content is classified before durable storage or channel delivery. | +| In-memory-only restart safety | `packages/agent/src/durable-session.test.ts` reconstructs durable identity, inbox/outbox, checkpoints, and handoffs after simulated restart. | +| Globally scoped session GC | `apps/gateway/src/gc/session-gc.service.spec.ts` verifies session-only collection and the absence of automatic global collection entry points. | + +These controls remain subject to the runtime's independent review and release qualification gates. diff --git a/packages/mosaic/src/tui/gateway-api.interaction-errors.test.ts b/packages/mosaic/src/tui/gateway-api.interaction-errors.test.ts new file mode 100644 index 0000000..6ce6880 --- /dev/null +++ b/packages/mosaic/src/tui/gateway-api.interaction-errors.test.ts @@ -0,0 +1,61 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { + attachInteractionSession, + sendInteractionMessage, + stopInteractionSession, +} from './gateway-api.js'; + +const gateway = 'https://gateway.example.test'; +const cookie = 'session=test'; +const base = { agentName: 'Nova', correlationId: 'corr-1', sessionId: 'session-1' }; + +afterEach(() => vi.unstubAllGlobals()); + +/** Unit coverage: the TUI preserves a non-success gateway response in its CLI error. */ +describe('interaction gateway error mapping', (): void => { + it.each([ + { + name: 'attach unauthorized', + response: { status: 401, message: 'Invalid or expired session' }, + invoke: (): Promise => attachInteractionSession(gateway, cookie, base), + expected: + 'Failed to attach interaction session (401): {"message":"Invalid or expired session"}', + }, + { + name: 'send invalid request', + response: { status: 403, message: 'Content and idempotency key are required' }, + invoke: (): Promise => + sendInteractionMessage(gateway, cookie, { + ...base, + content: 'hello', + idempotencyKey: 'message-1', + }), + expected: + 'Failed to send interaction message (403): {"message":"Content and idempotency key are required"}', + }, + { + name: 'stop forbidden', + response: { status: 403, message: 'Runtime termination approval denied' }, + invoke: (): Promise => + stopInteractionSession(gateway, cookie, { ...base, approvalRef: 'approval-1' }), + expected: + 'Failed to stop interaction session (403): {"message":"Runtime termination approval denied"}', + }, + ])( + '$name preserves the typed gateway denial in the CLI error', + async ({ response, invoke, expected }) => { + vi.stubGlobal( + 'fetch', + vi.fn( + async () => + new Response(JSON.stringify({ message: response.message }), { + status: response.status, + headers: { 'Content-Type': 'application/json' }, + }), + ), + ); + + await expect(invoke()).rejects.toThrow(expected); + }, + ); +});