@graphorin/server 0.6.1 → 0.7.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.
- package/CHANGELOG.md +71 -0
- package/README.md +3 -3
- package/dist/app-audit-binding.d.ts +15 -0
- package/dist/app-audit-binding.d.ts.map +1 -0
- package/dist/app-audit-binding.js +136 -0
- package/dist/app-audit-binding.js.map +1 -0
- package/dist/app-daemons.d.ts +29 -0
- package/dist/app-daemons.d.ts.map +1 -0
- package/dist/app-daemons.js +27 -0
- package/dist/app-daemons.js.map +1 -0
- package/dist/app-lifecycle.js +272 -0
- package/dist/app-lifecycle.js.map +1 -0
- package/dist/app-metrics.js +79 -0
- package/dist/app-metrics.js.map +1 -0
- package/dist/app-middleware.js +92 -0
- package/dist/app-middleware.js.map +1 -0
- package/dist/app-routes.js +131 -0
- package/dist/app-routes.js.map +1 -0
- package/dist/app-ws.js +65 -0
- package/dist/app-ws.js.map +1 -0
- package/dist/app.d.ts +16 -16
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +35 -607
- package/dist/app.js.map +1 -1
- package/dist/commentary/built-in-patterns.d.ts +1 -1
- package/dist/commentary/built-in-patterns.js +1 -1
- package/dist/commentary/built-in-patterns.js.map +1 -1
- package/dist/config.d.ts +100 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +30 -2
- package/dist/config.js.map +1 -1
- package/dist/health/checks.d.ts +15 -1
- package/dist/health/checks.d.ts.map +1 -1
- package/dist/health/checks.js +21 -0
- package/dist/health/checks.js.map +1 -1
- package/dist/health/routes.js +1 -1
- package/dist/index.d.ts +8 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +19 -24
- package/dist/index.js.map +1 -1
- package/dist/internal/context.d.ts +2 -2
- package/dist/internal/wire-error.js +20 -0
- package/dist/internal/wire-error.js.map +1 -0
- package/dist/lifecycle/pre-bind.d.ts +1 -1
- package/dist/metrics/catalog.d.ts.map +1 -1
- package/dist/metrics/catalog.js.map +1 -1
- package/dist/metrics/tools-bridge.d.ts +15 -0
- package/dist/metrics/tools-bridge.d.ts.map +1 -0
- package/dist/metrics/tools-bridge.js +103 -0
- package/dist/metrics/tools-bridge.js.map +1 -0
- package/dist/middleware/audit.d.ts +1 -1
- package/dist/middleware/auth.js +1 -1
- package/dist/middleware/csrf.js +1 -1
- package/dist/middleware/index.js +1 -1
- package/dist/middleware/scope.d.ts.map +1 -1
- package/dist/middleware/scope.js +44 -4
- package/dist/middleware/scope.js.map +1 -1
- package/dist/package.js +1 -1
- package/dist/package.js.map +1 -1
- package/dist/registry/index.d.ts +33 -0
- package/dist/registry/index.d.ts.map +1 -1
- package/dist/registry/index.js.map +1 -1
- package/dist/replay/routes.d.ts +1 -1
- package/dist/replay/routes.js +2 -2
- package/dist/routes/agents.d.ts.map +1 -1
- package/dist/routes/agents.js +64 -18
- package/dist/routes/agents.js.map +1 -1
- package/dist/routes/auth.js +2 -2
- package/dist/routes/auth.js.map +1 -1
- package/dist/routes/sessions.js +5 -5
- package/dist/routes/sessions.js.map +1 -1
- package/dist/routes/tokens.d.ts +1 -1
- package/dist/routes/tokens.d.ts.map +1 -1
- package/dist/routes/tokens.js +21 -1
- package/dist/routes/tokens.js.map +1 -1
- package/dist/routes/workflows.d.ts.map +1 -1
- package/dist/routes/workflows.js +165 -19
- package/dist/routes/workflows.js.map +1 -1
- package/dist/runtime/retention.d.ts +105 -0
- package/dist/runtime/retention.d.ts.map +1 -0
- package/dist/runtime/retention.js +154 -0
- package/dist/runtime/retention.js.map +1 -0
- package/dist/runtime/run-state.d.ts +2 -0
- package/dist/runtime/run-state.d.ts.map +1 -1
- package/dist/runtime/run-state.js +34 -2
- package/dist/runtime/run-state.js.map +1 -1
- package/dist/sse/events.js +3 -4
- package/dist/sse/events.js.map +1 -1
- package/dist/tools-audit-bridge.d.ts +29 -0
- package/dist/tools-audit-bridge.d.ts.map +1 -0
- package/dist/tools-audit-bridge.js +103 -0
- package/dist/tools-audit-bridge.js.map +1 -0
- package/dist/workflows/timer-daemon.d.ts +69 -0
- package/dist/workflows/timer-daemon.d.ts.map +1 -0
- package/dist/workflows/timer-daemon.js +37 -0
- package/dist/workflows/timer-daemon.js.map +1 -0
- package/dist/ws/dispatcher.d.ts +1 -1
- package/dist/ws/dispatcher.js +19 -12
- package/dist/ws/dispatcher.js.map +1 -1
- package/dist/ws/index.d.ts +2 -2
- package/dist/ws/index.js +3 -3
- package/dist/ws/replay-buffer.d.ts +35 -1
- package/dist/ws/replay-buffer.d.ts.map +1 -1
- package/dist/ws/replay-buffer.js +35 -3
- package/dist/ws/replay-buffer.js.map +1 -1
- package/dist/ws/upgrade.d.ts.map +1 -1
- package/dist/ws/upgrade.js +24 -18
- package/dist/ws/upgrade.js.map +1 -1
- package/package.json +30 -29
- package/src/app-audit-binding.ts +227 -0
- package/src/app-daemons.ts +73 -0
- package/src/app-lifecycle.ts +476 -0
- package/src/app-metrics.ts +144 -0
- package/src/app-middleware.ts +149 -0
- package/src/app-routes.ts +305 -0
- package/src/app-ws.ts +111 -0
- package/src/app.ts +317 -0
- package/src/commentary/audit-bridge.ts +135 -0
- package/src/commentary/built-in-patterns.ts +79 -0
- package/src/commentary/index.ts +32 -0
- package/src/commentary/sanitizer.ts +238 -0
- package/src/commentary/types.ts +130 -0
- package/src/config.ts +472 -0
- package/src/consolidator/daemon.ts +141 -0
- package/src/consolidator/index.ts +14 -0
- package/src/errors/index.ts +247 -0
- package/src/health/checks.ts +322 -0
- package/src/health/index.ts +34 -0
- package/src/health/routes.ts +154 -0
- package/src/index.ts +243 -0
- package/src/internal/context.ts +77 -0
- package/src/internal/ids.ts +17 -0
- package/src/internal/json.ts +47 -0
- package/src/internal/wire-error.ts +44 -0
- package/src/lifecycle/hooks.ts +66 -0
- package/src/lifecycle/index.ts +16 -0
- package/src/lifecycle/pre-bind.ts +138 -0
- package/src/metrics/catalog.ts +112 -0
- package/src/metrics/index.ts +12 -0
- package/src/metrics/registry.ts +337 -0
- package/src/metrics/tools-bridge.ts +124 -0
- package/src/middleware/audit.ts +114 -0
- package/src/middleware/auth.ts +195 -0
- package/src/middleware/cors.ts +72 -0
- package/src/middleware/csrf.ts +98 -0
- package/src/middleware/idempotency.ts +336 -0
- package/src/middleware/index.ts +38 -0
- package/src/middleware/rate-limit.ts +71 -0
- package/src/middleware/request-state.ts +79 -0
- package/src/middleware/scope.ts +161 -0
- package/src/registry/index.ts +278 -0
- package/src/replay/index.ts +14 -0
- package/src/replay/routes.ts +255 -0
- package/src/routes/agents.ts +363 -0
- package/src/routes/audit.ts +207 -0
- package/src/routes/auth.ts +80 -0
- package/src/routes/health.ts +42 -0
- package/src/routes/index.ts +16 -0
- package/src/routes/mcp.ts +97 -0
- package/src/routes/memory.ts +152 -0
- package/src/routes/sessions.ts +250 -0
- package/src/routes/skills.ts +91 -0
- package/src/routes/tokens.ts +166 -0
- package/src/routes/workflows.ts +616 -0
- package/src/runtime/retention.ts +284 -0
- package/src/runtime/run-state.ts +422 -0
- package/src/sse/events.ts +303 -0
- package/src/sse/index.ts +7 -0
- package/src/tools-audit-bridge.ts +120 -0
- package/src/triggers/daemon.ts +198 -0
- package/src/triggers/index.ts +16 -0
- package/src/triggers/routes.ts +139 -0
- package/src/workflows/timer-daemon.ts +102 -0
- package/src/ws/dispatcher.ts +537 -0
- package/src/ws/index.ts +45 -0
- package/src/ws/replay-buffer.ts +209 -0
- package/src/ws/subjects.ts +162 -0
- package/src/ws/ticket.ts +174 -0
- package/src/ws/upgrade.ts +507 -0
- package/dist/lifecycle/index.js +0 -3
- package/dist/routes/index.js +0 -12
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* REST route barrel.
|
|
3
|
+
*
|
|
4
|
+
* @packageDocumentation
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export { type AgentRoutesDeps, createAgentRoutes, createRunRoutes } from './agents.js';
|
|
8
|
+
export { type AuditApi, type AuditRoutesDeps, createAuditRoutes } from './audit.js';
|
|
9
|
+
export { type AuthRoutesDeps, createAuthRoutes } from './auth.js';
|
|
10
|
+
export { createHealthRoutes, type HealthRoutesDeps } from './health.js';
|
|
11
|
+
export { createMcpRoutes, type McpApi, type McpRoutesDeps } from './mcp.js';
|
|
12
|
+
export { createMemoryRoutes, type MemoryApi, type MemoryRoutesDeps } from './memory.js';
|
|
13
|
+
export { createSessionRoutes, type SessionApi, type SessionRoutesDeps } from './sessions.js';
|
|
14
|
+
export { createSkillsRoutes, type SkillsApi, type SkillsRoutesDeps } from './skills.js';
|
|
15
|
+
export { createTokensRoutes, type TokensRoutesDeps } from './tokens.js';
|
|
16
|
+
export { createWorkflowRoutes, type WorkflowRoutesDeps } from './workflows.js';
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP server-management REST routes.
|
|
3
|
+
*
|
|
4
|
+
* GET /mcp/servers (scope `mcp:admin`)
|
|
5
|
+
* POST /mcp/servers (idempotent; scope `mcp:admin`)
|
|
6
|
+
* DELETE /mcp/servers/:id (scope `mcp:admin`)
|
|
7
|
+
*
|
|
8
|
+
* @packageDocumentation
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { Context } from 'hono';
|
|
12
|
+
import { Hono } from 'hono';
|
|
13
|
+
import { z } from 'zod';
|
|
14
|
+
|
|
15
|
+
import type { ServerVariables } from '../internal/context.js';
|
|
16
|
+
import { createScopeMiddleware } from '../middleware/scope.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @stable
|
|
20
|
+
*/
|
|
21
|
+
export interface McpApi {
|
|
22
|
+
list(): Promise<ReadonlyArray<unknown>>;
|
|
23
|
+
register(input: {
|
|
24
|
+
readonly id: string;
|
|
25
|
+
readonly transport: string;
|
|
26
|
+
readonly url?: string;
|
|
27
|
+
}): Promise<unknown>;
|
|
28
|
+
remove(id: string): Promise<boolean>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const RegisterBodySchema = z
|
|
32
|
+
.object({
|
|
33
|
+
id: z.string().min(1),
|
|
34
|
+
transport: z.enum(['stdio', 'streamable-http', 'sse']),
|
|
35
|
+
url: z.string().min(1).optional(),
|
|
36
|
+
})
|
|
37
|
+
.strict();
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @stable
|
|
41
|
+
*/
|
|
42
|
+
export interface McpRoutesDeps {
|
|
43
|
+
readonly mcp: McpApi;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @stable
|
|
48
|
+
*/
|
|
49
|
+
export function createMcpRoutes(deps: McpRoutesDeps): Hono<{ Variables: ServerVariables }> {
|
|
50
|
+
const app = new Hono<{ Variables: ServerVariables }>();
|
|
51
|
+
|
|
52
|
+
app.get('/servers', createScopeMiddleware('mcp:admin'), async (c) => {
|
|
53
|
+
return c.json({ servers: await deps.mcp.list() });
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
app.post('/servers', createScopeMiddleware('mcp:admin'), async (c) => {
|
|
57
|
+
const parsed = RegisterBodySchema.safeParse(await safelyParseJson(c));
|
|
58
|
+
if (!parsed.success) {
|
|
59
|
+
return c.json(
|
|
60
|
+
{
|
|
61
|
+
error: 'config-invalid',
|
|
62
|
+
message: 'Invalid MCP server body.',
|
|
63
|
+
issues: parsed.error.issues.map((i) => ({ path: i.path, message: i.message })),
|
|
64
|
+
},
|
|
65
|
+
400,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
const payload: { id: string; transport: string; url?: string } = {
|
|
69
|
+
id: parsed.data.id,
|
|
70
|
+
transport: parsed.data.transport,
|
|
71
|
+
};
|
|
72
|
+
if (parsed.data.url !== undefined) payload.url = parsed.data.url;
|
|
73
|
+
const out = await deps.mcp.register(payload);
|
|
74
|
+
return c.json({ server: out }, 201);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
app.delete('/servers/:id', createScopeMiddleware('mcp:admin'), async (c) => {
|
|
78
|
+
const id = c.req.param('id');
|
|
79
|
+
const removed = await deps.mcp.remove(id);
|
|
80
|
+
if (!removed)
|
|
81
|
+
return c.json(
|
|
82
|
+
{ error: 'mcp-server-not-found', message: `MCP server '${id}' not found.` },
|
|
83
|
+
404,
|
|
84
|
+
);
|
|
85
|
+
return c.body(null, 204);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
return app;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function safelyParseJson(c: Context<{ Variables: ServerVariables }>): Promise<unknown> {
|
|
92
|
+
try {
|
|
93
|
+
return await c.req.json();
|
|
94
|
+
} catch {
|
|
95
|
+
return {};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory REST routes. The handlers delegate to a structurally-typed
|
|
3
|
+
* `MemoryApi` provided by the operator (in practice, a thin shim
|
|
4
|
+
* around `@graphorin/memory.createMemory({...})`).
|
|
5
|
+
*
|
|
6
|
+
* POST /memory/search (scope `memory:read`)
|
|
7
|
+
* POST /memory/facts (idempotent; scope `memory:write`)
|
|
8
|
+
* DELETE /memory/facts/:id (scope `memory:write`)
|
|
9
|
+
* POST /memory/blocks (idempotent; scope `memory:write`)
|
|
10
|
+
* DELETE /memory/blocks/:label (scope `memory:write`)
|
|
11
|
+
*
|
|
12
|
+
* @packageDocumentation
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { Context } from 'hono';
|
|
16
|
+
import { Hono } from 'hono';
|
|
17
|
+
import { z } from 'zod';
|
|
18
|
+
|
|
19
|
+
import type { ServerVariables } from '../internal/context.js';
|
|
20
|
+
import { createScopeMiddleware } from '../middleware/scope.js';
|
|
21
|
+
|
|
22
|
+
const ScopeSchema = z
|
|
23
|
+
.object({
|
|
24
|
+
userId: z.string().min(1),
|
|
25
|
+
agentId: z.string().min(1).optional(),
|
|
26
|
+
sessionId: z.string().min(1).optional(),
|
|
27
|
+
})
|
|
28
|
+
.strict();
|
|
29
|
+
|
|
30
|
+
const SearchBodySchema = z
|
|
31
|
+
.object({
|
|
32
|
+
scope: ScopeSchema,
|
|
33
|
+
query: z.string().min(1),
|
|
34
|
+
topK: z.number().int().positive().max(200).optional(),
|
|
35
|
+
})
|
|
36
|
+
.strict();
|
|
37
|
+
|
|
38
|
+
const RememberBodySchema = z
|
|
39
|
+
.object({
|
|
40
|
+
scope: ScopeSchema,
|
|
41
|
+
text: z.string().min(1),
|
|
42
|
+
sensitivity: z.enum(['public', 'internal', 'secret']).optional(),
|
|
43
|
+
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
44
|
+
})
|
|
45
|
+
.strict();
|
|
46
|
+
|
|
47
|
+
const BlockBodySchema = z
|
|
48
|
+
.object({
|
|
49
|
+
scope: ScopeSchema,
|
|
50
|
+
label: z.string().min(1),
|
|
51
|
+
body: z.string(),
|
|
52
|
+
})
|
|
53
|
+
.strict();
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @stable
|
|
57
|
+
*/
|
|
58
|
+
export interface MemoryApi {
|
|
59
|
+
search(input: z.infer<typeof SearchBodySchema>): Promise<ReadonlyArray<unknown>>;
|
|
60
|
+
remember(input: z.infer<typeof RememberBodySchema>): Promise<{ readonly factId: string }>;
|
|
61
|
+
forget(scope: { readonly userId: string }, factId: string): Promise<boolean>;
|
|
62
|
+
upsertBlock(input: z.infer<typeof BlockBodySchema>): Promise<{ readonly label: string }>;
|
|
63
|
+
deleteBlock(scope: { readonly userId: string }, label: string): Promise<boolean>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @stable
|
|
68
|
+
*/
|
|
69
|
+
export interface MemoryRoutesDeps {
|
|
70
|
+
readonly memory: MemoryApi;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* @stable
|
|
75
|
+
*/
|
|
76
|
+
export function createMemoryRoutes(deps: MemoryRoutesDeps): Hono<{ Variables: ServerVariables }> {
|
|
77
|
+
const app = new Hono<{ Variables: ServerVariables }>();
|
|
78
|
+
|
|
79
|
+
app.post('/search', createScopeMiddleware('memory:read'), async (c) => {
|
|
80
|
+
const parsed = SearchBodySchema.safeParse(await safelyParseJson(c));
|
|
81
|
+
if (!parsed.success) {
|
|
82
|
+
return invalid(c, parsed.error.issues);
|
|
83
|
+
}
|
|
84
|
+
const hits = await deps.memory.search(parsed.data);
|
|
85
|
+
return c.json({ hits });
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
app.post('/facts', createScopeMiddleware('memory:write'), async (c) => {
|
|
89
|
+
const parsed = RememberBodySchema.safeParse(await safelyParseJson(c));
|
|
90
|
+
if (!parsed.success) return invalid(c, parsed.error.issues);
|
|
91
|
+
const out = await deps.memory.remember(parsed.data);
|
|
92
|
+
return c.json({ fact: out }, 201);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
app.delete('/facts/:id', createScopeMiddleware('memory:write'), async (c) => {
|
|
96
|
+
const id = c.req.param('id');
|
|
97
|
+
const userId = c.req.query('userId');
|
|
98
|
+
if (userId === undefined || userId.length === 0) {
|
|
99
|
+
return c.json(
|
|
100
|
+
{ error: 'config-invalid', message: 'Query parameter `userId` is required.' },
|
|
101
|
+
400,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
const removed = await deps.memory.forget({ userId }, id);
|
|
105
|
+
if (!removed)
|
|
106
|
+
return c.json({ error: 'fact-not-found', message: `Fact '${id}' not found.` }, 404);
|
|
107
|
+
return c.body(null, 204);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
app.post('/blocks', createScopeMiddleware('memory:write'), async (c) => {
|
|
111
|
+
const parsed = BlockBodySchema.safeParse(await safelyParseJson(c));
|
|
112
|
+
if (!parsed.success) return invalid(c, parsed.error.issues);
|
|
113
|
+
const out = await deps.memory.upsertBlock(parsed.data);
|
|
114
|
+
return c.json({ block: out }, 201);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
app.delete('/blocks/:label', createScopeMiddleware('memory:write'), async (c) => {
|
|
118
|
+
const label = c.req.param('label');
|
|
119
|
+
const userId = c.req.query('userId');
|
|
120
|
+
if (userId === undefined || userId.length === 0) {
|
|
121
|
+
return c.json(
|
|
122
|
+
{ error: 'config-invalid', message: 'Query parameter `userId` is required.' },
|
|
123
|
+
400,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
const removed = await deps.memory.deleteBlock({ userId }, label);
|
|
127
|
+
if (!removed)
|
|
128
|
+
return c.json({ error: 'block-not-found', message: `Block '${label}' not found.` }, 404);
|
|
129
|
+
return c.body(null, 204);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
return app;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function invalid(c: Context<{ Variables: ServerVariables }>, issues: ReadonlyArray<z.ZodIssue>) {
|
|
136
|
+
return c.json(
|
|
137
|
+
{
|
|
138
|
+
error: 'config-invalid',
|
|
139
|
+
message: 'Invalid request body.',
|
|
140
|
+
issues: issues.map((i) => ({ path: i.path, message: i.message })),
|
|
141
|
+
},
|
|
142
|
+
400,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function safelyParseJson(c: Context<{ Variables: ServerVariables }>): Promise<unknown> {
|
|
147
|
+
try {
|
|
148
|
+
return await c.req.json();
|
|
149
|
+
} catch {
|
|
150
|
+
return {};
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session REST routes. Delegates to a structurally-typed `SessionApi`
|
|
3
|
+
* supplied by the operator (in practice, the `@graphorin/sessions`
|
|
4
|
+
* package's `SessionManager`); the contract is intentionally open so
|
|
5
|
+
* tests can stub it without dragging the full sessions facade in.
|
|
6
|
+
*
|
|
7
|
+
* GET /sessions (scope `sessions:read`)
|
|
8
|
+
* GET /sessions/:id (scope `sessions:read`)
|
|
9
|
+
* POST /sessions (idempotent; scope `sessions:write`)
|
|
10
|
+
* DELETE /sessions/:id (scope `sessions:write`)
|
|
11
|
+
* GET /sessions/:id/messages (scope `sessions:read`)
|
|
12
|
+
* GET /sessions/:id/handoffs (scope `sessions:read`)
|
|
13
|
+
* POST /sessions/:id/export (scope `sessions:export`)
|
|
14
|
+
*
|
|
15
|
+
* @packageDocumentation
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { Context } from 'hono';
|
|
19
|
+
import { Hono } from 'hono';
|
|
20
|
+
import { z } from 'zod';
|
|
21
|
+
|
|
22
|
+
import type { ServerVariables } from '../internal/context.js';
|
|
23
|
+
import { createScopeMiddleware } from '../middleware/scope.js';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Minimal contract route handlers consume. Real deployments wire
|
|
27
|
+
* `@graphorin/sessions.SessionManager` in directly; tests pass a
|
|
28
|
+
* lighter stub.
|
|
29
|
+
*
|
|
30
|
+
* @stable
|
|
31
|
+
*/
|
|
32
|
+
export interface SessionApi {
|
|
33
|
+
list(opts: {
|
|
34
|
+
readonly userId?: string;
|
|
35
|
+
readonly agentId?: string;
|
|
36
|
+
}): Promise<ReadonlyArray<unknown>>;
|
|
37
|
+
get(sessionId: string): Promise<unknown | null>;
|
|
38
|
+
create(input: {
|
|
39
|
+
readonly userId: string;
|
|
40
|
+
readonly agentId: string;
|
|
41
|
+
readonly sessionId?: string;
|
|
42
|
+
readonly title?: string;
|
|
43
|
+
readonly tags?: ReadonlyArray<string>;
|
|
44
|
+
}): Promise<unknown>;
|
|
45
|
+
remove(sessionId: string): Promise<boolean>;
|
|
46
|
+
listMessages(
|
|
47
|
+
sessionId: string,
|
|
48
|
+
opts: { readonly limit?: number },
|
|
49
|
+
): Promise<ReadonlyArray<unknown>>;
|
|
50
|
+
listHandoffs(sessionId: string): Promise<ReadonlyArray<unknown>>;
|
|
51
|
+
exportSession?(
|
|
52
|
+
sessionId: string,
|
|
53
|
+
opts: {
|
|
54
|
+
readonly includeAuditEntries?: boolean;
|
|
55
|
+
readonly hash?: boolean;
|
|
56
|
+
},
|
|
57
|
+
): Promise<unknown>;
|
|
58
|
+
replaySession?(
|
|
59
|
+
sessionId: string,
|
|
60
|
+
opts: { readonly raw?: boolean; readonly fromMessageId?: string },
|
|
61
|
+
): AsyncIterable<unknown>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const CreateBodySchema = z
|
|
65
|
+
.object({
|
|
66
|
+
userId: z.string().min(1),
|
|
67
|
+
agentId: z.string().min(1),
|
|
68
|
+
sessionId: z.string().min(1).optional(),
|
|
69
|
+
title: z.string().min(1).optional(),
|
|
70
|
+
tags: z.array(z.string()).optional(),
|
|
71
|
+
})
|
|
72
|
+
.strict();
|
|
73
|
+
|
|
74
|
+
const MessagesQuerySchema = z
|
|
75
|
+
.object({
|
|
76
|
+
limit: z.coerce.number().int().positive().max(1000).optional(),
|
|
77
|
+
})
|
|
78
|
+
.strict();
|
|
79
|
+
|
|
80
|
+
const ExportBodySchema = z
|
|
81
|
+
.object({
|
|
82
|
+
includeAuditEntries: z.boolean().optional(),
|
|
83
|
+
hash: z.boolean().optional(),
|
|
84
|
+
})
|
|
85
|
+
.strict()
|
|
86
|
+
.default({});
|
|
87
|
+
|
|
88
|
+
const _ReplayBodySchema = z
|
|
89
|
+
.object({
|
|
90
|
+
raw: z.boolean().optional(),
|
|
91
|
+
fromMessageId: z.string().min(1).optional(),
|
|
92
|
+
})
|
|
93
|
+
.strict()
|
|
94
|
+
.default({});
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* @stable
|
|
98
|
+
*/
|
|
99
|
+
export interface SessionRoutesDeps {
|
|
100
|
+
readonly sessions: SessionApi;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* @stable
|
|
105
|
+
*/
|
|
106
|
+
export function createSessionRoutes(deps: SessionRoutesDeps): Hono<{ Variables: ServerVariables }> {
|
|
107
|
+
const app = new Hono<{ Variables: ServerVariables }>();
|
|
108
|
+
|
|
109
|
+
app.get('/', createScopeMiddleware('sessions:read'), async (c) => {
|
|
110
|
+
const userId = c.req.query('userId');
|
|
111
|
+
const agentId = c.req.query('agentId');
|
|
112
|
+
const filter: { userId?: string; agentId?: string } = {};
|
|
113
|
+
if (userId !== undefined && userId.length > 0) filter.userId = userId;
|
|
114
|
+
if (agentId !== undefined && agentId.length > 0) filter.agentId = agentId;
|
|
115
|
+
const list = await deps.sessions.list(filter);
|
|
116
|
+
return c.json({ sessions: list });
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
app.post('/', createScopeMiddleware('sessions:write'), async (c) => {
|
|
120
|
+
const parsed = CreateBodySchema.safeParse(await safelyParseJson(c));
|
|
121
|
+
if (!parsed.success) {
|
|
122
|
+
return c.json(
|
|
123
|
+
{
|
|
124
|
+
error: 'config-invalid',
|
|
125
|
+
message: 'Invalid session body.',
|
|
126
|
+
issues: parsed.error.issues.map((i) => ({ path: i.path, message: i.message })),
|
|
127
|
+
},
|
|
128
|
+
400,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
const session = await deps.sessions.create({
|
|
132
|
+
userId: parsed.data.userId,
|
|
133
|
+
agentId: parsed.data.agentId,
|
|
134
|
+
...(parsed.data.sessionId !== undefined ? { sessionId: parsed.data.sessionId } : {}),
|
|
135
|
+
...(parsed.data.title !== undefined ? { title: parsed.data.title } : {}),
|
|
136
|
+
...(parsed.data.tags !== undefined ? { tags: parsed.data.tags } : {}),
|
|
137
|
+
});
|
|
138
|
+
return c.json({ session }, 201);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// W-107: per-resource requirement - a token scoped to ONE session
|
|
142
|
+
// reads it on every surface (WS subjects already gated per-resource;
|
|
143
|
+
// wide two-segment grants keep covering three-segment requirements).
|
|
144
|
+
app.get(
|
|
145
|
+
'/:id',
|
|
146
|
+
createScopeMiddleware((_path, params) => `sessions:read:${params.id}`),
|
|
147
|
+
async (c) => {
|
|
148
|
+
const id = c.req.param('id');
|
|
149
|
+
const session = await deps.sessions.get(id);
|
|
150
|
+
if (session === null) {
|
|
151
|
+
return c.json({ error: 'session-not-found', message: `Session '${id}' not found.` }, 404);
|
|
152
|
+
}
|
|
153
|
+
return c.json({ session });
|
|
154
|
+
},
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
app.delete(
|
|
158
|
+
'/:id',
|
|
159
|
+
createScopeMiddleware((_path, params) => `sessions:write:${params.id}`),
|
|
160
|
+
async (c) => {
|
|
161
|
+
const id = c.req.param('id');
|
|
162
|
+
const removed = await deps.sessions.remove(id);
|
|
163
|
+
if (!removed) {
|
|
164
|
+
return c.json({ error: 'session-not-found', message: `Session '${id}' not found.` }, 404);
|
|
165
|
+
}
|
|
166
|
+
return c.body(null, 204);
|
|
167
|
+
},
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
app.get(
|
|
171
|
+
'/:id/messages',
|
|
172
|
+
createScopeMiddleware((_path, params) => `sessions:read:${params.id}`),
|
|
173
|
+
async (c) => {
|
|
174
|
+
const id = c.req.param('id');
|
|
175
|
+
const parsed = MessagesQuerySchema.safeParse({ limit: c.req.query('limit') });
|
|
176
|
+
if (!parsed.success) {
|
|
177
|
+
return c.json(
|
|
178
|
+
{
|
|
179
|
+
error: 'config-invalid',
|
|
180
|
+
message: 'Invalid messages query.',
|
|
181
|
+
issues: parsed.error.issues.map((i) => ({ path: i.path, message: i.message })),
|
|
182
|
+
},
|
|
183
|
+
400,
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
const opts: { limit?: number } = {};
|
|
187
|
+
if (parsed.data.limit !== undefined) opts.limit = parsed.data.limit;
|
|
188
|
+
const messages = await deps.sessions.listMessages(id, opts);
|
|
189
|
+
return c.json({ sessionId: id, messages });
|
|
190
|
+
},
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
app.get(
|
|
194
|
+
'/:id/handoffs',
|
|
195
|
+
createScopeMiddleware((_path, params) => `sessions:read:${params.id}`),
|
|
196
|
+
async (c) => {
|
|
197
|
+
const id = c.req.param('id');
|
|
198
|
+
const handoffs = await deps.sessions.listHandoffs(id);
|
|
199
|
+
return c.json({ sessionId: id, handoffs });
|
|
200
|
+
},
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
app.post(
|
|
204
|
+
'/:id/export',
|
|
205
|
+
createScopeMiddleware((_path, params) => `sessions:export:${params.id}`),
|
|
206
|
+
async (c) => {
|
|
207
|
+
const id = c.req.param('id');
|
|
208
|
+
if (deps.sessions.exportSession === undefined) {
|
|
209
|
+
return c.json(
|
|
210
|
+
{ error: 'session-export-unsupported', message: 'Export is not enabled.' },
|
|
211
|
+
400,
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
const parsed = ExportBodySchema.safeParse(await safelyParseJson(c));
|
|
215
|
+
if (!parsed.success) {
|
|
216
|
+
return c.json(
|
|
217
|
+
{
|
|
218
|
+
error: 'config-invalid',
|
|
219
|
+
message: 'Invalid export body.',
|
|
220
|
+
issues: parsed.error.issues.map((i) => ({ path: i.path, message: i.message })),
|
|
221
|
+
},
|
|
222
|
+
400,
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
const exportOpts: { includeAuditEntries?: boolean; hash?: boolean } = {};
|
|
226
|
+
if (parsed.data.includeAuditEntries !== undefined) {
|
|
227
|
+
exportOpts.includeAuditEntries = parsed.data.includeAuditEntries;
|
|
228
|
+
}
|
|
229
|
+
if (parsed.data.hash !== undefined) exportOpts.hash = parsed.data.hash;
|
|
230
|
+
const result = await deps.sessions.exportSession(id, exportOpts);
|
|
231
|
+
return c.json({ sessionId: id, export: result });
|
|
232
|
+
},
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
// IP-14: the session-replay route lives in `replay/routes.ts` (the
|
|
236
|
+
// scope-laddered, audit-backed implementation). The Phase-14a stub
|
|
237
|
+
// that lived here SHADOWED it (mounted earlier on the same path)
|
|
238
|
+
// while advertising a `session:<id>/replay` subject the WS grammar
|
|
239
|
+
// cannot parse - deleted.
|
|
240
|
+
|
|
241
|
+
return app;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async function safelyParseJson(c: Context<{ Variables: ServerVariables }>): Promise<unknown> {
|
|
245
|
+
try {
|
|
246
|
+
return await c.req.json();
|
|
247
|
+
} catch {
|
|
248
|
+
return {};
|
|
249
|
+
}
|
|
250
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill REST routes.
|
|
3
|
+
*
|
|
4
|
+
* GET /skills (scope `skills:read`)
|
|
5
|
+
* POST /skills/install (idempotent; scope `skills:install`)
|
|
6
|
+
* GET /skills/:name (scope `skills:read`)
|
|
7
|
+
*
|
|
8
|
+
* @packageDocumentation
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { Context } from 'hono';
|
|
12
|
+
import { Hono } from 'hono';
|
|
13
|
+
import { z } from 'zod';
|
|
14
|
+
|
|
15
|
+
import type { ServerVariables } from '../internal/context.js';
|
|
16
|
+
import { createScopeMiddleware } from '../middleware/scope.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @stable
|
|
20
|
+
*/
|
|
21
|
+
export interface SkillsApi {
|
|
22
|
+
list(): Promise<ReadonlyArray<{ readonly name: string; readonly version?: string }>>;
|
|
23
|
+
get(name: string): Promise<unknown | null>;
|
|
24
|
+
install(input: {
|
|
25
|
+
readonly source: string;
|
|
26
|
+
readonly trust?: 'verified' | 'unverified';
|
|
27
|
+
}): Promise<unknown>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const InstallBodySchema = z
|
|
31
|
+
.object({
|
|
32
|
+
source: z.string().min(1),
|
|
33
|
+
trust: z.enum(['verified', 'unverified']).optional(),
|
|
34
|
+
})
|
|
35
|
+
.strict();
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @stable
|
|
39
|
+
*/
|
|
40
|
+
export interface SkillsRoutesDeps {
|
|
41
|
+
readonly skills: SkillsApi;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @stable
|
|
46
|
+
*/
|
|
47
|
+
export function createSkillsRoutes(deps: SkillsRoutesDeps): Hono<{ Variables: ServerVariables }> {
|
|
48
|
+
const app = new Hono<{ Variables: ServerVariables }>();
|
|
49
|
+
|
|
50
|
+
app.get('/', createScopeMiddleware('skills:read'), async (c) => {
|
|
51
|
+
return c.json({ skills: await deps.skills.list() });
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
app.get('/:name', createScopeMiddleware('skills:read'), async (c) => {
|
|
55
|
+
const name = c.req.param('name');
|
|
56
|
+
const skill = await deps.skills.get(name);
|
|
57
|
+
if (skill === null)
|
|
58
|
+
return c.json({ error: 'skill-not-found', message: `Skill '${name}' not found.` }, 404);
|
|
59
|
+
return c.json({ skill });
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
app.post('/install', createScopeMiddleware('skills:install'), async (c) => {
|
|
63
|
+
const parsed = InstallBodySchema.safeParse(await safelyParseJson(c));
|
|
64
|
+
if (!parsed.success) {
|
|
65
|
+
return c.json(
|
|
66
|
+
{
|
|
67
|
+
error: 'config-invalid',
|
|
68
|
+
message: 'Invalid install body.',
|
|
69
|
+
issues: parsed.error.issues.map((i) => ({ path: i.path, message: i.message })),
|
|
70
|
+
},
|
|
71
|
+
400,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
const installInput: { source: string; trust?: 'verified' | 'unverified' } = {
|
|
75
|
+
source: parsed.data.source,
|
|
76
|
+
};
|
|
77
|
+
if (parsed.data.trust !== undefined) installInput.trust = parsed.data.trust;
|
|
78
|
+
const out = await deps.skills.install(installInput);
|
|
79
|
+
return c.json({ skill: out }, 201);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return app;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function safelyParseJson(c: Context<{ Variables: ServerVariables }>): Promise<unknown> {
|
|
86
|
+
try {
|
|
87
|
+
return await c.req.json();
|
|
88
|
+
} catch {
|
|
89
|
+
return {};
|
|
90
|
+
}
|
|
91
|
+
}
|