8000 feat(storage): give every campaign its own database by Mnehmos · Pull Request #83 · Mnehmos/mnehmos.rpg.mcp · GitHub
[go: up one dir, main page]

Skip to content
Merged
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: 14 additions & 6 deletions src/server/audit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,19 @@ function detectSoftError(result: unknown): string | undefined {
}

export class AuditLogger {
private repo: AuditRepository;

constructor() {
const db = getDb(process.env.NODE_ENV === 'test' ? ':memory:' : 'rpg.db');
this.repo = new AuditRepository(db);
/**
* Resolved per write rather than held from construction.
*
* The logger is built at server boot, where there is no tenant and so no
* database to bind to. Resolving at write time also puts each audit row in
* its own campaign's database, which is where it belongs.
*
* Writes made outside a tenant scope — only the tenant-agnostic meta-tools
* reach that path — throw here and are swallowed by the caller's existing
* catch, so an unscoped call is simply not audited rather than failing.
*/
private repo(): AuditRepository {
return new AuditRepository(getDb());
}

wrapHandler(toolName: string, handler: (args: any) => Promise<any>) {
Expand All @@ -78,7 +86,7 @@ export class AuditLogger {
} finally {
try {
const softError = error ? undefined : detectSoftError(result);
this.repo.create({
this.repo().create({
action: toolName,
actorId: null,
targetId: null,
Expand Down
7 changes: 1 addition & 6 deletions src/server/consolidated/agent-manage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,7 @@ type AgentManageAction = typeof ACTIONS[number];
// ═══════════════════════════════════════════════════════════════════════════

function ensureDb() {
const dbPath = process.env.NODE_ENV === 'test'
? ':memory:'
: process.env.RPG_DATA_DIR
? `${process.env.RPG_DATA_DIR}/rpg.db`
: 'rpg.db';
const db = getDb(dbPath);
const db = getDb();
return {
db,
agentRepo: new AgentRepository(db),
Expand Down
7 changes: 1 addition & 6 deletions src/server/consolidated/batch-manage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,7 @@ const ALIASES: Record<string, BatchAction> = {
};

function ensureDb() {
const dbPath = process.env.NODE_ENV === 'test'
? ':memory:'
: process.env.RPG_DATA_DIR
? `${process.env.RPG_DATA_DIR}/rpg.db`
: 'rpg.db';
const db = getDb(dbPath);
const db = getDb();
return {
db,
charRepo: new CharacterRepository(db)
Expand Down
7 changes: 1 addition & 6 deletions src/server/consolidated/corpse-manage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,7 @@ type CorpseAction = typeof ACTIONS[number];
// ═══════════════════════════════════════════════════════════════════════════

function getRepo(): CorpseRepository {
const dbPath = process.env.NODE_ENV === 'test'
? ':memory:'
: process.env.RPG_DATA_DIR
? `${process.env.RPG_DATA_DIR}/rpg.db`
: 'rpg.db';
const db = getDb(dbPath);
const db = getDb();
return new CorpseRepository(db);
}

Expand Down
3 changes: 1 addition & 2 deletions src/server/consolidated/improvisation-manage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,7 @@ const TriggerEventEnum = z.enum([
// ═══════════════════════════════════════════════════════════════════════════

function ensureDb() {
const dbPath = process.env.NODE_ENV === 'test' ? ':memory:' : 'rpg.db';
const db = getDb(dbPath);
const db = getDb();
const effectsRepo = new CustomEffectsRepository(db);
const charRepo = new CharacterRepository(db);
return { db, effectsRepo, charRepo };
Expand Down
7 changes: 1 addition & 6 deletions src/server/consolidated/inventory-manage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,7 @@ type InventoryAction = typeof ACTIONS[number];
// ═══════════════════════════════════════════════════════════════════════════

function ensureDb() {
const dbPath = process.env.NODE_ENV === 'test'
? ':memory:'
: process.env.RPG_DATA_DIR
? `${process.env.RPG_DATA_DIR}/rpg.db`
: 'rpg.db';
const db = getDb(dbPath);
const db = getDb();
return {
itemRepo: new ItemRepository(db),
inventoryRepo: new InventoryRepository(db),
Expand Down
7 changes: 1 addition & 6 deletions src/server/consolidated/item-manage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,7 @@ type ItemAction = typeof ACTIONS[number];
// ═══════════════════════════════════════════════════════════════════════════

function ensureDb() {
const dbPath = process.env.NODE_ENV === 'test'
? ':memory:'
: process.env.RPG_DATA_DIR
? `${process.env.RPG_DATA_DIR}/rpg.db`
: 'rpg.db';
const db = getDb(dbPath);
const db = getDb();
const itemRepo = new ItemRepository(db);
return { itemRepo };
}
Expand Down
3 changes: 1 addition & 2 deletions src/server/consolidated/math-manage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,7 @@ type MathAction = typeof ACTIONS[number];
// ═══════════════════════════════════════════════════════════════════════════

function getRepo() {
const dbPath = process.env.NODE_ENV === 'test' ? ':memory:' : 'rpg.db';
const db = getDb(dbPath);
const db = getDb();
return { repo: new CalculationRepository(db), db };
}

Expand Down
7 changes: 1 addition & 6 deletions src/server/consolidated/narrative-manage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,7 @@ const SessionLogMetadata = z.object({
// ═══════════════════════════════════════════════════════════════════════════

function ensureDb() {
const dbPath = process.env.NODE_ENV === 'test'
? ':memory:'
: process.env.RPG_DATA_DIR
? `${process.env.RPG_DATA_DIR}/rpg.db`
: 'rpg.db';
return getDb(dbPath);
return getDb();
}

// ═══════════════════════════════════════════════════════════════════════════
Expand Down
7 changes: 1 addition & 6 deletions src/server/consolidated/party-manage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,7 @@ type PartyAction = typeof ACTIONS[number];
// ═══════════════════════════════════════════════════════════════════════════

function ensureDb() {
const dbPath = process.env.NODE_ENV === 'test'
? ':memory:'
: process.env.RPG_DATA_DIR
? `${process.env.RPG_DATA_DIR}/rpg.db`
: 'rpg.db';
const db = getDb(dbPath);
const db = getDb();
return {
db,
partyRepo: new PartyRepository(db),
Expand Down
3 changes: 1 addition & 2 deletions src/server/consolidated/perception-manage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,7 @@ export const CONSTRAINT_PERCEPTION_SUBSYSTEM_ID = 'constraint-perception';
// ─────────────────────────────────────────────────────────────────

function ensureDb() {
const dbPath = process.env.NODE_ENV === 'test' ? ':memory:' : 'rpg.db';
const db = getDb(dbPath);
const db = getDb();
return {
db,
characterRepo: new CharacterRepository(db),
Expand Down
7 changes: 1 addition & 6 deletions src/server/consolidated/quest-manage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,7 @@ type QuestManageAction = typeof ACTIONS[number];
// ═══════════════════════════════════════════════════════════════════════════

function ensureDb() {
const dbPath = process.env.NODE_ENV === 'test'
? ':memory:'
: process.env.RPG_DATA_DIR
? `${process.env.RPG_DATA_DIR}/rpg.db`
: 'rpg.db';
const db = getDb(dbPath);
const db = getDb();
return {
questRepo: new QuestRepository(db),
characterRepo: new CharacterRepository(db),
Expand Down
7 changes: 1 addition & 6 deletions src/server/consolidated/scene-manage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,7 @@ const ACTIONS = ['set', 'list', 'get', 'current'] as const;
type SceneAction = typeof ACTIONS[number];

function ensureDb() {
const dbPath = process.env.NODE_ENV === 'test'
? ':memory:'
: process.env.RPG_DATA_DIR
? `${process.env.RPG_DATA_DIR}/rpg.db`
: 'rpg.db';
return getDb(dbPath);
return getDb();
}

const SetSchema = z.object({
Expand Down
7 changes: 1 addition & 6 deletions src/server/consolidated/secret-manage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,7 @@ type SecretAction = typeof ACTIONS[number];
// ═══════════════════════════════════════════════════════════════════════════

function ensureDb() {
const dbPath = process.env.NODE_ENV === 'test'
? ':memory:'
: process.env.RPG_DATA_DIR
? `${process.env.RPG_DATA_DIR}/rpg.db`
: 'rpg.db';
const db = getDb(dbPath);
const db = getDb();
const secretRepo = new SecretRepository(db);
return { secretRepo };
}
Expand Down
7 changes: 1 addition & 6 deletions src/server/consolidated/session-manage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,7 @@ const ALIASES: Record<string, SessionAction> = {
};

function ensureDb() {
const dbPath = process.env.NODE_ENV === 'test'
? ':memory:'
: process.env.RPG_DATA_DIR
? `${process.env.RPG_DATA_DIR}/rpg.db`
: 'rpg.db';
const db = getDb(dbPath);
const db = getDb();
return {
db,
partyRepo: new PartyRepository(db),
Expand Down
7 changes: 1 addition & 6 deletions src/server/consolidated/spawn-manage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,12 +134,7 @@ const ACTIONS = [
type SpawnAction = typeof ACTIONS[number];

function ensureDb() {
const dbPath = process.env.NODE_ENV === 'test'
? ':memory:'
: process.env.RPG_DATA_DIR
? `${process.env.RPG_DATA_DIR}/rpg.db`
: 'rpg.db';
const db = getDb(dbPath);
const db = getDb();
return {
db,
charRepo: new CharacterRepository(db),
Expand Down
3 changes: 1 addition & 2 deletions src/server/consolidated/strategy-manage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,7 @@ type StrategyAction = typeof ACTIONS[number];
// ═══════════════════════════════════════════════════════════════════════════

function getRepos() {
const dbPath = process.env.NODE_ENV === 'test' ? ':memory:' : 'rpg.db';
const db = getDb(dbPath);
const db = getDb();
return {
nationRepo: new NationRepository(db),
diplomacyRepo: new DiplomacyRepository(db),
Expand Down
7 changes: 1 addition & 6 deletions src/server/consolidated/travel-manage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,7 @@ const ALIASES: Record<string, TravelAction> = {
};

function ensureDb() {
const dbPath = process.env.NODE_ENV === 'test'
? ':memory:'
: process.env.RPG_DATA_DIR
? `${process.env.RPG_DATA_DIR}/rpg.db`
: 'rpg.db';
const db = getDb(dbPath);
const db = getDb();
return {
db,
charRepo: new CharacterRepository(db),
Expand Down
3 changes: 1 addition & 2 deletions src/server/consolidated/turn-manage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,7 @@ type TurnAction = typeof ACTIONS[number];
// ═══════════════════════════════════════════════════════════════════════════

function getRepos() {
const dbPath = process.env.NODE_ENV === 'test' ? ':memory:' : 'rpg.db';
const db = getDb(dbPath);
const db = getDb();
return {
turnStateRepo: new TurnStateRepository(db),
nationRepo: new NationRepository(db),
Expand Down
14 changes: 9 additions & 5 deletions src/server/index.ts
< 10BC8 tr data-hunk="c86e149f25d2028da07dc08b75d1f5fe451bc61e28c30b7f38dd8d9f0f01b748" class="show-top-border">
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,10 @@ import { PubSub } from '../engine/pubsub.js';
import { registerEventTools } from './events.js';
import { AuditLogger } from './audit.js';
import { withSession } from './types.js';
import { closeDb, getDb, getDbPath } from '../storage/index.js';
import { closeDb, campaignDbPath } from '../storage/index.js';

// Agent runtime
import { ProviderFactory } from '../agent/provider/factory.js';
import { setAgentRuntime, buildAgentRuntime } from '../agent/runtime/deps.js';

/**
* Setup graceful shutdown handlers to ensure database is properly closed.
Expand Down Expand Up @@ -184,16 +183,21 @@ function buildServer(pubsub: PubSub, auditLogger: AuditLogger): McpServer {

async function main() {
setupShutdownHandlers();
console.error(`[Server] Database path: ${getDbPath()}`);
console.error(`[Server] Campaign databases: ${campaignDbPath('<campaign-id>')}`);

// =========================================================================
// AGENT RUNTIME: wire LLM providers + repos behind getAgentRuntime()
// =========================================================================
try {
const agentDb = getDb(getDbPath());
// Deliberately no setAgentRuntime() here any more. Runtime deps bind eight
// repositories to a single database, and under per-campaign databases the
// right one is not known until a request arrives with a verified tenant.
// agent_manage and combat_manage already build the runtime lazily from the
// request's own database, which is now the only correct place to do it.
// Provider initialization stays at boot: it is genuinely process-wide, and
// surfacing misconfiguration early is worth keeping.
const providerFactory = new ProviderFactory();
const providers = providerFactory.initialize();
setAgentRuntime(buildAgentRuntime(agentDb, providerFactory));

// Diagnostic: print enough to self-diagnose the "Provider not configured"
// error class without ever printing key values. The MCP-host-spawned-with-
Expand Down
44 changes: 28 additions & 16 deletions src/server/types.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,44 @@
import { z } from 'zod';
import { getTenant } from '../storage/tenant-context.js';

export interface SessionContext {
sessionId: string;
userId?: string;
worldId?: string;
}

// Create wrapper for tool handlers
/**
* Marks a request that carried no verified tenant.
*
* Only tenant-agnostic meta-tools (search_tools, load_tool_schema) legitimately
* reach a handler this way; anything touching storage fails closed inside
* getDb(). The value is deliberately not a plausible id — the previous
* `'default'` fallback read like a real session and quietly pooled unrelated
* callers into one bucket.
*/
const UNSCOPED = 'unscoped';

/**
* Wraps a tool handler with its session context.
*
* The context is derived from the *verified* tenant established by the HTTP
* transport, never from the arguments. Previously `sessionId` was read straight
* out of the tool arguments — which are assembled from model output — so a
* prompt-injected or hallucinated value became the request's identity. Any
* `sessionId` a caller still sends is now simply stripped by the tool's own
* schema and ignored.
*/
export function withSession<T extends z.ZodType<any>>(
schema: T,
handler: (args: z.infer<T>, ctx: SessionContext) => Promise<any>
) {
const sessionSchema = z.intersection(
schema,
z.object({
sessionId: z.string().optional().default('default')
})
);

return async (args: unknown) => {
const parsed = sessionSchema.parse(args);
// parsed is intersection of T and { sessionId }
// We can extract sessionId safely
const sessionId = (parsed as any).sessionId;

const ctx: SessionContext = { sessionId };
// We cast parsed back to T's inferred type.
// Since parsed is T & { sessionId }, it satisfies T.
const parsed = schema.parse(args);
const tenant = getTenant();
const ctx: SessionContext = {
sessionId: tenant ? `${tenant.accountId}:${tenant.campaignId}` : UNSCOPED,
worldId: tenant?.worldId,
};
return handler(parsed, ctx);
};
}
Loading
Loading
0