Compare commits
8 Commits
enhance/56
...
6e82b4ab5b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e82b4ab5b | ||
| ca9c2b5c23 | |||
| b580d37d51 | |||
| 59e49cfd15 | |||
| a99aded26d | |||
| 4df38f7e81 | |||
| 4e9e053800 | |||
| 851c67c27b |
@@ -5,3 +5,5 @@ pnpm-lock.yaml
|
|||||||
**/drizzle
|
**/drizzle
|
||||||
**/.next
|
**/.next
|
||||||
.claude/
|
.claude/
|
||||||
|
docs/tess/TASKS.md
|
||||||
|
docs/scratchpads/
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import type { CommandDef, SlashCommandPayload } from '@mosaicstack/types';
|
||||||
|
import { CommandAuthorizationService } from './command-authorization.service.js';
|
||||||
|
|
||||||
|
const adminCommand: CommandDef = {
|
||||||
|
name: 'gc',
|
||||||
|
description: 'GC',
|
||||||
|
aliases: [],
|
||||||
|
scope: 'admin',
|
||||||
|
execution: 'socket',
|
||||||
|
available: true,
|
||||||
|
};
|
||||||
|
const payload: SlashCommandPayload = { command: 'gc', conversationId: 'conversation-1' };
|
||||||
|
|
||||||
|
function createService(role: string): CommandAuthorizationService {
|
||||||
|
const entries = new Map<string, string>();
|
||||||
|
const db = {
|
||||||
|
select: () => ({ from: () => ({ where: () => ({ limit: async () => [{ role }] }) }) }),
|
||||||
|
};
|
||||||
|
const redis = {
|
||||||
|
get: async (key: string) => entries.get(key) ?? null,
|
||||||
|
set: async (key: string, value: string) => {
|
||||||
|
entries.set(key, value);
|
||||||
|
},
|
||||||
|
del: async (key: string) => Number(entries.delete(key)),
|
||||||
|
};
|
||||||
|
return new CommandAuthorizationService(db as never, redis);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('CommandAuthorizationService', () => {
|
||||||
|
it('consumes one exact actor-bound approval once', async (): Promise<void> => {
|
||||||
|
const service = createService('admin');
|
||||||
|
const approval = await service.createApproval('gc', payload, 'admin-1');
|
||||||
|
expect(
|
||||||
|
(await service.authorize(adminCommand, payload, 'admin-1', approval.approvalId)).allowed,
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
(await service.authorize(adminCommand, payload, 'admin-1', approval.approvalId)).allowed,
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects an approval when the structured action is mutated', async (): Promise<void> => {
|
||||||
|
const service = createService('admin');
|
||||||
|
const approval = await service.createApproval('gc', payload, 'admin-1');
|
||||||
|
const mutated = { ...payload, conversationId: 'other-conversation' };
|
||||||
|
expect(
|
||||||
|
(await service.authorize(adminCommand, mutated, 'admin-1', approval.approvalId)).allowed,
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('denies an admin command to a member before approval is considered', async (): Promise<void> => {
|
||||||
|
const service = createService('member');
|
||||||
|
const approval = await service.createApproval('gc', payload, 'member-1');
|
||||||
|
expect(
|
||||||
|
(await service.authorize(adminCommand, payload, 'member-1', approval.approvalId)).allowed,
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
135
apps/gateway/src/commands/command-authorization.service.ts
Normal file
135
apps/gateway/src/commands/command-authorization.service.ts
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
import { createHash, randomUUID } from 'node:crypto';
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { eq, users as usersTable, type Db } from '@mosaicstack/db';
|
||||||
|
import type { CommandDef, SlashCommandPayload } from '@mosaicstack/types';
|
||||||
|
import { DB } from '../database/database.module.js';
|
||||||
|
import { COMMANDS_REDIS } from './commands.tokens.js';
|
||||||
|
|
||||||
|
export type CommandRole = 'admin' | 'member' | 'viewer';
|
||||||
|
|
||||||
|
export interface CommandApproval {
|
||||||
|
approvalId: string;
|
||||||
|
actionDigest: string;
|
||||||
|
actorId: string;
|
||||||
|
command: string;
|
||||||
|
expiresAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CommandAuthorizationResult {
|
||||||
|
allowed: boolean;
|
||||||
|
reason?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class CommandAuthorizationService {
|
||||||
|
constructor(
|
||||||
|
@Inject(DB) private readonly db: Db,
|
||||||
|
@Inject(COMMANDS_REDIS)
|
||||||
|
private readonly redis: {
|
||||||
|
get(key: string): Promise<string | null>;
|
||||||
|
set(key: string, value: string, ...args: string[]): Promise<unknown>;
|
||||||
|
del(key: string): Promise<number>;
|
||||||
|
},
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async authorize(
|
||||||
|
command: CommandDef,
|
||||||
|
payload: SlashCommandPayload,
|
||||||
|
actorId: string,
|
||||||
|
approvalId?: string,
|
||||||
|
): Promise<CommandAuthorizationResult> {
|
||||||
|
const role = await this.resolveRole(actorId);
|
||||||
|
if (!role || !this.hasScope(role, command.scope)) {
|
||||||
|
return { allowed: false, reason: 'not authorized for this command scope' };
|
||||||
|
}
|
||||||
|
if (command.scope !== 'admin') return { allowed: true };
|
||||||
|
if (!approvalId) return { allowed: false, reason: 'durable approval is required' };
|
||||||
|
const actionDigest = this.actionDigest(command.name, payload);
|
||||||
|
const approved = await this.consumeApproval(approvalId, actorId, actionDigest);
|
||||||
|
return approved
|
||||||
|
? { allowed: true }
|
||||||
|
: {
|
||||||
|
allowed: false,
|
||||||
|
reason: 'approval is invalid, expired, replayed, or does not match this action',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async createApproval(
|
||||||
|
command: string,
|
||||||
|
payload: SlashCommandPayload,
|
||||||
|
actorId: string,
|
||||||
|
): Promise<CommandApproval> {
|
||||||
|
const approvalId = randomUUID();
|
||||||
|
const expiresAt = new Date(Date.now() + 5 * 60_000).toISOString();
|
||||||
|
const approval: CommandApproval = {
|
||||||
|
approvalId,
|
||||||
|
actionDigest: this.actionDigest(command, payload),
|
||||||
|
actorId,
|
||||||
|
command,
|
||||||
|
expiresAt,
|
||||||
|
};
|
||||||
|
await this.redis.set(this.key(approvalId), JSON.stringify(approval), 'EX', '300');
|
||||||
|
return approval;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async resolveRole(actorId: string): Promise<CommandRole | null> {
|
||||||
|
const [user] = await this.db
|
||||||
|
.select({ role: usersTable.role })
|
||||||
|
.from(usersTable)
|
||||||
|
.where(eq(usersTable.id, actorId))
|
||||||
|
.limit(1);
|
||||||
|
const role = user?.role;
|
||||||
|
return role === 'admin' || role === 'member' || role === 'viewer' ? role : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private hasScope(role: CommandRole, scope: CommandDef['scope']): boolean {
|
||||||
|
if (role === 'admin') return true;
|
||||||
|
return role === 'member' && (scope === 'core' || scope === 'agent');
|
||||||
|
}
|
||||||
|
|
||||||
|
private async consumeApproval(
|
||||||
|
approvalId: string,
|
||||||
|
actorId: string,
|
||||||
|
actionDigest: string,
|
||||||
|
): Promise<boolean> {
|
||||||
|
const key = this.key(approvalId);
|
||||||
|
const encoded = await this.redis.get(key);
|
||||||
|
if (!encoded) return false;
|
||||||
|
const parsed: unknown = JSON.parse(encoded);
|
||||||
|
if (
|
||||||
|
!this.isApproval(parsed) ||
|
||||||
|
parsed.actorId !== actorId ||
|
||||||
|
parsed.actionDigest !== actionDigest ||
|
||||||
|
Date.parse(parsed.expiresAt) <= Date.now()
|
||||||
|
)
|
||||||
|
return false;
|
||||||
|
return (await this.redis.del(key)) === 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private actionDigest(command: string, payload: SlashCommandPayload): string {
|
||||||
|
return createHash('sha256')
|
||||||
|
.update(
|
||||||
|
JSON.stringify({
|
||||||
|
command,
|
||||||
|
args: payload.args?.trim() ?? '',
|
||||||
|
conversationId: payload.conversationId,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.digest('hex');
|
||||||
|
}
|
||||||
|
|
||||||
|
private isApproval(value: unknown): value is CommandApproval {
|
||||||
|
return (
|
||||||
|
typeof value === 'object' &&
|
||||||
|
value !== null &&
|
||||||
|
'approvalId' in value &&
|
||||||
|
'actionDigest' in value &&
|
||||||
|
'actorId' in value &&
|
||||||
|
'expiresAt' in value
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private key(approvalId: string): string {
|
||||||
|
return `tess:command-approval:${approvalId}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import type { SlashCommandPayload } from '@mosaicstack/types';
|
||||||
|
import { CommandExecutorService } from './command-executor.service.js';
|
||||||
|
|
||||||
|
const registry = {
|
||||||
|
getManifest: vi.fn(() => ({
|
||||||
|
version: 1,
|
||||||
|
commands: [
|
||||||
|
{
|
||||||
|
name: 'gc',
|
||||||
|
description: 'System-wide garbage collection',
|
||||||
|
aliases: [],
|
||||||
|
scope: 'admin' as const,
|
||||||
|
execution: 'socket' as const,
|
||||||
|
available: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
skills: [],
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
|
const sessionGc = {
|
||||||
|
sweepOrphans: vi.fn().mockResolvedValue({ orphanedSessions: 1, totalCleaned: [], duration: 1 }),
|
||||||
|
};
|
||||||
|
|
||||||
|
const authorization = {
|
||||||
|
authorize: vi.fn((_command: unknown, _payload: unknown, actorId: string) =>
|
||||||
|
Promise.resolve(
|
||||||
|
actorId === 'member-1'
|
||||||
|
? { allowed: false, reason: 'durable approval is required' }
|
||||||
|
: { allowed: false, reason: 'not authorized for this command scope' },
|
||||||
|
),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
function buildExecutor(): CommandExecutorService {
|
||||||
|
return new CommandExecutorService(
|
||||||
|
registry as never,
|
||||||
|
{ getSession: vi.fn() } as never,
|
||||||
|
{ clear: vi.fn(), set: vi.fn() } as never,
|
||||||
|
sessionGc as never,
|
||||||
|
{ set: vi.fn() } as never,
|
||||||
|
{ agents: {} } as never,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
authorization as never,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('TESS-M1-SEC-001 command authorization abuse cases', () => {
|
||||||
|
const payload: SlashCommandPayload = { command: 'gc', conversationId: 'conversation-1' };
|
||||||
|
|
||||||
|
beforeEach((): void => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('denies a forged admin identity and does not execute a system-wide command', async (): Promise<void> => {
|
||||||
|
const result = await buildExecutor().execute(payload, 'admin-forged-by-client');
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.message).toContain('not authorized');
|
||||||
|
expect(sessionGc.sweepOrphans).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('denies a privileged command without a server-bound durable approval', async (): Promise<void> => {
|
||||||
|
const result = await buildExecutor().execute(payload, 'member-1');
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.message).toContain('approval');
|
||||||
|
expect(sessionGc.sweepOrphans).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -10,6 +10,7 @@ import { ReloadService } from '../reload/reload.service.js';
|
|||||||
import { McpClientService } from '../mcp-client/mcp-client.service.js';
|
import { McpClientService } from '../mcp-client/mcp-client.service.js';
|
||||||
import { BRAIN } from '../brain/brain.tokens.js';
|
import { BRAIN } from '../brain/brain.tokens.js';
|
||||||
import { COMMANDS_REDIS } from './commands.tokens.js';
|
import { COMMANDS_REDIS } from './commands.tokens.js';
|
||||||
|
import { CommandAuthorizationService } from './command-authorization.service.js';
|
||||||
import { CommandRegistryService } from './command-registry.service.js';
|
import { CommandRegistryService } from './command-registry.service.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -32,9 +33,16 @@ export class CommandExecutorService {
|
|||||||
@Optional()
|
@Optional()
|
||||||
@Inject(McpClientService)
|
@Inject(McpClientService)
|
||||||
private readonly mcpClient: McpClientService | null,
|
private readonly mcpClient: McpClientService | null,
|
||||||
|
@Optional()
|
||||||
|
@Inject(CommandAuthorizationService)
|
||||||
|
private readonly authorization: CommandAuthorizationService | null = null,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async execute(payload: SlashCommandPayload, userId: string): Promise<SlashCommandResultPayload> {
|
async execute(
|
||||||
|
payload: SlashCommandPayload,
|
||||||
|
userId: string,
|
||||||
|
approvalId?: string,
|
||||||
|
): Promise<SlashCommandResultPayload> {
|
||||||
const { command, args, conversationId } = payload;
|
const { command, args, conversationId } = payload;
|
||||||
|
|
||||||
const def = this.registry.getManifest().commands.find((c) => c.name === command);
|
const def = this.registry.getManifest().commands.find((c) => c.name === command);
|
||||||
@@ -47,6 +55,11 @@ export class CommandExecutorService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const authorization = await this.authorization?.authorize(def, payload, userId, approvalId);
|
||||||
|
if (authorization && !authorization.allowed) {
|
||||||
|
return { command, conversationId, success: false, message: authorization.reason };
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
switch (command) {
|
switch (command) {
|
||||||
case 'model':
|
case 'model':
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { createQueue, type QueueHandle } from '@mosaicstack/queue';
|
|||||||
import { ChatModule } from '../chat/chat.module.js';
|
import { ChatModule } from '../chat/chat.module.js';
|
||||||
import { GCModule } from '../gc/gc.module.js';
|
import { GCModule } from '../gc/gc.module.js';
|
||||||
import { ReloadModule } from '../reload/reload.module.js';
|
import { ReloadModule } from '../reload/reload.module.js';
|
||||||
|
import { CommandAuthorizationService } from './command-authorization.service.js';
|
||||||
import { CommandExecutorService } from './command-executor.service.js';
|
import { CommandExecutorService } from './command-executor.service.js';
|
||||||
import { CommandRegistryService } from './command-registry.service.js';
|
import { CommandRegistryService } from './command-registry.service.js';
|
||||||
import { COMMANDS_REDIS } from './commands.tokens.js';
|
import { COMMANDS_REDIS } from './commands.tokens.js';
|
||||||
@@ -24,6 +25,7 @@ const COMMANDS_QUEUE_HANDLE = 'COMMANDS_QUEUE_HANDLE';
|
|||||||
inject: [COMMANDS_QUEUE_HANDLE],
|
inject: [COMMANDS_QUEUE_HANDLE],
|
||||||
},
|
},
|
||||||
CommandRegistryService,
|
CommandRegistryService,
|
||||||
|
CommandAuthorizationService,
|
||||||
CommandExecutorService,
|
CommandExecutorService,
|
||||||
],
|
],
|
||||||
exports: [CommandRegistryService, CommandExecutorService],
|
exports: [CommandRegistryService, CommandExecutorService],
|
||||||
|
|||||||
@@ -68,9 +68,10 @@ The MVP is complete when ALL declared workstreams are complete AND every cross-c
|
|||||||
## Workstreams
|
## Workstreams
|
||||||
|
|
||||||
| # | ID | Name | Status | Manifest | Notes |
|
| # | ID | Name | Status | Manifest | Notes |
|
||||||
| --- | --- | ------------------------------------------- | ----------------- | ----------------------------------------------------------------------- | --------------------------------------------------- |
|
| --- | ---- | ------------------------------------------- | ----------------- | ----------------------------------------------------------------------- | --------------------------------------------------- |
|
||||||
| W1 | FED | Federation v1 | planning-complete | [docs/federation/MISSION-MANIFEST.md](./federation/MISSION-MANIFEST.md) | 7 milestones, ~175K tokens, issues #460–#466 filed |
|
| W1 | FED | Federation v1 | planning-complete | [docs/federation/MISSION-MANIFEST.md](./federation/MISSION-MANIFEST.md) | 7 milestones, ~175K tokens, issues #460–#466 filed |
|
||||||
| W2+ | TBD | (additional workstreams declared as scoped) | — | — | Scope creep is expected and explicitly accommodated |
|
| W2 | TESS | Tess interaction agent | planning-complete | [docs/tess/MISSION-MANIFEST.md](./tess/MISSION-MANIFEST.md) | 5 milestones; issue #706; M1 issue #707 ready |
|
||||||
|
| W3+ | TBD | (additional workstreams declared as scoped) | — | — | Scope creep is expected and explicitly accommodated |
|
||||||
|
|
||||||
### Likely Additional Workstreams (Not Yet Declared)
|
### Likely Additional Workstreams (Not Yet Declared)
|
||||||
|
|
||||||
|
|||||||
96
docs/PRD.md
96
docs/PRD.md
@@ -79,6 +79,102 @@ Jarvis (v0.2.0) is a self-hosted AI assistant with a Python FastAPI backend and
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Tess Interaction Agent Workstream (TESS)
|
||||||
|
|
||||||
|
### Problem and Objective
|
||||||
|
|
||||||
|
Jason needs one durable, operator-facing Mosaic agent outside Hermes that is reachable through a dedicated Discord channel and CLI, can attach to and operate the Mosaic fleet and transitional Hermes agents, and preserves context across restarts and compaction. Mos remains the coding/general fleet orchestrator; Tess is the complementary human interaction, visibility, control, and migration agent.
|
||||||
|
|
||||||
|
The objective is to ship **Tess** (from _tessera_, a piece of a mosaic) as a Pi-native, GPT-5.6 Sol agent with high reasoning. Tess must use Mosaic-owned contracts and plugins so Hermes can be replaced incrementally rather than becoming a permanent architectural dependency.
|
||||||
|
|
||||||
|
### Scope
|
||||||
|
|
||||||
|
#### In Scope
|
||||||
|
|
||||||
|
1. `TESS-ARP-001`: A runtime-neutral `AgentRuntimeProvider` contract supporting `listSessions`, `streamSession`, `sendMessage`, `terminate`, `getSessionTree`, `attach`, health, capability discovery, and normalized events/errors.
|
||||||
|
2. `TESS-PI-001`: A long-running Pi-native Tess agent profile/service pinned to GPT-5.6 Sol with high reasoning, explicit tool policy, lifecycle hooks, durable checkpoints, and restart recovery.
|
||||||
|
3. `TESS-DSC-001`: Dedicated Discord channel binding to Tess through the Mosaic gateway, with allowlists/RBAC, thread/reply policy, streaming, attachments, approvals, and correlation IDs.
|
||||||
|
4. `TESS-CLI-001`: `mosaic tess` CLI commands for chat, status, session listing, attach/detach, send/steer/stop, provider health, and recovery.
|
||||||
|
5. `TESS-FLT-001`: Fleet plugin capabilities for roster/status/heartbeat inspection, message delivery, session hierarchy, safe attach, and controlled restart/recovery.
|
||||||
|
6. `TESS-MOS-001`: Explicit Mos coordination boundary and tools: hand off orchestration requests, observe mission/task state, receive results, and never silently compete for orchestration authority.
|
||||||
|
7. `TESS-HRM-001`: Transitional Hermes adapter for profiles/agents, sessions, streaming/messages, Kanban, skills, memory, tools, cron, and health, using capability negotiation and fail-closed unsupported operations.
|
||||||
|
8. `TESS-MEM-001`: Unified memory/retrieval plugin with scoped search/recent/capture/stats, startup context injection, provenance, redaction, namespace isolation, and flat-file/project truth precedence.
|
||||||
|
9. `TESS-STA-001`: Durable agent state, inbox, handoff, compaction-recovery, and resume reconstruction.
|
||||||
|
10. `TESS-PLG-001`: Plugin/tool catalog covering runtime bootstrap, repository/PR workflow, fleet diagnostics, incident-safe read operations, Discord interaction, and extensible MCP/skill discovery.
|
||||||
|
11. `TESS-TRN-001`: Replaceable transport providers: tmux/fleet now, Matrix/native Mosaic transport later, with no Discord/CLI business logic coupled to transport details.
|
||||||
|
12. `TESS-SEC-001`: RBAC, per-operation authorization, explicit approval for destructive/privileged/customer-visible actions, audit events, secret/PII redaction, tenant isolation, and bounded command execution.
|
||||||
|
13. `TESS-SEC-002`: Command execution SHALL enforce declared scope/role server-side; admin/system and destructive operations SHALL require policy-bound durable approval.
|
||||||
|
14. `TESS-SEC-003`: Every session list/read/attach/send/terminate operation SHALL enforce server-derived owner and tenant scope; guessed or client-supplied IDs SHALL grant no authority.
|
||||||
|
15. `TESS-SEC-004`: MCP tools SHALL derive actor/tenant from authenticated context and SHALL NOT accept caller-controlled identity fields.
|
||||||
|
16. `TESS-SEC-005`: Discord plugin ingress SHALL authenticate service identity, enforce guild/channel/user allowlists, propagate correlation/message IDs, and reject replay.
|
||||||
|
17. `TESS-SEC-006`: Secret/PII classification and redaction SHALL occur before persistence and before channel egress, including tool metadata and authentication flows.
|
||||||
|
18. `TESS-SEC-007`: Approvals SHALL be one-time, expiring, actor/tenant-bound, and cryptographically bound to the exact structured action digest.
|
||||||
|
19. `TESS-SEC-008`: Ingress, provider sends, tool side effects, and responses SHALL use durable inbox/outbox/checkpoints and idempotency records for restart-safe replay.
|
||||||
|
20. `TESS-SEC-009`: Garbage collection and retention SHALL be session/tenant scoped unless executed as a separately authorized and audited system-wide job.
|
||||||
|
21. `TESS-OBS-001`: Structured logs, traces, health/readiness, provider latency/errors, session lifecycle, tool audit, and actionable recovery diagnostics.
|
||||||
|
22. `TESS-MIG-001`: Capability inventory and staged Hermes-to-Mosaic migration matrix with coexistence, cutover, rollback, and deprecation gates.
|
||||||
|
|
||||||
|
#### Out of Scope
|
||||||
|
|
||||||
|
1. Replacing Mos as coding/general fleet orchestrator.
|
||||||
|
2. Making Hermes the Mosaic core or coupling Mosaic domain logic to Hermes schemas.
|
||||||
|
3. Migrating every historical chat verbatim; only policy-compliant indexed summaries and user-selected sessions are migrated.
|
||||||
|
4. Unrestricted shell execution from Discord.
|
||||||
|
5. Full web UI parity in the first Tess operational milestone; gateway contracts must remain web-consumable.
|
||||||
|
6. Replacing tmux before Matrix/native transport reaches operational parity.
|
||||||
|
|
||||||
|
### Stakeholder and User Requirements
|
||||||
|
|
||||||
|
- Jason must be able to converse with the same Tess session from Discord and CLI.
|
||||||
|
- Jason must be able to see what is running, stale, blocked, or unhealthy without attaching manually to every session.
|
||||||
|
- Jason must be able to attach to Tess and authorized fleet sessions through supported CLI controls.
|
||||||
|
- Tess must collaborate with Mos and the fleet while preserving a single clear orchestration authority.
|
||||||
|
- The system must migrate useful Hermes/OpenClaw capabilities intentionally, with evidence, instead of copying implementations wholesale.
|
||||||
|
|
||||||
|
### Non-Functional Requirements
|
||||||
|
|
||||||
|
1. **Security:** default-deny provider/tool capabilities, least privilege, no secrets in logs/prompts/commits, Discord user/channel authorization, and auditable approvals.
|
||||||
|
2. **Reliability:** durable inbox/checkpoints; idempotent message handling; reconnect with bounded backoff; no message loss or duplicate execution across gateway restart.
|
||||||
|
3. **Performance:** first acknowledgement within 2 seconds when connected; streamed agent output begins within 5 seconds excluding model/provider delay; status reads return within 2 seconds under nominal local conditions.
|
||||||
|
4. **Observability:** every ingress message and resulting provider/tool operation carries a correlation ID across Discord, gateway, Tess, provider, and audit events.
|
||||||
|
5. **Maintainability:** channel, runtime, transport, memory, and external-agent integrations remain adapter-based with contract tests.
|
||||||
|
6. **Privacy:** only scoped context enters external runtimes; persisted messages/memories follow retention and redaction policy.
|
||||||
|
7. **Portability:** Tess runs through Pi/Mosaic contracts and does not require Hermes to start or serve native Mosaic operations.
|
||||||
|
|
||||||
|
### Acceptance Criteria
|
||||||
|
|
||||||
|
1. `AC-TESS-01`: A dedicated Discord channel and `mosaic tess chat` connect to one durable Tess session and stream responses bidirectionally.
|
||||||
|
2. `AC-TESS-02`: `mosaic tess status|sessions|tree|attach|send|stop` operate against authorized provider capabilities with stable typed outputs and actionable errors.
|
||||||
|
3. `AC-TESS-03`: Tess runs GPT-5.6 Sol at high reasoning and its effective runtime/model/tool policy is visible through status without exposing credentials.
|
||||||
|
4. `AC-TESS-04`: Tess can inspect and message the Mosaic fleet, hand orchestration work to Mos, and demonstrate that Tess does not independently claim Mos-owned orchestration work.
|
||||||
|
5. `AC-TESS-05`: Hermes adapter demonstrates session listing, streaming/message delivery, hierarchy mapping, and at least one approved capability in each of Kanban, skills, memory, tools, and cron—or reports unsupported capabilities fail-closed.
|
||||||
|
6. `AC-TESS-06`: Restart/compaction test preserves session identity, pending inbox, last durable checkpoint, and a resumable handoff without duplicate side effects.
|
||||||
|
7. `AC-TESS-07`: Unauthorized Discord users/channels, cross-tenant access, unsafe tool calls, forged approvals, and sensitive-output cases are denied and audited.
|
||||||
|
8. `AC-TESS-08`: tmux/fleet and Matrix/native transport implementations pass the same provider contract suite; Matrix may remain non-default until readiness gates pass.
|
||||||
|
9. `AC-TESS-09`: Baseline quality gates, unit/integration/contract tests, Discord+CLI E2E, restart/recovery tests, independent code review, and security review are green.
|
||||||
|
10. `AC-TESS-10`: Migration matrix documents every audited Hermes/OpenClaw capability as native, adapted, deferred, or rejected, with cutover and rollback evidence.
|
||||||
|
11. `AC-TESS-11`: User, admin, developer, API/OpenAPI, operations/recovery, and plugin-authoring documentation is current and linked from the sitemap.
|
||||||
|
|
||||||
|
### Constraints, Dependencies, Risks, and Assumptions
|
||||||
|
|
||||||
|
- Dependency: Mosaic gateway remains the single API surface; Pi is the native runtime; Valkey/PostgreSQL provide canonical durable state where required.
|
||||||
|
- Dependency: Discord bot credentials and dedicated channel ID are deployment secrets provisioned outside source control.
|
||||||
|
- Risk: Tess could drift into a second orchestrator. Mitigation: explicit role policy, Mos handoff contract, authority checks, and E2E boundary tests.
|
||||||
|
- Risk: broad Hermes compatibility can freeze legacy semantics into Mosaic. Mitigation: Mosaic-owned normalized contracts and capability negotiation.
|
||||||
|
- Risk: Discord creates a privileged remote-control surface. Mitigation: pairing/allowlists, RBAC, approvals, rate limits, audit, and safe tool classes.
|
||||||
|
- Risk: transcript ingestion can violate privacy or overload memory. Mitigation: scoped opt-in import, redacted summaries, provenance, retention, and deduplication.
|
||||||
|
- Risk: current root filesystem has limited headroom. Mitigation: isolated worktrees, no duplicated dependency installation unless required, and cleanup only after active-lane verification.
|
||||||
|
- `ASSUMPTION:` The public name is **Tess**, because the user requested a name and the tessera/Mosaic relationship is distinctive; config must permit later display-name changes without renaming APIs or storage keys.
|
||||||
|
- `ASSUMPTION:` The dedicated Discord channel ID and final guild policy will be supplied/provisioned during deployment, so implementation uses explicit configuration and fail-fast startup validation.
|
||||||
|
- `ASSUMPTION:` tmux/fleet is the production transport for the first operational milestone; Matrix/native transport is implemented behind the same contract and promoted only after parity/reliability verification.
|
||||||
|
- `ASSUMPTION:` Project/task truth remains in canonical Mosaic/project stores; semantic memory systems are retrieval/mirror layers, not hidden authorities.
|
||||||
|
|
||||||
|
### Testing and Delivery Intent
|
||||||
|
|
||||||
|
Delivery uses five gated milestones: runtime contracts/security; Pi service/state; Discord/CLI; fleet/Hermes/plugin suite; migration/Matrix/recovery/qualification. Every source-code task requires tests, independent review, a PR to `main`, terminal-green CI, and issue/task closure. Production activation additionally requires a clean-host Pi launch, dedicated Discord channel smoke test, CLI attach test, restart/recovery drill, and rollback procedure.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
### High-Level System Diagram
|
### High-Level System Diagram
|
||||||
|
|||||||
@@ -15,8 +15,9 @@
|
|||||||
## Workstream Rollup
|
## Workstream Rollup
|
||||||
|
|
||||||
| id | status | workstream | progress | tasks file | notes |
|
| id | status | workstream | progress | tasks file | notes |
|
||||||
| --- | ----------------- | ------------------- | ---------------- | ------------------------------------------------- | --------------------------------------------------------------- |
|
| --- | ----------------- | ---------------------- | ---------------- | ------------------------------------------------- | --------------------------------------------------------------- |
|
||||||
| W1 | planning-complete | Federation v1 (FED) | 0 / 7 milestones | [docs/federation/TASKS.md](./federation/TASKS.md) | M1 task breakdown populated; M2–M7 deferred to mission planning |
|
| W1 | planning-complete | Federation v1 (FED) | 0 / 7 milestones | [docs/federation/TASKS.md](./federation/TASKS.md) | M1 task breakdown populated; M2–M7 deferred to mission planning |
|
||||||
|
| W2 | planning-complete | Tess interaction agent | 0 / 5 milestones | [docs/tess/TASKS.md](./tess/TASKS.md) | Issue #706; independent planning gate PASS; M1 issue #707 ready |
|
||||||
|
|
||||||
## Cross-Cutting Tracking
|
## Cross-Cutting Tracking
|
||||||
|
|
||||||
|
|||||||
33
docs/scratchpads/561-python-is-python3.md
Normal file
33
docs/scratchpads/561-python-is-python3.md
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# Issue #561 — Bare python on agent hosts
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
|
||||||
|
Make the durable bootstrap/provisioning guidance ensure agent hosts provide a bare `python` command that resolves to Python 3.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- Add Debian/Ubuntu `python-is-python3` to agent-host prerequisites in bootstrap docs.
|
||||||
|
- Check for actual OS package provisioning scripts and update only if an existing agent-host package install path exists.
|
||||||
|
- Do not touch live host state.
|
||||||
|
- Do not update `docs/TASKS.md`; repo guidance says workers read it but never modify it.
|
||||||
|
|
||||||
|
## Recon
|
||||||
|
|
||||||
|
- Issue #561 confirms repeated `python: command not found` failures from fleet agents that emit `python foo.py`.
|
||||||
|
- `guides/BOOTSTRAP.md` and `packages/mosaic/framework/guides/BOOTSTRAP.md` are the source and packaged framework copies of the bootstrap guide.
|
||||||
|
- Targeted repo sweep found no agent-host Debian package provisioning script. Existing `apt-get install` hits are CI/test helper paths or unrelated deployment docs.
|
||||||
|
|
||||||
|
## Plan
|
||||||
|
|
||||||
|
1. Add a host prerequisite section to both bootstrap guide copies.
|
||||||
|
2. Include `python-is-python3` in the Debian/Ubuntu package list with an issue comment.
|
||||||
|
3. Note the non-Debian equivalent as a `/usr/bin/python -> python3` symlink.
|
||||||
|
4. Validate markdown/diff, run shell syntax checks where applicable, run required review, commit, queue guard, and push.
|
||||||
|
|
||||||
|
## Validation Log
|
||||||
|
|
||||||
|
- `rg` recon: no existing agent-host Debian package provisioning script; only CI/test helper `apt-get install` paths and unrelated deployment docs.
|
||||||
|
- `git diff --check`: passed.
|
||||||
|
- `bash -n packages/mosaic/framework/install.sh tools/install.sh packages/mosaic/framework/tools/bootstrap/init-project.sh packages/mosaic/framework/tools/_scripts/mosaic-bootstrap-repo`: passed. No touched shell scripts.
|
||||||
|
- `~/.config/mosaic/tools/codex/codex-code-review.sh --uncommitted`: approved, 0 findings.
|
||||||
|
- `pnpm format:check`: initially blocked because `node_modules` was absent and `prettier` was unavailable; `pnpm install --frozen-lockfile` initially hit an invalid `/root` pnpm store path. Reran install with `--store-dir /home/hermes/agent-work/.pnpm-store`, then `pnpm format:check` passed.
|
||||||
49
docs/scratchpads/703-wrapper-interactive-auth.md
Normal file
49
docs/scratchpads/703-wrapper-interactive-auth.md
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# #703 Git Wrapper Interactive and Auth Resilience
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
|
||||||
|
Restore the deployed Git wrapper contract: issue-create supports interactive invocation and Gitea mutation behavior tolerates a stale Tea authenticated user by validating current identity and using the existing host-scoped API fallback.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- `packages/mosaic/framework/tools/git/issue-create.sh`
|
||||||
|
- `packages/mosaic/framework/tools/git/detect-platform.sh`
|
||||||
|
- Git wrapper regression harnesses
|
||||||
|
- This scratchpad
|
||||||
|
|
||||||
|
## Requirements / acceptance evidence
|
||||||
|
|
||||||
|
1. `issue-create -i` and `--interactive` prompt for missing issue fields without exposing credentials.
|
||||||
|
2. Explicit command-line fields retain precedence and do not trigger prompt input.
|
||||||
|
3. Gitea wrapper resolves the current user dynamically from the target host and does not rely on the saved Tea user identity.
|
||||||
|
4. A Tea `GetUserByName` failure falls back to authenticated API creation.
|
||||||
|
5. Existing body-safety, login-resolution, issue-create, and pr-create paths remain green.
|
||||||
|
6. Source framework is re-seeded to deployed `~/.config/mosaic`, then deployed wrappers are verified end to end.
|
||||||
|
|
||||||
|
## Plan
|
||||||
|
|
||||||
|
1. Add failing shell regression harness for interactive input and stale Tea user fallback.
|
||||||
|
2. Implement minimal helper and parser changes.
|
||||||
|
3. Run wrapper harnesses, syntax checks, and repository baseline checks.
|
||||||
|
4. Re-seed deployed framework and run live wrapper verification.
|
||||||
|
5. Commit, queue guard, push, open PR, and stop for independent review.
|
||||||
|
|
||||||
|
## Progress
|
||||||
|
|
||||||
|
- Issue #703 filed before code; issue comment records #536 root cause and stale-login trigger.
|
||||||
|
- Deployed wrapper `issue-create.sh -i` reproduced: `Unknown option: -i` (exit 1).
|
||||||
|
- Live Tea mutation did not reproduce `GetUserByName` on this host because the current mosaicstack Tea login is valid. The test harness models the reported stale authenticated-user condition.
|
||||||
|
- Implemented `-i` / `--interactive` prompt collection and a dynamic Tea `/user` validation. A stale Tea identity now selects the existing host-scoped Gitea API fallback before mutation for both issue and PR creation.
|
||||||
|
- Re-seeded the framework with `MOSAIC_INSTALL_MODE=keep MOSAIC_SYNC_ONLY=1 bash packages/mosaic/framework/install.sh`. Installed and source wrapper SHA-256 values matched.
|
||||||
|
- Live deployed verification: interactive issue-create opened then closed #704; installed dynamic identity resolved `jason.woltje`.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- PASS: `packages/mosaic/framework/tools/git/test-issue-create-interactive-auth.sh`
|
||||||
|
- PASS: `packages/mosaic/framework/tools/git/test-issue-create-body-safety.sh`
|
||||||
|
- PASS: `packages/mosaic/framework/tools/git/test-gitea-login-resolution.sh`
|
||||||
|
- PASS: `packages/mosaic/framework/tools/git/test-pr-metadata-gitea.sh`
|
||||||
|
- PASS: `packages/mosaic/framework/tools/git/test-pr-merge-gitea-empty-uid.sh`
|
||||||
|
- PASS: `bash packages/mosaic/framework/tools/git/test-lane-brief-pr-linkage.sh`
|
||||||
|
- PASS: `bash -n packages/mosaic/framework/tools/git/*.sh`
|
||||||
|
- PASS: Prettier check for this scratchpad
|
||||||
58
docs/scratchpads/tess-20260712.md
Normal file
58
docs/scratchpads/tess-20260712.md
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
# Scratchpad — Tess Interaction Agent
|
||||||
|
|
||||||
|
## 2026-07-12 — Mission intake
|
||||||
|
|
||||||
|
**Objective:** Build a Pi-native GPT-5.6 Sol high-reasoning Mosaic interaction agent, named Tess, as Jason's primary Discord/CLI access point for Mosaic fleet and transitional Hermes capabilities. Tess complements Mos and must not become a competing orchestrator.
|
||||||
|
|
||||||
|
**Issue:** #706
|
||||||
|
|
||||||
|
**Budget:** No explicit cap provided. Original working estimate was 290K implementation/review tokens. That estimate is superseded after six security prerequisite tasks were added; revised arithmetic total is pending because the calculation tool was blocked by runtime consent. Run at most two workers; prefer one implementation lane plus one independent review/discovery lane. Re-estimate after planning approval and each milestone.
|
||||||
|
|
||||||
|
**Evidence gathered:**
|
||||||
|
- Mosaic already provides Pi lifecycle hooks, fleet/tmux sessions, Matrix connector/controller pieces, typed chat events, Discord/Telegram channel plugins, and command/plugin registries.
|
||||||
|
- Current `IProviderAdapter` is an LLM model/completion abstraction, not an external agent/session provider.
|
||||||
|
- Required new seam is `AgentRuntimeProvider`: sessions, stream, message, terminate, hierarchy, attach, health, capabilities.
|
||||||
|
- Recurring cross-runtime needs: unified memory/retrieval, Discord routing/approvals, agent state/inbox/compaction recovery, runtime bootstrap, fleet/incident controls, and GitOps workflow.
|
||||||
|
- Project truth must remain in canonical project/Mosaic stores; semantic memory is retrieval/mirror.
|
||||||
|
|
||||||
|
**Decisions:**
|
||||||
|
1. Name: Tess (tessera). Stable machine key `tess`; display name configurable.
|
||||||
|
2. Mos owns orchestration; Tess delegates Mos-owned work through an explicit coordination contract.
|
||||||
|
3. Gateway owns ingress/auth/routing; Discord and CLI remain thin clients.
|
||||||
|
4. tmux/fleet ships first behind an adapter; Matrix/native Mosaic is the forward transport.
|
||||||
|
5. Hermes integration is transitional and capability-negotiated; unsupported operations fail closed.
|
||||||
|
6. No unrestricted Discord shell. Privileged/destructive/customer-visible actions require authorization and approval.
|
||||||
|
|
||||||
|
**Plan:**
|
||||||
|
1. Land requirements/architecture/task graph.
|
||||||
|
2. Deliver runtime contracts and security model.
|
||||||
|
3. Deliver durable Pi service/state.
|
||||||
|
4. Deliver Discord and CLI.
|
||||||
|
5. Deliver fleet/Mos/Hermes/memory/tool plugins.
|
||||||
|
6. Deliver Matrix/native transport, migration matrix, recovery, docs, and qualification.
|
||||||
|
|
||||||
|
**Progress:** Issue #706 created. PRD/manifest/tasks initialized on clean branch `feat/tess-interaction-agent` from `origin/main`.
|
||||||
|
|
||||||
|
**Risks:** 14 GB root filesystem headroom; active fleet lanes; broad migration scope; Discord privilege boundary; possible duplicate orchestration authority.
|
||||||
|
|
||||||
|
## 2026-07-12 — Independent planning and threat review
|
||||||
|
|
||||||
|
**Verdict received:** BLOCK TESS-PLAN-001. Coding remains stopped.
|
||||||
|
|
||||||
|
**Blocking findings:** formal threat model absent; verification matrix absent; migration inventory implied but absent; non-existent task paths; AC-TESS-03 lacked a crisp test. Security review also identified command scope bypass, cross-tenant session attachment, MCP actor impersonation, unsafe Discord service ingress, pre-persistence/egress secret leakage, non-durable replay, and globally scoped GC.
|
||||||
|
|
||||||
|
**Remediation applied:**
|
||||||
|
- Added `docs/tess/ARCHITECTURE.md`.
|
||||||
|
- Added `docs/tess/THREAT-MODEL.md` with TM-01..12.
|
||||||
|
- Added `docs/tess/VERIFICATION-MATRIX.md` mapping AC-TESS-01..11.
|
||||||
|
- Added `docs/tess/MIGRATION-INVENTORY.md`.
|
||||||
|
- Added hard requirements TESS-SEC-002..009.
|
||||||
|
- Added six prerequisite security tasks before provider/ingress implementation.
|
||||||
|
- Corrected task paths to existing package surfaces.
|
||||||
|
- Added explicit GPT-5.6 Sol/high/tool-policy status verification for AC-TESS-03.
|
||||||
|
|
||||||
|
**Re-review 1:** BLOCK only on composite `repo` values that looked like nonexistent paths. Remediated by declaring comma-separated roots and validating every root.
|
||||||
|
|
||||||
|
**Final focused review:** PASS. Deterministic audit validated all task repository roots with zero missing paths; no planning placeholders remained; security prerequisites still gate Tess exposure; observability traceability is explicit.
|
||||||
|
|
||||||
|
**Current gate:** planning PR must merge to `main` with terminal-green CI before any source-code worker starts.
|
||||||
27
docs/scratchpads/tess-m1-sec-001.md
Normal file
27
docs/scratchpads/tess-m1-sec-001.md
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# TESS-M1-SEC-001 — Command authorization and exact-action approval
|
||||||
|
|
||||||
|
- Issue/milestone: #707 / M1
|
||||||
|
- Branch: `fix/tess-command-authz`
|
||||||
|
- Requirement: `TESS-SEC-002`, with approval binding controls from `TESS-SEC-007`
|
||||||
|
- Scope: `apps/gateway` only, plus required in-repo security/developer documentation.
|
||||||
|
|
||||||
|
## Plan
|
||||||
|
|
||||||
|
1. Locate the gateway command executor, command metadata, authorization context, and existing test conventions.
|
||||||
|
2. Write abuse/authz tests before production changes. Expected red cases: non-admin blocked from admin/system command; forged caller scope cannot authorize; privileged/destructive action requires durable exact-action approval; expired/replayed/mutated approvals deny.
|
||||||
|
3. Implement server-derived role/scope enforcement and durable approval validation/consumption with audit results.
|
||||||
|
4. Run focused security tests, then repository baseline gates: typecheck, lint, format-check, test.
|
||||||
|
5. Run independent security/code review, commit, queue-guard, push, and open the PR to `main` through the stated Gitea API fallback. Stop after PR creation.
|
||||||
|
|
||||||
|
## Assumptions
|
||||||
|
|
||||||
|
- The existing gateway persistence interface is the available durable approval boundary. If no persistence abstraction exists, a minimal injectable repository interface will be introduced rather than an in-memory approval implementation, because TESS-SEC-002/007 require durable enforcement.
|
||||||
|
- “Exact action” is a canonical digest over structured command identity and normalized arguments; role/scope checks always use authenticated server context, not client-declared claims.
|
||||||
|
|
||||||
|
## TDD evidence
|
||||||
|
|
||||||
|
- Pending: abuse/authz test written and observed red before implementation.
|
||||||
|
|
||||||
|
## Verification evidence
|
||||||
|
|
||||||
|
- Pending.
|
||||||
71
docs/tess/ARCHITECTURE.md
Normal file
71
docs/tess/ARCHITECTURE.md
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
# Tess Architecture
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Tess is the Mosaic operator interaction plane. Mos remains the coding/general fleet orchestration authority. Tess receives authorized operator intent, presents fleet/session state, delegates Mos-owned work to Mos, and exposes native Mosaic plus transitional external-agent capabilities through normalized providers.
|
||||||
|
|
||||||
|
## Component Boundaries
|
||||||
|
|
||||||
|
```text
|
||||||
|
Discord plugin ─┐
|
||||||
|
├─ authenticated ingress envelope ─> Mosaic Gateway
|
||||||
|
mosaic tess CLI ┘ │
|
||||||
|
├─ policy/approval/audit
|
||||||
|
├─ Tess durable session service (Pi GPT-5.6 Sol high)
|
||||||
|
├─ AgentRuntimeProvider registry
|
||||||
|
│ ├─ native Pi provider
|
||||||
|
│ ├─ fleet/tmux provider
|
||||||
|
│ ├─ Hermes adapter
|
||||||
|
│ └─ Matrix/native transport provider
|
||||||
|
├─ memory/state/inbox plugins
|
||||||
|
└─ Mos coordination adapter ─> Mos / fleet queue
|
||||||
|
```
|
||||||
|
|
||||||
|
## Core Contract
|
||||||
|
|
||||||
|
`AgentRuntimeProvider` is separate from the existing model-completion `IProviderAdapter`. It normalizes external and native agent runtimes without leaking provider-specific schemas.
|
||||||
|
|
||||||
|
Required operations:
|
||||||
|
|
||||||
|
- `capabilities()` and `health()`
|
||||||
|
- `listSessions(scope)`
|
||||||
|
- `getSessionTree(scope)`
|
||||||
|
- `streamSession(sessionRef, cursor, scope)`
|
||||||
|
- `sendMessage(sessionRef, message, idempotencyKey, scope)`
|
||||||
|
- `attach(sessionRef, mode, scope)` / `detach()`
|
||||||
|
- `terminate(sessionRef, approvalRef, scope)`
|
||||||
|
|
||||||
|
Every call receives an immutable, server-derived actor/tenant/channel scope and correlation ID. Caller-supplied actor IDs are forbidden. Unsupported capabilities fail closed with typed errors.
|
||||||
|
|
||||||
|
## Authority Model
|
||||||
|
|
||||||
|
| Intent | Owner | Tess behavior |
|
||||||
|
| --------------------------------------------------------------------------- | ----------------------- | ---------------------------------------------------------------------- |
|
||||||
|
| Conversation, status, retrieval, safe diagnostics | Tess | Execute within policy |
|
||||||
|
| Code/project decomposition, worker assignment, reviews, merge orchestration | Mos | Create a correlated handoff and observe result |
|
||||||
|
| Destructive, privileged, external/customer-visible action | Human approval + policy | Propose, wait for durable one-time approval, then execute idempotently |
|
||||||
|
| Provider-specific unsupported action | None | Fail closed; never emulate silently |
|
||||||
|
|
||||||
|
## Session and State Model
|
||||||
|
|
||||||
|
A Tess session has stable `sessionId`, `tenantId`, `ownerId`, provider/runtime identity, ingress bindings, cursor, checkpoint, inbox/outbox, and idempotency records. Discord and CLI bind to the same authorized session. Ownership is verified server-side on every list/read/attach/send/terminate operation.
|
||||||
|
|
||||||
|
Valkey may hold ephemeral coordination state; PostgreSQL is canonical for durable session bindings, approvals, audit, checkpoints, inbox/outbox, and idempotency. Pi session files are replay sources, not cross-agent truth.
|
||||||
|
|
||||||
|
## Transport Strategy
|
||||||
|
|
||||||
|
- **Initial:** fleet/tmux provider, including exact target, socket, identity, heartbeat, and safe attach semantics.
|
||||||
|
- **Forward:** Matrix/native Mosaic provider using authenticated identity, idempotent transaction IDs, replay cursors, and the same contract suite.
|
||||||
|
- Discord/CLI never call tmux or Matrix directly.
|
||||||
|
|
||||||
|
## Plugin Families
|
||||||
|
|
||||||
|
1. Channel: Discord now; other channels later.
|
||||||
|
2. Runtime: Pi, fleet/tmux, Hermes, Matrix/native.
|
||||||
|
3. Operator tools: fleet health, Mos handoff, GitOps wrappers, incident-safe diagnostics.
|
||||||
|
4. Memory/state: search/recent/capture, durable inbox, checkpoint, handoff, compaction recovery.
|
||||||
|
5. Migration: capability inventory, adapters, cutover, rollback, telemetry.
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
Tess runs as a rostered, systemd-supervised Pi agent using GPT-5.6 Sol and high reasoning. Secrets are supplied through approved runtime secret mechanisms. Startup fails when required model, gateway identity, Discord binding, or durable-state dependencies are missing. Health reports effective model/reasoning/tool policy without credential material.
|
||||||
34
docs/tess/MIGRATION-INVENTORY.md
Normal file
34
docs/tess/MIGRATION-INVENTORY.md
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# Tess Capability Migration Inventory
|
||||||
|
|
||||||
|
Status values: `native` · `adapt` · `defer` · `reject`. This is the initial inventory; M5 requires implementation and evidence fields to be completed before cutover.
|
||||||
|
|
||||||
|
| Capability | Current source | Target | Initial status | Cutover/rollback intent |
|
||||||
|
| ---------------------------------------- | ---------------------------------------- | ------------------------------------------ | -------------- | ----------------------------------------------------------------------- |
|
||||||
|
| Interactive agent chat/session streaming | Hermes/Pi/OpenClaw | Mosaic Tess session service | native | Dual-run per channel; revert binding to legacy gateway |
|
||||||
|
| Discord dedicated-channel routing | Hermes/Claude/OpenClaw plugins | Mosaic Discord plugin + gateway | native | Per-channel binding switch; legacy bot disabled only after soak |
|
||||||
|
| CLI/TUI session interaction and attach | Hermes/Pi/tmux | `mosaic tess` + AgentRuntimeProvider | native | Keep direct tmux attach as break-glass rollback |
|
||||||
|
| Session list/tree/send/terminate | Hermes/fleet | AgentRuntimeProvider | native | Capability-negotiated adapter remains during migration |
|
||||||
|
| Mos/fleet orchestration handoff | tmux messaging/Mosaic fleet | Mosaic coord/fleet provider | native | tmux handoff remains initial transport |
|
||||||
|
| Kanban/projects/tasks | Hermes Kanban | Mosaic queue/coord/project providers | adapt | Read projection first; mutating cutover after parity/audit |
|
||||||
|
| Skills catalog/load/manage | Hermes skills/Pi skills | Mosaic skill registry/provider | adapt | Import metadata/provenance; preserve source skill until validated |
|
||||||
|
| Tools and MCP | Hermes/OpenClaw/MCP | Mosaic tool registry/MCP | adapt | Default deny; migrate allowlisted tools one capability at a time |
|
||||||
|
| Cron/scheduled work | Hermes cron | Mosaic scheduler/queue | adapt | Shadow schedules; prevent duplicate execution; rollback owner field |
|
||||||
|
| Memory search/recent/capture | jarvis-brain/OpenViking/OpenBrain/Hermes | Mosaic memory provider | adapt | Flat/project stores remain truth; semantic systems are mirrors |
|
||||||
|
| User/profile preferences | Hermes memory/user profile | Mosaic user/memory domain | adapt | Provenance + explicit conflict rules; exportable rollback snapshot |
|
||||||
|
| Agent state/inbox/handoff | OpenClaw extensions/session files | Mosaic durable state service | native | Read legacy handoff during coexistence; write Mosaic only after cutover |
|
||||||
|
| Runtime contract/bootstrap | Mosaic framework/Hermes/OpenClaw | Mosaic compose/runtime provider | native | Legacy launchers remain until clean-host parity passes |
|
||||||
|
| Repository/PR workflow | Mosaic wrappers/Hermes tools | Mosaic operator plugin | native | Wrapper-only; no raw-provider fallback |
|
||||||
|
| Incident-safe diagnostics | Hermes skills/tools | Mosaic scoped operator plugin | adapt | Read-only first; privileged recovery requires approval |
|
||||||
|
| Broad unrestricted shell from Discord | Hermes/OpenClaw configurations | None | reject | No cutover; replace with allowlisted typed operations |
|
||||||
|
| Raw full transcript bulk migration | Hermes/Claude/OpenClaw histories | Indexed summaries/selective import | reject | Keep source archives subject to retention; no automatic copy |
|
||||||
|
| Voice/video interaction | Hermes optional tools | Future Mosaic channel plugins | defer | Not required for Tess operational release |
|
||||||
|
| Matrix transport | Mosaic connector | AgentRuntimeProvider Matrix implementation | native | Non-default until contract/reliability parity; tmux rollback |
|
||||||
|
|
||||||
|
## Cutover Gates
|
||||||
|
|
||||||
|
1. Capability contract and security tests pass.
|
||||||
|
2. Data mapping/provenance and retention are documented.
|
||||||
|
3. Shadow or dual-run shows no unauthorized access, loss, or duplicate effects.
|
||||||
|
4. Operator runbook and rollback are exercised.
|
||||||
|
5. Channel/provider binding changes are reversible without schema rollback.
|
||||||
|
6. Legacy capability is disabled only after a defined soak period and evidence review.
|
||||||
46
docs/tess/MISSION-MANIFEST.md
Normal file
46
docs/tess/MISSION-MANIFEST.md
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
# Mission Manifest — Tess Interaction Agent
|
||||||
|
|
||||||
|
## Mission
|
||||||
|
|
||||||
|
- **ID:** tess-20260712
|
||||||
|
- **Issue:** #706
|
||||||
|
- **Branch:** `feat/tess-interaction-agent`
|
||||||
|
- **Phase:** Execution
|
||||||
|
- **Current Milestone:** TESS-M1 — Runtime contracts and security foundation
|
||||||
|
- **Progress:** 0 / 5 delivery milestones complete
|
||||||
|
- **Status:** active
|
||||||
|
- **Owner:** Mosaic orchestrator; Mos is coordinating fleet authority
|
||||||
|
- **Source PRD:** `docs/PRD.md` — `TESS-*` requirements
|
||||||
|
- **Scratchpad:** `docs/scratchpads/tess-20260712.md`
|
||||||
|
|
||||||
|
## Mission Statement
|
||||||
|
|
||||||
|
Ship Tess as Jason's durable Pi-native GPT-5.6 Sol high-reasoning interaction agent for Discord and CLI, with safe visibility/control of Mosaic fleet and transitional Hermes capabilities, while Mos remains the coding/general orchestration authority.
|
||||||
|
|
||||||
|
## Invariants
|
||||||
|
|
||||||
|
1. Mosaic is the enterprise AI hub; Hermes is a reference migration adapter.
|
||||||
|
2. Gateway is the single API surface.
|
||||||
|
3. Mos owns coding/general fleet orchestration; Tess owns human interaction, visibility, mediation, and migration access.
|
||||||
|
4. Runtime, transport, channel, memory, and external-agent integrations are replaceable adapters.
|
||||||
|
5. No source task completes before merged PR, terminal-green CI, independent review, and linked task/issue closure.
|
||||||
|
|
||||||
|
## Milestones
|
||||||
|
|
||||||
|
| ID | Issue | Name | Status | Exit gate |
|
||||||
|
| ------- | ----- | ------------------------------------------------------------------- | ----------- | ----------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| TESS-M1 | #707 | Runtime contracts and security foundation | ready | AgentRuntimeProvider, normalized events/capabilities/errors, RBAC/audit contracts and contract tests merged |
|
||||||
|
| TESS-M2 | #708 | Durable Pi Tess service and state | not-started | GPT-5.6 Sol high service starts, resumes, checkpoints, and passes restart/compaction tests |
|
||||||
|
| TESS-M3 | #709 | Discord and CLI interaction surfaces | not-started | One durable session works through dedicated Discord binding and `mosaic tess`, including attach and approvals |
|
||||||
|
| TESS-M4 | #710 | Fleet, Mos, Hermes, memory, state, and tool plugins | not-started | Fleet/Mos boundary and transitional capability matrix demonstrated end-to-end |
|
||||||
|
| TESS-M5 | #711 | Matrix/native migration, recovery, documentation, and qualification | not-started | Transport parity, migration/rollback matrix, security review, docs, greenfield and deployment validation complete |
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
|
||||||
|
All `AC-TESS-*` criteria in `docs/PRD.md` are mapped to reproducible evidence. The final operational test must prove Discord + CLI session continuity, fleet/Mos coordination, authorized Hermes transition capabilities, denial/audit paths, restart recovery, and rollback.
|
||||||
|
|
||||||
|
## Session History
|
||||||
|
|
||||||
|
| Session | Date | Runtime | Outcome |
|
||||||
|
| ------- | ---------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| S1 | 2026-07-12 | Hermes / GPT-5.6 Sol | User commission captured; Mosaic/OpenViking/session/code archaeology completed; issue #706 created; PRD and task control plane initialized. |
|
||||||
34
docs/tess/TASKS.md
Normal file
34
docs/tess/TASKS.md
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# Tasks — Tess Interaction Agent
|
||||||
|
|
||||||
|
> Mission: `tess-20260712` · Issue: #706 · PRD requirements: `TESS-*`
|
||||||
|
> Orchestrator is sole writer. Workers must not modify this file.
|
||||||
|
> `repo` contains one or more comma-separated repository-relative roots; every listed root must exist before dispatch.
|
||||||
|
|
||||||
|
| id | status | description | issue | agent | repo | branch | depends_on | estimate | notes |
|
||||||
|
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
||||||
|
| TESS-PLAN-001 | done | Finalize PRD, architecture, authority boundary, threat model, migration inventory, and verification matrix | #706 | sonnet | docs, packages/types, apps/gateway | feat/tess-interaction-agent | — | 22K | Independent gate PASS after two remediation rounds; completion effective when planning PR merges |
|
||||||
|
| TESS-M1-SEC-001 | not-started | Enforce command scopes/roles and durable exact-action approval for privileged/destructive commands | #707 | codex | apps/gateway | fix/tess-command-authz | TESS-PLAN-001 | 25K | TESS-SEC-002; security TDD |
|
||||||
|
| TESS-M1-SEC-002 | not-started | Enforce owner/tenant binding on session list/read/attach/send/terminate across REST and WS | #707 | codex | apps/gateway | fix/tess-session-ownership | TESS-PLAN-001 | 30K | TESS-SEC-003; security TDD |
|
||||||
|
| TESS-M1-SEC-003 | not-started | Bind MCP actor/tenant to authenticated context and add per-tool scopes | #707 | codex | apps/gateway | fix/tess-mcp-identity | TESS-PLAN-001 | 22K | TESS-SEC-004; security TDD |
|
||||||
|
| TESS-M1-SEC-004 | not-started | Add authenticated Discord service ingress, allowlists, correlation and replay protection | #707 | codex | plugins/discord, apps/gateway | fix/tess-discord-ingress | TESS-PLAN-001 | 28K | TESS-SEC-005; security TDD |
|
||||||
|
| TESS-M1-SEC-005 | not-started | Redact/classify secret and PII before persistence/egress; harden provider login flow | #707 | codex | apps/gateway, packages/log | fix/tess-redaction | TESS-PLAN-001 | 28K | TESS-SEC-006; seeded canary tests |
|
||||||
|
| TESS-M1-SEC-006 | not-started | Scope session GC/retention or separate authorized global retention job | #707 | codex | apps/gateway, packages/log | fix/tess-session-gc-scope | TESS-PLAN-001 | 18K | TESS-SEC-009; isolation TDD |
|
||||||
|
| TESS-M1-001 | not-started | Define AgentRuntimeProvider, capabilities, session tree, normalized stream events/errors, attach semantics | #707 | codex | packages/types, packages/agent | feat/tess-runtime-contract | TESS-PLAN-001 | 25K | TESS-ARP-001, TESS-TRN-001; contract TDD |
|
||||||
|
| TESS-M1-002 | not-started | Implement provider registry/service with immutable actor scope, approval, audit and correlation boundaries | #707 | codex | apps/gateway, packages/agent | feat/tess-provider-registry | TESS-M1-001,TESS-M1-SEC-001,TESS-M1-SEC-002,TESS-M1-SEC-003 | 30K | TESS-SEC-001..004,007; security TDD |
|
||||||
|
| TESS-M1-003 | not-started | Implement tmux/fleet runtime provider and safe attach/message/terminate capability policy | #707 | codex | packages/mosaic, packages/agent | feat/tess-fleet-provider | TESS-M1-002 | 30K | TESS-FLT-001; exact target/identity tests |
|
||||||
|
| TESS-M1-OBS-001 | not-started | Implement correlation propagation, structured runtime/provider/tool audit, health/readiness and safe effective-policy status | #707 | codex | apps/gateway, packages/agent, packages/log | feat/tess-observability | TESS-M1-002 | 24K | TESS-OBS-001; no credential material |
|
||||||
|
| TESS-M1-V | not-started | Independent architecture/security review and complete contract/abuse-suite verification | #707 | sonnet | apps/gateway, packages/agent, packages/log, plugins/discord | review/tess-m1 | TESS-M1-SEC-001,TESS-M1-SEC-002,TESS-M1-SEC-003,TESS-M1-SEC-004,TESS-M1-SEC-005,TESS-M1-SEC-006,TESS-M1-003,TESS-M1-OBS-001 | 20K | Gate M2 |
|
||||||
|
| TESS-M2-001 | not-started | Add Tess roster/profile/service pinned to GPT-5.6 Sol high with fail-fast config and observable effective policy | #708 | codex | packages/mosaic/framework | feat/tess-pi-service | TESS-M1-V | 22K | TESS-PI-001; explicit AC-TESS-03 test |
|
||||||
|
| TESS-M2-002 | not-started | Implement durable session identity, inbox/outbox, approval, checkpoint, handoff, compaction and restart recovery | #708 | codex | apps/gateway, packages/agent, packages/db | feat/tess-durable-state | TESS-M2-001 | 38K | TESS-STA-001, TESS-SEC-007..008; recovery TDD |
|
||||||
|
| TESS-M2-V | not-started | Clean-host Pi launch plus model/policy status and restart/compaction/duplicate-side-effect verification | #708 | sonnet | apps/gateway/src/__tests__/integration, packages/mosaic/src | review/tess-m2 | TESS-M2-002 | 18K | Gate M3; AC-TESS-03/06 |
|
||||||
|
| TESS-M3-001 | not-started | Bind dedicated Tess Discord channel with streaming, threads, attachments, pairing/RBAC and approvals | #709 | codex | plugins/discord, apps/gateway | feat/tess-discord | TESS-M2-V,TESS-M1-SEC-004 | 35K | TESS-DSC-001 |
|
||||||
|
| TESS-M3-002 | not-started | Implement `mosaic tess` chat/status/sessions/tree/attach/send/stop/health/recover CLI | #709 | codex | packages/mosaic | feat/tess-cli | TESS-M2-V | 30K | TESS-CLI-001 |
|
||||||
|
| TESS-M3-V | not-started | Discord+CLI same-session E2E, denial/approval tests, and operator-flow review | #709 | sonnet | apps/gateway/src/__tests__/integration, plugins/discord, packages/mosaic/src | review/tess-m3 | TESS-M3-001,TESS-M3-002 | 20K | Gate M4 |
|
||||||
|
| TESS-M4-001 | not-started | Implement Mos coordination handoff/observe/result contract with authority-boundary tests | #710 | codex | packages/coord, apps/gateway | feat/tess-mos-coordination | TESS-M3-V | 25K | TESS-MOS-001 |
|
||||||
|
| TESS-M4-002 | not-started | Implement transitional Hermes runtime/capability adapter | #710 | codex | packages/agent, apps/gateway | feat/tess-hermes-adapter | TESS-M3-V | 40K | TESS-HRM-001; no legacy schema in core contracts |
|
||||||
|
| TESS-M4-003 | not-started | Implement memory/retrieval, state/inbox, runtime bootstrap, fleet diagnostics and GitOps plugin foundations | #710 | codex | packages/memory, packages/agent, packages/mosaic | feat/tess-operator-plugins | TESS-M3-V | 40K | TESS-MEM-001, TESS-PLG-001 |
|
||||||
|
| TESS-M4-V | not-started | Cross-provider capability, privacy, authority and failure-path qualification | #710 | sonnet | apps/gateway/src/__tests__/integration, packages/agent | review/tess-m4 | TESS-M4-001,TESS-M4-002,TESS-M4-003 | 22K | Gate M5 |
|
||||||
|
| TESS-M5-001 | not-started | Implement Matrix/native runtime provider behind common contracts and parity suite | #711 | codex | packages/mosaic, packages/agent | feat/tess-matrix-provider | TESS-M4-V | 30K | TESS-TRN-001 |
|
||||||
|
| TESS-M5-002 | not-started | Complete migration inventory, cutover, rollback, retention and deprecation evidence | #711 | sonnet | docs/tess | feat/tess-migration-docs | TESS-M4-V | 18K | TESS-MIG-001 |
|
||||||
|
| TESS-M5-003 | not-started | Complete OpenAPI, user/admin/developer/plugin/operations docs and checklist | #711 | codex | docs | feat/tess-docs | TESS-M5-001,TESS-M5-002 | 22K | Documentation hard gate |
|
||||||
|
| TESS-M5-V | not-started | Full baseline, contract, integration, Discord/CLI E2E, security review, recovery drill and rollback qualification | #711 | sonnet | apps/gateway, packages/agent, plugins/discord, packages/mosaic | review/tess-final | TESS-M5-003 | 35K | Maps AC-TESS-01..11 to evidence |
|
||||||
46
docs/tess/THREAT-MODEL.md
Normal file
46
docs/tess/THREAT-MODEL.md
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
# Tess Threat Model
|
||||||
|
|
||||||
|
## Assets and Trust Boundaries
|
||||||
|
|
||||||
|
Assets: operator identity, tenant/project data, agent sessions, fleet control, approvals, credentials, memories, tool outputs, audit evidence, and provider transports.
|
||||||
|
|
||||||
|
Trust boundaries: Discord→plugin, CLI→gateway, plugin→gateway service identity, gateway→Pi/provider, Tess→Mos/fleet, Tess→Hermes, MCP→gateway, persistence, and tmux/Matrix transports.
|
||||||
|
|
||||||
|
## Threat Matrix
|
||||||
|
|
||||||
|
| ID | Severity | Threat | Required control | Required verification |
|
||||||
|
| ----- | -------- | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------- |
|
||||||
|
| TM-01 | critical | Client invokes admin/system command without role | Server-side scope/role enforcement in executor; durable approval for privileged/destructive commands | Authenticated non-admin and forged-scope tests deny and audit |
|
||||||
|
| TM-02 | critical | Cross-user/tenant list, attach, send, or terminate by guessed session ID | Owner/tenant binding on every session operation; admin override is explicit and audited | Cross-tenant matrix for REST, WS, CLI, Discord and provider methods |
|
||||||
|
| TM-03 | high | MCP caller supplies another `userId` | Remove actor IDs from schemas; derive actor/tenant from authenticated context; per-tool scopes | Forged actor/tool calls deny; no victim data returned |
|
||||||
|
| TM-04 | high | Discord ingress impersonates user/channel or bypasses gateway auth | Service-to-service identity, guild/channel/user allowlists, signed/correlated envelope, replay protection | Invalid service identity, unlisted IDs, replayed message IDs all deny |
|
||||||
|
| TM-05 | high | Secrets/PII leak in chat, auth links, tool args, logs, memory, or DB | Redact before persistence/egress; DM/out-of-band auth flow; short-lived hashed token state; output classification | Seeded secret/PII canary absent from durable stores/logs/public channel |
|
||||||
|
| TM-06 | high | Prompt/tool injection escalates from content to privileged action | Treat messages/files/tool output as untrusted data; structured proposals only; allowlisted tools; approval binds exact action digest | Injection corpus cannot invoke unapproved tools or alter authority |
|
||||||
|
| TM-07 | high | Approval forged, replayed, or applied to modified action | One-time approval with actor, tenant, action digest, expiry, correlation and consumption record | Forged/replayed/expired/mutated approvals deny and audit |
|
||||||
|
| TM-08 | medium | Restart causes message loss or duplicate side effects | Durable inbox/outbox/checkpoint; idempotency keys; transactional state transitions; bounded replay | Kill/restart at each state transition; exactly-once effect or safe dedupe |
|
||||||
|
| TM-09 | medium | Session GC/retention crosses tenant/session scope | Session/user-scoped GC or separately authorized global retention job | GC one session; unrelated logs/memory remain unchanged |
|
||||||
|
| TM-10 | high | tmux/Matrix transport target or identity spoofing | Exact target/socket binding, peer identity verification, Matrix whoami, authenticated transport metadata | Wrong socket/peer/room/identity refuses delivery/attach |
|
||||||
|
| TM-11 | medium | Hermes adapter exposes unsupported or broader legacy powers | Capability negotiation, default deny, normalized scopes, adapter sandbox/timeouts | Unsupported and over-scoped operations fail closed |
|
||||||
|
| TM-12 | medium | Tess competes with Mos or bypasses orchestration gates | Authority policy and correlated Mos handoff; no Tess worker-claim capability by default | Coding/decomposition intent produces handoff, not direct claim |
|
||||||
|
|
||||||
|
## Security Invariants
|
||||||
|
|
||||||
|
1. Authentication is not authorization; every command/tool/provider operation is authorized server-side.
|
||||||
|
2. Actor, tenant, roles, and channel bindings come only from authenticated gateway context.
|
||||||
|
3. No client-provided session ID grants ownership or attachment.
|
||||||
|
4. No privileged action executes without a matching, unexpired, one-time approval when policy requires it.
|
||||||
|
5. Redaction occurs before persistence and before channel egress.
|
||||||
|
6. Every externally caused operation is replay-safe and correlated.
|
||||||
|
7. Provider capability absence is a denial, not an invitation to shell around it.
|
||||||
|
|
||||||
|
## Existing Findings That Block Tess
|
||||||
|
|
||||||
|
- 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.
|
||||||
|
|
||||||
|
These are tracked as M1 security prerequisites and must pass independent security review before Tess ingress is enabled.
|
||||||
30
docs/tess/VERIFICATION-MATRIX.md
Normal file
30
docs/tess/VERIFICATION-MATRIX.md
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# Tess Verification Matrix
|
||||||
|
|
||||||
|
| Acceptance criterion | Requirements | Planned evidence | Gate |
|
||||||
|
| -------------------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | ---------------- |
|
||||||
|
| AC-TESS-01 | TESS-PI-001, TESS-DSC-001, TESS-CLI-001 | Discord/CLI same-session integration and streaming E2E | M3-V |
|
||||||
|
| AC-TESS-02 | TESS-ARP-001, TESS-CLI-001, TESS-FLT-001 | CLI contract tests for status/sessions/tree/attach/send/stop, typed denial/error snapshots | M3-V |
|
||||||
|
| AC-TESS-03 | TESS-PI-001, TESS-OBS-001 | Clean service launch; status asserts GPT-5.6 Sol, high reasoning and effective tool policy with secret canaries absent | M2-V, M3-V |
|
||||||
|
| AC-TESS-04 | TESS-MOS-001, TESS-FLT-001 | Authority E2E: coding request creates Mos handoff; safe status runs in Tess; no competing worker claim | M4-V |
|
||||||
|
| AC-TESS-05 | TESS-HRM-001 | Hermes capability contract suite: sessions/stream/send/tree plus Kanban/skills/memory/tools/cron supported-or-denied matrix | M4-V |
|
||||||
|
| AC-TESS-06 | TESS-STA-001, TESS-SEC-008 | Kill/restart/compaction fault injection across inbox/outbox/checkpoint transitions; duplicate side-effect detector | M2-V, M5-V |
|
||||||
|
| AC-TESS-07 | TESS-SEC-001..009 | Threat-model abuse suite: authz, tenant isolation, forged identity/approval, injection, redaction, transport identity, GC scope | M1-V, M3-V, M5-V |
|
||||||
|
| AC-TESS-08 | TESS-TRN-001 | Common provider contract suite against tmux/fleet and Matrix/native; identity and replay tests | M5-V |
|
||||||
|
| AC-TESS-09 | all | `pnpm typecheck`, lint, format, unit/integration/contract/E2E; independent code and security reviews; CI URLs | Every milestone |
|
||||||
|
| AC-TESS-10 | TESS-MIG-001 | Completed capability inventory with native/adapted/deferred/rejected state, owner, cutover/rollback evidence | M5-V |
|
||||||
|
| AC-TESS-11 | TESS-PLG-001, TESS-OBS-001 | OpenAPI and user/admin/developer/plugin/ops docs, sitemap links, documentation checklist | M5-V |
|
||||||
|
|
||||||
|
## Security Abuse Suite Minimum
|
||||||
|
|
||||||
|
- Role/scope matrix for every command and provider capability.
|
||||||
|
- Cross-tenant and cross-user session ID matrix across REST, WS, Discord, CLI, MCP, and providers.
|
||||||
|
- Discord service identity, guild/channel/user allowlist, replay, attachment, and mention/DM policy cases.
|
||||||
|
- Prompt/tool injection corpus and structured-proposal enforcement.
|
||||||
|
- Approval action-digest mutation, replay, expiry, tenant, and actor mismatch cases.
|
||||||
|
- Secret/PII canaries through message, attachment, tool args/output, logs, memory, audit, and error paths.
|
||||||
|
- Restart fault injection before/after enqueue, provider send, side effect, response persistence, and acknowledgement.
|
||||||
|
- Wrong tmux socket/target and Matrix identity/room/replay cases.
|
||||||
|
|
||||||
|
## Evidence Rules
|
||||||
|
|
||||||
|
Evidence must include command/test name, terminal result, CI run URL, PR/merge reference, environment, and artifact/log location. A worker self-report is not evidence until independently verified.
|
||||||
@@ -15,6 +15,22 @@ This guide covers how to bootstrap a project so AI agents (Claude, Codex, etc.)
|
|||||||
7. Branching/merging is consistent: `branch -> main` via PR with squash-only merges
|
7. Branching/merging is consistent: `branch -> main` via PR with squash-only merges
|
||||||
8. Steered-autonomy execution is enabled so agents can run end-to-end with escalation-only human intervention
|
8. Steered-autonomy execution is enabled so agents can run end-to-end with escalation-only human intervention
|
||||||
|
|
||||||
|
## Agent Host Prerequisites
|
||||||
|
|
||||||
|
Agent hosts must provide the Python runtime shape that runtime agents and
|
||||||
|
Mosaic automation assume is present.
|
||||||
|
|
||||||
|
For Debian/Ubuntu hosts:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt-get update
|
||||||
|
# #561: bare python invocations from agents must resolve.
|
||||||
|
sudo apt-get install -y python3 python-is-python3
|
||||||
|
```
|
||||||
|
|
||||||
|
For non-Debian hosts, install the equivalent Python 3 runtime and ensure
|
||||||
|
`/usr/bin/python` resolves to `python3` (for example, via a managed symlink).
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -15,6 +15,22 @@ This guide covers how to bootstrap a project so AI agents (Claude, Codex, etc.)
|
|||||||
7. Branching/merging is consistent: `branch -> main` via PR with squash-only merges
|
7. Branching/merging is consistent: `branch -> main` via PR with squash-only merges
|
||||||
8. Steered-autonomy execution is enabled so agents can run end-to-end with escalation-only human intervention
|
8. Steered-autonomy execution is enabled so agents can run end-to-end with escalation-only human intervention
|
||||||
|
|
||||||
|
## Agent Host Prerequisites
|
||||||
|
|
||||||
|
Agent hosts must provide the Python runtime shape that runtime agents and
|
||||||
|
Mosaic automation assume is present.
|
||||||
|
|
||||||
|
For Debian/Ubuntu hosts:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt-get update
|
||||||
|
# #561: bare python invocations from agents must resolve.
|
||||||
|
sudo apt-get install -y python3 python-is-python3
|
||||||
|
```
|
||||||
|
|
||||||
|
For non-Debian hosts, install the equivalent Python 3 runtime and ensure
|
||||||
|
`/usr/bin/python` resolves to `python3` (for example, via a managed symlink).
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -15,13 +15,23 @@
|
|||||||
#
|
#
|
||||||
# After loading, service-specific env vars are exported.
|
# After loading, service-specific env vars are exported.
|
||||||
# Run `load_credentials --help` for details.
|
# Run `load_credentials --help` for details.
|
||||||
|
#
|
||||||
|
# Resolution order (first match wins):
|
||||||
|
# 1. $MOSAIC_CREDENTIALS_FILE (explicit override — never second-guessed)
|
||||||
|
# 2. $HOME/.config/mosaic/credentials.json
|
||||||
|
# 3. /etc/mosaic/credentials.json (host-level fallback)
|
||||||
|
# The /etc fallback exists for HOME-redirected profile environments, where
|
||||||
|
# $HOME points at a per-profile directory that has no credentials file.
|
||||||
|
# Operators symlink /etc/mosaic/credentials.json to the host's canonical
|
||||||
|
# file once, instead of exporting MOSAIC_CREDENTIALS_FILE per invocation.
|
||||||
|
|
||||||
if [[ -z "${MOSAIC_CREDENTIALS_FILE:-}" ]]; then
|
if [[ -z "${MOSAIC_CREDENTIALS_FILE:-}" ]]; then
|
||||||
for _cand in "$HOME/.config/mosaic/credentials.json"; do
|
for _cand in "$HOME/.config/mosaic/credentials.json" "/etc/mosaic/credentials.json"; do
|
||||||
if [[ -f "$_cand" ]]; then MOSAIC_CREDENTIALS_FILE="$_cand"; break; fi
|
if [[ -f "$_cand" ]]; then MOSAIC_CREDENTIALS_FILE="$_cand"; break; fi
|
||||||
done
|
done
|
||||||
: "${MOSAIC_CREDENTIALS_FILE:=$HOME/.config/mosaic/credentials.json}"
|
: "${MOSAIC_CREDENTIALS_FILE:=$HOME/.config/mosaic/credentials.json}"
|
||||||
fi
|
fi
|
||||||
|
export MOSAIC_CREDENTIALS_FILE
|
||||||
|
|
||||||
_mosaic_require_jq() {
|
_mosaic_require_jq() {
|
||||||
if ! command -v jq &>/dev/null; then
|
if ! command -v jq &>/dev/null; then
|
||||||
|
|||||||
@@ -86,7 +86,16 @@ gitea_url_matches_host() {
|
|||||||
|
|
||||||
get_gitea_service_for_host() {
|
get_gitea_service_for_host() {
|
||||||
local host="$1"
|
local host="$1"
|
||||||
local cred_file="${MOSAIC_CREDENTIALS_FILE:-$HOME/.config/mosaic/credentials.json}"
|
local cred_file="${MOSAIC_CREDENTIALS_FILE:-}"
|
||||||
|
if [[ -z "$cred_file" ]]; then
|
||||||
|
# Same resolution chain as _lib/credentials.sh: profile HOME, then
|
||||||
|
# host-level /etc only if it exists; neither existing keeps the
|
||||||
|
# $HOME default (matches the lib's final := fallback).
|
||||||
|
cred_file="$HOME/.config/mosaic/credentials.json"
|
||||||
|
if [[ ! -f "$cred_file" && -f /etc/mosaic/credentials.json ]]; then
|
||||||
|
cred_file="/etc/mosaic/credentials.json"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
case "$host" in
|
case "$host" in
|
||||||
git.mosaicstack.dev)
|
git.mosaicstack.dev)
|
||||||
@@ -231,6 +240,33 @@ get_gitea_login_for_host() {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Validate the current authenticated Gitea user for a resolved Tea login.
|
||||||
|
# Tea stores a user name with each login which can become stale after user rename,
|
||||||
|
# token rotation, or server migration. Querying /user derives the identity from the
|
||||||
|
# active credential instead of trusting that saved name. Callers fall back to the
|
||||||
|
# host-scoped API path when this validation fails.
|
||||||
|
get_gitea_authenticated_user() {
|
||||||
|
local login_name="$1" response
|
||||||
|
|
||||||
|
command -v tea >/dev/null 2>&1 || return 1
|
||||||
|
response=$(tea api --login "$login_name" /user 2>/dev/null) || return 1
|
||||||
|
TEA_AUTHENTICATED_USER_JSON="$response" python3 - <<'PY'
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
try:
|
||||||
|
user = json.loads(os.environ["TEA_AUTHENTICATED_USER_JSON"])
|
||||||
|
except (KeyError, json.JSONDecodeError):
|
||||||
|
raise SystemExit(1)
|
||||||
|
|
||||||
|
login = user.get("login") if isinstance(user, dict) else None
|
||||||
|
if isinstance(login, str) and login:
|
||||||
|
print(login)
|
||||||
|
raise SystemExit(0)
|
||||||
|
raise SystemExit(1)
|
||||||
|
PY
|
||||||
|
}
|
||||||
|
|
||||||
get_default_tea_login() {
|
get_default_tea_login() {
|
||||||
local logins_json
|
local logins_json
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ Examples:
|
|||||||
$(basename "$0") -i 42 -l "in-progress" -m "0.2.0"
|
$(basename "$0") -i 42 -l "in-progress" -m "0.2.0"
|
||||||
$(basename "$0") -i 42 -a @me
|
$(basename "$0") -i 42 -a @me
|
||||||
EOF
|
EOF
|
||||||
exit 1
|
exit "${1:-1}"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Parse arguments
|
# Parse arguments
|
||||||
@@ -60,7 +60,7 @@ while [[ $# -gt 0 ]]; do
|
|||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
-h|--help)
|
-h|--help)
|
||||||
usage
|
usage 0
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
echo "Unknown option: $1" >&2
|
echo "Unknown option: $1" >&2
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ TITLE=""
|
|||||||
BODY=""
|
BODY=""
|
||||||
LABELS=""
|
LABELS=""
|
||||||
MILESTONE=""
|
MILESTONE=""
|
||||||
|
INTERACTIVE=false
|
||||||
|
|
||||||
# get_remote_host and get_gitea_token are provided by detect-platform.sh
|
# get_remote_host and get_gitea_token are provided by detect-platform.sh
|
||||||
|
|
||||||
@@ -66,13 +67,15 @@ Options:
|
|||||||
-b, --body BODY Issue body/description
|
-b, --body BODY Issue body/description
|
||||||
-l, --labels LABELS Comma-separated labels (e.g., "bug,feature")
|
-l, --labels LABELS Comma-separated labels (e.g., "bug,feature")
|
||||||
-m, --milestone NAME Milestone name to assign
|
-m, --milestone NAME Milestone name to assign
|
||||||
|
-i, --interactive Prompt for missing issue fields
|
||||||
-h, --help Show this help message
|
-h, --help Show this help message
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
$(basename "$0") -t "Fix login bug" -l "bug,priority-high"
|
$(basename "$0") -t "Fix login bug" -l "bug,priority-high"
|
||||||
$(basename "$0") -t "Add dark mode" -b "Implement theme switching" -m "0.2.0"
|
$(basename "$0") -t "Add dark mode" -b "Implement theme switching" -m "0.2.0"
|
||||||
|
$(basename "$0") -i
|
||||||
EOF
|
EOF
|
||||||
exit 1
|
exit "${1:-1}"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Parse arguments
|
# Parse arguments
|
||||||
@@ -94,8 +97,12 @@ while [[ $# -gt 0 ]]; do
|
|||||||
MILESTONE="$2"
|
MILESTONE="$2"
|
||||||
shift 2
|
shift 2
|
||||||
;;
|
;;
|
||||||
|
-i|--interactive)
|
||||||
|
INTERACTIVE=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
-h|--help)
|
-h|--help)
|
||||||
usage
|
usage 0
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
echo "Unknown option: $1" >&2
|
echo "Unknown option: $1" >&2
|
||||||
@@ -104,6 +111,13 @@ while [[ $# -gt 0 ]]; do
|
|||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
|
if [[ "$INTERACTIVE" == true ]]; then
|
||||||
|
[[ -n "$TITLE" ]] || read -r -p "Issue title: " TITLE
|
||||||
|
[[ -n "$BODY" ]] || read -r -p "Issue body (optional): " BODY || true
|
||||||
|
[[ -n "$LABELS" ]] || read -r -p "Labels, comma-separated (optional): " LABELS || true
|
||||||
|
[[ -n "$MILESTONE" ]] || read -r -p "Milestone (optional): " MILESTONE || true
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ -z "$TITLE" ]]; then
|
if [[ -z "$TITLE" ]]; then
|
||||||
echo "Error: Title is required (-t)" >&2
|
echo "Error: Title is required (-t)" >&2
|
||||||
usage
|
usage
|
||||||
@@ -127,6 +141,11 @@ case "$PLATFORM" in
|
|||||||
gitea_issue_create_api
|
gitea_issue_create_api
|
||||||
exit $?
|
exit $?
|
||||||
}
|
}
|
||||||
|
if ! get_gitea_authenticated_user "$GITEA_LOGIN_NAME" >/dev/null; then
|
||||||
|
echo "Warning: Tea authenticated-user validation failed (possible stale user/login); trying Gitea API fallback..." >&2
|
||||||
|
gitea_issue_create_api
|
||||||
|
exit $?
|
||||||
|
fi
|
||||||
REPO_ARGS=(--repo "$REPO_SLUG" --login "$GITEA_LOGIN_NAME")
|
REPO_ARGS=(--repo "$REPO_SLUG" --login "$GITEA_LOGIN_NAME")
|
||||||
CMD=(tea issue create "${REPO_ARGS[@]}" --title "$TITLE")
|
CMD=(tea issue create "${REPO_ARGS[@]}" --title "$TITLE")
|
||||||
[[ -n "$BODY" ]] && CMD+=(--description "$BODY")
|
[[ -n "$BODY" ]] && CMD+=(--description "$BODY")
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ Examples:
|
|||||||
$(basename "$0") -m "0.2.0" # Issues in milestone 0.2.0
|
$(basename "$0") -m "0.2.0" # Issues in milestone 0.2.0
|
||||||
$(basename "$0") --repo ddk/ai-bma # List issues from anywhere
|
$(basename "$0") --repo ddk/ai-bma # List issues from anywhere
|
||||||
EOF
|
EOF
|
||||||
exit 1
|
exit "${1:-1}"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Parse arguments
|
# Parse arguments
|
||||||
@@ -67,7 +67,7 @@ while [[ $# -gt 0 ]]; do
|
|||||||
shift 2
|
shift 2
|
||||||
;;
|
;;
|
||||||
-h|--help)
|
-h|--help)
|
||||||
usage
|
usage 0
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
echo "Unknown option: $1" >&2
|
echo "Unknown option: $1" >&2
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ Examples:
|
|||||||
$(basename "$0") -t "0.0.1" -d "Pre-MVP Foundation Sprint"
|
$(basename "$0") -t "0.0.1" -d "Pre-MVP Foundation Sprint"
|
||||||
$(basename "$0") -t "0.1.0" -d "MVP Release" --due "2025-03-01"
|
$(basename "$0") -t "0.1.0" -d "MVP Release" --due "2025-03-01"
|
||||||
EOF
|
EOF
|
||||||
exit 1
|
exit "${1:-1}"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Parse arguments
|
# Parse arguments
|
||||||
@@ -60,7 +60,7 @@ while [[ $# -gt 0 ]]; do
|
|||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
-h|--help)
|
-h|--help)
|
||||||
usage
|
usage 0
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
echo "Unknown option: $1" >&2
|
echo "Unknown option: $1" >&2
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ Examples:
|
|||||||
$(basename "$0") -i 42 -b "Implements the feature described in #42"
|
$(basename "$0") -i 42 -b "Implements the feature described in #42"
|
||||||
$(basename "$0") -t "WIP: New feature" --draft
|
$(basename "$0") -t "WIP: New feature" --draft
|
||||||
EOF
|
EOF
|
||||||
exit 1
|
exit "${1:-1}"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Parse arguments
|
# Parse arguments
|
||||||
@@ -125,7 +125,7 @@ while [[ $# -gt 0 ]]; do
|
|||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
-h|--help)
|
-h|--help)
|
||||||
usage
|
usage 0
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
echo "Unknown option: $1" >&2
|
echo "Unknown option: $1" >&2
|
||||||
@@ -183,6 +183,11 @@ case "$PLATFORM" in
|
|||||||
gitea_pr_create_api
|
gitea_pr_create_api
|
||||||
exit $?
|
exit $?
|
||||||
}
|
}
|
||||||
|
if ! get_gitea_authenticated_user "$GITEA_LOGIN_NAME" >/dev/null; then
|
||||||
|
echo "Warning: Tea authenticated-user validation failed (possible stale user/login); trying Gitea API fallback..." >&2
|
||||||
|
gitea_pr_create_api
|
||||||
|
exit $?
|
||||||
|
fi
|
||||||
REPO_ARGS=(--repo "$REPO_SLUG" --login "$GITEA_LOGIN_NAME")
|
REPO_ARGS=(--repo "$REPO_SLUG" --login "$GITEA_LOGIN_NAME")
|
||||||
CMD=(tea pr create "${REPO_ARGS[@]}" --title "$TITLE")
|
CMD=(tea pr create "${REPO_ARGS[@]}" --title "$TITLE")
|
||||||
[[ -n "$BODY" ]] && CMD+=(--description "$BODY")
|
[[ -n "$BODY" ]] && CMD+=(--description "$BODY")
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ Examples:
|
|||||||
$(basename "$0") -s merged -a username # Merged PRs by user
|
$(basename "$0") -s merged -a username # Merged PRs by user
|
||||||
$(basename "$0") --repo ddk/ai-bma # List PRs from anywhere
|
$(basename "$0") --repo ddk/ai-bma # List PRs from anywhere
|
||||||
EOF
|
EOF
|
||||||
exit 1
|
exit "${1:-1}"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Parse arguments
|
# Parse arguments
|
||||||
@@ -61,7 +61,7 @@ while [[ $# -gt 0 ]]; do
|
|||||||
shift 2
|
shift 2
|
||||||
;;
|
;;
|
||||||
-h|--help)
|
-h|--help)
|
||||||
usage
|
usage 0
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
echo "Unknown option: $1" >&2
|
echo "Unknown option: $1" >&2
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ Examples:
|
|||||||
$(basename "$0") -n 42 -d # Squash merge and delete branch
|
$(basename "$0") -n 42 -d # Squash merge and delete branch
|
||||||
$(basename "$0") -n 42 --skip-queue-guard # Skip queue guard wait
|
$(basename "$0") -n 42 --skip-queue-guard # Skip queue guard wait
|
||||||
EOF
|
EOF
|
||||||
exit 1
|
exit "${1:-1}"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Parse arguments
|
# Parse arguments
|
||||||
@@ -63,7 +63,7 @@ while [[ $# -gt 0 ]]; do
|
|||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
-h|--help)
|
-h|--help)
|
||||||
usage
|
usage 0
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
echo "Unknown option: $1" >&2
|
echo "Unknown option: $1" >&2
|
||||||
|
|||||||
@@ -45,6 +45,11 @@ JSON
|
|||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ "${1:-}" == "api" ]]; then
|
||||||
|
printf '%s\n' '{"login":"ci-bot"}'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
printf 'tea %s\n' "$*" >> "$MOSAIC_TEST_LOG"
|
printf 'tea %s\n' "$*" >> "$MOSAIC_TEST_LOG"
|
||||||
if [[ "${MOSAIC_TEA_FAIL_PR_CREATE:-}" == "1" && "$*" == pr\ create* ]]; then
|
if [[ "${MOSAIC_TEA_FAIL_PR_CREATE:-}" == "1" && "$*" == pr\ create* ]]; then
|
||||||
echo 'GetUserByName: simulated stale login failure' >&2
|
echo 'GetUserByName: simulated stale login failure' >&2
|
||||||
|
|||||||
53
packages/mosaic/framework/tools/git/test-help-exit-code.sh
Executable file
53
packages/mosaic/framework/tools/git/test-help-exit-code.sh
Executable file
@@ -0,0 +1,53 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Regression harness for #701: -h/--help must exit 0, bad args must still exit nonzero.
|
||||||
|
#
|
||||||
|
# Covers the 7 wrappers whose usage() previously hard-coded `exit 1`, so every
|
||||||
|
# --help invocation exited nonzero and logged a phantom isError across fleet lanes.
|
||||||
|
# Asserts, per wrapper:
|
||||||
|
# 1. `--help` exits 0 and prints usage.
|
||||||
|
# 2. `-h` exits 0 and prints usage.
|
||||||
|
# 3. A genuine unknown flag still exits nonzero (usage() default path untouched).
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
|
WRAPPERS=(
|
||||||
|
issue-assign.sh
|
||||||
|
issue-create.sh
|
||||||
|
issue-list.sh
|
||||||
|
milestone-create.sh
|
||||||
|
pr-create.sh
|
||||||
|
pr-list.sh
|
||||||
|
pr-merge.sh
|
||||||
|
)
|
||||||
|
|
||||||
|
fail=0
|
||||||
|
|
||||||
|
for wrapper in "${WRAPPERS[@]}"; do
|
||||||
|
path="$SCRIPT_DIR/$wrapper"
|
||||||
|
|
||||||
|
if ! output=$(bash "$path" --help 2>&1); then
|
||||||
|
echo "FAIL: $wrapper --help exited nonzero" >&2
|
||||||
|
fail=1
|
||||||
|
elif [[ "$output" != Usage:* ]]; then
|
||||||
|
echo "FAIL: $wrapper --help did not print usage" >&2
|
||||||
|
fail=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! bash "$path" -h >/dev/null 2>&1; then
|
||||||
|
echo "FAIL: $wrapper -h exited nonzero" >&2
|
||||||
|
fail=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if bash "$path" --this-is-not-a-real-flag >/dev/null 2>&1; then
|
||||||
|
echo "FAIL: $wrapper accepted an unknown flag (should have exited nonzero)" >&2
|
||||||
|
fail=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ "$fail" -eq 0 ]]; then
|
||||||
|
echo "help-exit-code regression passed (7/7 wrappers)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit "$fail"
|
||||||
@@ -55,6 +55,11 @@ JSON
|
|||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ "${1:-}" == "api" ]]; then
|
||||||
|
printf '%s\n' '{"login":"ci-bot"}'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ "${1:-}" == "issue" && "${2:-}" == "create" ]]; then
|
if [[ "${1:-}" == "issue" && "${2:-}" == "create" ]]; then
|
||||||
desc=""
|
desc=""
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
|
|||||||
88
packages/mosaic/framework/tools/git/test-issue-create-interactive-auth.sh
Executable file
88
packages/mosaic/framework/tools/git/test-issue-create-interactive-auth.sh
Executable file
@@ -0,0 +1,88 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Regression harness for #703: interactive issue creation and stale Tea-user fallback.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
WORK_DIR="${MOSAIC_TEST_WORK_DIR:-$PWD/.mosaic-test-work/issue-create-interactive-auth}"
|
||||||
|
REPO_DIR="$WORK_DIR/repo"
|
||||||
|
BIN_DIR="$WORK_DIR/bin"
|
||||||
|
LOG_FILE="$WORK_DIR/calls.log"
|
||||||
|
CREDENTIALS_FILE="$WORK_DIR/credentials.json"
|
||||||
|
|
||||||
|
rm -rf "$WORK_DIR"
|
||||||
|
mkdir -p "$REPO_DIR" "$BIN_DIR"
|
||||||
|
git -C "$REPO_DIR" init -q
|
||||||
|
git -C "$REPO_DIR" remote add origin https://git.mosaicstack.dev/mosaicstack/stack.git
|
||||||
|
|
||||||
|
cat > "$CREDENTIALS_FILE" <<'JSON'
|
||||||
|
{"gitea":{"mosaicstack":{"url":"https://git.mosaicstack.dev","token":"test-token"}}}
|
||||||
|
JSON
|
||||||
|
|
||||||
|
cat > "$BIN_DIR/tea" <<'SH'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
if [[ "$*" == "login list --output json" ]]; then
|
||||||
|
printf '%s\n' '[{"name":"mosaicstack","url":"https://git.mosaicstack.dev"}]'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
if [[ "${1:-}" == "api" ]]; then
|
||||||
|
if [[ "${MOSAIC_TEA_STALE_USER:-0}" == "1" ]]; then
|
||||||
|
echo 'GetUserByName: stale configured user' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
printf '%s\n' '{"login":"current-user"}'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
printf 'tea %s\n' "$*" >> "$MOSAIC_TEST_LOG"
|
||||||
|
exit 0
|
||||||
|
SH
|
||||||
|
|
||||||
|
cat > "$BIN_DIR/curl" <<'SH'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
printf 'curl %s\n' "$*" >> "$MOSAIC_TEST_LOG"
|
||||||
|
printf '%s\n' '{"number":703}'
|
||||||
|
SH
|
||||||
|
chmod +x "$BIN_DIR/tea" "$BIN_DIR/curl"
|
||||||
|
|
||||||
|
run_wrapper() {
|
||||||
|
(
|
||||||
|
cd "$REPO_DIR"
|
||||||
|
PATH="$BIN_DIR:$PATH" \
|
||||||
|
MOSAIC_CREDENTIALS_FILE="$CREDENTIALS_FILE" \
|
||||||
|
MOSAIC_TEST_LOG="$LOG_FILE" \
|
||||||
|
"$@"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
: > "$LOG_FILE"
|
||||||
|
printf 'Interactive title\nInteractive body\nlabel-a,label-b\nM1\n' | run_wrapper "$SCRIPT_DIR/issue-create.sh" -i >/dev/null
|
||||||
|
|
||||||
|
grep -q -- 'tea issue create --repo mosaicstack/stack --login mosaicstack --title Interactive title --description Interactive body --labels label-a,label-b --milestone M1' "$LOG_FILE"
|
||||||
|
|
||||||
|
# Explicit values take precedence in interactive mode: no title input is
|
||||||
|
# supplied, but the wrapper still creates the issue with the explicit title.
|
||||||
|
: > "$LOG_FILE"
|
||||||
|
printf '\n\n\n' | run_wrapper "$SCRIPT_DIR/issue-create.sh" -i -t 'Explicit title' >/dev/null
|
||||||
|
grep -q -- 'tea issue create --repo mosaicstack/stack --login mosaicstack --title Explicit title' "$LOG_FILE"
|
||||||
|
|
||||||
|
: > "$LOG_FILE"
|
||||||
|
run_wrapper env MOSAIC_TEA_STALE_USER=1 "$SCRIPT_DIR/issue-create.sh" -t 'Fallback title' -b 'Fallback body' >/dev/null 2>"$WORK_DIR/issue-stderr"
|
||||||
|
grep -q -- 'curl .*https://git.mosaicstack.dev/api/v1/repos/mosaicstack/stack/issues' "$LOG_FILE"
|
||||||
|
grep -q -- 'Tea authenticated-user validation failed' "$WORK_DIR/issue-stderr"
|
||||||
|
if grep -q -- 'tea issue create' "$LOG_FILE"; then
|
||||||
|
echo 'FAIL: issue-create invoked Tea mutation after stale-user validation failed' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
: > "$LOG_FILE"
|
||||||
|
run_wrapper env MOSAIC_TEA_STALE_USER=1 "$SCRIPT_DIR/pr-create.sh" -t 'PR fallback' -H feature/wrapfix >/dev/null 2>"$WORK_DIR/pr-stderr"
|
||||||
|
grep -q -- 'curl .*https://git.mosaicstack.dev/api/v1/repos/mosaicstack/stack/pulls' "$LOG_FILE"
|
||||||
|
grep -q -- 'Tea authenticated-user validation failed' "$WORK_DIR/pr-stderr"
|
||||||
|
if grep -q -- 'tea pr create' "$LOG_FILE"; then
|
||||||
|
echo 'FAIL: pr-create invoked Tea mutation after stale-user validation failed' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo 'issue-create interactive/auth regression harness passed'
|
||||||
@@ -87,6 +87,10 @@ message crosses the wire as base64 (`-b`) to avoid all shell-quoting hazards.
|
|||||||
|
|
||||||
- `agent-send.sh` — inter-agent wrapper (preamble + local/remote dispatch).
|
- `agent-send.sh` — inter-agent wrapper (preamble + local/remote dispatch).
|
||||||
- `send-message.sh` — low-level reliable single-pane submitter (`-b` base64 input).
|
- `send-message.sh` — low-level reliable single-pane submitter (`-b` base64 input).
|
||||||
|
- `auto-submit-drafts.sh` — watchdog that flushes stable unsubmitted prompt
|
||||||
|
drafts on a coordinator pane (default target `mos-claude`); run it as a
|
||||||
|
long-lived process alongside the coordinator session.
|
||||||
|
- `agent-send.test.sh` — regression + grammar lock for `agent-send.sh`.
|
||||||
- `test-send-message-socket.sh` — smoke test for named-socket isolation.
|
- `test-send-message-socket.sh` — smoke test for named-socket isolation.
|
||||||
|
|
||||||
## Distribution
|
## Distribution
|
||||||
|
|||||||
80
packages/mosaic/framework/tools/tmux/auto-submit-drafts.sh
Executable file
80
packages/mosaic/framework/tools/tmux/auto-submit-drafts.sh
Executable file
@@ -0,0 +1,80 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# auto-submit-drafts.sh — watchdog for Claude Code panes that receive channel
|
||||||
|
# messages but leave them as unsubmitted prompt drafts. Intended for Mos only.
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
TARGET="${1:-mos-claude}"
|
||||||
|
INTERVAL="${INTERVAL:-2}"
|
||||||
|
STABLE_SECONDS="${STABLE_SECONDS:-4}"
|
||||||
|
LOG_PREFIX="[auto-submit-drafts:$TARGET]"
|
||||||
|
|
||||||
|
last_prompt=""
|
||||||
|
first_seen=0
|
||||||
|
|
||||||
|
prompt_text() {
|
||||||
|
tmux capture-pane -t "$TARGET" -p 2>/dev/null | python3 -c '
|
||||||
|
import sys, re
|
||||||
|
lines = sys.stdin.read().splitlines()
|
||||||
|
idx = None
|
||||||
|
for i in range(len(lines)-1, -1, -1):
|
||||||
|
if "❯" in lines[i]:
|
||||||
|
idx = i
|
||||||
|
break
|
||||||
|
if idx is None:
|
||||||
|
raise SystemExit
|
||||||
|
parts = []
|
||||||
|
after = lines[idx].split("❯", 1)[1]
|
||||||
|
parts.append(after)
|
||||||
|
for line in lines[idx+1:]:
|
||||||
|
# Stop at Claude Code separator/border lines.
|
||||||
|
if "─" in line or "╰" in line or "╭" in line:
|
||||||
|
break
|
||||||
|
s = line.replace("\u00a0", " ")
|
||||||
|
s = re.sub(r"[\x00-\x1f\x7f]", "", s).strip()
|
||||||
|
if s:
|
||||||
|
parts.append(s)
|
||||||
|
text = " ".join(parts).replace("\u00a0", " ")
|
||||||
|
text = re.sub(r"[\x00-\x1f\x7f]", "", text).strip()
|
||||||
|
print(text)
|
||||||
|
'
|
||||||
|
}
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
if ! tmux has-session -t "$TARGET" 2>/dev/null; then
|
||||||
|
echo "$LOG_PREFIX target missing; waiting" >&2
|
||||||
|
sleep "$INTERVAL"
|
||||||
|
last_prompt=""
|
||||||
|
first_seen=0
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
current="$(prompt_text || true)"
|
||||||
|
now="$(date +%s)"
|
||||||
|
|
||||||
|
if [[ -z "$current" ]]; then
|
||||||
|
last_prompt=""
|
||||||
|
first_seen=0
|
||||||
|
sleep "$INTERVAL"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$current" != "$last_prompt" ]]; then
|
||||||
|
last_prompt="$current"
|
||||||
|
first_seen="$now"
|
||||||
|
sleep "$INTERVAL"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
age=$(( now - first_seen ))
|
||||||
|
if (( age >= STABLE_SECONDS )); then
|
||||||
|
echo "$LOG_PREFIX submitting stable draft after ${age}s: ${current:0:120}" >&2
|
||||||
|
tmux send-keys -t "$TARGET" C-j
|
||||||
|
sleep 0.8
|
||||||
|
tmux send-keys -t "$TARGET" C-m
|
||||||
|
sleep 2
|
||||||
|
last_prompt=""
|
||||||
|
first_seen=0
|
||||||
|
else
|
||||||
|
sleep "$INTERVAL"
|
||||||
|
fi
|
||||||
|
done
|
||||||
@@ -77,10 +77,20 @@ snippet=$(printf '%s' "$MSG" | tr '\n' ' ' | tr -s ' ' | sed 's/[^[:print:]]//g'
|
|||||||
|
|
||||||
# 1) Paste the body as a bracketed paste so multi-line content does not submit
|
# 1) Paste the body as a bracketed paste so multi-line content does not submit
|
||||||
# line-by-line. load-buffer/paste-buffer is far safer than `send-keys -l`.
|
# line-by-line. load-buffer/paste-buffer is far safer than `send-keys -l`.
|
||||||
printf '%s' "$MSG" | "${tmux_cmd[@]}" load-buffer -b __mosaic_send -
|
# Buffer name MUST be unique per invocation: concurrent senders on the shared
|
||||||
|
# tmux server race a fixed name (load overwrites load, -d deletes underneath),
|
||||||
|
# cross-delivering or dropping messages — bit the fleet on the 2026-07-09
|
||||||
|
# simultaneous restart (briefs swapped between sessions).
|
||||||
|
BUF="__mosaic_send_$$_$(date +%s%N)"
|
||||||
|
printf '%s' "$MSG" | "${tmux_cmd[@]}" load-buffer -b "$BUF" -
|
||||||
# -p = bracketed paste when the client supports it; fall back if not.
|
# -p = bracketed paste when the client supports it; fall back if not.
|
||||||
"${tmux_cmd[@]}" paste-buffer -d -p -b __mosaic_send -t "$EFFECTIVE_TARGET" 2>/dev/null \
|
"${tmux_cmd[@]}" paste-buffer -d -p -b "$BUF" -t "$EFFECTIVE_TARGET" 2>/dev/null \
|
||||||
|| "${tmux_cmd[@]}" paste-buffer -d -b __mosaic_send -t "$EFFECTIVE_TARGET"
|
|| "${tmux_cmd[@]}" paste-buffer -d -b "$BUF" -t "$EFFECTIVE_TARGET" \
|
||||||
|
|| "${tmux_cmd[@]}" delete-buffer -b "$BUF" 2>/dev/null
|
||||||
|
# ^ -d deletes the buffer only on a SUCCESSFUL paste; if both attempts fail
|
||||||
|
# (e.g. the target vanished since the liveness check), delete explicitly —
|
||||||
|
# named buffers are exempt from tmux's buffer-limit eviction, so orphans
|
||||||
|
# would otherwise accumulate forever.
|
||||||
sleep 0.5
|
sleep 0.5
|
||||||
|
|
||||||
# 2) Submit, then verify; flush with another Enter if it is still a draft.
|
# 2) Submit, then verify; flush with another Enter if it is still a draft.
|
||||||
|
|||||||
@@ -47,4 +47,32 @@ if capture_default | grep -qF "agent socket hello"; then
|
|||||||
fail "agent-send.sh leaked named-socket message to default tmux server"
|
fail "agent-send.sh leaked named-socket message to default tmux server"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Concurrency: parallel senders on one server must not cross-deliver or drop.
|
||||||
|
# Locks the unique-per-invocation paste buffer (a fixed buffer name raced:
|
||||||
|
# load overwrote load, -d deleted underneath — messages swapped between panes).
|
||||||
|
CONC_N=5
|
||||||
|
for i in $(seq 1 "$CONC_N"); do
|
||||||
|
tmux -L "$SOCKET" new-session -d -s "conc-$i" -c "$TMPDIR" 'bash --noprofile --norc -i'
|
||||||
|
done
|
||||||
|
pids=()
|
||||||
|
for i in $(seq 1 "$CONC_N"); do
|
||||||
|
"$SEND_MESSAGE" -L "$SOCKET" -t "=conc-$i" -m "CONCPAYLOAD-${i}-END" >/dev/null &
|
||||||
|
pids+=($!)
|
||||||
|
done
|
||||||
|
for pid in "${pids[@]}"; do
|
||||||
|
wait "$pid" || fail "concurrent send-message.sh invocation exited non-zero"
|
||||||
|
done
|
||||||
|
sleep 0.2
|
||||||
|
for i in $(seq 1 "$CONC_N"); do
|
||||||
|
pane=$(tmux -L "$SOCKET" capture-pane -t "=conc-$i:0.0" -p)
|
||||||
|
printf '%s' "$pane" | grep -qF "CONCPAYLOAD-${i}-END" \
|
||||||
|
|| fail "concurrent send dropped payload for pane conc-$i"
|
||||||
|
for j in $(seq 1 "$CONC_N"); do
|
||||||
|
[ "$j" = "$i" ] && continue
|
||||||
|
if printf '%s' "$pane" | grep -qF "CONCPAYLOAD-${j}-END"; then
|
||||||
|
fail "concurrent send cross-delivered payload $j to pane conc-$i"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
echo "ok - named tmux socket send tools"
|
echo "ok - named tmux socket send tools"
|
||||||
|
|||||||
Reference in New Issue
Block a user