docs(tess): remediate M5 qualification findings (#750)
This commit was merged in pull request #750.
This commit is contained in:
@@ -115,6 +115,38 @@ describe('Hermes runtime provider reachability', (): void => {
|
|||||||
await app?.close();
|
await app?.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('returns gateway denial responses from the actual guarded interaction routes', async (): Promise<void> => {
|
||||||
|
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<void> => {
|
it('requires authentication and reaches the Hermes provider registered by AgentModule', async (): Promise<void> => {
|
||||||
if (!app) throw new Error('Nest application did not initialize');
|
if (!app) throw new Error('Nest application did not initialize');
|
||||||
const registry = app.get(AGENT_RUNTIME_PROVIDER_REGISTRY);
|
const registry = app.get(AGENT_RUNTIME_PROVIDER_REGISTRY);
|
||||||
|
|||||||
32
docs/SITEMAP.md
Normal file
32
docs/SITEMAP.md
Normal file
@@ -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)
|
||||||
@@ -124,7 +124,7 @@ paths:
|
|||||||
{
|
{
|
||||||
summary: Submit Mos handoff,
|
summary: Submit Mos handoff,
|
||||||
parameters: [{ $ref: '#/components/parameters/correlation' }],
|
parameters: [{ $ref: '#/components/parameters/correlation' }],
|
||||||
requestBody: { $ref: '#/components/requestBodies/MosHandoff' },
|
requestBody: { $ref: '#/components/requestBodies/Handoff' },
|
||||||
responses: { '200': { description: Receipt } },
|
responses: { '200': { description: Receipt } },
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -261,7 +261,7 @@ components:
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
MosHandoff:
|
Handoff:
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
content:
|
content:
|
||||||
|
|||||||
@@ -18,12 +18,12 @@ Implement a transport-neutral coordination contract allowing a configured intera
|
|||||||
|
|
||||||
## Design checkpoint — 2026-07-12
|
## 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
|
## Progress checkpoint — 2026-07-13
|
||||||
|
|
||||||
- Implemented `MosCoordinationPort` with handoff/observe/result only, an authority-checking client, and deterministic native adapter in `@mosaicstack/coord`.
|
- Implemented `InteractionCoordinationPort` 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 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.
|
- 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`.
|
- Did not modify `apps/gateway/src/commands/command-authorization.service.ts`.
|
||||||
|
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ Termination is fail-closed: a runtime approval verifier consumes a one-time, exa
|
|||||||
|
|
||||||
### Mos Coordination Boundary
|
### 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,
|
verbs `handoff`, `observe`, and `result`. Gateway derives the actor, tenant,
|
||||||
correlation, and interaction-agent identity from authenticated context plus
|
correlation, and interaction-agent identity from authenticated context plus
|
||||||
trusted configuration; callers never provide an orchestration target. It
|
trusted configuration; callers never provide an orchestration target. It
|
||||||
|
|||||||
@@ -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.
|
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.
|
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.
|
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.
|
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.
|
No claim here authorizes bulk transcript copying, data-schema migration, or enabling an unsupported transitional capability.
|
||||||
|
|||||||
@@ -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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| transcripts, profiles, preferences | no Hermes importer/schema mapping landed | no automatic migration |
|
||||||
|
|||||||
@@ -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`).
|
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.
|
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.
|
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.
|
6. Capture the binding reversal, affected scope, correlation IDs, and reason in the approved operational record before retrying a cutover.
|
||||||
|
|||||||
@@ -20,29 +20,29 @@ interface CoordinationScope {
|
|||||||
readonly requesterAgentId: string; // trusted gateway/configuration data
|
readonly requesterAgentId: string; // trusted gateway/configuration data
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MosHandoffRequest {
|
interface HandoffRequest {
|
||||||
readonly idempotencyKey: string;
|
readonly idempotencyKey: string;
|
||||||
readonly summary: string;
|
readonly summary: string;
|
||||||
readonly context?: string;
|
readonly context?: string;
|
||||||
readonly missionId?: string;
|
readonly missionId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MosHandoffReceipt {
|
interface HandoffReceipt {
|
||||||
readonly handoffId: string;
|
readonly handoffId: string;
|
||||||
readonly targetAgentId: string;
|
readonly targetAgentId: string;
|
||||||
readonly status: 'accepted' | 'queued';
|
readonly status: 'accepted' | 'queued';
|
||||||
readonly correlationId: string;
|
readonly correlationId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MosHandoff {
|
interface Handoff {
|
||||||
readonly handoffId: string;
|
readonly handoffId: string;
|
||||||
readonly targetAgentId: string;
|
readonly targetAgentId: string;
|
||||||
readonly request: MosHandoffRequest;
|
readonly request: HandoffRequest;
|
||||||
readonly scope: CoordinationScope;
|
readonly scope: CoordinationScope;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MosCoordinationPort {
|
interface InteractionCoordinationPort {
|
||||||
handoff(handoff: MosHandoff): Promise<MosHandoffReceipt>;
|
handoff(handoff: Handoff): Promise<HandoffReceipt>;
|
||||||
observe(handoffId: string, scope: CoordinationScope): Promise<CoordinationObservation>;
|
observe(handoffId: string, scope: CoordinationScope): Promise<CoordinationObservation>;
|
||||||
result(handoffId: string, scope: CoordinationScope): Promise<CoordinationResult>;
|
result(handoffId: string, scope: CoordinationScope): Promise<CoordinationResult>;
|
||||||
}
|
}
|
||||||
@@ -58,20 +58,20 @@ and the requester agent from trusted authentication/configuration only.
|
|||||||
|
|
||||||
## Enforcement point
|
## 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
|
trusted configured requester/target identities and rejects all of the following
|
||||||
before calling a transport: unconfigured requester, self-delegation, target
|
before calling a transport: unconfigured requester, self-delegation, target
|
||||||
identity drift, cross-tenant observe/result lookup, and attempts to observe or
|
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,
|
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.
|
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
|
deterministic adapter. It preserves the immutable handoff ID, tenant, requester
|
||||||
identity, and correlation ID while demonstrating the handoff → observe → result
|
identity, and correlation ID while demonstrating the handoff → observe → result
|
||||||
round trip. It is a queue/port adapter, not a Mos-side consumer.
|
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
|
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.
|
runtime calls a transport directly.
|
||||||
|
|
||||||
## Required tests
|
## Required tests
|
||||||
|
|||||||
@@ -33,14 +33,18 @@ Trust boundaries: Discord→plugin, CLI→gateway, plugin→gateway service iden
|
|||||||
6. Every externally caused operation is replay-safe and correlated.
|
6. Every externally caused operation is replay-safe and correlated.
|
||||||
7. Provider capability absence is a denial, not an invitation to shell around it.
|
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.
|
The original M1 findings below are closed by landed controls and retained for audit traceability.
|
||||||
- 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.
|
|
||||||
|
|
||||||
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.
|
||||||
|
|||||||
@@ -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<unknown> => 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<unknown> =>
|
||||||
|
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<unknown> =>
|
||||||
|
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);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user