Compare commits

..

1 Commits

Author SHA1 Message Date
Hermes Agent
f6d6d0bb62 refactor(#747): de-hardcode interaction coordination names
All checks were successful
ci/woodpecker/pr/ci Pipeline was successful
2026-07-13 12:17:21 -05:00
26 changed files with 28 additions and 1141 deletions

View File

@@ -115,38 +115,6 @@ 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

@@ -1,20 +1,9 @@
const PATH_METADATA = 'path';
import { describe, expect, it, vi } from 'vitest'; import { describe, expect, it, vi } from 'vitest';
import { InteractionCoordinationController } from './interaction-coordination.controller.js'; import { InteractionCoordinationController } from './interaction-coordination.controller.js';
const user = { id: 'operator-1', tenantId: 'tenant-a' }; const user = { id: 'operator-1', tenantId: 'tenant-a' };
describe('InteractionCoordinationController', () => { describe('InteractionCoordinationController', () => {
it('exposes the neutral canonical route and Mos compatibility alias over identical handlers', () => {
expect(Reflect.getMetadata(PATH_METADATA, InteractionCoordinationController)).toEqual([
'api/coord/interaction',
'api/coord/mos',
]);
expect(InteractionCoordinationController.prototype.handoff).toBeTypeOf('function');
expect(InteractionCoordinationController.prototype.observe).toBeTypeOf('function');
expect(InteractionCoordinationController.prototype.result).toBeTypeOf('function');
});
it('derives actor and tenant from the authenticated user rather than handoff input', async () => { it('derives actor and tenant from the authenticated user rather than handoff input', async () => {
const coordination = { const coordination = {
handoff: vi.fn(async () => ({ handoffId: 'handoff-1' })), handoff: vi.fn(async () => ({ handoffId: 'handoff-1' })),

View File

@@ -22,8 +22,7 @@ import type {
import { InteractionCoordinationService } from './interaction-coordination.service.js'; import { InteractionCoordinationService } from './interaction-coordination.service.js';
/** Authenticated interaction-plane boundary for the handoff/observe/result-only interaction coordination contract. */ /** Authenticated interaction-plane boundary for the handoff/observe/result-only interaction coordination contract. */
/** `api/coord/interaction` is canonical; the Mos path remains a compatibility alias. */ @Controller('api/coord/mos')
@Controller(['api/coord/interaction', 'api/coord/mos'])
@UseGuards(AuthGuard) @UseGuards(AuthGuard)
export class InteractionCoordinationController { export class InteractionCoordinationController {
constructor( constructor(

View File

@@ -1,88 +0,0 @@
import 'reflect-metadata';
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
import { Global, Module } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import { FastifyAdapter, type NestFastifyApplication } from '@nestjs/platform-fastify';
import { AUTH } from '../auth/auth.tokens.js';
import { AuthGuard } from '../auth/auth.guard.js';
import { InteractionCoordinationController } from './interaction-coordination.controller.js';
import { InteractionCoordinationService } from './interaction-coordination.service.js';
@Global()
@Module({
providers: [
{
provide: AUTH,
useValue: {
api: {
getSession: vi.fn(async ({ headers }: { headers: Headers }) =>
headers.get('cookie') === 'session=trusted'
? { user: { id: 'operator-1', tenantId: 'tenant-1' }, session: { id: 'session-1' } }
: null,
),
},
},
},
AuthGuard,
],
exports: [AUTH, AuthGuard],
})
class AuthenticatedRequestModule {}
describe('InteractionCoordinationController route aliases', (): void => {
let app: NestFastifyApplication | undefined;
const coordination = {
handoff: vi.fn(async () => ({ handoffId: 'handoff-1' })),
observe: vi.fn(async () => ({ status: 'running' })),
result: vi.fn(async () => ({ status: 'completed' })),
};
beforeAll(async (): Promise<void> => {
const moduleRef = await Test.createTestingModule({
imports: [AuthenticatedRequestModule],
controllers: [InteractionCoordinationController],
providers: [{ provide: InteractionCoordinationService, useValue: coordination }],
}).compile();
app = moduleRef.createNestApplication<NestFastifyApplication>(new FastifyAdapter());
await app.init();
await app.getHttpAdapter().getInstance().ready();
});
afterAll(async (): Promise<void> => app?.close());
it('routes handoff, observe, and result through the same AuthGuard-protected service for both prefixes', async (): Promise<void> => {
if (!app) throw new Error('test app was not initialized');
for (const prefix of ['/api/coord/interaction', '/api/coord/mos']) {
const headers = { cookie: 'session=trusted', 'x-correlation-id': `corr-${prefix}` };
expect(
(
await app.inject({
method: 'POST',
url: `${prefix}/handoff`,
headers,
payload: { idempotencyKey: `key-${prefix}`, summary: 'handoff' },
})
).statusCode,
).toBe(201);
expect(
(await app.inject({ method: 'GET', url: `${prefix}/handoff-1/observe`, headers }))
.statusCode,
).toBe(200);
expect(
(await app.inject({ method: 'GET', url: `${prefix}/handoff-1/result`, headers }))
.statusCode,
).toBe(200);
}
expect(coordination.handoff).toHaveBeenCalledTimes(2);
expect(coordination.observe).toHaveBeenCalledTimes(2);
expect(coordination.result).toHaveBeenCalledTimes(2);
expect(
(
await app.inject({
method: 'POST',
url: '/api/coord/interaction/handoff',
payload: { idempotencyKey: 'denied', summary: 'x' },
})
).statusCode,
).toBe(401);
});
});

View File

@@ -1,32 +0,0 @@
# 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)

View File

@@ -1,389 +0,0 @@
openapi: 3.1.0
info: { title: Mosaic Tess Gateway, version: 1.0.0 }
security: [{ sessionAuth: [] }]
paths:
/api/interaction/{agentName}/sessions:
{
get:
{
summary: List authorized runtime sessions,
parameters:
[
{ $ref: '#/components/parameters/agentName' },
{ $ref: '#/components/parameters/provider' },
{ $ref: '#/components/parameters/correlation' },
],
responses: { '200': { description: Sessions } },
},
}
/api/interaction/{agentName}/transitional-capabilities:
{
get:
{
summary: Get transitional capability matrix,
parameters:
[
{ $ref: '#/components/parameters/agentName' },
{ $ref: '#/components/parameters/provider' },
{ $ref: '#/components/parameters/correlation' },
],
responses: { '200': { description: Matrix } },
},
}
/api/interaction/{agentName}/tree:
{
get:
{
summary: Get authorized session tree,
parameters:
[
{ $ref: '#/components/parameters/agentName' },
{ $ref: '#/components/parameters/provider' },
{ $ref: '#/components/parameters/correlation' },
],
responses: { '200': { description: Tree } },
},
}
/api/interaction/{agentName}/sessions/{sessionId}/enroll:
{
post:
{
summary: Enroll a durable session,
parameters:
[
{ $ref: '#/components/parameters/agentName' },
{ $ref: '#/components/parameters/sessionId' },
{ $ref: '#/components/parameters/correlation' },
],
requestBody: { $ref: '#/components/requestBodies/Enroll' },
responses: { '200': { description: Enrolled } },
},
}
/api/interaction/{agentName}/sessions/{sessionId}/attach:
{
post:
{
summary: Attach to a runtime session,
parameters:
[
{ $ref: '#/components/parameters/agentName' },
{ $ref: '#/components/parameters/sessionId' },
{ $ref: '#/components/parameters/correlation' },
],
requestBody: { $ref: '#/components/requestBodies/Attach' },
responses: { '200': { description: Attachment } },
},
}
/api/interaction/{agentName}/sessions/{sessionId}/send:
{
post:
{
summary: Queue a durable provider send,
parameters:
[
{ $ref: '#/components/parameters/agentName' },
{ $ref: '#/components/parameters/sessionId' },
{ $ref: '#/components/parameters/correlation' },
],
requestBody: { $ref: '#/components/requestBodies/Send' },
responses: { '200': { description: Queued } },
},
}
/api/interaction/{agentName}/sessions/{sessionId}/stop:
{
post:
{
summary: Stop a session with approval,
parameters:
[
{ $ref: '#/components/parameters/agentName' },
{ $ref: '#/components/parameters/sessionId' },
{ $ref: '#/components/parameters/correlation' },
],
requestBody: { $ref: '#/components/requestBodies/Stop' },
responses: { '200': { description: Stopped }, '403': { description: Approval denied } },
},
}
/api/interaction/{agentName}/sessions/{sessionId}/recover:
{
post:
{
summary: Recover interrupted durable work,
parameters:
[
{ $ref: '#/components/parameters/agentName' },
{ $ref: '#/components/parameters/sessionId' },
{ $ref: '#/components/parameters/correlation' },
],
responses: { '200': { description: Recovered } },
},
}
/api/coord/mos/handoff:
{
post:
{
summary: Submit Mos handoff,
parameters: [{ $ref: '#/components/parameters/correlation' }],
requestBody: { $ref: '#/components/requestBodies/Handoff' },
responses: { '200': { description: Receipt } },
},
}
/api/coord/mos/{handoffId}/observe:
{
get:
{
summary: Observe Mos handoff,
parameters:
[
{ $ref: '#/components/parameters/handoffId' },
{ $ref: '#/components/parameters/correlation' },
],
responses: { '200': { description: Observation } },
},
}
/api/coord/mos/{handoffId}/result:
{
get:
{
summary: Get Mos handoff result,
parameters:
[
{ $ref: '#/components/parameters/handoffId' },
{ $ref: '#/components/parameters/correlation' },
],
responses: { '200': { description: Result } },
},
}
/api/interaction/{agentName}/sessions/{sessionId}/stream:
{
get:
{
summary: Stream runtime events,
parameters:
[
{ $ref: '#/components/parameters/agentName' },
{ $ref: '#/components/parameters/sessionId' },
{ $ref: '#/components/parameters/correlation' },
],
responses:
{
'200':
{
description: Event stream,
content: { text/event-stream: { schema: { type: string } } },
},
},
},
}
/api/memory/preferences:
{
get: { summary: List preferences, responses: { '200': { description: Preferences } } },
post:
{
summary: Upsert preference,
requestBody: { $ref: '#/components/requestBodies/Preference' },
responses: { '200': { description: Preference } },
},
}
/api/memory/preferences/{key}:
{
get:
{
summary: Get preference,
parameters: [{ $ref: '#/components/parameters/key' }],
responses: { '200': { description: Preference } },
},
delete:
{
summary: Delete preference,
parameters: [{ $ref: '#/components/parameters/key' }],
responses: { '204': { description: Deleted } },
},
}
/api/memory/insights:
{
get: { summary: List insights, responses: { '200': { description: Insights } } },
post:
{
summary: Create insight,
requestBody: { $ref: '#/components/requestBodies/Insight' },
responses: { '200': { description: Insight } },
},
}
/api/memory/insights/{id}:
{
get:
{
summary: Get insight,
parameters: [{ $ref: '#/components/parameters/id' }],
responses: { '200': { description: Insight } },
},
delete:
{
summary: Delete insight,
parameters: [{ $ref: '#/components/parameters/id' }],
responses: { '204': { description: Deleted } },
},
}
/api/memory/search:
{
post:
{
summary: Search memory,
requestBody: { $ref: '#/components/requestBodies/Search' },
responses: { '200': { description: Search results } },
},
}
components:
securitySchemes: { sessionAuth: { type: http, scheme: bearer } }
parameters:
agentName: { name: agentName, in: path, required: true, schema: { type: string } }
sessionId: { name: sessionId, in: path, required: true, schema: { type: string } }
provider: { name: provider, in: query, required: true, schema: { type: string } }
correlation: { name: X-Correlation-Id, in: header, required: true, schema: { type: string } }
key: { name: key, in: path, required: true, schema: { type: string } }
id: { name: id, in: path, required: true, schema: { type: string } }
requestBodies:
Enroll:
{
required: true,
content:
{
application/json:
{
schema:
{
type: object,
required: [providerId, runtimeSessionId],
properties:
{ providerId: { type: string }, runtimeSessionId: { type: string } },
},
},
},
}
Handoff:
{
required: true,
content:
{
application/json:
{
schema:
{
type: object,
required: [idempotencyKey, summary],
properties:
{
idempotencyKey: { type: string },
summary: { type: string },
context: { type: string },
missionId: { type: string },
},
},
},
},
}
Attach:
{
content:
{
application/json: { schema: { type: object, properties: { mode: { enum: [read] } } } },
},
}
Send:
{
required: true,
content:
{
application/json:
{
schema:
{
type: object,
required: [content, idempotencyKey],
properties: { content: { type: string }, idempotencyKey: { type: string } },
},
},
},
}
Stop:
{
required: true,
content:
{
application/json:
{
schema:
{
type: object,
required: [approvalRef],
properties: { approvalRef: { type: string } },
},
},
},
}
Preference:
{
required: true,
content:
{
application/json:
{
schema:
{
type: object,
required: [key, value],
properties:
{
key: { type: string },
value: {},
category: { type: string },
source: { type: string },
},
},
},
},
}
Insight:
{
required: true,
content:
{
application/json:
{
schema:
{
type: object,
required: [content],
properties:
{
content: { type: string },
source: { type: string },
category: { type: string },
metadata: { type: object },
},
},
},
},
}
Search:
{
required: true,
content:
{
application/json:
{
schema:
{
type: object,
required: [query],
properties:
{
query: { type: string },
limit: { type: integer },
maxDistance: { type: number },
},
},
},
},
}

View File

@@ -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 `InMemoryInteractionCoordinationPort` 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 `InMemoryMosCoordinationPort` 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 `InteractionCoordinationPort` with handoff/observe/result only, an authority-checking client, and deterministic native adapter in `@mosaicstack/coord`. - Implemented `MosCoordinationPort` 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 `MosCoordinationService`, 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

@@ -1,5 +0,0 @@
# Tess Administration
Configure agent/provider identities outside client input. Verify `/health/ready` and provider health before enabling interaction clients. Every interaction request requires an authenticated actor and correlation header; tenant and owner scope are server-derived. Do not log or return service credentials.
For an incident, preserve correlation IDs, inspect provider status and durable checkpoint/inbox/outbox state, then use the recovery endpoint. Do not retry an ambiguous external effect automatically. Stop operations require an exact one-time approval reference; provisioning or granting a broad admin capability does not replace that check.

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 `InteractionCoordinationPort` `@mosaicstack/coord` exposes only the transport-neutral `MosCoordinationPort`
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

@@ -1,3 +0,0 @@
# Tess Developer Guide
Interaction adapters pass only server-derived actor/tenant scope, channel, and correlation to runtime providers. Durable session state owns inbox/outbox/checkpoint recovery. Use the OpenAPI contract rather than inventing routes; unsupported provider capabilities fail closed.

View File

@@ -1,8 +0,0 @@
# TESS-M5-003 Documentation Checklist
- [x] `openapi-tess.yaml`: authenticated interaction endpoints including SSE stream, Mos handoff/observe/result, and memory preferences, insights, and search.
- [x] User guide: authorized session and handoff workflows.
- [x] Admin guide: provisioning, policy, health, and approval boundary.
- [x] Developer guide: scope, durable state, and provider adapter contract.
- [x] Plugin guide: replaceable-adapter, redaction, and identity-as-data rules.
- [x] Operations guide: readiness, recovery, ambiguous-effect safety, and tracing.

View File

@@ -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 `InteractionCoordinationService` (`apps/gateway/src/coord/interaction-coordination.service.ts`) for orchestration handoff. The interaction agent does not take configured orchestrator authority. 5. Use `MosCoordinationService` for orchestration handoff. Tess does not take Mos 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.

View File

@@ -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 | `InteractionCoordinationService` in `apps/gateway/src/coord/interaction-coordination.service.ts` retains authenticated handoff/observe/result ownership checks | native Mosaic path | | orchestration handoff | `apps/gateway/src/coord/mos-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 |

View File

@@ -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 `InteractionCoordinationService` (`apps/gateway/src/coord/interaction-coordination.service.ts`); do not create a second orchestrator path. 5. For an in-flight coordination request, use the owned handoff observation/result flow in `MosCoordinationService`; 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.

View File

@@ -20,29 +20,29 @@ interface CoordinationScope {
readonly requesterAgentId: string; // trusted gateway/configuration data readonly requesterAgentId: string; // trusted gateway/configuration data
} }
interface HandoffRequest { interface MosHandoffRequest {
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 HandoffReceipt { interface MosHandoffReceipt {
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 Handoff { interface MosHandoff {
readonly handoffId: string; readonly handoffId: string;
readonly targetAgentId: string; readonly targetAgentId: string;
readonly request: HandoffRequest; readonly request: MosHandoffRequest;
readonly scope: CoordinationScope; readonly scope: CoordinationScope;
} }
interface InteractionCoordinationPort { interface MosCoordinationPort {
handoff(handoff: Handoff): Promise<HandoffReceipt>; handoff(handoff: MosHandoff): Promise<MosHandoffReceipt>;
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>;
} }
@@ -52,26 +52,22 @@ The port deliberately omits generic orchestrator verbs. It is tenant- and
correlation-scoped; its gateway implementation obtains `actorId`, `tenantId`, correlation-scoped; its gateway implementation obtains `actorId`, `tenantId`,
and the requester agent from trusted authentication/configuration only. and the requester agent from trusted authentication/configuration only.
## HTTP routes
`/api/coord/interaction` is the canonical HTTP coordination prefix for handoff, observe, and result. `/api/coord/mos` remains a backward-compatible alias with the same handlers and DTOs; new integrations use the neutral canonical prefix.
## Enforcement point ## Enforcement point
`apps/gateway` owns an `InteractionCoordinationService` (`apps/gateway/src/coord/interaction-coordination.service.ts`) boundary that compares the `apps/gateway` owns a `MosCoordinationService` 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 `InMemoryInteractionCoordinationPort` as the concrete, M4 ships a native in-process `InMemoryMosCoordinationPort` 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 `InteractionCoordinationPort`; no channel client or interaction implement the same `MosCoordinationPort`; no channel client or interaction
runtime calls a transport directly. runtime calls a transport directly.
## Required tests ## Required tests

View File

@@ -1,3 +0,0 @@
# Tess Operations and Recovery
Check `/health/ready`, provider health, and effective policy before recovery. Recover durable sessions through the interaction recovery operation; it requeues only interrupted work and does not replay ambiguous external effects. Preserve correlation IDs for incident tracing and use Mos handoff observation/result endpoints for orchestration visibility.

View File

@@ -1,3 +0,0 @@
# Tess Plugin Authoring
Plugins are replaceable adapters. Declare capabilities, derive scope from trusted context, preserve correlation IDs, redact before persistence/egress, and return unsupported operations as fail-closed results. Names and identities are configuration data, not literals in keys or defaults.

File diff suppressed because one or more lines are too long

View File

@@ -33,18 +33,14 @@ 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.
## Closed Prerequisite Findings ## Existing Findings That Block Tess
The original M1 findings below are closed by landed controls and retained for audit traceability. - 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.
| Former finding | Closed evidence | These are tracked as M1 security prerequisites and must pass independent security review before Tess ingress is enabled.
| ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| 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.

View File

@@ -1,5 +0,0 @@
# Tess User Guide
All HTTP interaction calls require authenticated session credentials and `X-Correlation-Id`. Use `GET /api/interaction/{agentName}/sessions?provider=...` to list only visible runtime sessions, then enroll with `POST .../sessions/{sessionId}/enroll` body `{providerId,runtimeSessionId}`. Attach uses `{mode:"read"}`; send uses `{content,idempotencyKey}`. Stop requires `{approvalRef}` and fails with 403 without the exact durable approval. Recovery only requeues interrupted durable work.
Memory is user-scoped: preferences support list/get/upsert/delete; insights support list/get/create/delete; search body is `{query,limit?,maxDistance?}`. Mos work is handed off with `POST /api/coord/mos/handoff`; observe and result use the returned handoff ID.

View File

@@ -1,61 +0,0 @@
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);
},
);
});

View File

@@ -1,135 +0,0 @@
# Mos Cross-Agent Git Communications Protocol
`mos-comms.sh` is a portable, append-only relay for orchestrators on separate
hosts. It uses a git repository supplied by the operator as transport. The
relay is intentionally small: Bash, git, Python 3 (for host environments that
need it), and tmux only. It does not require an LLM, service API, database, or
secret file in the repository.
## Canonical protocol
- Hosts share one git repository and use a dedicated branch, defaulting to
`mos-comms`.
- Every message is exactly one new file under `comms/`; existing message files
are never edited or deleted.
- A sender writes
`comms/<UTC>__from-<AGENT_NAME>__<random>.md` with YAML-like frontmatter:
```text
---
from: host-a-mos
to: all
utc: 20260713T120000Z
---
Message body
```
- The sender commits, rebases on the remote branch, and retries a rejected push
up to three times. Independent files make normal concurrent sends
conflict-free.
- `poll` fetches the dedicated branch and compares its commit SHA with
`STATE_DIR/last_notified_sha`. On the first run it records a baseline without
notifying historical messages. Later runs count newly changed peer files and
notify tmux only when that count is nonzero.
- `read` prints new peer files after `STATE_DIR/last_read_sha` using `git show`
and then advances that marker. Message bodies surface here as data, not code.
## Commands
```bash
mos-comms.sh setup
mos-comms.sh send 'A deliberate message for peers'
mos-comms.sh poll
mos-comms.sh read
mos-comms.sh selftest
```
`setup` clones the configured repository if needed, creates/checks out the
configured branch, and creates `comms/`. The first sender creates the remote
branch if it is not present yet.
`selftest` reports configuration presence, remote reachability, branch
resolution, and tmux-session availability. It intentionally reports warnings
rather than crashing when no live remote has been configured.
## Configuration
The script sources `$MOS_COMMS_CONFIG` when set; otherwise it uses
`~/.config/mos-comms/mos-comms.config`. Copy
`mos-comms.config.example` to that path and set `AGENT_NAME` and
`COMMS_REMOTE` locally. Defaults are:
| Variable | Default |
| --- | --- |
| `COMMS_REPO_DIR` | `~/.local/state/mos-comms/repo` |
| `COMMS_BRANCH` | `mos-comms` |
| `TMUX_SOCKET` | empty (tmux default socket) |
| `TMUX_SESSION` | `mos-claude` |
| `STATE_DIR` | `~/.local/state/mos-comms` |
`GIT_CRED_HELPER` is optional. If configured, every git operation runs with
`git -c credential.helper=$GIT_CRED_HELPER`. Credential values do not belong in
the config or repository.
## Threat model
1. **Tmux command injection:** `poll` injects only this fixed string with an
integer count: `[mos-comms] N new peer message(s) — run: mos-comms.sh read`.
It never interpolates a message body, filename, or author into `send-keys`.
A crafted message cannot become keystrokes.
2. **Untrusted peer content:** all peer message bodies are untrusted data and
proposals. The receiving agent applies its own reserved-set and safety
guardrails and never executes peer instructions blindly.
3. **No mechanical auto-reply:** receiving only produces a notification. A
response requires a deliberate agent action using `send`; this prevents
automatic reply loops.
4. **Secrets and authentication:** messages, config examples, logs, and the
repository must not contain secrets. Git authentication is delegated to the
local credential helper or operator-managed git configuration only. The
script does not print credentials.
5. **Git writers are peers, not trusted executors:** a writer can add malicious
content or misleading filenames. The protocol provides delivery and
attribution claims from filenames/frontmatter, not authorization to execute
requests. Repository access should be limited to intended relay hosts.
## Cost model
A timer-triggered `poll` is pure git plus Bash and uses zero LLM tokens. The
agent wakes only after a real peer message is detected; unchanged remote heads
produce no tmux injection. Reading and replying are deliberate operations.
## New-host install
1. Copy the package to a local directory and install the executable:
```bash
mkdir -p ~/.local/bin ~/.config/mos-comms ~/.config/systemd/user
cp mos-comms.sh ~/.local/bin/mos-comms.sh
chmod 700 ~/.local/bin/mos-comms.sh
cp mos-comms.config.example ~/.config/mos-comms/mos-comms.config
chmod 600 ~/.config/mos-comms/mos-comms.config
```
2. Edit the **local-only** config. Set a unique `AGENT_NAME` and the
operator-provisioned `COMMS_REMOTE`. Configure git credentials separately
(or set a credential-helper name, never a token value).
3. Confirm prerequisites and create the local checkout:
```bash
~/.local/bin/mos-comms.sh selftest
~/.local/bin/mos-comms.sh setup
```
4. Install and enable the per-user timer:
```bash
cp systemd/mos-comms.service systemd/mos-comms.timer ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now mos-comms.timer
systemctl --user list-timers mos-comms.timer
```
5. Send a deliberate test message from one host, then run `read` on the other
host after its poll notification. Keep the relay branch dedicated to this
protocol; do not share it with source-code work.

View File

@@ -1,23 +0,0 @@
# Copy to ~/.config/mos-comms/mos-comms.config (mode 600) or point
# MOS_COMMS_CONFIG at another local-only file. Do not commit this file with
# operator values or credentials.
# Required deployment identity; letters, numbers, dots, and hyphens only.
AGENT_NAME="host-a-mos"
# Required: URL/path of the shared, operator-provisioned git repository.
# Authentication is delegated to git or the optional credential helper below.
COMMS_REMOTE=""
# Optional local paths and branch.
COMMS_REPO_DIR="$HOME/.local/state/mos-comms/repo"
COMMS_BRANCH="mos-comms"
STATE_DIR="$HOME/.local/state/mos-comms"
# Empty selects tmux's default socket. Set only when a named socket is used.
TMUX_SOCKET=""
TMUX_SESSION="mos-claude"
# Optional git credential-helper name/path. Never place credential values here.
# Example: GIT_CRED_HELPER="cache"
GIT_CRED_HELPER=""

View File

@@ -1,291 +0,0 @@
#!/usr/bin/env bash
# Portable, append-only git relay for deliberate cross-agent communication.
set -euo pipefail
CONFIG_PATH="${MOS_COMMS_CONFIG:-$HOME/.config/mos-comms/mos-comms.config}"
CONFIG_PRESENT=0
if [[ -f "$CONFIG_PATH" ]]; then
# Configuration is local operator input. It must not be committed to the relay repo.
# shellcheck source=/dev/null
source "$CONFIG_PATH"
CONFIG_PRESENT=1
fi
AGENT_NAME="${AGENT_NAME:-}"
COMMS_REMOTE="${COMMS_REMOTE:-}"
COMMS_REPO_DIR="${COMMS_REPO_DIR:-$HOME/.local/state/mos-comms/repo}"
COMMS_BRANCH="${COMMS_BRANCH:-mos-comms}"
TMUX_SOCKET="${TMUX_SOCKET:-}"
TMUX_SESSION="${TMUX_SESSION:-mos-claude}"
STATE_DIR="${STATE_DIR:-$HOME/.local/state/mos-comms}"
GIT_CRED_HELPER="${GIT_CRED_HELPER:-}"
usage() {
cat <<'EOF'
Usage: mos-comms.sh setup | poll | read | send <text> | selftest
Messages are append-only files on the configured shared git branch. `poll` is
mechanical: it only checks git and emits a fixed tmux notification.
EOF
}
die() {
printf 'mos-comms: %s\n' "$*" >&2
exit 1
}
warn() {
printf 'mos-comms: warning: %s\n' "$*" >&2
}
gitc() {
if [[ -n "$GIT_CRED_HELPER" ]]; then
git -c credential.helper="$GIT_CRED_HELPER" "$@"
else
git "$@"
fi
}
require_identity() {
[[ -n "$AGENT_NAME" ]] || die 'AGENT_NAME is required in the local config'
[[ "$AGENT_NAME" =~ ^[A-Za-z0-9.-]+$ ]] || die 'AGENT_NAME may contain only letters, numbers, dots, and hyphens'
}
require_remote() {
[[ -n "$COMMS_REMOTE" ]] || die 'COMMS_REMOTE is required in the local config'
}
ensure_state_dir() {
mkdir -p "$STATE_DIR"
chmod 700 "$STATE_DIR"
}
remote_branch_exists() {
gitc -C "$COMMS_REPO_DIR" show-ref --verify --quiet "refs/remotes/origin/$COMMS_BRANCH"
}
setup() {
require_remote
require_identity
mkdir -p "$(dirname "$COMMS_REPO_DIR")"
if [[ ! -d "$COMMS_REPO_DIR/.git" ]]; then
gitc clone "$COMMS_REMOTE" "$COMMS_REPO_DIR"
fi
gitc -C "$COMMS_REPO_DIR" remote set-url origin "$COMMS_REMOTE"
gitc -C "$COMMS_REPO_DIR" fetch origin
if remote_branch_exists; then
# Preserve any local append-only commit that is awaiting a retry; do not
# reset a checked-out branch to origin here.
if gitc -C "$COMMS_REPO_DIR" show-ref --verify --quiet "refs/heads/$COMMS_BRANCH"; then
gitc -C "$COMMS_REPO_DIR" checkout "$COMMS_BRANCH"
else
gitc -C "$COMMS_REPO_DIR" checkout --track -b "$COMMS_BRANCH" "origin/$COMMS_BRANCH"
fi
gitc -C "$COMMS_REPO_DIR" branch --set-upstream-to="origin/$COMMS_BRANCH" "$COMMS_BRANCH"
else
gitc -C "$COMMS_REPO_DIR" checkout -B "$COMMS_BRANCH"
fi
mkdir -p "$COMMS_REPO_DIR/comms"
ensure_state_dir
}
ensure_remote_branch() {
if ! remote_branch_exists; then
warn "origin branch '$COMMS_BRANCH' does not exist yet"
return 1
fi
}
message_is_from_self() {
local file="$1"
[[ "$file" == *"__from-${AGENT_NAME}__"* ]]
}
changed_message_files() {
local from_sha="$1"
local to_sha="$2"
if [[ -n "$from_sha" ]] && gitc -C "$COMMS_REPO_DIR" cat-file -e "$from_sha^{commit}" 2>/dev/null; then
gitc -C "$COMMS_REPO_DIR" diff --name-only "$from_sha" "$to_sha" -- comms/
else
gitc -C "$COMMS_REPO_DIR" ls-tree -r --name-only "$to_sha" -- comms/
fi
}
tmuxc() {
if [[ -n "$TMUX_SOCKET" ]]; then
tmux -L "$TMUX_SOCKET" "$@"
else
tmux "$@"
fi
}
send() {
require_identity
require_remote
[[ $# -ge 1 ]] || die 'send requires message text'
setup
local body="$*"
local utc filename path
utc="$(date -u +%Y%m%dT%H%M%SZ)"
filename="${utc}__from-${AGENT_NAME}__${RANDOM}${RANDOM}.md"
path="$COMMS_REPO_DIR/comms/$filename"
umask 077
{
printf '%s\n' '---'
printf 'from: %s\n' "$AGENT_NAME"
printf '%s\n' 'to: all'
printf 'utc: %s\n' "$utc"
printf '%s\n\n' '---'
printf '%s\n' "$body"
} >"$path"
gitc -C "$COMMS_REPO_DIR" add -- "comms/$filename"
if ! gitc -C "$COMMS_REPO_DIR" config user.name >/dev/null; then
gitc -C "$COMMS_REPO_DIR" config user.name "$AGENT_NAME mos-comms"
fi
if ! gitc -C "$COMMS_REPO_DIR" config user.email >/dev/null; then
gitc -C "$COMMS_REPO_DIR" config user.email "$AGENT_NAME@localhost"
fi
gitc -C "$COMMS_REPO_DIR" commit -m "comms: ${AGENT_NAME} ${utc}"
local attempt
for attempt in 1 2 3; do
if remote_branch_exists; then
gitc -C "$COMMS_REPO_DIR" pull --rebase origin "$COMMS_BRANCH"
fi
if gitc -C "$COMMS_REPO_DIR" push origin "HEAD:refs/heads/$COMMS_BRANCH"; then
printf 'sent: %s\n' "$filename"
return 0
fi
if [[ "$attempt" -lt 3 ]]; then
warn "push rejected; retrying ($attempt/3)"
gitc -C "$COMMS_REPO_DIR" fetch origin
fi
done
die 'push was rejected after 3 attempts; local message commit remains available for retry'
}
poll() {
require_identity
require_remote
setup
gitc -C "$COMMS_REPO_DIR" fetch origin
ensure_remote_branch || return 0
local head marker last_notified
head="$(gitc -C "$COMMS_REPO_DIR" rev-parse "origin/$COMMS_BRANCH")"
marker="$STATE_DIR/last_notified_sha"
if [[ ! -f "$marker" ]]; then
printf '%s\n' "$head" >"$marker"
printf 'poll: baseline recorded; existing history was not notified\n'
return 0
fi
last_notified="$(<"$marker")"
if [[ "$last_notified" == "$head" ]]; then
return 0
fi
local file count=0
while IFS= read -r file; do
[[ -n "$file" ]] || continue
if ! message_is_from_self "$file"; then
((count += 1))
fi
done < <(changed_message_files "$last_notified" "$head")
if ((count > 0)); then
# Intentionally static except for an integer: never inject peer data into tmux.
tmuxc send-keys -t "$TMUX_SESSION" "[mos-comms] $count new peer message(s) — run: mos-comms.sh read"
tmuxc send-keys -t "$TMUX_SESSION" Enter
fi
printf '%s\n' "$head" >"$marker"
}
read_messages() {
require_identity
require_remote
setup
gitc -C "$COMMS_REPO_DIR" fetch origin
ensure_remote_branch || return 0
local head marker last_read file found=0
head="$(gitc -C "$COMMS_REPO_DIR" rev-parse "origin/$COMMS_BRANCH")"
marker="$STATE_DIR/last_read_sha"
last_read=""
if [[ -f "$marker" ]]; then
last_read="$(<"$marker")"
fi
while IFS= read -r file; do
[[ -n "$file" ]] || continue
if message_is_from_self "$file"; then
continue
fi
found=1
printf '%s\n' "===== $file ====="
# Peer content is printed as data. It is never evaluated or sent to tmux.
gitc -C "$COMMS_REPO_DIR" show "origin/$COMMS_BRANCH:$file"
printf '\n'
done < <(changed_message_files "$last_read" "$head")
printf '%s\n' "$head" >"$marker"
if ((found == 0)); then
printf 'No new peer messages.\n'
fi
}
selftest() {
local remote_ok=0 branch_ok=0 tmux_ok=0 config_ok=0
if ((CONFIG_PRESENT == 1)); then
printf 'PASS config present: %s\n' "$CONFIG_PATH"
config_ok=1
else
printf 'FAIL config present: %s\n' "$CONFIG_PATH"
fi
if [[ -z "$COMMS_REMOTE" ]]; then
printf 'WARN remote reachable: COMMS_REMOTE is not configured\n'
elif gitc ls-remote "$COMMS_REMOTE" >/dev/null 2>&1; then
printf 'PASS remote reachable\n'
remote_ok=1
else
printf 'FAIL remote reachable\n'
fi
if ((remote_ok == 1)) && gitc ls-remote --exit-code --heads "$COMMS_REMOTE" "$COMMS_BRANCH" >/dev/null 2>&1; then
printf 'PASS branch resolves: %s\n' "$COMMS_BRANCH"
branch_ok=1
elif [[ -z "$COMMS_REMOTE" ]]; then
printf 'WARN branch resolves: skipped without a remote\n'
else
printf 'FAIL branch resolves: %s\n' "$COMMS_BRANCH"
fi
if tmuxc has-session -t "$TMUX_SESSION" 2>/dev/null; then
printf 'PASS tmux session exists: %s\n' "$TMUX_SESSION"
tmux_ok=1
else
printf 'FAIL tmux session exists: %s\n' "$TMUX_SESSION"
fi
# Report all checks without making a no-remote first-run unusable.
if ((config_ok == 1 && remote_ok == 1 && branch_ok == 1 && tmux_ok == 1)); then
printf 'PASS selftest complete\n'
else
printf 'FAIL selftest incomplete (see checks above)\n'
fi
}
case "${1:-}" in
setup) [[ $# -eq 1 ]] || usage; setup ;;
poll) [[ $# -eq 1 ]] || usage; poll ;;
read) [[ $# -eq 1 ]] || usage; read_messages ;;
send) shift; send "$@" ;;
selftest) [[ $# -eq 1 ]] || usage; selftest ;;
-h|--help|help) usage ;;
*) usage; exit 2 ;;
esac

View File

@@ -1,6 +0,0 @@
[Unit]
Description=Mos cross-host git communications poll
[Service]
Type=oneshot
ExecStart=%h/.local/bin/mos-comms.sh poll

View File

@@ -1,9 +0,0 @@
[Unit]
Description=Poll Mos cross-host git communications
[Timer]
OnBootSec=2min
OnUnitActiveSec=15min
[Install]
WantedBy=timers.target