test(gateway): exercise interaction denial routes
All checks were successful
ci/woodpecker/pr/ci Pipeline was successful
All checks were successful
ci/woodpecker/pr/ci Pipeline was successful
This commit is contained in:
@@ -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<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> => {
|
||||
if (!app) throw new Error('Nest application did not initialize');
|
||||
const registry = app.get(AGENT_RUNTIME_PROVIDER_REGISTRY);
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -18,11 +18,11 @@ 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 `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`.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<MosHandoffReceipt>;
|
||||
interface InteractionCoordinationPort {
|
||||
handoff(handoff: Handoff): Promise<HandoffReceipt>;
|
||||
observe(handoffId: string, scope: CoordinationScope): Promise<CoordinationObservation>;
|
||||
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,
|
||||
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
|
||||
|
||||
@@ -11,7 +11,7 @@ const base = { agentName: 'Nova', correlationId: 'corr-1', sessionId: 'session-1
|
||||
|
||||
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 => {
|
||||
it.each([
|
||||
{
|
||||
@@ -23,7 +23,7 @@ describe('interaction gateway error mapping', (): void => {
|
||||
},
|
||||
{
|
||||
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> =>
|
||||
sendInteractionMessage(gateway, cookie, {
|
||||
...base,
|
||||
@@ -31,7 +31,7 @@ describe('interaction gateway error mapping', (): void => {
|
||||
idempotencyKey: 'message-1',
|
||||
}),
|
||||
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',
|
||||
|
||||
Reference in New Issue
Block a user