@studio-foundation/api 0.3.0-beta.1 → 0.3.0-beta.5
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/package.json +7 -4
- package/ARCHITECTURE.md +0 -52
- package/src/.gitkeep +0 -0
- package/src/api.ts +0 -8
- package/src/bootstrap.ts +0 -259
- package/src/event-bus.ts +0 -64
- package/src/index.ts +0 -58
- package/src/integration-runtime.ts +0 -180
- package/src/integration-store.ts +0 -125
- package/src/integrations/linear/failure-handler.ts +0 -93
- package/src/integrations/linear/webhook-handler.ts +0 -156
- package/src/integrations/registry.ts +0 -12
- package/src/integrations/types.ts +0 -37
- package/src/launcher.ts +0 -214
- package/src/logger.ts +0 -50
- package/src/plans.ts +0 -43
- package/src/routes/agents.ts +0 -131
- package/src/routes/config.ts +0 -154
- package/src/routes/contracts.ts +0 -205
- package/src/routes/pipelines.ts +0 -146
- package/src/routes/projects.ts +0 -237
- package/src/routes/runs.ts +0 -468
- package/src/routes/skills.ts +0 -136
- package/src/routes/tools.ts +0 -222
- package/src/routes/users.ts +0 -169
- package/src/routes/validate.ts +0 -196
- package/src/routes/webhooks.ts +0 -120
- package/src/server.ts +0 -155
- package/src/spawners/http-api-spawner.ts +0 -96
- package/src/user-store-pg.ts +0 -138
- package/src/user-store.ts +0 -125
- package/src/utils/repo-resolver.ts +0 -3
- package/src/webhook-dispatcher.ts +0 -142
- package/src/webhook-store.ts +0 -141
- package/tests/agents.test.ts +0 -164
- package/tests/cancel.test.ts +0 -120
- package/tests/config.test.ts +0 -196
- package/tests/contracts.test.ts +0 -302
- package/tests/event-bus.test.ts +0 -53
- package/tests/http-api-spawner.test.ts +0 -158
- package/tests/integration-runtime.test.ts +0 -199
- package/tests/integration-store.test.ts +0 -66
- package/tests/integrations/linear/failure-handler.test.ts +0 -113
- package/tests/integrations/linear/webhook-handler.test.ts +0 -191
- package/tests/launcher.test.ts +0 -380
- package/tests/linear-webhook.test.ts +0 -390
- package/tests/pipelines.test.ts +0 -166
- package/tests/projects.test.ts +0 -283
- package/tests/runs.test.ts +0 -379
- package/tests/server.test.ts +0 -208
- package/tests/skills.test.ts +0 -149
- package/tests/sse.test.ts +0 -129
- package/tests/tools.test.ts +0 -233
- package/tests/user-store.test.ts +0 -93
- package/tests/users.test.ts +0 -189
- package/tests/utils/repo-resolver.test.ts +0 -105
- package/tests/validate.test.ts +0 -233
- package/tests/webhook-dispatcher.test.ts +0 -214
- package/tests/webhook-store.test.ts +0 -98
- package/tests/webhooks.test.ts +0 -176
- package/tsconfig.json +0 -24
- package/vitest.config.ts +0 -7
package/src/plans.ts
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
// api/src/plans.ts
|
|
2
|
-
// Plan configuration types and default plans
|
|
3
|
-
|
|
4
|
-
export interface PlanLimits {
|
|
5
|
-
runs_per_day: number; // -1 = unlimited
|
|
6
|
-
max_concurrent: number;
|
|
7
|
-
max_tokens_per_run: number; // -1 = unlimited
|
|
8
|
-
rate_limit_per_minute: number;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export type PlansConfig = Record<string, PlanLimits>;
|
|
12
|
-
|
|
13
|
-
export const DEFAULT_PLANS: PlansConfig = {
|
|
14
|
-
free: {
|
|
15
|
-
runs_per_day: 5,
|
|
16
|
-
max_concurrent: 1,
|
|
17
|
-
max_tokens_per_run: 50_000,
|
|
18
|
-
rate_limit_per_minute: 10,
|
|
19
|
-
},
|
|
20
|
-
pro: {
|
|
21
|
-
runs_per_day: 100,
|
|
22
|
-
max_concurrent: 5,
|
|
23
|
-
max_tokens_per_run: 500_000,
|
|
24
|
-
rate_limit_per_minute: 60,
|
|
25
|
-
},
|
|
26
|
-
unlimited: {
|
|
27
|
-
runs_per_day: -1,
|
|
28
|
-
max_concurrent: 20,
|
|
29
|
-
max_tokens_per_run: -1,
|
|
30
|
-
rate_limit_per_minute: 300,
|
|
31
|
-
},
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
/** Merge user-supplied partial plans with defaults. Unknown plans fall back to 'free'. */
|
|
35
|
-
export function resolvePlans(configPlans?: Record<string, PlanLimits>): PlansConfig {
|
|
36
|
-
if (!configPlans) return DEFAULT_PLANS;
|
|
37
|
-
return { ...DEFAULT_PLANS, ...configPlans };
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** Get plan limits for a user, defaulting to 'free' if plan is unknown. */
|
|
41
|
-
export function getPlanLimits(plans: PlansConfig, planName: string): PlanLimits {
|
|
42
|
-
return plans[planName] ?? plans.free ?? DEFAULT_PLANS.free;
|
|
43
|
-
}
|
package/src/routes/agents.ts
DELETED
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
import { readdir, readFile, writeFile, unlink, mkdir } from 'node:fs/promises';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
|
-
import * as yaml from 'js-yaml';
|
|
4
|
-
import type { FastifyInstance } from 'fastify';
|
|
5
|
-
import type { ServerDeps } from '../server.js';
|
|
6
|
-
|
|
7
|
-
export async function agentsRoutes(
|
|
8
|
-
fastify: FastifyInstance,
|
|
9
|
-
options: { deps: ServerDeps }
|
|
10
|
-
): Promise<void> {
|
|
11
|
-
const agentsDir = join(options.deps.configsDir, 'agents');
|
|
12
|
-
|
|
13
|
-
const errorSchema = { type: 'object', properties: { error: { type: 'string' } } };
|
|
14
|
-
|
|
15
|
-
// GET /api/agents
|
|
16
|
-
fastify.get('/agents', {
|
|
17
|
-
schema: {
|
|
18
|
-
tags: ['agents'],
|
|
19
|
-
summary: 'List all agent names',
|
|
20
|
-
response: {
|
|
21
|
-
200: {
|
|
22
|
-
type: 'object',
|
|
23
|
-
properties: {
|
|
24
|
-
agents: { type: 'array', items: { type: 'string' } },
|
|
25
|
-
},
|
|
26
|
-
},
|
|
27
|
-
},
|
|
28
|
-
},
|
|
29
|
-
}, async (_request, reply) => {
|
|
30
|
-
let entries: string[];
|
|
31
|
-
try {
|
|
32
|
-
entries = await readdir(agentsDir);
|
|
33
|
-
} catch {
|
|
34
|
-
return reply.send({ agents: [] });
|
|
35
|
-
}
|
|
36
|
-
const agents = entries
|
|
37
|
-
.filter(f => f.endsWith('.agent.yaml'))
|
|
38
|
-
.map(f => f.slice(0, -'.agent.yaml'.length));
|
|
39
|
-
return reply.send({ agents });
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
// GET /api/agents/:name
|
|
43
|
-
fastify.get<{ Params: { name: string } }>('/agents/:name', {
|
|
44
|
-
schema: {
|
|
45
|
-
tags: ['agents'],
|
|
46
|
-
summary: 'Get an agent by name',
|
|
47
|
-
params: {
|
|
48
|
-
type: 'object',
|
|
49
|
-
properties: { name: { type: 'string' } },
|
|
50
|
-
required: ['name'],
|
|
51
|
-
},
|
|
52
|
-
response: {
|
|
53
|
-
200: { type: 'object', additionalProperties: true },
|
|
54
|
-
404: errorSchema,
|
|
55
|
-
},
|
|
56
|
-
},
|
|
57
|
-
}, async (request, reply) => {
|
|
58
|
-
const filePath = join(agentsDir, `${request.params.name}.agent.yaml`);
|
|
59
|
-
let content: string;
|
|
60
|
-
try {
|
|
61
|
-
content = await readFile(filePath, 'utf-8');
|
|
62
|
-
} catch {
|
|
63
|
-
return reply.status(404).send({ error: 'Agent not found' });
|
|
64
|
-
}
|
|
65
|
-
return reply.send(yaml.load(content));
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
// PUT /api/agents/:name
|
|
69
|
-
fastify.put<{
|
|
70
|
-
Params: { name: string };
|
|
71
|
-
Body: Record<string, unknown>;
|
|
72
|
-
}>('/agents/:name', {
|
|
73
|
-
schema: {
|
|
74
|
-
tags: ['agents'],
|
|
75
|
-
summary: 'Create or update an agent',
|
|
76
|
-
params: {
|
|
77
|
-
type: 'object',
|
|
78
|
-
properties: { name: { type: 'string' } },
|
|
79
|
-
required: ['name'],
|
|
80
|
-
},
|
|
81
|
-
body: { type: 'object' },
|
|
82
|
-
response: {
|
|
83
|
-
200: {
|
|
84
|
-
type: 'object',
|
|
85
|
-
properties: {
|
|
86
|
-
name: { type: 'string' },
|
|
87
|
-
content: { type: 'object', additionalProperties: true },
|
|
88
|
-
},
|
|
89
|
-
},
|
|
90
|
-
400: errorSchema,
|
|
91
|
-
},
|
|
92
|
-
},
|
|
93
|
-
}, async (request, reply) => {
|
|
94
|
-
const body = request.body;
|
|
95
|
-
|
|
96
|
-
if (!body['name'] || typeof body['name'] !== 'string') {
|
|
97
|
-
return reply.status(400).send({ error: "Agent must have a 'name' field (string)" });
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
await mkdir(agentsDir, { recursive: true });
|
|
101
|
-
const filePath = join(agentsDir, `${request.params.name}.agent.yaml`);
|
|
102
|
-
await writeFile(filePath, yaml.dump(body), 'utf-8');
|
|
103
|
-
|
|
104
|
-
return reply.send({ name: request.params.name, content: body });
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
// DELETE /api/agents/:name
|
|
108
|
-
fastify.delete<{ Params: { name: string } }>('/agents/:name', {
|
|
109
|
-
schema: {
|
|
110
|
-
tags: ['agents'],
|
|
111
|
-
summary: 'Delete an agent',
|
|
112
|
-
params: {
|
|
113
|
-
type: 'object',
|
|
114
|
-
properties: { name: { type: 'string' } },
|
|
115
|
-
required: ['name'],
|
|
116
|
-
},
|
|
117
|
-
response: {
|
|
118
|
-
204: { type: 'null', description: 'No content' },
|
|
119
|
-
404: errorSchema,
|
|
120
|
-
},
|
|
121
|
-
},
|
|
122
|
-
}, async (request, reply) => {
|
|
123
|
-
const filePath = join(agentsDir, `${request.params.name}.agent.yaml`);
|
|
124
|
-
try {
|
|
125
|
-
await unlink(filePath);
|
|
126
|
-
} catch {
|
|
127
|
-
return reply.status(404).send({ error: 'Agent not found' });
|
|
128
|
-
}
|
|
129
|
-
return reply.status(204).send();
|
|
130
|
-
});
|
|
131
|
-
}
|
package/src/routes/config.ts
DELETED
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
|
-
import * as yaml from 'js-yaml';
|
|
4
|
-
import type { FastifyInstance } from 'fastify';
|
|
5
|
-
import type { ServerDeps } from '../server.js';
|
|
6
|
-
|
|
7
|
-
const KNOWN_PROVIDERS = new Set(['anthropic', 'openai', 'ollama']);
|
|
8
|
-
|
|
9
|
-
interface RawConfig {
|
|
10
|
-
providers?: Record<string, { apiKey?: string }>;
|
|
11
|
-
defaults?: Record<string, unknown>;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
async function readConfig(configsDir: string): Promise<RawConfig> {
|
|
15
|
-
try {
|
|
16
|
-
const content = await readFile(join(configsDir, 'config.yaml'), 'utf-8');
|
|
17
|
-
return (yaml.load(content) as RawConfig) ?? {};
|
|
18
|
-
} catch {
|
|
19
|
-
return {};
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
async function writeConfig(configsDir: string, config: RawConfig): Promise<void> {
|
|
24
|
-
await mkdir(configsDir, { recursive: true });
|
|
25
|
-
await writeFile(join(configsDir, 'config.yaml'), yaml.dump(config), 'utf-8');
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function maskConfig(config: RawConfig): RawConfig {
|
|
29
|
-
if (!config.providers) return config;
|
|
30
|
-
const maskedProviders: Record<string, { apiKey: string }> = {};
|
|
31
|
-
for (const [name, provider] of Object.entries(config.providers)) {
|
|
32
|
-
maskedProviders[name] = { apiKey: provider.apiKey ? '***' : '' };
|
|
33
|
-
}
|
|
34
|
-
return { ...config, providers: maskedProviders };
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export async function configRoutes(
|
|
38
|
-
fastify: FastifyInstance,
|
|
39
|
-
options: { deps: ServerDeps }
|
|
40
|
-
): Promise<void> {
|
|
41
|
-
const { configsDir } = options.deps;
|
|
42
|
-
|
|
43
|
-
const configResponseSchema = {
|
|
44
|
-
type: 'object',
|
|
45
|
-
properties: {
|
|
46
|
-
providers: {
|
|
47
|
-
type: 'object',
|
|
48
|
-
additionalProperties: {
|
|
49
|
-
type: 'object',
|
|
50
|
-
properties: { apiKey: { type: 'string' } },
|
|
51
|
-
},
|
|
52
|
-
},
|
|
53
|
-
defaults: {
|
|
54
|
-
type: 'object',
|
|
55
|
-
properties: {
|
|
56
|
-
provider: { type: 'string' },
|
|
57
|
-
model: { type: 'string' },
|
|
58
|
-
},
|
|
59
|
-
},
|
|
60
|
-
},
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
// GET /api/config
|
|
64
|
-
fastify.get('/config', {
|
|
65
|
-
schema: {
|
|
66
|
-
tags: ['config'],
|
|
67
|
-
summary: 'Get current config',
|
|
68
|
-
response: {
|
|
69
|
-
200: configResponseSchema,
|
|
70
|
-
},
|
|
71
|
-
},
|
|
72
|
-
}, async (_request, reply) => {
|
|
73
|
-
const config = await readConfig(configsDir);
|
|
74
|
-
return reply.send({
|
|
75
|
-
providers: {},
|
|
76
|
-
...maskConfig(config),
|
|
77
|
-
});
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
// PATCH /api/config
|
|
81
|
-
fastify.patch<{ Body: Partial<RawConfig> }>('/config', {
|
|
82
|
-
schema: {
|
|
83
|
-
tags: ['config'],
|
|
84
|
-
summary: 'Patch config (merge defaults and providers)',
|
|
85
|
-
body: { type: 'object' },
|
|
86
|
-
response: {
|
|
87
|
-
200: configResponseSchema,
|
|
88
|
-
},
|
|
89
|
-
},
|
|
90
|
-
}, async (request, reply) => {
|
|
91
|
-
const config = await readConfig(configsDir);
|
|
92
|
-
const patch = request.body;
|
|
93
|
-
|
|
94
|
-
if (patch.defaults) {
|
|
95
|
-
config.defaults = { ...config.defaults, ...patch.defaults };
|
|
96
|
-
}
|
|
97
|
-
if (patch.providers) {
|
|
98
|
-
config.providers = { ...config.providers, ...patch.providers };
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
await writeConfig(configsDir, config);
|
|
102
|
-
return reply.send({
|
|
103
|
-
providers: {},
|
|
104
|
-
...maskConfig(config),
|
|
105
|
-
});
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
// POST /api/config/providers
|
|
109
|
-
fastify.post<{ Body: { provider: string; apiKeyEnvVar: string } }>(
|
|
110
|
-
'/config/providers',
|
|
111
|
-
{
|
|
112
|
-
schema: {
|
|
113
|
-
tags: ['config'],
|
|
114
|
-
summary: 'Add or update a provider',
|
|
115
|
-
body: {
|
|
116
|
-
type: 'object',
|
|
117
|
-
required: ['provider', 'apiKeyEnvVar'],
|
|
118
|
-
properties: {
|
|
119
|
-
provider: { type: 'string' },
|
|
120
|
-
apiKeyEnvVar: { type: 'string' },
|
|
121
|
-
},
|
|
122
|
-
},
|
|
123
|
-
response: {
|
|
124
|
-
200: configResponseSchema,
|
|
125
|
-
400: {
|
|
126
|
-
type: 'object',
|
|
127
|
-
properties: { error: { type: 'string' } },
|
|
128
|
-
},
|
|
129
|
-
},
|
|
130
|
-
},
|
|
131
|
-
},
|
|
132
|
-
async (request, reply) => {
|
|
133
|
-
const { provider, apiKeyEnvVar } = request.body;
|
|
134
|
-
|
|
135
|
-
if (!KNOWN_PROVIDERS.has(provider)) {
|
|
136
|
-
return reply.status(400).send({
|
|
137
|
-
error: `Unknown provider: '${provider}'. Supported: ${[...KNOWN_PROVIDERS].join(', ')}`,
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const config = await readConfig(configsDir);
|
|
142
|
-
config.providers = {
|
|
143
|
-
...config.providers,
|
|
144
|
-
[provider]: { apiKey: `\${${apiKeyEnvVar}}` },
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
await writeConfig(configsDir, config);
|
|
148
|
-
return reply.send({
|
|
149
|
-
providers: {},
|
|
150
|
-
...maskConfig(config),
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
);
|
|
154
|
-
}
|
package/src/routes/contracts.ts
DELETED
|
@@ -1,205 +0,0 @@
|
|
|
1
|
-
import { readdir, readFile, writeFile, unlink, mkdir } from 'node:fs/promises';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
|
-
import * as yaml from 'js-yaml';
|
|
4
|
-
import type { FastifyInstance } from 'fastify';
|
|
5
|
-
import type { ServerDeps } from '../server.js';
|
|
6
|
-
import { loadContract, validateOutput } from '@studio-foundation/engine';
|
|
7
|
-
import type { ToolCall } from '@studio-foundation/contracts';
|
|
8
|
-
|
|
9
|
-
export async function contractsRoutes(
|
|
10
|
-
fastify: FastifyInstance,
|
|
11
|
-
options: { deps: ServerDeps }
|
|
12
|
-
): Promise<void> {
|
|
13
|
-
const contractsDir = join(options.deps.configsDir, 'contracts');
|
|
14
|
-
const errorSchema = { type: 'object', properties: { error: { type: 'string' } } };
|
|
15
|
-
|
|
16
|
-
// GET /api/contracts
|
|
17
|
-
fastify.get('/contracts', {
|
|
18
|
-
schema: {
|
|
19
|
-
tags: ['contracts'],
|
|
20
|
-
summary: 'List all contract names',
|
|
21
|
-
response: {
|
|
22
|
-
200: {
|
|
23
|
-
type: 'object',
|
|
24
|
-
properties: {
|
|
25
|
-
contracts: { type: 'array', items: { type: 'string' } },
|
|
26
|
-
},
|
|
27
|
-
},
|
|
28
|
-
},
|
|
29
|
-
},
|
|
30
|
-
}, async (_request, reply) => {
|
|
31
|
-
let entries: string[];
|
|
32
|
-
try {
|
|
33
|
-
entries = await readdir(contractsDir);
|
|
34
|
-
} catch {
|
|
35
|
-
return reply.send({ contracts: [] });
|
|
36
|
-
}
|
|
37
|
-
const contracts = entries
|
|
38
|
-
.filter(f => f.endsWith('.contract.yaml'))
|
|
39
|
-
.map(f => f.slice(0, -'.contract.yaml'.length));
|
|
40
|
-
return reply.send({ contracts });
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
// GET /api/contracts/:name
|
|
44
|
-
fastify.get<{ Params: { name: string } }>('/contracts/:name', {
|
|
45
|
-
schema: {
|
|
46
|
-
tags: ['contracts'],
|
|
47
|
-
summary: 'Get a contract by name',
|
|
48
|
-
params: {
|
|
49
|
-
type: 'object',
|
|
50
|
-
properties: { name: { type: 'string' } },
|
|
51
|
-
required: ['name'],
|
|
52
|
-
},
|
|
53
|
-
response: {
|
|
54
|
-
200: { type: 'object', additionalProperties: true },
|
|
55
|
-
404: errorSchema,
|
|
56
|
-
},
|
|
57
|
-
},
|
|
58
|
-
}, async (request, reply) => {
|
|
59
|
-
const filePath = join(contractsDir, `${request.params.name}.contract.yaml`);
|
|
60
|
-
let content: string;
|
|
61
|
-
try {
|
|
62
|
-
content = await readFile(filePath, 'utf-8');
|
|
63
|
-
} catch {
|
|
64
|
-
return reply.status(404).send({ error: 'Contract not found' });
|
|
65
|
-
}
|
|
66
|
-
return reply.send(yaml.load(content));
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
// PUT /api/contracts/:name
|
|
70
|
-
fastify.put<{
|
|
71
|
-
Params: { name: string };
|
|
72
|
-
Body: Record<string, unknown>;
|
|
73
|
-
}>('/contracts/:name', {
|
|
74
|
-
schema: {
|
|
75
|
-
tags: ['contracts'],
|
|
76
|
-
summary: 'Create or update a contract',
|
|
77
|
-
params: {
|
|
78
|
-
type: 'object',
|
|
79
|
-
properties: { name: { type: 'string' } },
|
|
80
|
-
required: ['name'],
|
|
81
|
-
},
|
|
82
|
-
body: { type: 'object' },
|
|
83
|
-
response: {
|
|
84
|
-
200: {
|
|
85
|
-
type: 'object',
|
|
86
|
-
properties: {
|
|
87
|
-
name: { type: 'string' },
|
|
88
|
-
content: { type: 'object', additionalProperties: true },
|
|
89
|
-
},
|
|
90
|
-
},
|
|
91
|
-
400: errorSchema,
|
|
92
|
-
},
|
|
93
|
-
},
|
|
94
|
-
}, async (request, reply) => {
|
|
95
|
-
const body = request.body;
|
|
96
|
-
|
|
97
|
-
if (!body['name'] || typeof body['name'] !== 'string') {
|
|
98
|
-
return reply.status(400).send({ error: "Contract must have a 'name' field (string)" });
|
|
99
|
-
}
|
|
100
|
-
if (body['version'] === undefined) {
|
|
101
|
-
return reply.status(400).send({ error: "Contract must have a 'version' field" });
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
await mkdir(contractsDir, { recursive: true });
|
|
105
|
-
const filePath = join(contractsDir, `${request.params.name}.contract.yaml`);
|
|
106
|
-
await writeFile(filePath, yaml.dump(body), 'utf-8');
|
|
107
|
-
|
|
108
|
-
return reply.send({ name: request.params.name, content: body });
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
// DELETE /api/contracts/:name
|
|
112
|
-
fastify.delete<{ Params: { name: string } }>('/contracts/:name', {
|
|
113
|
-
schema: {
|
|
114
|
-
tags: ['contracts'],
|
|
115
|
-
summary: 'Delete a contract',
|
|
116
|
-
params: {
|
|
117
|
-
type: 'object',
|
|
118
|
-
properties: { name: { type: 'string' } },
|
|
119
|
-
required: ['name'],
|
|
120
|
-
},
|
|
121
|
-
response: {
|
|
122
|
-
204: { type: 'null', description: 'No content' },
|
|
123
|
-
404: errorSchema,
|
|
124
|
-
},
|
|
125
|
-
},
|
|
126
|
-
}, async (request, reply) => {
|
|
127
|
-
const filePath = join(contractsDir, `${request.params.name}.contract.yaml`);
|
|
128
|
-
try {
|
|
129
|
-
await unlink(filePath);
|
|
130
|
-
} catch {
|
|
131
|
-
return reply.status(404).send({ error: 'Contract not found' });
|
|
132
|
-
}
|
|
133
|
-
return reply.status(204).send();
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
// POST /api/contracts/:name/validate
|
|
137
|
-
fastify.post<{
|
|
138
|
-
Params: { name: string };
|
|
139
|
-
Body: { output: unknown; tool_calls?: ToolCall[] };
|
|
140
|
-
}>('/contracts/:name/validate', {
|
|
141
|
-
schema: {
|
|
142
|
-
tags: ['contracts'],
|
|
143
|
-
summary: 'Validate an output against a contract without running a pipeline',
|
|
144
|
-
params: {
|
|
145
|
-
type: 'object',
|
|
146
|
-
properties: { name: { type: 'string' } },
|
|
147
|
-
required: ['name'],
|
|
148
|
-
},
|
|
149
|
-
body: {
|
|
150
|
-
type: 'object',
|
|
151
|
-
required: ['output'],
|
|
152
|
-
properties: {
|
|
153
|
-
output: { type: 'object', additionalProperties: true },
|
|
154
|
-
tool_calls: {
|
|
155
|
-
type: 'array',
|
|
156
|
-
items: {
|
|
157
|
-
type: 'object',
|
|
158
|
-
properties: {
|
|
159
|
-
id: { type: 'string' },
|
|
160
|
-
name: { type: 'string' },
|
|
161
|
-
arguments: { type: 'object', additionalProperties: true },
|
|
162
|
-
result: {},
|
|
163
|
-
error: { type: 'string' },
|
|
164
|
-
},
|
|
165
|
-
required: ['id', 'name'],
|
|
166
|
-
},
|
|
167
|
-
},
|
|
168
|
-
},
|
|
169
|
-
},
|
|
170
|
-
response: {
|
|
171
|
-
200: {
|
|
172
|
-
type: 'object',
|
|
173
|
-
required: ['valid', 'errors', 'warnings', 'post_validation'],
|
|
174
|
-
properties: {
|
|
175
|
-
valid: { type: 'boolean' },
|
|
176
|
-
errors: { type: 'array', items: { type: 'string' } },
|
|
177
|
-
warnings: { type: 'array', items: { type: 'string' } },
|
|
178
|
-
post_validation: {
|
|
179
|
-
type: 'object',
|
|
180
|
-
required: ['accepted'],
|
|
181
|
-
properties: {
|
|
182
|
-
accepted: { type: 'boolean' },
|
|
183
|
-
rejection_reason: { type: 'string' },
|
|
184
|
-
rejection_details: { type: 'array', items: { type: 'string' } },
|
|
185
|
-
},
|
|
186
|
-
},
|
|
187
|
-
},
|
|
188
|
-
},
|
|
189
|
-
400: errorSchema,
|
|
190
|
-
404: errorSchema,
|
|
191
|
-
},
|
|
192
|
-
},
|
|
193
|
-
}, async (request, reply) => {
|
|
194
|
-
let contract;
|
|
195
|
-
try {
|
|
196
|
-
contract = await loadContract(request.params.name, contractsDir);
|
|
197
|
-
} catch {
|
|
198
|
-
return reply.status(404).send({ error: 'Contract not found' });
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
const { output, tool_calls = [] } = request.body;
|
|
202
|
-
const result = validateOutput(contract, output, tool_calls);
|
|
203
|
-
return reply.send(result);
|
|
204
|
-
});
|
|
205
|
-
}
|
package/src/routes/pipelines.ts
DELETED
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
import { readdir, readFile, writeFile, unlink } from 'node:fs/promises';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
|
-
import yaml from 'js-yaml';
|
|
4
|
-
import type { FastifyInstance } from 'fastify';
|
|
5
|
-
import type { ServerDeps } from '../server.js';
|
|
6
|
-
|
|
7
|
-
function pipelinePath(configsDir: string, name: string): string {
|
|
8
|
-
return join(configsDir, 'pipelines', `${name}.pipeline.yaml`);
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export async function pipelinesRoutes(
|
|
12
|
-
fastify: FastifyInstance,
|
|
13
|
-
options: { deps: ServerDeps }
|
|
14
|
-
): Promise<void> {
|
|
15
|
-
const { configsDir } = options.deps;
|
|
16
|
-
|
|
17
|
-
// GET /api/pipelines — list all pipeline names
|
|
18
|
-
fastify.get('/pipelines', {
|
|
19
|
-
schema: {
|
|
20
|
-
tags: ['pipelines'],
|
|
21
|
-
summary: 'List all pipeline names',
|
|
22
|
-
response: {
|
|
23
|
-
200: {
|
|
24
|
-
type: 'object',
|
|
25
|
-
properties: {
|
|
26
|
-
pipelines: { type: 'array', items: { type: 'string' } },
|
|
27
|
-
},
|
|
28
|
-
},
|
|
29
|
-
},
|
|
30
|
-
},
|
|
31
|
-
}, async (_request, reply) => {
|
|
32
|
-
const pipelinesDir = join(configsDir, 'pipelines');
|
|
33
|
-
let entries: string[];
|
|
34
|
-
try {
|
|
35
|
-
entries = await readdir(pipelinesDir);
|
|
36
|
-
} catch {
|
|
37
|
-
return reply.send({ pipelines: [] });
|
|
38
|
-
}
|
|
39
|
-
const pipelines = entries
|
|
40
|
-
.filter(f => f.endsWith('.pipeline.yaml'))
|
|
41
|
-
.map(f => f.replace('.pipeline.yaml', ''));
|
|
42
|
-
return reply.send({ pipelines });
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
const errorSchema = { type: 'object', properties: { error: { type: 'string' } } };
|
|
46
|
-
|
|
47
|
-
// GET /api/pipelines/:name — read a pipeline (YAML parsed to JSON)
|
|
48
|
-
fastify.get<{ Params: { name: string } }>('/pipelines/:name', {
|
|
49
|
-
schema: {
|
|
50
|
-
tags: ['pipelines'],
|
|
51
|
-
summary: 'Get a pipeline by name',
|
|
52
|
-
params: {
|
|
53
|
-
type: 'object',
|
|
54
|
-
properties: { name: { type: 'string' } },
|
|
55
|
-
required: ['name'],
|
|
56
|
-
},
|
|
57
|
-
response: {
|
|
58
|
-
200: { type: 'object', additionalProperties: true },
|
|
59
|
-
404: errorSchema,
|
|
60
|
-
},
|
|
61
|
-
},
|
|
62
|
-
}, async (request, reply) => {
|
|
63
|
-
const { name } = request.params;
|
|
64
|
-
let content: string;
|
|
65
|
-
try {
|
|
66
|
-
content = await readFile(pipelinePath(configsDir, name), 'utf-8');
|
|
67
|
-
} catch {
|
|
68
|
-
return reply.status(404).send({ error: `Pipeline '${name}' not found` });
|
|
69
|
-
}
|
|
70
|
-
const parsed = yaml.load(content);
|
|
71
|
-
return reply.send(parsed);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
// PUT /api/pipelines/:name — create or update a pipeline (YAML or JSON body)
|
|
75
|
-
fastify.put<{ Params: { name: string }; Body: unknown }>(
|
|
76
|
-
'/pipelines/:name',
|
|
77
|
-
{
|
|
78
|
-
schema: {
|
|
79
|
-
tags: ['pipelines'],
|
|
80
|
-
summary: 'Create or update a pipeline (YAML text or JSON body)',
|
|
81
|
-
params: {
|
|
82
|
-
type: 'object',
|
|
83
|
-
properties: { name: { type: 'string' } },
|
|
84
|
-
required: ['name'],
|
|
85
|
-
},
|
|
86
|
-
body: { oneOf: [{ type: 'object', additionalProperties: true }, { type: 'string' }] },
|
|
87
|
-
response: {
|
|
88
|
-
200: {
|
|
89
|
-
type: 'object',
|
|
90
|
-
properties: { name: { type: 'string' } },
|
|
91
|
-
},
|
|
92
|
-
400: errorSchema,
|
|
93
|
-
},
|
|
94
|
-
},
|
|
95
|
-
},
|
|
96
|
-
async (request, reply) => {
|
|
97
|
-
const { name } = request.params;
|
|
98
|
-
const contentType = request.headers['content-type'] ?? '';
|
|
99
|
-
|
|
100
|
-
let yamlContent: string;
|
|
101
|
-
if (contentType.includes('application/json')) {
|
|
102
|
-
yamlContent = yaml.dump(request.body);
|
|
103
|
-
} else {
|
|
104
|
-
yamlContent = request.body as string;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
try {
|
|
108
|
-
yaml.load(yamlContent);
|
|
109
|
-
} catch (err) {
|
|
110
|
-
const message = err instanceof Error ? err.message : 'Invalid YAML';
|
|
111
|
-
return reply.status(400).send({ error: message });
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
await writeFile(pipelinePath(configsDir, name), yamlContent, 'utf-8');
|
|
115
|
-
return reply.send({ name });
|
|
116
|
-
}
|
|
117
|
-
);
|
|
118
|
-
|
|
119
|
-
// DELETE /api/pipelines/:name — delete a pipeline
|
|
120
|
-
fastify.delete<{ Params: { name: string } }>('/pipelines/:name', {
|
|
121
|
-
schema: {
|
|
122
|
-
tags: ['pipelines'],
|
|
123
|
-
summary: 'Delete a pipeline',
|
|
124
|
-
params: {
|
|
125
|
-
type: 'object',
|
|
126
|
-
properties: { name: { type: 'string' } },
|
|
127
|
-
required: ['name'],
|
|
128
|
-
},
|
|
129
|
-
response: {
|
|
130
|
-
200: {
|
|
131
|
-
type: 'object',
|
|
132
|
-
properties: { deleted: { type: 'string' } },
|
|
133
|
-
},
|
|
134
|
-
404: errorSchema,
|
|
135
|
-
},
|
|
136
|
-
},
|
|
137
|
-
}, async (request, reply) => {
|
|
138
|
-
const { name } = request.params;
|
|
139
|
-
try {
|
|
140
|
-
await unlink(pipelinePath(configsDir, name));
|
|
141
|
-
} catch {
|
|
142
|
-
return reply.status(404).send({ error: `Pipeline '${name}' not found` });
|
|
143
|
-
}
|
|
144
|
-
return reply.send({ deleted: name });
|
|
145
|
-
});
|
|
146
|
-
}
|