@wopr-network/defcon 1.9.0 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,8 @@
1
+ import type { IDomainEventRepository } from "../repositories/interfaces.js";
2
+ import type { EngineEvent, IEventBusAdapter } from "./event-types.js";
3
+ /** IEventBusAdapter that persists every engine event (except definition.changed) to the domain_events table. */
4
+ export declare class DomainEventPersistAdapter implements IEventBusAdapter {
5
+ private readonly repo;
6
+ constructor(repo: IDomainEventRepository);
7
+ emit(event: EngineEvent): Promise<void>;
8
+ }
@@ -0,0 +1,14 @@
1
+ /** IEventBusAdapter that persists every engine event (except definition.changed) to the domain_events table. */
2
+ export class DomainEventPersistAdapter {
3
+ repo;
4
+ constructor(repo) {
5
+ this.repo = repo;
6
+ }
7
+ async emit(event) {
8
+ // Skip events that have no entityId (e.g. definition.changed)
9
+ if (!("entityId" in event) || !event.entityId)
10
+ return;
11
+ const { type, entityId, emittedAt, ...rest } = event;
12
+ await this.repo.append(type, entityId, rest);
13
+ }
14
+ }
@@ -136,3 +136,8 @@ export declare const AdminGateRerunSchema: z.ZodObject<{
136
136
  entity_id: z.ZodString;
137
137
  gate_name: z.ZodString;
138
138
  }, z.core.$strip>;
139
+ export declare const AdminEventsListSchema: z.ZodObject<{
140
+ entity_id: z.ZodString;
141
+ type: z.ZodOptional<z.ZodString>;
142
+ limit: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
143
+ }, z.core.$strip>;
@@ -145,3 +145,8 @@ export const AdminGateRerunSchema = z.object({
145
145
  entity_id: z.string().min(1),
146
146
  gate_name: z.string().min(1),
147
147
  });
148
+ export const AdminEventsListSchema = z.object({
149
+ entity_id: z.string().min(1),
150
+ type: z.string().min(1).optional(),
151
+ limit: z.coerce.number().int().min(1).max(500).default(100),
152
+ });
@@ -12,9 +12,11 @@ import { createHttpServer } from "../api/server.js";
12
12
  import { exportSeed } from "../config/exporter.js";
13
13
  import { loadSeed } from "../config/seed-loader.js";
14
14
  import { resolveCorsOrigin } from "../cors.js";
15
+ import { DomainEventPersistAdapter } from "../engine/domain-event-adapter.js";
15
16
  import { Engine } from "../engine/engine.js";
16
17
  import { EventEmitter } from "../engine/event-emitter.js";
17
18
  import { withTransaction } from "../main.js";
19
+ import { DrizzleDomainEventRepository } from "../repositories/drizzle/domain-event.repo.js";
18
20
  import { DrizzleEntityRepository } from "../repositories/drizzle/entity.repo.js";
19
21
  import { DrizzleEventRepository } from "../repositories/drizzle/event.repo.js";
20
22
  import { DrizzleFlowRepository } from "../repositories/drizzle/flow.repo.js";
@@ -162,12 +164,14 @@ program
162
164
  const invocationRepo = new DrizzleInvocationRepository(db);
163
165
  const gateRepo = new DrizzleGateRepository(db);
164
166
  const transitionLogRepo = new DrizzleTransitionLogRepository(db);
167
+ const domainEventRepo = new DrizzleDomainEventRepository(db);
165
168
  const eventEmitter = new EventEmitter();
166
169
  eventEmitter.register({
167
170
  emit: async (event) => {
168
171
  process.stderr.write(`[event] ${event.type} ${JSON.stringify(event)}\n`);
169
172
  },
170
173
  });
