test(gateway): exercise interaction denial routes
All checks were successful
ci/woodpecker/pr/ci Pipeline was successful

This commit is contained in:
Jarvis
2026-07-13 14:36:28 -05:00
parent 32d431b42b
commit 658ad2887f
6 changed files with 48 additions and 16 deletions

View File

@@ -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);

View File

@@ -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:

View File

@@ -18,11 +18,11 @@ 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 `InteractionCoordinationService`, 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`.

View File

@@ -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

View File

@@ -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>;
} }
@@ -65,13 +65,13 @@ 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

View File

@@ -11,7 +11,7 @@ const base = { agentName: 'Nova', correlationId: 'corr-1', sessionId: 'session-1
afterEach(() => vi.unstubAllGlobals()); afterEach(() => vi.unstubAllGlobals());
/** The TUI must preserve gateway denial type/status rather than treating it as success. */ /** Unit coverage: the TUI preserves a non-success gateway response in its CLI error. */
describe('interaction gateway error mapping', (): void => { describe('interaction gateway error mapping', (): void => {
it.each([ it.each([
{ {
@@ -23,7 +23,7 @@ describe('interaction gateway error mapping', (): void => {
}, },
{ {
name: 'send invalid request', name: 'send invalid request',
response: { status: 400, message: 'Content and idempotency key are required' }, response: { status: 403, message: 'Content and idempotency key are required' },
invoke: (): Promise<unknown> => invoke: (): Promise<unknown> =>
sendInteractionMessage(gateway, cookie, { sendInteractionMessage(gateway, cookie, {
...base, ...base,
@@ -31,7 +31,7 @@ describe('interaction gateway error mapping', (): void => {
idempotencyKey: 'message-1', idempotencyKey: 'message-1',
}), }),
expected: expected:
'Failed to send interaction message (400): {"message":"Content and idempotency key are required"}', 'Failed to send interaction message (403): {"message":"Content and idempotency key are required"}',
}, },
{ {
name: 'stop forbidden', name: 'stop forbidden',