mcp-agent-broker 1.0.0 → 1.0.1
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.
- package/README.md +59 -13
- package/dist/infrastructure/db/schema.js +11 -1
- package/dist/modules/identity/agent.service.js +3 -3
- package/dist/transport/mcp/index.js +21 -2
- package/package.json +4 -3
- package/seed_api_key.ts +30 -0
- package/docker-compose.yml +0 -41
- package/drizzle.config.ts +0 -10
- package/src/app.ts +0 -31
- package/src/infrastructure/db/index.ts +0 -10
- package/src/infrastructure/db/migrations/0000_flawless_vanisher.sql +0 -103
- package/src/infrastructure/db/migrations/0001_fearless_sersi.sql +0 -36
- package/src/infrastructure/db/migrations/meta/0000_snapshot.json +0 -714
- package/src/infrastructure/db/migrations/meta/0001_snapshot.json +0 -947
- package/src/infrastructure/db/migrations/meta/_journal.json +0 -20
- package/src/infrastructure/db/schema.ts +0 -148
- package/src/infrastructure/redis/index.ts +0 -7
- package/src/modules/event/event.service.ts +0 -50
- package/src/modules/identity/agent.service.ts +0 -58
- package/src/modules/lease/lease.service.ts +0 -89
- package/src/modules/task/task.service.ts +0 -46
- package/src/transport/mcp/index.ts +0 -167
- package/src/workers/outbox-relay.ts +0 -61
- package/test_client.ts +0 -34
- package/test_task.ts +0 -68
- package/tsconfig.json +0 -14
package/README.md
CHANGED
|
@@ -1,31 +1,77 @@
|
|
|
1
1
|
# MCP Agent Broker
|
|
2
2
|
|
|
3
|
-
The Model Context Protocol (MCP) Agent Broker is a centralized service designed to orchestrate and manage autonomous coding agents.
|
|
3
|
+
The Model Context Protocol (MCP) Agent Broker is a centralized service designed to orchestrate and manage autonomous coding agents.
|
|
4
|
+
|
|
5
|
+
It acts as a middleware that securely maps AI Agents (Principals) to tasks, enforcing distributed locks (Fencing Tokens) so multiple agents don't step on each other's toes when working on the same repository.
|
|
4
6
|
|
|
5
7
|
## Features
|
|
6
8
|
* **MCP Server Integration:** Natively implements the MCP SDK via Server-Sent Events (SSE).
|
|
9
|
+
* **Secure Authentication:** Validates API keys against securely hashed database records.
|
|
7
10
|
* **Distributed Locking:** Safely hand off tasks between implementer agents and reviewer agents using fencing tokens.
|
|
8
11
|
* **Transactional Outbox:** Guaranteed delivery of domain events without dual-write issues.
|
|
9
12
|
* **Event Relay:** Real-time event broadcasting over Redis Pub/Sub.
|
|
10
13
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
* Redis
|
|
15
|
-
* MinIO
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Getting Started
|
|
16
17
|
|
|
17
|
-
|
|
18
|
+
### 1. Start the Infrastructure
|
|
19
|
+
The broker requires PostgreSQL and Redis. We provide a `docker-compose.yml` to spin up everything (including the broker itself) instantly.
|
|
18
20
|
|
|
19
|
-
1. Start the infrastructure:
|
|
20
21
|
```bash
|
|
21
22
|
docker compose up -d
|
|
22
23
|
```
|
|
24
|
+
*Note: If you are doing local development, you can run `docker compose up -d postgres redis` and then run the app locally using `npm run dev`.*
|
|
25
|
+
|
|
26
|
+
### 2. Generate an API Key
|
|
27
|
+
To connect to the broker, your Agent must have an API Key. We provide a seed script to generate your first Workspace, Principal, and API Key.
|
|
23
28
|
|
|
24
|
-
2. Run the server:
|
|
25
29
|
```bash
|
|
26
|
-
|
|
30
|
+
npx tsx seed_api_key.ts
|
|
27
31
|
```
|
|
28
|
-
|
|
32
|
+
*Take note of the API Key (e.g. `test-api-key-123`) output by the terminal.*
|
|
33
|
+
|
|
34
|
+
### 3. Connect your Agents
|
|
35
|
+
Point your MCP-compatible client to the broker's endpoint and pass your API key as a Bearer token.
|
|
36
|
+
|
|
37
|
+
**Programmatic Example (TypeScript):**
|
|
38
|
+
```typescript
|
|
39
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
40
|
+
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
41
|
+
|
|
42
|
+
const transport = new SSEClientTransport(new URL("http://localhost:3000/mcp/sse"), {
|
|
43
|
+
eventSourceInit: {
|
|
44
|
+
headers: { Authorization: "Bearer test-api-key-123" }
|
|
45
|
+
} as any,
|
|
46
|
+
requestInit: {
|
|
47
|
+
headers: { Authorization: "Bearer test-api-key-123" }
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const client = new Client({ name: "my-agent", version: "1.0.0" }, { capabilities: {} });
|
|
52
|
+
await client.connect(transport);
|
|
53
|
+
|
|
54
|
+
// Register your agent!
|
|
55
|
+
await client.request({
|
|
56
|
+
method: "tools/call",
|
|
57
|
+
params: {
|
|
58
|
+
name: "register_agent",
|
|
59
|
+
arguments: {
|
|
60
|
+
workspaceId: "00000000-0000-0000-0000-000000000000",
|
|
61
|
+
name: "My Implementer Agent",
|
|
62
|
+
version: "1.0",
|
|
63
|
+
capabilities: ["execute"]
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}, CallToolResultSchema);
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### 4. Listen to Real-Time Events
|
|
70
|
+
Any downstream CI dashboard or review agent can connect to the Redis server and subscribe to `workspace:{workspace_id}:events` to see real-time updates as tasks are created and leases are acquired!
|
|
71
|
+
|
|
72
|
+
---
|
|
29
73
|
|
|
30
|
-
|
|
31
|
-
|
|
74
|
+
## Future Improvements for Contributors
|
|
75
|
+
* Implement standard OAuth2 / OIDC for user-facing dashboards.
|
|
76
|
+
* Extend the `Review Module` to enforce reviewer sign-offs before pushing to main branches.
|
|
77
|
+
* Implement the Fencing Token check in a Git Hook or CI/CD layer.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.idempotencyRecords = exports.outbox = exports.events = exports.leases = exports.taskAttempts = exports.tasks = exports.agentInstances = exports.agentDefinitions = exports.workspaces = exports.leaseTypeEnum = exports.leaseStatusEnum = exports.agentStatusEnum = exports.workspaceStatusEnum = exports.taskStateEnum = void 0;
|
|
3
|
+
exports.idempotencyRecords = exports.outbox = exports.events = exports.leases = exports.taskAttempts = exports.tasks = exports.agentInstances = exports.agentDefinitions = exports.apiKeys = exports.principals = exports.workspaces = exports.leaseTypeEnum = exports.leaseStatusEnum = exports.agentStatusEnum = exports.workspaceStatusEnum = exports.taskStateEnum = void 0;
|
|
4
4
|
const pg_core_1 = require("drizzle-orm/pg-core");
|
|
5
5
|
exports.taskStateEnum = (0, pg_core_1.pgEnum)('task_state', [
|
|
6
6
|
'draft', 'ready', 'leased', 'running', 'blocked',
|
|
@@ -19,6 +19,16 @@ exports.workspaces = (0, pg_core_1.pgTable)('workspaces', {
|
|
|
19
19
|
createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
20
20
|
updatedAt: (0, pg_core_1.timestamp)('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
|
21
21
|
});
|
|
22
|
+
exports.principals = (0, pg_core_1.pgTable)('principals', {
|
|
23
|
+
id: (0, pg_core_1.uuid)('id').primaryKey(),
|
|
24
|
+
name: (0, pg_core_1.text)('name').notNull(),
|
|
25
|
+
createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
26
|
+
});
|
|
27
|
+
exports.apiKeys = (0, pg_core_1.pgTable)('api_keys', {
|
|
28
|
+
keyHash: (0, pg_core_1.text)('key_hash').primaryKey(),
|
|
29
|
+
principalId: (0, pg_core_1.uuid)('principal_id').notNull().references(() => exports.principals.id),
|
|
30
|
+
createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
31
|
+
});
|
|
22
32
|
exports.agentDefinitions = (0, pg_core_1.pgTable)('agent_definitions', {
|
|
23
33
|
id: (0, pg_core_1.uuid)('id').primaryKey(),
|
|
24
34
|
name: (0, pg_core_1.text)('name').notNull(),
|
|
@@ -17,7 +17,7 @@ class AgentService {
|
|
|
17
17
|
return workspace;
|
|
18
18
|
}
|
|
19
19
|
async registerAgent(params) {
|
|
20
|
-
const workspace = await this.ensureDefaultWorkspace();
|
|
20
|
+
const workspace = await this.ensureDefaultWorkspace(); // Fallback logic or just rely on params
|
|
21
21
|
// 1. Create or find agent definition
|
|
22
22
|
let [definition] = await index_js_1.db.select().from(schema_js_1.agentDefinitions)
|
|
23
23
|
.where((0, drizzle_orm_1.eq)(schema_js_1.agentDefinitions.name, params.name))
|
|
@@ -44,8 +44,8 @@ class AgentService {
|
|
|
44
44
|
const [instance] = await index_js_1.db.insert(schema_js_1.agentInstances).values({
|
|
45
45
|
id: (0, crypto_1.randomUUID)(),
|
|
46
46
|
agentDefinitionId: definition.id,
|
|
47
|
-
workspaceId:
|
|
48
|
-
principalId:
|
|
47
|
+
workspaceId: params.workspaceId,
|
|
48
|
+
principalId: params.principalId,
|
|
49
49
|
capabilitiesJson: params.capabilities,
|
|
50
50
|
status: "connected",
|
|
51
51
|
}).returning();
|
|
@@ -4,6 +4,10 @@ exports.setupMcpTransport = setupMcpTransport;
|
|
|
4
4
|
const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
|
|
5
5
|
const sse_js_1 = require("@modelcontextprotocol/sdk/server/sse.js");
|
|
6
6
|
const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
|
|
7
|
+
const crypto_1 = require("crypto");
|
|
8
|
+
const drizzle_orm_1 = require("drizzle-orm");
|
|
9
|
+
const index_js_2 = require("../../infrastructure/db/index.js");
|
|
10
|
+
const schema_js_1 = require("../../infrastructure/db/schema.js");
|
|
7
11
|
const agent_service_js_1 = require("../../modules/identity/agent.service.js");
|
|
8
12
|
const task_service_js_1 = require("../../modules/task/task.service.js");
|
|
9
13
|
const lease_service_js_1 = require("../../modules/lease/lease.service.js");
|
|
@@ -11,6 +15,19 @@ async function setupMcpTransport(fastify) {
|
|
|
11
15
|
// Store transports for the POST /mcp/message route
|
|
12
16
|
const transports = new Map();
|
|
13
17
|
fastify.get("/mcp/sse", async (request, reply) => {
|
|
18
|
+
const authHeader = request.headers.authorization;
|
|
19
|
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
20
|
+
reply.status(401).send({ error: "Missing or invalid Authorization header" });
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const token = authHeader.split(' ')[1];
|
|
24
|
+
const keyHash = (0, crypto_1.createHash)('sha256').update(token).digest('hex');
|
|
25
|
+
const [apiKeyRecord] = await index_js_2.db.select().from(schema_js_1.apiKeys).where((0, drizzle_orm_1.eq)(schema_js_1.apiKeys.keyHash, keyHash));
|
|
26
|
+
if (!apiKeyRecord) {
|
|
27
|
+
reply.status(401).send({ error: "Invalid API Key" });
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const principalId = apiKeyRecord.principalId;
|
|
14
31
|
// Hijack the Fastify response so we can stream SSE headers manually
|
|
15
32
|
reply.hijack();
|
|
16
33
|
const transport = new sse_js_1.SSEServerTransport("/mcp/message", reply.raw);
|
|
@@ -75,7 +92,9 @@ async function setupMcpTransport(fastify) {
|
|
|
75
92
|
if (req.params.name === "register_agent") {
|
|
76
93
|
try {
|
|
77
94
|
const args = req.params.arguments;
|
|
78
|
-
const
|
|
95
|
+
const agentInstance = await agent_service_js_1.agentService.registerAgent({
|
|
96
|
+
workspaceId: args.workspaceId || (0, crypto_1.randomUUID)(), // Temporary fallback
|
|
97
|
+
principalId: principalId,
|
|
79
98
|
name: args.name,
|
|
80
99
|
version: args.version,
|
|
81
100
|
capabilities: args.capabilities,
|
|
@@ -84,7 +103,7 @@ async function setupMcpTransport(fastify) {
|
|
|
84
103
|
content: [
|
|
85
104
|
{
|
|
86
105
|
type: "text",
|
|
87
|
-
text: `Agent registered successfully! Instance ID: ${
|
|
106
|
+
text: `Agent registered successfully! Instance ID: ${agentInstance.id}`
|
|
88
107
|
}
|
|
89
108
|
]
|
|
90
109
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-agent-broker",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"main": "dist/app.js",
|
|
5
5
|
"bin": {
|
|
6
6
|
"mcp-agent-broker": "dist/app.js"
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"build": "tsc",
|
|
10
10
|
"start": "node dist/app.js",
|
|
11
11
|
"dev": "tsx watch src/app.ts",
|
|
12
|
-
"test": "
|
|
12
|
+
"test": "vitest run"
|
|
13
13
|
},
|
|
14
14
|
"keywords": [],
|
|
15
15
|
"author": "",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"@types/pg": "^8.20.0",
|
|
31
31
|
"drizzle-kit": "^0.31.10",
|
|
32
32
|
"tsx": "^4.23.1",
|
|
33
|
-
"typescript": "^7.0.2"
|
|
33
|
+
"typescript": "^7.0.2",
|
|
34
|
+
"vitest": "^4.1.10"
|
|
34
35
|
}
|
|
35
36
|
}
|
package/seed_api_key.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { db } from './src/infrastructure/db/index.js';
|
|
2
|
+
import { principals, apiKeys, workspaces } from './src/infrastructure/db/schema.js';
|
|
3
|
+
import { randomUUID, createHash } from 'crypto';
|
|
4
|
+
|
|
5
|
+
async function seed() {
|
|
6
|
+
const principalId = randomUUID();
|
|
7
|
+
const rawKey = 'test-api-key-123';
|
|
8
|
+
const keyHash = createHash('sha256').update(rawKey).digest('hex');
|
|
9
|
+
|
|
10
|
+
const workspaceId = '00000000-0000-0000-0000-000000000000';
|
|
11
|
+
await db.insert(workspaces).values({
|
|
12
|
+
id: workspaceId,
|
|
13
|
+
name: 'Test Workspace',
|
|
14
|
+
}).onConflictDoNothing();
|
|
15
|
+
|
|
16
|
+
await db.insert(principals).values({
|
|
17
|
+
id: principalId,
|
|
18
|
+
name: 'Test Service Account',
|
|
19
|
+
}).onConflictDoNothing();
|
|
20
|
+
|
|
21
|
+
await db.insert(apiKeys).values({
|
|
22
|
+
keyHash,
|
|
23
|
+
principalId,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
console.log('Seeded principal:', principalId);
|
|
27
|
+
console.log('API Key:', rawKey);
|
|
28
|
+
process.exit(0);
|
|
29
|
+
}
|
|
30
|
+
seed().catch(console.error);
|
package/docker-compose.yml
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
services:
|
|
2
|
-
postgres:
|
|
3
|
-
image: postgres:16-alpine
|
|
4
|
-
container_name: mab-postgres
|
|
5
|
-
environment:
|
|
6
|
-
POSTGRES_USER: mab_user
|
|
7
|
-
POSTGRES_PASSWORD: mab_password
|
|
8
|
-
POSTGRES_DB: mab_db
|
|
9
|
-
ports:
|
|
10
|
-
- "5433:5432"
|
|
11
|
-
volumes:
|
|
12
|
-
- postgres_data:/var/lib/postgresql/data
|
|
13
|
-
restart: unless-stopped
|
|
14
|
-
|
|
15
|
-
redis:
|
|
16
|
-
image: redis:7-alpine
|
|
17
|
-
container_name: mab-redis
|
|
18
|
-
ports:
|
|
19
|
-
- "6379:6379"
|
|
20
|
-
volumes:
|
|
21
|
-
- redis_data:/data
|
|
22
|
-
restart: unless-stopped
|
|
23
|
-
|
|
24
|
-
minio:
|
|
25
|
-
image: minio/minio:latest
|
|
26
|
-
container_name: mab-minio
|
|
27
|
-
environment:
|
|
28
|
-
MINIO_ROOT_USER: mab_admin
|
|
29
|
-
MINIO_ROOT_PASSWORD: mab_password123
|
|
30
|
-
command: server /data --console-address ":9001"
|
|
31
|
-
ports:
|
|
32
|
-
- "9000:9000"
|
|
33
|
-
- "9001:9001"
|
|
34
|
-
volumes:
|
|
35
|
-
- minio_data:/data
|
|
36
|
-
restart: unless-stopped
|
|
37
|
-
|
|
38
|
-
volumes:
|
|
39
|
-
postgres_data:
|
|
40
|
-
redis_data:
|
|
41
|
-
minio_data:
|
package/drizzle.config.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { defineConfig } from "drizzle-kit";
|
|
2
|
-
|
|
3
|
-
export default defineConfig({
|
|
4
|
-
schema: "./src/infrastructure/db/schema.ts",
|
|
5
|
-
out: "./src/infrastructure/db/migrations",
|
|
6
|
-
dialect: "postgresql",
|
|
7
|
-
dbCredentials: {
|
|
8
|
-
url: process.env.DATABASE_URL || "postgres://mab_user:mab_password@127.0.0.1:5433/mab_db"
|
|
9
|
-
}
|
|
10
|
-
});
|
package/src/app.ts
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import Fastify from 'fastify';
|
|
3
|
-
import { setupMcpTransport } from './transport/mcp/index.js';
|
|
4
|
-
import { db } from './infrastructure/db/index.js';
|
|
5
|
-
import { outboxRelay } from './workers/outbox-relay.js';
|
|
6
|
-
|
|
7
|
-
const fastify = Fastify({
|
|
8
|
-
logger: true
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
fastify.get('/health', async (request, reply) => {
|
|
12
|
-
return { status: 'ok', service: 'mcp-agent-broker' };
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
// Start the server
|
|
16
|
-
const start = async () => {
|
|
17
|
-
try {
|
|
18
|
-
await setupMcpTransport(fastify);
|
|
19
|
-
|
|
20
|
-
// Start background workers
|
|
21
|
-
outboxRelay.start();
|
|
22
|
-
|
|
23
|
-
await fastify.listen({ port: 3000, host: '0.0.0.0' });
|
|
24
|
-
fastify.log.info(`Server listening on ${fastify.server.address()}`);
|
|
25
|
-
} catch (err) {
|
|
26
|
-
fastify.log.error(err);
|
|
27
|
-
process.exit(1);
|
|
28
|
-
}
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
start();
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { drizzle } from 'drizzle-orm/node-postgres';
|
|
2
|
-
import { Pool } from 'pg';
|
|
3
|
-
import * as schema from './schema';
|
|
4
|
-
|
|
5
|
-
// Use a connection pool for the Fastify app
|
|
6
|
-
const pool = new Pool({
|
|
7
|
-
connectionString: process.env.DATABASE_URL || "postgres://mab_user:mab_password@127.0.0.1:5433/mab_db"
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
export const db = drizzle(pool, { schema });
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
CREATE TYPE "public"."lease_status" AS ENUM('active', 'released', 'expired', 'revoked');--> statement-breakpoint
|
|
2
|
-
CREATE TYPE "public"."lease_type" AS ENUM('execution', 'review');--> statement-breakpoint
|
|
3
|
-
CREATE TYPE "public"."task_state" AS ENUM('draft', 'ready', 'leased', 'running', 'blocked', 'awaiting_review', 'changes_requested', 'completed', 'failed', 'canceled', 'superseded');--> statement-breakpoint
|
|
4
|
-
CREATE TABLE "events" (
|
|
5
|
-
"event_seq" bigserial PRIMARY KEY NOT NULL,
|
|
6
|
-
"event_id" uuid NOT NULL,
|
|
7
|
-
"event_type" text NOT NULL,
|
|
8
|
-
"event_version" integer NOT NULL,
|
|
9
|
-
"workspace_id" uuid NOT NULL,
|
|
10
|
-
"aggregate_type" text NOT NULL,
|
|
11
|
-
"aggregate_id" uuid NOT NULL,
|
|
12
|
-
"aggregate_version" bigint NOT NULL,
|
|
13
|
-
"actor_type" text NOT NULL,
|
|
14
|
-
"actor_id" uuid,
|
|
15
|
-
"correlation_id" uuid NOT NULL,
|
|
16
|
-
"causation_id" uuid,
|
|
17
|
-
"payload" jsonb NOT NULL,
|
|
18
|
-
"occurred_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
19
|
-
CONSTRAINT "events_event_id_unique" UNIQUE("event_id"),
|
|
20
|
-
CONSTRAINT "events_aggregate_type_aggregate_id_aggregate_version_unique" UNIQUE("aggregate_type","aggregate_id","aggregate_version")
|
|
21
|
-
);
|
|
22
|
-
--> statement-breakpoint
|
|
23
|
-
CREATE TABLE "idempotency_records" (
|
|
24
|
-
"principal_id" uuid NOT NULL,
|
|
25
|
-
"operation" text NOT NULL,
|
|
26
|
-
"idempotency_key" text NOT NULL,
|
|
27
|
-
"request_hash" text NOT NULL,
|
|
28
|
-
"response_json" jsonb,
|
|
29
|
-
"response_status" text NOT NULL,
|
|
30
|
-
"resource_id" uuid,
|
|
31
|
-
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
32
|
-
"expires_at" timestamp with time zone NOT NULL,
|
|
33
|
-
CONSTRAINT "idempotency_records_principal_id_operation_idempotency_key_pk" PRIMARY KEY("principal_id","operation","idempotency_key")
|
|
34
|
-
);
|
|
35
|
-
--> statement-breakpoint
|
|
36
|
-
CREATE TABLE "leases" (
|
|
37
|
-
"id" uuid PRIMARY KEY NOT NULL,
|
|
38
|
-
"workspace_id" uuid NOT NULL,
|
|
39
|
-
"task_id" uuid NOT NULL,
|
|
40
|
-
"task_attempt_id" uuid,
|
|
41
|
-
"agent_instance_id" uuid NOT NULL,
|
|
42
|
-
"lease_type" "lease_type" NOT NULL,
|
|
43
|
-
"status" "lease_status" NOT NULL,
|
|
44
|
-
"fencing_token" bigint NOT NULL,
|
|
45
|
-
"acquired_at" timestamp with time zone NOT NULL,
|
|
46
|
-
"renewed_at" timestamp with time zone NOT NULL,
|
|
47
|
-
"expires_at" timestamp with time zone NOT NULL,
|
|
48
|
-
"released_at" timestamp with time zone,
|
|
49
|
-
CONSTRAINT "leases_task_id_lease_type_fencing_token_unique" UNIQUE("task_id","lease_type","fencing_token")
|
|
50
|
-
);
|
|
51
|
-
--> statement-breakpoint
|
|
52
|
-
CREATE TABLE "outbox" (
|
|
53
|
-
"event_id" uuid PRIMARY KEY NOT NULL,
|
|
54
|
-
"available_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
55
|
-
"attempts" integer DEFAULT 0 NOT NULL,
|
|
56
|
-
"locked_by" text,
|
|
57
|
-
"locked_until" timestamp with time zone,
|
|
58
|
-
"published_at" timestamp with time zone,
|
|
59
|
-
"last_error" text,
|
|
60
|
-
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
|
61
|
-
);
|
|
62
|
-
--> statement-breakpoint
|
|
63
|
-
CREATE TABLE "task_attempts" (
|
|
64
|
-
"id" uuid PRIMARY KEY NOT NULL,
|
|
65
|
-
"workspace_id" uuid NOT NULL,
|
|
66
|
-
"task_id" uuid NOT NULL,
|
|
67
|
-
"attempt_number" integer NOT NULL,
|
|
68
|
-
"agent_instance_id" uuid NOT NULL,
|
|
69
|
-
"status" text NOT NULL,
|
|
70
|
-
"started_at" timestamp with time zone,
|
|
71
|
-
"ended_at" timestamp with time zone,
|
|
72
|
-
"outcome" text,
|
|
73
|
-
CONSTRAINT "task_attempts_task_id_attempt_number_unique" UNIQUE("task_id","attempt_number")
|
|
74
|
-
);
|
|
75
|
-
--> statement-breakpoint
|
|
76
|
-
CREATE TABLE "tasks" (
|
|
77
|
-
"id" uuid PRIMARY KEY NOT NULL,
|
|
78
|
-
"workspace_id" uuid NOT NULL,
|
|
79
|
-
"repository_id" uuid NOT NULL,
|
|
80
|
-
"session_id" uuid,
|
|
81
|
-
"objective" text NOT NULL,
|
|
82
|
-
"description" text DEFAULT '' NOT NULL,
|
|
83
|
-
"acceptance_criteria" jsonb DEFAULT '[]' NOT NULL,
|
|
84
|
-
"constraints" jsonb DEFAULT '[]' NOT NULL,
|
|
85
|
-
"priority" smallint DEFAULT 3 NOT NULL,
|
|
86
|
-
"state" "task_state" DEFAULT 'draft' NOT NULL,
|
|
87
|
-
"base_commit_sha" text,
|
|
88
|
-
"active_execution_lease_id" uuid,
|
|
89
|
-
"active_review_lease_id" uuid,
|
|
90
|
-
"next_fencing_token" bigint DEFAULT 0 NOT NULL,
|
|
91
|
-
"version" bigint DEFAULT 1 NOT NULL,
|
|
92
|
-
"created_by" uuid NOT NULL,
|
|
93
|
-
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
94
|
-
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
95
|
-
"completed_at" timestamp with time zone
|
|
96
|
-
);
|
|
97
|
-
--> statement-breakpoint
|
|
98
|
-
ALTER TABLE "leases" ADD CONSTRAINT "leases_task_id_tasks_id_fk" FOREIGN KEY ("task_id") REFERENCES "public"."tasks"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
|
99
|
-
ALTER TABLE "leases" ADD CONSTRAINT "leases_task_attempt_id_task_attempts_id_fk" FOREIGN KEY ("task_attempt_id") REFERENCES "public"."task_attempts"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
|
100
|
-
ALTER TABLE "outbox" ADD CONSTRAINT "outbox_event_id_events_event_id_fk" FOREIGN KEY ("event_id") REFERENCES "public"."events"("event_id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
101
|
-
ALTER TABLE "task_attempts" ADD CONSTRAINT "task_attempts_task_id_tasks_id_fk" FOREIGN KEY ("task_id") REFERENCES "public"."tasks"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
|
102
|
-
CREATE INDEX "events_workspace_cursor" ON "events" USING btree ("workspace_id","event_seq");--> statement-breakpoint
|
|
103
|
-
CREATE INDEX "idempotency_expiry" ON "idempotency_records" USING btree ("expires_at");
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
CREATE TYPE "public"."agent_status" AS ENUM('connected', 'degraded', 'disconnected', 'revoked');--> statement-breakpoint
|
|
2
|
-
CREATE TYPE "public"."workspace_status" AS ENUM('active', 'archived');--> statement-breakpoint
|
|
3
|
-
CREATE TABLE "agent_definitions" (
|
|
4
|
-
"id" uuid PRIMARY KEY NOT NULL,
|
|
5
|
-
"name" text NOT NULL,
|
|
6
|
-
"vendor" text,
|
|
7
|
-
"version" text NOT NULL,
|
|
8
|
-
"integration_type" text,
|
|
9
|
-
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
10
|
-
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
|
11
|
-
);
|
|
12
|
-
--> statement-breakpoint
|
|
13
|
-
CREATE TABLE "agent_instances" (
|
|
14
|
-
"id" uuid PRIMARY KEY NOT NULL,
|
|
15
|
-
"agent_definition_id" uuid NOT NULL,
|
|
16
|
-
"workspace_id" uuid NOT NULL,
|
|
17
|
-
"principal_id" uuid NOT NULL,
|
|
18
|
-
"status" "agent_status" DEFAULT 'connected' NOT NULL,
|
|
19
|
-
"capabilities_json" jsonb DEFAULT '[]' NOT NULL,
|
|
20
|
-
"last_heartbeat_at" timestamp with time zone,
|
|
21
|
-
"revoked_at" timestamp with time zone,
|
|
22
|
-
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
23
|
-
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
|
24
|
-
);
|
|
25
|
-
--> statement-breakpoint
|
|
26
|
-
CREATE TABLE "workspaces" (
|
|
27
|
-
"id" uuid PRIMARY KEY NOT NULL,
|
|
28
|
-
"organization_id" uuid,
|
|
29
|
-
"name" text NOT NULL,
|
|
30
|
-
"status" "workspace_status" DEFAULT 'active' NOT NULL,
|
|
31
|
-
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
32
|
-
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
|
33
|
-
);
|
|
34
|
-
--> statement-breakpoint
|
|
35
|
-
ALTER TABLE "agent_instances" ADD CONSTRAINT "agent_instances_agent_definition_id_agent_definitions_id_fk" FOREIGN KEY ("agent_definition_id") REFERENCES "public"."agent_definitions"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
|
36
|
-
ALTER TABLE "agent_instances" ADD CONSTRAINT "agent_instances_workspace_id_workspaces_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspaces"("id") ON DELETE no action ON UPDATE no action;
|