174
+ eventEmitter.register(new DomainEventPersistAdapter(domainEventRepo));
171
175
  const engine = new Engine({
172
176
  entityRepo,
173
177
  flowRepo,
@@ -185,6 +189,7 @@ program
185
189
  gates: gateRepo,
186
190
  transitions: transitionLogRepo,
187
191
  eventRepo: new DrizzleEventRepository(db),
192
+ domainEvents: domainEventRepo,
188
193
  engine,
189
194
  withTransaction: (fn) => withTransaction(sqlite, fn),
190
195
  };
@@ -1,7 +1,7 @@
1
1
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
2
  import type { Engine } from "../engine/engine.js";
3
3
  import type { Logger } from "../logger.js";
4
- import type { IEntityRepository, IEventRepository, IFlowRepository, IGateRepository, IInvocationRepository, ITransitionLogRepository } from "../repositories/interfaces.js";
4
+ import type { IDomainEventRepository, IEntityRepository, IEventRepository, IFlowRepository, IGateRepository, IInvocationRepository, ITransitionLogRepository } from "../repositories/interfaces.js";
5
5
  export interface McpServerDeps {
6
6
  entities: IEntityRepository;
7
7
  flows: IFlowRepository;
@@ -9,6 +9,7 @@ export interface McpServerDeps {
9
9
  gates: IGateRepository;
10
10
  transitions: ITransitionLogRepository;
11
11
  eventRepo: IEventRepository;
12
+ domainEvents?: IDomainEventRepository;
12
13
  engine?: Engine;
13
14
  logger?: Logger;
14
15
  withTransaction?: <T>(fn: () => T | Promise<T>) => Promise<T>;
@@ -6,7 +6,7 @@ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprot
6
6
  import { DEFAULT_TIMEOUT_PROMPT } from "../engine/constants.js";
7
7
  import { isTerminal } from "../engine/state-machine.js";
8
8
  import { consoleLogger } from "../logger.js";
9
- import { AdminEntityCancelSchema, AdminEntityMigrateSchema, AdminEntityResetSchema, AdminFlowCreateSchema, AdminFlowPauseSchema, AdminFlowRestoreSchema, AdminFlowSnapshotSchema, AdminFlowUpdateSchema, AdminGateAttachSchema, AdminGateCreateSchema, AdminGateRerunSchema, AdminStateCreateSchema, AdminStateUpdateSchema, AdminTransitionCreateSchema, AdminTransitionUpdateSchema, AdminWorkerDrainSchema, } from "./admin-schemas.js";
9
+ import { AdminEntityCancelSchema, AdminEntityMigrateSchema, AdminEntityResetSchema, AdminEventsListSchema, AdminFlowCreateSchema, AdminFlowPauseSchema, AdminFlowRestoreSchema, AdminFlowSnapshotSchema, AdminFlowUpdateSchema, AdminGateAttachSchema, AdminGateCreateSchema, AdminGateRerunSchema, AdminStateCreateSchema, AdminStateUpdateSchema, AdminTransitionCreateSchema, AdminTransitionUpdateSchema, AdminWorkerDrainSchema, } from "./admin-schemas.js";
10
10
  import { handleFlowClaim } from "./handlers/flow.js";
11
11
  import { FlowFailSchema, FlowGetPromptSchema, FlowReportSchema, FlowSeedSchema, QueryEntitiesSchema, QueryEntitySchema, QueryFlowSchema, QueryInvocationsSchema, } from "./tool-schemas.js";
12
12
  function getSystemDefaultGateTimeoutMs() {
@@ -394,6 +394,19 @@ const TOOL_DEFINITIONS = [
394
394
  required: ["entity_id", "gate_name"],
395
395
  },
396
396
  },
397
+ {
398
+ name: "admin.events.list",
399
+ description: "List domain events for an entity. Returns append-only audit log entries ordered by sequence.",
400
+ inputSchema: {
401
+ type: "object",
402
+ properties: {
403
+ entity_id: { type: "string", description: "Entity ID to list events for" },
404
+ type: { type: "string", description: "Optional event type filter (e.g. 'entity.transitioned')" },
405
+ limit: { type: "number", description: "Max events to return (default 100, max 500)" },
406
+ },
407
+ required: ["entity_id"],
408
+ },
409
+ },
397
410
  ];
398
411
  export function createMcpServer(deps, opts) {
399
412
  const server = new Server({ name: "defcon", version: "0.1.0" }, { capabilities: { tools: {} } });
@@ -486,6 +499,8 @@ export async function callToolHandler(deps, name, safeArgs, opts) {
486
499
  return await handleAdminWorkerUndrain(deps, safeArgs);
487
500
  case "admin.gate.rerun":
488
501
  return await handleAdminGateRerun(deps, safeArgs);
502
+ case "admin.events.list":
503
+ return await handleAdminEventsList(deps, safeArgs);
489
504
  default:
490
505
  return errorResult(`Unknown tool: ${name}`);
491
506
  }
@@ -1121,3 +1136,16 @@ async function handleAdminFlowRestore(deps, args) {
1121
1136
  emitDefinitionChanged(deps.eventRepo, flow.id, "admin.flow.restore", { version: v.data.version });
1122
1137
  return jsonResult({ restored: true, version: v.data.version });
1123
1138
  }
1139
+ async function handleAdminEventsList(deps, args) {
1140
+ if (!deps.domainEvents) {
1141
+ return errorResult("Domain events repository not available");
1142
+ }
1143
+ const parsed = validateInput(AdminEventsListSchema, args);
1144
+ if (!parsed.ok)
1145
+ return parsed.result;
1146
+ const events = await deps.domainEvents.list(parsed.data.entity_id, {
1147
+ type: parsed.data.type,
1148
+ limit: parsed.data.limit,
1149
+ });
1150
+ return { content: [{ type: "text", text: JSON.stringify(events, null, 2) }] };
1151
+ }
@@ -0,0 +1,14 @@
1
+ import type { BetterSQLite3Database } from "drizzle-orm/better-sqlite3";
2
+ import type { DomainEvent, IDomainEventRepository } from "../interfaces.js";
3
+ import type * as schema from "./schema.js";
4
+ type Db = BetterSQLite3Database<typeof schema>;
5
+ export declare class DrizzleDomainEventRepository implements IDomainEventRepository {
6
+ private readonly db;
7
+ constructor(db: Db);
8
+ append(type: string, entityId: string, payload: Record<string, unknown>): Promise<DomainEvent>;
9
+ list(entityId: string, opts?: {
10
+ type?: string;
11
+ limit?: number;
12
+ }): Promise<DomainEvent[]>;
13
+ }
14
+ export {};
@@ -0,0 +1,44 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { and, eq, sql } from "drizzle-orm";
3
+ import { domainEvents } from "./schema.js";
4
+ export class DrizzleDomainEventRepository {
5
+ db;
6
+ constructor(db) {
7
+ this.db = db;
8
+ }
9
+ async append(type, entityId, payload) {
10
+ return this.db.transaction((tx) => {
11
+ const maxRow = tx
12
+ .select({ maxSeq: sql `coalesce(max(${domainEvents.sequence}), 0)` })
13
+ .from(domainEvents)
14
+ .where(eq(domainEvents.entityId, entityId))
15
+ .get();
16
+ const sequence = (maxRow?.maxSeq ?? 0) + 1;
17
+ const id = randomUUID();
18
+ const emittedAt = Date.now();
19
+ tx.insert(domainEvents).values({ id, type, entityId, payload, sequence, emittedAt }).run();
20
+ return { id, type, entityId, payload, sequence, emittedAt };
21
+ });
22
+ }
23
+ async list(entityId, opts) {
24
+ const conditions = [eq(domainEvents.entityId, entityId)];
25
+ if (opts?.type) {
26
+ conditions.push(eq(domainEvents.type, opts.type));
27
+ }
28
+ const rows = this.db
29
+ .select()
30
+ .from(domainEvents)
31
+ .where(and(...conditions))
32
+ .orderBy(domainEvents.sequence)
33
+ .limit(opts?.limit ?? 100)
34
+ .all();
35
+ return rows.map((r) => ({
36
+ id: r.id,
37
+ type: r.type,
38
+ entityId: r.entityId,
39
+ payload: r.payload,
40
+ sequence: r.sequence,
41
+ emittedAt: r.emittedAt,
42
+ }));
43
+ }
44
+ }
@@ -1,3 +1,4 @@
1
+ export { DrizzleDomainEventRepository } from "./domain-event.repo.js";
1
2
  export { DrizzleEntityRepository } from "./entity.repo.js";
2
3
  export { DrizzleEventRepository } from "./event.repo.js";
3
4
  export { DrizzleFlowRepository } from "./flow.repo.js";
@@ -1,4 +1,5 @@
1
1
  // Drizzle ORM implementations of repository interfaces
2
+ export { DrizzleDomainEventRepository } from "./domain-event.repo.js";
2
3
  export { DrizzleEntityRepository } from "./entity.repo.js";
3
4
  export { DrizzleEventRepository } from "./event.repo.js";
4
5
  export { DrizzleFlowRepository } from "./flow.repo.js";
@@ -2034,3 +2034,118 @@ export declare const events: import("drizzle-orm/sqlite-core").SQLiteTableWithCo
2034
2034
  };
2035
2035
  dialect: "sqlite";
2036
2036
  }>;
2037
+ export declare const domainEvents: import("drizzle-orm/sqlite-core").SQLiteTableWithColumns<{
2038
+ name: "domain_events";
2039
+ schema: undefined;
2040
+ columns: {
2041
+ id: import("drizzle-orm/sqlite-core").SQLiteColumn<{
2042
+ name: "id";
2043
+ tableName: "domain_events";
2044
+ dataType: "string";
2045
+ columnType: "SQLiteText";
2046
+ data: string;
2047
+ driverParam: string;
2048
+ notNull: true;
2049
+ hasDefault: false;
2050
+ isPrimaryKey: true;
2051
+ isAutoincrement: false;
2052
+ hasRuntimeDefault: false;
2053
+ enumValues: [string, ...string[]];
2054
+ baseColumn: never;
2055
+ identity: undefined;
2056
+ generated: undefined;
2057
+ }, {}, {
2058
+ length: number | undefined;
2059
+ }>;
2060
+ type: import("drizzle-orm/sqlite-core").SQLiteColumn<{
2061
+ name: "type";
2062
+ tableName: "domain_events";
2063
+ dataType: "string";
2064
+ columnType: "SQLiteText";
2065
+ data: string;
2066
+ driverParam: string;
2067
+ notNull: true;
2068
+ hasDefault: false;
2069
+ isPrimaryKey: false;
2070
+ isAutoincrement: false;
2071
+ hasRuntimeDefault: false;
2072
+ enumValues: [string, ...string[]];
2073
+ baseColumn: never;
2074
+ identity: undefined;
2075
+ generated: undefined;
2076
+ }, {}, {
2077
+ length: number | undefined;
2078
+ }>;
2079
+ entityId: import("drizzle-orm/sqlite-core").SQLiteColumn<{
2080
+ name: "entity_id";
2081
+ tableName: "domain_events";
2082
+ dataType: "string";
2083
+ columnType: "SQLiteText";
2084
+ data: string;
2085
+ driverParam: string;
2086
+ notNull: true;
2087
+ hasDefault: false;
2088
+ isPrimaryKey: false;
2089
+ isAutoincrement: false;
2090
+ hasRuntimeDefault: false;
2091
+ enumValues: [string, ...string[]];
2092
+ baseColumn: never;
2093
+ identity: undefined;
2094
+ generated: undefined;
2095
+ }, {}, {
2096
+ length: number | undefined;
2097
+ }>;
2098
+ payload: import("drizzle-orm/sqlite-core").SQLiteColumn<{
2099
+ name: "payload";
2100
+ tableName: "domain_events";
2101
+ dataType: "json";
2102
+ columnType: "SQLiteTextJson";
2103
+ data: unknown;
2104
+ driverParam: string;
2105
+ notNull: true;
2106
+ hasDefault: false;
2107
+ isPrimaryKey: false;
2108
+ isAutoincrement: false;
2109
+ hasRuntimeDefault: false;
2110
+ enumValues: undefined;
2111
+ baseColumn: never;
2112
+ identity: undefined;
2113
+ generated: undefined;
2114
+ }, {}, {}>;
2115
+ sequence: import("drizzle-orm/sqlite-core").SQLiteColumn<{
2116
+ name: "sequence";
2117
+ tableName: "domain_events";
2118
+ dataType: "number";
2119
+ columnType: "SQLiteInteger";
2120
+ data: number;
2121
+ driverParam: number;
2122
+ notNull: true;
2123
+ hasDefault: false;
2124
+ isPrimaryKey: false;
2125
+ isAutoincrement: false;
2126
+ hasRuntimeDefault: false;
2127
+ enumValues: undefined;
2128
+ baseColumn: never;
2129
+ identity: undefined;
2130
+ generated: undefined;
2131
+ }, {}, {}>;
2132
+ emittedAt: import("drizzle-orm/sqlite-core").SQLiteColumn<{
2133
+ name: "emitted_at";
2134
+ tableName: "domain_events";
2135
+ dataType: "number";
2136
+ columnType: "SQLiteInteger";
2137
+ data: number;
2138
+ driverParam: number;
2139
+ notNull: true;
2140
+ hasDefault: false;
2141
+ isPrimaryKey: false;
2142
+ isAutoincrement: false;
2143
+ hasRuntimeDefault: false;
2144
+ enumValues: undefined;
2145
+ baseColumn: never;
2146
+ identity: undefined;
2147
+ generated: undefined;
2148
+ }, {}, {}>;
2149
+ };
2150
+ dialect: "sqlite";
2151
+ }>;
@@ -162,3 +162,14 @@ export const events = sqliteTable("events", {
162
162
  entityIdIdx: index("events_entity_id_idx").on(table.entityId),
163
163
  emittedAtIdx: index("events_emitted_at_idx").on(table.emittedAt),
164
164
  }));
