Compare commits
4 Commits
feat/tess-
...
5ad29926d9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5ad29926d9 | ||
| ca9c2b5c23 | |||
| b580d37d51 | |||
| 59e49cfd15 |
@@ -5,3 +5,5 @@ pnpm-lock.yaml
|
||||
**/drizzle
|
||||
**/.next
|
||||
.claude/
|
||||
docs/tess/TASKS.md
|
||||
docs/scratchpads/
|
||||
|
||||
@@ -83,16 +83,15 @@ export class CommandExecutorService {
|
||||
success: true,
|
||||
message: 'Retry last message requested.',
|
||||
};
|
||||
case 'gc': {
|
||||
// Admin-only: system-wide GC sweep across all sessions
|
||||
const result = await this.sessionGC.sweepOrphans();
|
||||
case 'gc':
|
||||
// Global retention requires a separate, authorized and audited job.
|
||||
// Session cleanup is performed only through the session lifecycle.
|
||||
return {
|
||||
command: 'gc',
|
||||
success: true,
|
||||
message: `GC sweep complete: ${result.orphanedSessions} orphaned sessions cleaned in ${result.duration}ms.`,
|
||||
success: false,
|
||||
message: 'Global GC is disabled pending an authorized retention job.',
|
||||
conversationId,
|
||||
};
|
||||
}
|
||||
case 'agent':
|
||||
return await this.handleAgent(args ?? null, conversationId, userId);
|
||||
case 'provider':
|
||||
|
||||
@@ -176,14 +176,12 @@ describe('CommandExecutorService — integration', () => {
|
||||
expect(result.command).toBe('nonexistent');
|
||||
});
|
||||
|
||||
// /gc handler calls SessionGCService.sweepOrphans (admin-only, no userId arg)
|
||||
it('/gc calls SessionGCService.sweepOrphans without arguments', async () => {
|
||||
it('/gc refuses an unaudited global sweep', async () => {
|
||||
const payload: SlashCommandPayload = { command: 'gc', conversationId };
|
||||
const result = await executor.execute(payload, userId);
|
||||
expect(mockSessionGC.sweepOrphans).toHaveBeenCalledWith();
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.message).toContain('GC sweep complete');
|
||||
expect(result.message).toContain('3 orphaned sessions');
|
||||
expect(mockSessionGC.sweepOrphans).not.toHaveBeenCalled();
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.message).toContain('disabled pending an authorized retention job');
|
||||
});
|
||||
|
||||
// /system with args calls SystemOverrideService.set
|
||||
|
||||
@@ -12,7 +12,12 @@ type MockRedis = {
|
||||
describe('SessionGCService', () => {
|
||||
let service: SessionGCService;
|
||||
let mockRedis: MockRedis;
|
||||
let mockLogService: { logs: { promoteToWarm: ReturnType<typeof vi.fn> } };
|
||||
let mockLogService: {
|
||||
logs: {
|
||||
promoteSessionToWarm: ReturnType<typeof vi.fn>;
|
||||
promoteToWarm: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper: build a scan mock that returns all provided keys in a single
|
||||
@@ -30,6 +35,7 @@ describe('SessionGCService', () => {
|
||||
|
||||
mockLogService = {
|
||||
logs: {
|
||||
promoteSessionToWarm: vi.fn().mockResolvedValue(0),
|
||||
promoteToWarm: vi.fn().mockResolvedValue(0),
|
||||
},
|
||||
};
|
||||
@@ -64,49 +70,18 @@ describe('SessionGCService', () => {
|
||||
expect(result.sessionId).toBe('test-session-id');
|
||||
});
|
||||
|
||||
it('fullCollect() deletes all session keys', async () => {
|
||||
mockRedis.scan = makeScanMock(['mosaic:session:abc:system', 'mosaic:session:xyz:foo']);
|
||||
const result = await service.fullCollect();
|
||||
expect(mockRedis.del).toHaveBeenCalled();
|
||||
expect(result.valkeyKeys).toBe(2);
|
||||
it('collect() demotes logs only for the requested session', async () => {
|
||||
await service.collect('owned-session');
|
||||
|
||||
expect(mockLogService.logs.promoteSessionToWarm).toHaveBeenCalledWith(
|
||||
'owned-session',
|
||||
expect.any(Date),
|
||||
);
|
||||
expect(mockLogService.logs.promoteToWarm).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('fullCollect() with no keys returns 0 valkeyKeys', async () => {
|
||||
mockRedis.scan = makeScanMock([]);
|
||||
const result = await service.fullCollect();
|
||||
expect(result.valkeyKeys).toBe(0);
|
||||
expect(mockRedis.del).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('fullCollect() returns duration', async () => {
|
||||
const result = await service.fullCollect();
|
||||
expect(result.duration).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
|
||||
it('sweepOrphans() extracts unique session IDs and collects them', async () => {
|
||||
// First scan call returns the global session list; subsequent calls return
|
||||
// per-session keys during collect().
|
||||
mockRedis.scan = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce([
|
||||
'0',
|
||||
['mosaic:session:abc:system', 'mosaic:session:abc:messages', 'mosaic:session:xyz:system'],
|
||||
])
|
||||
// collect('abc') scan
|
||||
.mockResolvedValueOnce(['0', ['mosaic:session:abc:system', 'mosaic:session:abc:messages']])
|
||||
// collect('xyz') scan
|
||||
.mockResolvedValueOnce(['0', ['mosaic:session:xyz:system']]);
|
||||
mockRedis.del.mockResolvedValue(1);
|
||||
|
||||
const result = await service.sweepOrphans();
|
||||
expect(result.orphanedSessions).toBeGreaterThanOrEqual(0);
|
||||
expect(result.duration).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
|
||||
it('sweepOrphans() returns empty when no session keys', async () => {
|
||||
mockRedis.scan = makeScanMock([]);
|
||||
const result = await service.sweepOrphans();
|
||||
expect(result.orphanedSessions).toBe(0);
|
||||
expect(result.totalCleaned).toHaveLength(0);
|
||||
it('does not expose automatic global GC entry points', () => {
|
||||
expect('fullCollect' in service).toBe(false);
|
||||
expect('sweepOrphans' in service).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Inject, Injectable, Logger, type OnModuleInit } from '@nestjs/common';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { QueueHandle } from '@mosaicstack/queue';
|
||||
import type { LogService } from '@mosaicstack/log';
|
||||
import { LOG_SERVICE } from '../log/log.tokens.js';
|
||||
@@ -13,49 +13,13 @@ export interface GCResult {
|
||||
};
|
||||
}
|
||||
|
||||
export interface GCSweepResult {
|
||||
orphanedSessions: number;
|
||||
totalCleaned: GCResult[];
|
||||
duration: number;
|
||||
}
|
||||
|
||||
export interface FullGCResult {
|
||||
valkeyKeys: number;
|
||||
logsDemoted: number;
|
||||
jobsPurged: number;
|
||||
tempFilesRemoved: number;
|
||||
duration: number;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class SessionGCService implements OnModuleInit {
|
||||
private readonly logger = new Logger(SessionGCService.name);
|
||||
|
||||
export class SessionGCService {
|
||||
constructor(
|
||||
@Inject(REDIS) private readonly redis: QueueHandle['redis'],
|
||||
@Inject(LOG_SERVICE) private readonly logService: LogService,
|
||||
) {}
|
||||
|
||||
onModuleInit(): void {
|
||||
// Fire-and-forget: run full GC asynchronously so it does not block the
|
||||
// NestJS bootstrap chain. Cold-start GC typically takes 100–500 ms
|
||||
// depending on Valkey key count; deferring it removes that latency from
|
||||
// the TTFB of the first HTTP request.
|
||||
this.fullCollect()
|
||||
.then((result) => {
|
||||
this.logger.log(
|
||||
`Full GC complete: ${result.valkeyKeys} Valkey keys, ` +
|
||||
`${result.logsDemoted} logs demoted, ` +
|
||||
`${result.jobsPurged} jobs purged, ` +
|
||||
`${result.tempFilesRemoved} temp dirs removed ` +
|
||||
`(${result.duration}ms)`,
|
||||
);
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
this.logger.error('Cold-start GC failed', err instanceof Error ? err.stack : String(err));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan Valkey for all keys matching a pattern using SCAN (non-blocking).
|
||||
* KEYS is avoided because it blocks the Valkey event loop for the full scan
|
||||
@@ -86,79 +50,13 @@ export class SessionGCService implements OnModuleInit {
|
||||
result.cleaned.valkeyKeys = valkeyKeys.length;
|
||||
}
|
||||
|
||||
// 2. PG: demote hot-tier agent_logs for this session to warm
|
||||
const cutoff = new Date(); // demote all hot logs for this session
|
||||
const logsDemoted = await this.logService.logs.promoteToWarm(cutoff);
|
||||
// 2. PG: demote hot-tier agent logs for this session only.
|
||||
const cutoff = new Date();
|
||||
const logsDemoted = await this.logService.logs.promoteSessionToWarm(sessionId, cutoff);
|
||||
if (logsDemoted > 0) {
|
||||
result.cleaned.logsDemoted = logsDemoted;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sweep GC — find orphaned artifacts from dead sessions.
|
||||
* System-wide operation: only call from admin-authorized paths or internal
|
||||
* scheduled jobs. Individual session cleanup is handled by collect().
|
||||
*/
|
||||
async sweepOrphans(): Promise<GCSweepResult> {
|
||||
const start = Date.now();
|
||||
const cleaned: GCResult[] = [];
|
||||
|
||||
// 1. Find all session-scoped Valkey keys (non-blocking SCAN)
|
||||
const allSessionKeys = await this.scanKeys('mosaic:session:*');
|
||||
|
||||
// Extract unique session IDs from keys
|
||||
const sessionIds = new Set<string>();
|
||||
for (const key of allSessionKeys) {
|
||||
const match = key.match(/^mosaic:session:([^:]+):/);
|
||||
if (match) sessionIds.add(match[1]!);
|
||||
}
|
||||
|
||||
// 2. For each session ID, collect stale keys
|
||||
for (const sessionId of sessionIds) {
|
||||
const gcResult = await this.collect(sessionId);
|
||||
if (Object.keys(gcResult.cleaned).length > 0) {
|
||||
cleaned.push(gcResult);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
orphanedSessions: cleaned.length,
|
||||
totalCleaned: cleaned,
|
||||
duration: Date.now() - start,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Full GC — aggressive collection for cold start.
|
||||
* Assumes no sessions survived the restart.
|
||||
*/
|
||||
async fullCollect(): Promise<FullGCResult> {
|
||||
const start = Date.now();
|
||||
|
||||
// 1. Valkey: delete ALL session-scoped keys (non-blocking SCAN)
|
||||
const sessionKeys = await this.scanKeys('mosaic:session:*');
|
||||
if (sessionKeys.length > 0) {
|
||||
await this.redis.del(...sessionKeys);
|
||||
}
|
||||
|
||||
// 2. NOTE: channel keys are NOT collected on cold start
|
||||
// (discord/telegram plugins may reconnect and resume)
|
||||
|
||||
// 3. PG: demote stale hot-tier logs older than 24h to warm
|
||||
const hotCutoff = new Date(Date.now() - 24 * 60 * 60 * 1000);
|
||||
const logsDemoted = await this.logService.logs.promoteToWarm(hotCutoff);
|
||||
|
||||
// 4. No summarization job purge API available yet
|
||||
const jobsPurged = 0;
|
||||
|
||||
return {
|
||||
valkeyKeys: sessionKeys.length,
|
||||
logsDemoted,
|
||||
jobsPurged,
|
||||
tempFilesRemoved: 0,
|
||||
duration: Date.now() - start,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,11 +6,10 @@ import {
|
||||
type OnModuleDestroy,
|
||||
} from '@nestjs/common';
|
||||
import { SummarizationService } from './summarization.service.js';
|
||||
import { SessionGCService } from '../gc/session-gc.service.js';
|
||||
import {
|
||||
QueueService,
|
||||
QUEUE_SUMMARIZATION,
|
||||
QUEUE_GC,
|
||||
QUEUE_SUMMARIZATION,
|
||||
QUEUE_TIER_MANAGEMENT,
|
||||
} from '../queue/queue.service.js';
|
||||
import type { Worker } from 'bullmq';
|
||||
@@ -23,14 +22,12 @@ export class CronService implements OnModuleInit, OnModuleDestroy {
|
||||
|
||||
constructor(
|
||||
@Inject(SummarizationService) private readonly summarization: SummarizationService,
|
||||
@Inject(SessionGCService) private readonly sessionGC: SessionGCService,
|
||||
@Inject(QueueService) private readonly queueService: QueueService,
|
||||
) {}
|
||||
|
||||
async onModuleInit(): Promise<void> {
|
||||
const summarizationSchedule = process.env['SUMMARIZATION_CRON'] ?? '0 */6 * * *'; // every 6 hours
|
||||
const tierManagementSchedule = process.env['TIER_MANAGEMENT_CRON'] ?? '0 3 * * *'; // daily at 3am
|
||||
const gcSchedule = process.env['SESSION_GC_CRON'] ?? '0 4 * * *'; // daily at 4am
|
||||
|
||||
// M6-003: Summarization repeatable job
|
||||
await this.queueService.addRepeatableJob(
|
||||
@@ -56,15 +53,12 @@ export class CronService implements OnModuleInit, OnModuleDestroy {
|
||||
});
|
||||
this.registeredWorkers.push(tierWorker);
|
||||
|
||||
// M6-004: GC repeatable job
|
||||
await this.queueService.addRepeatableJob(QUEUE_GC, 'session-gc', {}, gcSchedule);
|
||||
const gcWorker = this.queueService.registerWorker(QUEUE_GC, async () => {
|
||||
await this.sessionGC.sweepOrphans();
|
||||
});
|
||||
this.registeredWorkers.push(gcWorker);
|
||||
// Retire any repeatable global GC schedule created by older deployments.
|
||||
// Session cleanup is now triggered only by an authorized session lifecycle operation.
|
||||
await this.queueService.removeRepeatableJobs(QUEUE_GC, 'session-gc');
|
||||
|
||||
this.logger.log(
|
||||
`BullMQ jobs scheduled: summarization="${summarizationSchedule}", tier="${tierManagementSchedule}", gc="${gcSchedule}"`,
|
||||
`BullMQ jobs scheduled: summarization="${summarizationSchedule}", tier="${tierManagementSchedule}"`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -162,6 +162,23 @@ export class QueueService implements OnModuleInit, OnModuleDestroy {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove every existing repeatable schedule for a job name. This supports
|
||||
* safe retirement of previously registered system-wide jobs.
|
||||
*/
|
||||
async removeRepeatableJobs(queueName: string, jobName: string): Promise<number> {
|
||||
const queue = this.getQueue(queueName);
|
||||
const jobs = await queue.getRepeatableJobs();
|
||||
const matchingJobs = jobs.filter((job) => job.name === jobName);
|
||||
await Promise.all(matchingJobs.map((job) => queue.removeRepeatableByKey(job.key)));
|
||||
if (matchingJobs.length > 0) {
|
||||
this.logger.log(
|
||||
`Removed ${matchingJobs.length} repeatable "${jobName}" job(s) from "${queueName}"`,
|
||||
);
|
||||
}
|
||||
return matchingJobs.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a Worker for the given queue name with error handling and
|
||||
* exponential backoff.
|
||||
|
||||
@@ -67,10 +67,10 @@ The MVP is complete when ALL declared workstreams are complete AND every cross-c
|
||||
|
||||
## Workstreams
|
||||
|
||||
| # | 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 |
|
||||
| W2 | TESS | Tess interaction agent | planning-complete | [docs/tess/MISSION-MANIFEST.md](./tess/MISSION-MANIFEST.md) | 5 milestones; issue #706; M1 issue #707 ready |
|
||||
| # | 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 |
|
||||
| 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)
|
||||
|
||||
@@ -85,7 +85,7 @@ Jarvis (v0.2.0) is a self-hosted AI assistant with a Python FastAPI backend and
|
||||
|
||||
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.
|
||||
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
|
||||
|
||||
|
||||
@@ -14,10 +14,10 @@
|
||||
|
||||
## Workstream Rollup
|
||||
|
||||
| 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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
| 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
|
||||
|
||||
|
||||
@@ -301,6 +301,10 @@ Each OIDC provider requires its client ID, client secret, and issuer URL togethe
|
||||
| `TELEGRAM_BOT_TOKEN` | Telegram bot token (enables Telegram plugin) |
|
||||
| `TELEGRAM_GATEWAY_URL` | Gateway URL for Telegram plugin to call |
|
||||
|
||||
### Session retention and garbage collection
|
||||
|
||||
Session cleanup is scoped to one session identifier and only removes that session's Valkey keys and demotes that session's hot logs. Gateway startup and scheduled jobs do not perform global session cleanup; startup removes legacy repeatable `session-gc` schedules created by older deployments. The `/gc` command is intentionally disabled until a distinct global-retention job supplies explicit authorization and audit evidence. This prevents one tenant or session's cleanup from changing another's retained data.
|
||||
|
||||
### Observability
|
||||
|
||||
| Variable | Default | Description |
|
||||
|
||||
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
|
||||
@@ -56,7 +56,3 @@
|
||||
**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.
|
||||
|
||||
## Ops config (Mos, 2026-07-12)
|
||||
|
||||
- **Tess dedicated Discord channel ID:** `1525947814033494126` (Jason configuring bot in Discord portal). Target for TESS-M3-001 (#709) binding. Treat as config, not a hardcoded secret. Live go-live = owner gate T1 @ M5.
|
||||
|
||||
22
docs/scratchpads/tess-m1-sec-006-session-gc-scope.md
Normal file
22
docs/scratchpads/tess-m1-sec-006-session-gc-scope.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# Scratchpad — TESS-M1-SEC-006 Session GC scope
|
||||
|
||||
- **Task / issue:** TESS-M1-SEC-006 / #707
|
||||
- **Branch:** `fix/tess-session-gc-scope` from `origin/main` at `59e49cfd`
|
||||
- **Objective:** Make session cleanup session-scoped and prevent automatic global retention/GC without an authorized, auditable operation.
|
||||
- **Scope:** `apps/gateway`, `packages/log`, admin/developer operations documentation.
|
||||
- **Budget:** Task estimate 18K; no explicit hard cap supplied.
|
||||
- **Assumption:** No authorized global retention service exists today. Existing full/sweep GC must therefore be disabled from startup and cron paths, while single-session cleanup remains available.
|
||||
|
||||
## Plan
|
||||
|
||||
1. Add failing isolation tests proving single-session cleanup only demotes its own logs and automatic startup/scheduled GC cannot globally delete session data.
|
||||
2. Add session-scoped log repository retention and make `collect(sessionId)` use it.
|
||||
3. Remove automatic full/sweep GC invocation; preserve any future global operation behind an explicit authorization/audit seam.
|
||||
4. Document the operational boundary, run gates, review, and commit without push.
|
||||
|
||||
## Verification evidence
|
||||
|
||||
- Isolation TDD: `pnpm --filter @mosaicstack/gateway test -- session-gc.service.spec.ts commands.integration.spec.ts command-executor-p8012.spec.ts` — 61 passed.
|
||||
- `pnpm typecheck` — passed.
|
||||
- `pnpm lint` — passed.
|
||||
- `pnpm format:check` remains red only on the known pre-existing Tess documentation debt; changed files are Prettier-clean.
|
||||
@@ -39,12 +39,12 @@ Every call receives an immutable, server-derived actor/tenant/channel scope and
|
||||
|
||||
## 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 |
|
||||
| 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
|
||||
|
||||
|
||||
@@ -2,27 +2,27 @@
|
||||
|
||||
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 |
|
||||
| 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
|
||||
|
||||
|
||||
@@ -24,17 +24,16 @@ Ship Tess as Jason's durable Pi-native GPT-5.6 Sol high-reasoning interaction ag
|
||||
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.
|
||||
6. **The HMI agent's name/identity is configuration, never a hardcoded literal.** The agent name (display + roster key) is supplied at provisioning — user-chosen or system-generated during config — and carried as data in the roster/profile. "Tess" is only the default/example instance name and this mission's build codename; no source, schema key, service identifier, or config default may hardcode it. (M2 exit gate must prove a distinct name provisions cleanly with zero code change.)
|
||||
|
||||
## 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 |
|
||||
| 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
|
||||
|
||||
@@ -42,6 +41,6 @@ All `AC-TESS-*` criteria in `docs/PRD.md` are mapped to reproducible evidence. T
|
||||
|
||||
## 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. |
|
||||
| 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. |
|
||||
|
||||
@@ -8,20 +8,20 @@ Trust boundaries: Discord→plugin, CLI→gateway, plugin→gateway service iden
|
||||
|
||||
## 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 |
|
||||
| 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,18 +1,18 @@
|
||||
# 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 |
|
||||
| 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
|
||||
|
||||
|
||||
@@ -58,9 +58,28 @@ export function createAgentLogsRepo(db: Db) {
|
||||
return rows[0];
|
||||
},
|
||||
|
||||
/**
|
||||
* Transition hot logs for one session to warm tier. Session retention is
|
||||
* default-deny: no other session's logs can be changed by this operation.
|
||||
*/
|
||||
async promoteSessionToWarm(sessionId: string, olderThan: Date): Promise<number> {
|
||||
const result = await db
|
||||
.update(agentLogs)
|
||||
.set({ tier: 'warm', summarizedAt: new Date() })
|
||||
.where(
|
||||
and(
|
||||
eq(agentLogs.sessionId, sessionId),
|
||||
eq(agentLogs.tier, 'hot'),
|
||||
lt(agentLogs.createdAt, olderThan),
|
||||
),
|
||||
)
|
||||
.returning();
|
||||
return result.length;
|
||||
},
|
||||
|
||||
/**
|
||||
* Transition hot logs older than the cutoff to warm tier.
|
||||
* Returns the number of logs transitioned.
|
||||
* Reserved for a separately authorized global retention job.
|
||||
*/
|
||||
async promoteToWarm(olderThan: Date): Promise<number> {
|
||||
const result = await db
|
||||
|
||||
@@ -240,6 +240,33 @@ get_gitea_login_for_host() {
|
||||
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() {
|
||||
local logins_json
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ TITLE=""
|
||||
BODY=""
|
||||
LABELS=""
|
||||
MILESTONE=""
|
||||
INTERACTIVE=false
|
||||
|
||||
# get_remote_host and get_gitea_token are provided by detect-platform.sh
|
||||
|
||||
@@ -66,11 +67,13 @@ Options:
|
||||
-b, --body BODY Issue body/description
|
||||
-l, --labels LABELS Comma-separated labels (e.g., "bug,feature")
|
||||
-m, --milestone NAME Milestone name to assign
|
||||
-i, --interactive Prompt for missing issue fields
|
||||
-h, --help Show this help message
|
||||
|
||||
Examples:
|
||||
$(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") -i
|
||||
EOF
|
||||
exit "${1:-1}"
|
||||
}
|
||||
@@ -94,6 +97,10 @@ while [[ $# -gt 0 ]]; do
|
||||
MILESTONE="$2"
|
||||
shift 2
|
||||
;;
|
||||
-i|--interactive)
|
||||
INTERACTIVE=true
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage 0
|
||||
;;
|
||||
@@ -104,6 +111,13 @@ while [[ $# -gt 0 ]]; do
|
||||
esac
|
||||
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
|
||||
echo "Error: Title is required (-t)" >&2
|
||||
usage
|
||||
@@ -127,6 +141,11 @@ case "$PLATFORM" in
|
||||
gitea_issue_create_api
|
||||
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")
|
||||
CMD=(tea issue create "${REPO_ARGS[@]}" --title "$TITLE")
|
||||
[[ -n "$BODY" ]] && CMD+=(--description "$BODY")
|
||||
|
||||
@@ -183,6 +183,11 @@ case "$PLATFORM" in
|
||||
gitea_pr_create_api
|
||||
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")
|
||||
CMD=(tea pr create "${REPO_ARGS[@]}" --title "$TITLE")
|
||||
[[ -n "$BODY" ]] && CMD+=(--description "$BODY")
|
||||
|
||||
@@ -45,6 +45,11 @@ JSON
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "${1:-}" == "api" ]]; then
|
||||
printf '%s\n' '{"login":"ci-bot"}'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
printf 'tea %s\n' "$*" >> "$MOSAIC_TEST_LOG"
|
||||
if [[ "${MOSAIC_TEA_FAIL_PR_CREATE:-}" == "1" && "$*" == pr\ create* ]]; then
|
||||
echo 'GetUserByName: simulated stale login failure' >&2
|
||||
|
||||
@@ -55,6 +55,11 @@ JSON
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "${1:-}" == "api" ]]; then
|
||||
printf '%s\n' '{"login":"ci-bot"}'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "${1:-}" == "issue" && "${2:-}" == "create" ]]; then
|
||||
desc=""
|
||||
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'
|
||||
Reference in New Issue
Block a user