Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 15 additions & 5 deletions .github/workflows/secret-scan.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,18 @@ jobs:
with:
fetch-depth: 0

- uses: gitleaks/gitleaks-action@v2
with:
args: --config=.gitleaks.toml --redact
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install gitleaks CLI
run: |
VERSION="8.28.0"
curl -sSL "https://github.com/gitleaks/gitleaks/releases/download/v${VERSION}/gitleaks_${VERSION}_linux_x64.tar.gz" \
| tar -xz gitleaks
sudo mv gitleaks /usr/local/bin/gitleaks
gitleaks version

- name: Run gitleaks scan
run: |
if [ -f ".gitleaks.toml" ]; then
gitleaks git --redact --config ".gitleaks.toml"
else
gitleaks git --redact
fi
23 changes: 23 additions & 0 deletions .github/workflows/status-consistency.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: Status Consistency

on:
pull_request:
push:
branches:
- main

jobs:
verify-status:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '20'

- name: Verify status consistency
run: |
node scripts/verify-status-consistency.mjs
27 changes: 16 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,17 +87,22 @@ Production deployment configuration is maintained in a separate private reposito

This project uses [Spec Kitty](https://github.com/Priivacy-ai/spec-kitty) for spec-driven development. Feature specifications live in `kitty-specs/`.

Current status snapshot (source: `python scripts/pride-status.py` on 2026-02-23):

| Spec | Description | Status |
|------|-------------|--------|
| `001` | MCP Server AWS Deployment | Complete |
| `002` | Session Context Management | Complete |
| `003` | Platform Architecture Overview | Spec-Only |
| `004` | Workflow Enforcement | Complete |
| `005` | Content Intelligence (Profile Engine) | Complete (Phases A–C, WP01–WP14) |
| `006` | Content Infrastructure | Complete (WP01–WP12) |
| `007` | Org-Scale Agentic Governance | Planning |
Status is canonicalized in `status/feature-readiness.json` and verified in CI via:
- `node scripts/verify-status-consistency.mjs`
- `.github/workflows/status-consistency.yml`

Generated feature status table:

<!-- GENERATED: status/feature-readiness.json -->
| Feature | Name | Lifecycle | Implementation | Production |
|---|---|---|---|---|
| `001` | MCP Server AWS Deployment | `execution` | `integrated` | `not_ready` |
| `002` | Session & Context Management | `done` | `validated` | `production_ready` |
| `003` | joyus-ai Platform Architecture Overview | `spec-only` | `none` | `not_ready` |
| `004` | Workflow Enforcement | `done` | `validated` | `production_ready` |
| `005` | Content Intelligence | `done` | `validated` | `production_ready` |
| `006` | Content Infrastructure | `done` | `integrated` | `not_ready` |
| `007` | Org-Scale Agentic Governance | `planning` | `none` | `not_ready` |

Project-level architecture decisions, implementation plan, and constitution are in `spec/`.

Expand Down
20 changes: 20 additions & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,26 @@ An open-source, multi-tenant AI agent platform that encodes organizational knowl

---

## Canonical Status Snapshot

Status source of truth:
- `status/feature-readiness.json`
- `status/generated/feature-table.md`
- `status/generated/phase-summary.md`

<!-- GENERATED: status/feature-readiness.json -->
Updated: 2026-03-05T19:20:00Z

Lifecycle counts:
- done: 4
- execution: 1
- planning: 1
- spec-only: 1

Production-readiness counts:
- not_ready: 4
- production_ready: 3

## Shipped

- **MCP Server Core** - OAuth authentication, tool executors for project management, chat, code hosting, and productivity integrations. Dockerized runtime available.
Expand Down
29 changes: 22 additions & 7 deletions joyus-ai-mcp-server/src/content/mediation/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,23 @@ export function createAuthMiddleware(db: DrizzleClient) {
return;
}

const keyHash = hashApiKey(apiKey);
const rows = await db
.select()
.from(contentApiKeys)
.where(eq(contentApiKeys.keyHash, keyHash))
.limit(1);
const keyRecord = rows[0];
let keyRecord: typeof contentApiKeys.$inferSelect | undefined;
try {
const keyHash = hashApiKey(apiKey);
const rows = await db
.select()
.from(contentApiKeys)
.where(eq(contentApiKeys.keyHash, keyHash))
.limit(1);
keyRecord = rows[0];
} catch {
// Fail closed: auth lookup error must never allow request flow.
res.status(503).json({
error: 'auth_service_unavailable',
message: 'API key validation service unavailable',
});
return;
}

if (!keyRecord || !keyRecord.isActive) {
res.status(401).json({ error: 'invalid_api_key', message: 'Invalid or inactive API key' });
Expand Down Expand Up @@ -107,6 +117,11 @@ export function createAuthMiddleware(db: DrizzleClient) {
...(audience ? { audience } : {}),
});

if (typeof payload.sub !== 'string' || payload.sub.length === 0) {
res.status(401).json({ error: 'invalid_user_token', message: 'Invalid user token subject' });
return;
}

req.userId = payload.sub;
next();
} catch (err) {
Expand Down
137 changes: 130 additions & 7 deletions joyus-ai-mcp-server/src/content/mediation/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

import { Router, type Request, type Response } from 'express';
import { drizzle } from 'drizzle-orm/node-postgres';
import { createId } from '@paralleldrive/cuid2';
import { createAuthMiddleware } from './auth.js';
import { MediationSessionService } from './session.js';
import type { GenerationService } from '../generation/index.js';
Expand All @@ -30,6 +31,50 @@ export interface MediationDependencies {
entitlementCache: EntitlementCache;
}

function requestIdFrom(req: Request): string {
const header = req.headers['x-request-id'];
if (typeof header === 'string' && header.length > 0) return header;
if (Array.isArray(header) && header[0]) return header[0];
return createId();
}

export function sessionMatchesRequestContext(
session: { userId: string; tenantId: string; apiKeyId: string },
req: { userId?: string; tenantId?: string; apiKeyRecord?: { id: string } },
): boolean {
return (
session.userId === req.userId &&
session.tenantId === req.tenantId &&
session.apiKeyId === req.apiKeyRecord?.id
);
}

function logMediationEvent(
level: 'info' | 'error',
event: string,
req: Request,
details: Record<string, unknown>,
): void {
const payload = {
level,
event,
requestId: requestIdFrom(req),
tenantId: req.tenantId ?? null,
sessionId: typeof details.sessionId === 'string' ? details.sessionId : null,
profileId: typeof details.profileId === 'string' ? details.profileId : null,
userId: req.userId ?? null,
...details,
timestamp: new Date().toISOString(),
};

const serialized = JSON.stringify(payload);
if (level === 'error') {
console.error(serialized);
return;
}
console.info(serialized);
}

export function createMediationRouter(deps: MediationDependencies): Router {
const router = Router();
const { db, generationService, entitlementService, entitlementCache } = deps;
Expand All @@ -46,6 +91,7 @@ export function createMediationRouter(deps: MediationDependencies): Router {

// POST /sessions — create a new mediation session
router.post('/sessions', async (req: Request, res: Response): Promise<void> => {
const requestId = requestIdFrom(req);
try {
const profileId = req.body?.profileId as string | undefined;
const result = await sessionService.createSession(
Expand All @@ -54,31 +100,59 @@ export function createMediationRouter(deps: MediationDependencies): Router {
req.userId!,
profileId,
);
logMediationEvent('info', 'mediation.session.created', req, {
requestId,
sessionId: result.sessionId,
profileId: result.activeProfileId,
});
res.status(201).json(result);
} catch (err) {
} catch (err: unknown) {
logMediationEvent('error', 'mediation.session.create_failed', req, {
requestId,
profileId: req.body?.profileId ?? null,
error: err instanceof Error ? err.message : String(err),
});
res.status(500).json({ error: 'internal_error', message: 'Failed to create session' });
}
});

// POST /sessions/:sessionId/messages — send a message and get a generated response
router.post('/sessions/:sessionId/messages', async (req: Request, res: Response): Promise<void> => {
const requestId = requestIdFrom(req);
const startedAt = Date.now();
try {
const { sessionId } = req.params;
const body = req.body as { message?: string; maxSources?: number };
const { message, maxSources } = body;

if (!message) {
logMediationEvent('error', 'mediation.message.validation_failed', req, {
requestId,
sessionId,
profileId: null,
reason: 'missing_message',
});
res.status(400).json({ error: 'missing_message', message: 'message field is required' });
return;
}

// Validate session exists, belongs to this user, and is not closed
const session = await sessionService.getSession(sessionId);
if (!session || session.endedAt) {
logMediationEvent('error', 'mediation.message.session_not_found', req, {
requestId,
sessionId,
profileId: null,
});
res.status(404).json({ error: 'session_not_found', message: 'Session not found or already closed' });
return;
}
if (session.userId !== req.userId) {
if (!sessionMatchesRequestContext(session, req)) {
logMediationEvent('error', 'mediation.message.session_forbidden', req, {
requestId,
sessionId,
profileId: session.activeProfileId,
});
res.status(404).json({ error: 'session_not_found', message: 'Session not found' });
return;
}
Expand Down Expand Up @@ -106,6 +180,14 @@ export function createMediationRouter(deps: MediationDependencies): Router {
// Increment message counter
await sessionService.incrementMessageCount(sessionId);

logMediationEvent('info', 'mediation.message.completed', req, {
requestId,
sessionId,
profileId: session.activeProfileId,
durationMs: Date.now() - startedAt,
citations: result.citations.length,
});

res.json({
message: result.text,
citations: result.citations,
Expand All @@ -116,37 +198,78 @@ export function createMediationRouter(deps: MediationDependencies): Router {
responseTime: result.metadata.durationMs,
},
});
} catch (err) {
} catch (err: unknown) {
logMediationEvent('error', 'mediation.message.failed', req, {
requestId,
sessionId: req.params.sessionId,
profileId: null,
error: err instanceof Error ? err.message : String(err),
durationMs: Date.now() - startedAt,
});
res.status(500).json({ error: 'internal_error', message: 'Failed to process message' });
}
});

// GET /sessions/:sessionId — retrieve session details
router.get('/sessions/:sessionId', async (req: Request, res: Response): Promise<void> => {
const requestId = requestIdFrom(req);
try {
const session = await sessionService.getSession(req.params.sessionId);
if (!session || session.userId !== req.userId) {
if (!session || !sessionMatchesRequestContext(session, req)) {
logMediationEvent('error', 'mediation.session.lookup_forbidden', req, {
requestId,
sessionId: req.params.sessionId,
profileId: session?.activeProfileId ?? null,
});
res.status(404).json({ error: 'session_not_found', message: 'Session not found' });
return;
}
logMediationEvent('info', 'mediation.session.lookup', req, {
requestId,
sessionId: session.id,
profileId: session.activeProfileId,
});
res.json(session);
} catch (err) {
} catch (err: unknown) {
logMediationEvent('error', 'mediation.session.lookup_failed', req, {
requestId,
sessionId: req.params.sessionId,
profileId: null,
error: err instanceof Error ? err.message : String(err),
});
res.status(500).json({ error: 'internal_error', message: 'Failed to retrieve session' });
}
});

// DELETE /sessions/:sessionId — close a session
router.delete('/sessions/:sessionId', async (req: Request, res: Response): Promise<void> => {
const requestId = requestIdFrom(req);
try {
const session = await sessionService.getSession(req.params.sessionId);
if (!session || session.userId !== req.userId) {
if (!session || !sessionMatchesRequestContext(session, req)) {
logMediationEvent('error', 'mediation.session.close_forbidden', req, {
requestId,
sessionId: req.params.sessionId,
profileId: session?.activeProfileId ?? null,
});
res.status(404).json({ error: 'session_not_found', message: 'Session not found' });
return;
}
await sessionService.closeSession(req.params.sessionId);
entitlementCache.invalidate(req.params.sessionId);
logMediationEvent('info', 'mediation.session.closed', req, {
requestId,
sessionId: req.params.sessionId,
profileId: session.activeProfileId,
});
res.status(204).send();
} catch (err) {
} catch (err: unknown) {
logMediationEvent('error', 'mediation.session.close_failed', req, {
requestId,
sessionId: req.params.sessionId,
profileId: null,
error: err instanceof Error ? err.message : String(err),
});
res.status(500).json({ error: 'internal_error', message: 'Failed to close session' });
}
});
Expand Down
Loading