165
+ export const domainEvents = sqliteTable("domain_events", {
166
+ id: text("id").primaryKey(),
167
+ type: text("type").notNull(),
168
+ entityId: text("entity_id").notNull(),
169
+ payload: text("payload", { mode: "json" }).notNull(),
170
+ sequence: integer("sequence").notNull(),
171
+ emittedAt: integer("emitted_at").notNull(),
172
+ }, (table) => ({
173
+ entitySeqIdx: uniqueIndex("domain_events_entity_seq_idx").on(table.entityId, table.sequence),
174
+ typeIdx: index("domain_events_type_idx").on(table.type, table.emittedAt),
175
+ }));
@@ -356,6 +356,25 @@ export interface IEventRepository {
356
356
  /** Get the most recent events across all entities, ordered by emittedAt descending. */
357
357
  findRecent(limit?: number): Promise<EventRow[]>;
358
358
  }
359
+ /** A persisted domain event from the append-only audit log. */
360
+ export interface DomainEvent {
361
+ id: string;
362
+ type: string;
363
+ entityId: string;
364
+ payload: Record<string, unknown>;
365
+ sequence: number;
366
+ emittedAt: number;
367
+ }
368
+ /** Data-access contract for the append-only domain_events table. */
369
+ export interface IDomainEventRepository {
370
+ /** Append a domain event. Sequence is computed as max(sequence)+1 for the entity. */
371
+ append(type: string, entityId: string, payload: Record<string, unknown>): Promise<DomainEvent>;
372
+ /** List domain events for an entity, optionally filtered by type, ordered by sequence ascending. */
373
+ list(entityId: string, opts?: {
374
+ type?: string;
375
+ limit?: number;
376
+ }): Promise<DomainEvent[]>;
377
+ }
359
378
  /** Data-access contract for gate definitions and result recording. */
360
379
  export interface IGateRepository {
361
380
  /** Create a new gate definition. */
@@ -0,0 +1,11 @@
1
+ CREATE TABLE `domain_events` (
2
+ `id` text PRIMARY KEY NOT NULL,
3
+ `type` text NOT NULL,
4
+ `entity_id` text NOT NULL,
5
+ `payload` text NOT NULL,
6
+ `sequence` integer NOT NULL,
7
+ `emitted_at` integer NOT NULL
8
+ );
9
+ --> statement-breakpoint
10
+ CREATE UNIQUE INDEX `domain_events_entity_seq_idx` ON `domain_events` (`entity_id`,`sequence`);--> statement-breakpoint
11
+ CREATE INDEX `domain_events_type_idx` ON `domain_events` (`type`,`emitted_at`);