@agentage/extension 0.1.1 → 0.2.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/README.md +6 -5
- package/dist/agcid/agcid.d.ts +1 -0
- package/dist/agcid/agcid.js +2 -2
- package/dist/agcid/current.d.ts +7 -0
- package/dist/agcid/current.js +4 -0
- package/dist/agcid/scope.d.ts +1 -1
- package/dist/agcid/scope.js +3 -4
- package/dist/command/args.d.ts +14 -0
- package/dist/command/args.js +60 -0
- package/dist/command/check-cli.d.ts +1 -0
- package/dist/command/check-cli.js +24 -0
- package/dist/command/command.types.d.ts +14 -0
- package/dist/command/command.types.js +2 -0
- package/dist/command/out.d.ts +19 -0
- package/dist/command/out.js +32 -0
- package/dist/command/run-command.d.ts +15 -0
- package/dist/command/run-command.js +53 -0
- package/dist/context/context.d.ts +8 -14
- package/dist/context/context.js +19 -21
- package/dist/context/settings.d.ts +5 -0
- package/dist/context/settings.js +32 -0
- package/dist/context/test-context.d.ts +44 -0
- package/dist/context/test-context.js +72 -0
- package/dist/discovery/discovery.d.ts +12 -1
- package/dist/discovery/discovery.js +21 -27
- package/dist/discovery/discovery.schema.d.ts +2 -26
- package/dist/discovery/discovery.schema.js +6 -4
- package/dist/discovery/openapi.d.ts +3 -0
- package/dist/discovery/openapi.js +19 -0
- package/dist/envelope/envelope.js +1 -0
- package/dist/envelope/envelope.schema.d.ts +0 -4
- package/dist/envelope/envelope.schema.js +3 -2
- package/dist/health/health.d.ts +5 -2
- package/dist/health/health.js +23 -8
- package/dist/health/health.schema.d.ts +2 -9
- package/dist/health/health.schema.js +3 -3
- package/dist/index.d.ts +19 -12
- package/dist/index.js +19 -12
- package/dist/log/log.d.ts +1 -0
- package/dist/log/log.js +7 -8
- package/dist/manifest/manifest.schema.d.ts +5 -75
- package/dist/manifest/manifest.schema.js +8 -4
- package/dist/manifest/primitives.schema.d.ts +1 -0
- package/dist/manifest/primitives.schema.js +3 -1
- package/dist/manifest/read-manifest.d.ts +1 -0
- package/dist/manifest/read-manifest.js +26 -3
- package/dist/mcp/mcp-app.d.ts +8 -0
- package/dist/mcp/mcp-app.js +74 -0
- package/dist/mcp/server.d.ts +8 -0
- package/dist/mcp/server.js +36 -0
- package/dist/mcp/snapshot.d.ts +11 -0
- package/dist/mcp/snapshot.js +69 -0
- package/dist/mcp/tools.d.ts +24 -0
- package/dist/mcp/tools.js +17 -0
- package/dist/pages/pages-app.d.ts +8 -0
- package/dist/pages/pages-app.js +29 -0
- package/dist/pages/pages.d.ts +4 -0
- package/dist/pages/pages.js +16 -0
- package/dist/runtime/build-info.d.ts +1 -0
- package/dist/runtime/build-info.js +4 -4
- package/dist/runtime/main.js +5 -2
- package/dist/runtime/part-runners.d.ts +20 -0
- package/dist/runtime/part-runners.js +68 -0
- package/dist/runtime/run-part.d.ts +4 -2
- package/dist/runtime/run-part.js +47 -34
- package/dist/server/app.d.ts +16 -2
- package/dist/server/app.js +28 -12
- package/dist/server/errors.d.ts +0 -2
- package/dist/server/errors.js +1 -1
- package/dist/server/serve.d.ts +0 -1
- package/dist/server/serve.js +2 -1
- package/dist/store/migrate.d.ts +2 -0
- package/dist/store/migrate.js +18 -0
- package/dist/store/migrations.d.ts +10 -0
- package/dist/store/migrations.js +55 -0
- package/dist/store/store.d.ts +10 -0
- package/dist/store/store.js +25 -0
- package/dist/store/store.types.d.ts +7 -0
- package/dist/store/store.types.js +1 -0
- package/dist/store/test-store.d.ts +5 -0
- package/dist/store/test-store.js +39 -0
- package/dist/testing/test-server.d.ts +6 -0
- package/dist/testing/test-server.js +31 -9
- package/dist/tool/build-info.js +2 -3
- package/dist/tool/build.d.ts +5 -2
- package/dist/tool/build.js +16 -24
- package/dist/tool/docker.d.ts +0 -1
- package/dist/tool/docker.js +2 -1
- package/dist/tool/exec.d.ts +4 -1
- package/dist/tool/exec.js +13 -5
- package/dist/tool/index.d.ts +9 -10
- package/dist/tool/index.js +9 -10
- package/dist/tool/kit-version.d.ts +1 -0
- package/dist/tool/kit-version.js +3 -0
- package/dist/tool/main.d.ts +6 -1
- package/dist/tool/main.js +58 -30
- package/dist/tool/recipe.d.ts +0 -1
- package/dist/tool/recipe.js +7 -3
- package/dist/tool/record-extras.d.ts +4 -0
- package/dist/tool/record-extras.js +60 -0
- package/dist/tool/stage.d.ts +3 -0
- package/dist/tool/stage.js +45 -12
- package/dist/tool/up/compose-run.d.ts +14 -0
- package/dist/tool/up/compose-run.js +24 -0
- package/dist/tool/up/compose.d.ts +18 -0
- package/dist/tool/up/compose.js +77 -0
- package/dist/tool/up/dev-issuer-app.d.ts +22 -0
- package/dist/tool/up/dev-issuer-app.js +80 -0
- package/dist/tool/up/dev-issuer.d.ts +2 -0
- package/dist/tool/up/dev-issuer.js +5 -0
- package/dist/tool/up/down.d.ts +12 -0
- package/dist/tool/up/down.js +38 -0
- package/dist/tool/up/gateway.d.ts +27 -0
- package/dist/tool/up/gateway.js +66 -0
- package/dist/tool/up/lines.d.ts +2 -0
- package/dist/tool/up/lines.js +3 -0
- package/dist/tool/up/local-record.d.ts +44 -0
- package/dist/tool/up/local-record.js +28 -0
- package/dist/tool/up/token.d.ts +6 -0
- package/dist/tool/up/token.js +18 -0
- package/dist/tool/up/up.d.ts +19 -0
- package/dist/tool/up/up.js +81 -0
- package/dist/tool/up/wait.d.ts +8 -0
- package/dist/tool/up/wait.js +24 -0
- package/dist/ui/ui-app.d.ts +6 -0
- package/dist/ui/ui-app.js +26 -0
- package/dist/worker/cron.d.ts +11 -0
- package/dist/worker/cron.js +73 -0
- package/dist/worker/job-options.d.ts +9 -0
- package/dist/worker/job-options.js +17 -0
- package/dist/worker/run-log.d.ts +9 -0
- package/dist/worker/run-log.js +38 -0
- package/dist/worker/runs.d.ts +20 -0
- package/dist/worker/runs.js +59 -0
- package/dist/worker/scheduler.d.ts +20 -0
- package/dist/worker/scheduler.js +97 -0
- package/dist/worker/worker.types.d.ts +5 -0
- package/dist/worker/worker.types.js +2 -0
- package/package.json +11 -2
- package/schemas/extension-discovery.schema.json +5 -0
- package/schemas/extension-package.schema.json +3 -0
- package/schemas/extension-record.schema.json +5 -0
- package/schemas/health.schema.json +1 -1
|
@@ -11,9 +11,32 @@ export const validateManifest = (value) => {
|
|
|
11
11
|
return result.data;
|
|
12
12
|
throw new Error(`invalid agentage manifest in package.json: ${formatIssues(result.error.issues)}`);
|
|
13
13
|
};
|
|
14
|
+
const parseJson = (text, file) => {
|
|
15
|
+
try {
|
|
16
|
+
return JSON.parse(text);
|
|
17
|
+
}
|
|
18
|
+
catch (err) {
|
|
19
|
+
throw new Error(`${file} is not valid JSON: ${err.message}`);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
// undefined only for a missing file (JSON never parses to it); any other read error is thrown.
|
|
23
|
+
export const readJsonIfExists = async (file) => {
|
|
24
|
+
let text;
|
|
25
|
+
try {
|
|
26
|
+
text = await readFile(file, 'utf8');
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
if (err.code === 'ENOENT')
|
|
30
|
+
return undefined;
|
|
31
|
+
throw err;
|
|
32
|
+
}
|
|
33
|
+
return parseJson(text, file);
|
|
34
|
+
};
|
|
14
35
|
// The manifest is the `agentage` block of package.json, read as data without running code.
|
|
15
36
|
export const readManifest = async (dir) => {
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
37
|
+
const pkg = await readJsonIfExists(join(dir, 'package.json'));
|
|
38
|
+
if (pkg === undefined) {
|
|
39
|
+
throw new Error(`no package.json in ${dir}: run it in an extension package folder`);
|
|
40
|
+
}
|
|
41
|
+
return validateManifest(pkg);
|
|
19
42
|
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type Express } from 'express';
|
|
2
|
+
import { type PartAppBase } from '../server/app.js';
|
|
3
|
+
import { type ToolEntry } from './tools.js';
|
|
4
|
+
export interface McpAppOptions extends PartAppBase {
|
|
5
|
+
tools: ToolEntry[];
|
|
6
|
+
listing: 'open' | 'token';
|
|
7
|
+
}
|
|
8
|
+
export declare const createMcpApp: (options: McpAppOptions) => Express;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { Readable } from 'node:stream';
|
|
2
|
+
import { pipeline } from 'node:stream/promises';
|
|
3
|
+
import cors from 'cors';
|
|
4
|
+
import express, {} from 'express';
|
|
5
|
+
import { createMcpHandler } from '@modelcontextprotocol/server';
|
|
6
|
+
import { fail } from '../envelope/envelope.js';
|
|
7
|
+
import { BODY_LIMIT, createPartApp, PUBLIC_CORS } from '../server/app.js';
|
|
8
|
+
import { serverFactory } from './server.js';
|
|
9
|
+
import {} from './tools.js';
|
|
10
|
+
const MCP_HEADERS = ['Mcp-Protocol-Version', 'Mcp-Method', 'Mcp-Name', 'Accept'];
|
|
11
|
+
const MCP_CORS = {
|
|
12
|
+
...PUBLIC_CORS,
|
|
13
|
+
allowedHeaders: [...PUBLIC_CORS.allowedHeaders, ...MCP_HEADERS],
|
|
14
|
+
};
|
|
15
|
+
const parsed = (body) => {
|
|
16
|
+
try {
|
|
17
|
+
return JSON.parse(body.toString('utf8'));
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
const isListing = (body) => (Array.isArray(body) ? body : [body]).some((message) => message?.method === 'tools/list');
|
|
24
|
+
const webRequest = (req, body) => {
|
|
25
|
+
const headers = new Headers();
|
|
26
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
27
|
+
if (typeof value === 'string')
|
|
28
|
+
headers.set(key, value);
|
|
29
|
+
}
|
|
30
|
+
return new globalThis.Request(`http://${req.get('host') ?? 'mcp'}/mcp`, {
|
|
31
|
+
method: 'POST',
|
|
32
|
+
headers,
|
|
33
|
+
body,
|
|
34
|
+
});
|
|
35
|
+
};
|
|
36
|
+
const send = async (response, res) => {
|
|
37
|
+
res.status(response.status);
|
|
38
|
+
response.headers.forEach((value, key) => void res.setHeader(key, value));
|
|
39
|
+
if (!response.body)
|
|
40
|
+
return void res.end();
|
|
41
|
+
await pipeline(Readable.fromWeb(response.body), res);
|
|
42
|
+
};
|
|
43
|
+
// The kit verifies no token yet, so a closed listing refuses every tools/list.
|
|
44
|
+
const handle = (handler, listing) => async (req, res) => {
|
|
45
|
+
const body = Buffer.isBuffer(req.body) ? req.body : Buffer.alloc(0);
|
|
46
|
+
const message = parsed(body);
|
|
47
|
+
if (listing === 'token' && isListing(message))
|
|
48
|
+
throw fail('UNAUTHORIZED', 'tools/list needs a token');
|
|
49
|
+
await send(await handler.fetch(webRequest(req, body), { parsedBody: message }), res);
|
|
50
|
+
};
|
|
51
|
+
const methodNotAllowed = (_req, res) => {
|
|
52
|
+
res.setHeader('Allow', 'POST');
|
|
53
|
+
res
|
|
54
|
+
.status(405)
|
|
55
|
+
.json({ jsonrpc: '2.0', error: { code: -32000, message: 'Method not allowed.' }, id: null });
|
|
56
|
+
};
|
|
57
|
+
// Stateless Streamable HTTP on SDK v2: POST /mcp only, GET and DELETE (sessions) answer 405.
|
|
58
|
+
export const createMcpApp = (options) => {
|
|
59
|
+
const identity = { id: options.pkg.agentage.id, version: options.pkg.version };
|
|
60
|
+
const handler = createMcpHandler(serverFactory(identity, options.tools, options.log), {
|
|
61
|
+
legacy: 'stateless',
|
|
62
|
+
onerror: (err) => options.log.warn('mcp request refused', { reason: err.message }),
|
|
63
|
+
});
|
|
64
|
+
const raw = express.raw({ type: () => true, limit: BODY_LIMIT });
|
|
65
|
+
return createPartApp({
|
|
66
|
+
...options,
|
|
67
|
+
part: 'mcp',
|
|
68
|
+
mount: (app) => {
|
|
69
|
+
app.use('/mcp', cors(MCP_CORS));
|
|
70
|
+
app.post('/mcp', raw, handle(handler, options.listing));
|
|
71
|
+
app.all('/mcp', methodNotAllowed);
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/server';
|
|
2
|
+
import { type Log } from '../log/log.js';
|
|
3
|
+
import { type ToolEntry } from './tools.js';
|
|
4
|
+
export interface ServerIdentity {
|
|
5
|
+
id: string;
|
|
6
|
+
version: string;
|
|
7
|
+
}
|
|
8
|
+
export declare const serverFactory: (identity: ServerIdentity, tools: ToolEntry[], log: Log) => () => McpServer;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/server';
|
|
2
|
+
import { currentAgcid } from '../agcid/current.js';
|
|
3
|
+
import { fail, isFailure } from '../envelope/envelope.js';
|
|
4
|
+
import {} from '../log/log.js';
|
|
5
|
+
import { prefixed } from './tools.js';
|
|
6
|
+
const text = (value) => [{ type: 'text', text: JSON.stringify(value) }];
|
|
7
|
+
// A failed tool is isError with the REST error shape; anything unexpected is a generic INTERNAL.
|
|
8
|
+
const errorResult = (err, log) => {
|
|
9
|
+
const failure = isFailure(err) ? err : fail('INTERNAL', 'Internal error');
|
|
10
|
+
if (failure.status >= 500)
|
|
11
|
+
log.error(err);
|
|
12
|
+
const error = { code: failure.code, message: failure.message, agcid: currentAgcid() ?? '' };
|
|
13
|
+
return { isError: true, content: text({ error }), structuredContent: { error } };
|
|
14
|
+
};
|
|
15
|
+
const callback = (entry, log) => async (args) => {
|
|
16
|
+
try {
|
|
17
|
+
const output = entry.spec.output.parse(await entry.handler(args));
|
|
18
|
+
return { content: text(output), structuredContent: output };
|
|
19
|
+
}
|
|
20
|
+
catch (err) {
|
|
21
|
+
return errorResult(err, log);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
// One fresh server per request (stateless); serverInfo agentage-{id} at the package version.
|
|
25
|
+
export const serverFactory = (identity, tools, log) => () => {
|
|
26
|
+
const server = new McpServer({ name: `agentage-${identity.id}`, version: identity.version });
|
|
27
|
+
for (const entry of tools) {
|
|
28
|
+
const config = {
|
|
29
|
+
description: entry.spec.description,
|
|
30
|
+
inputSchema: entry.spec.input,
|
|
31
|
+
outputSchema: entry.spec.output,
|
|
32
|
+
};
|
|
33
|
+
server.registerTool(prefixed(identity.id, entry.name), config, callback(entry, log));
|
|
34
|
+
}
|
|
35
|
+
return server;
|
|
36
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare const PROTOCOL_VERSIONS: readonly ["2025-11-25", "2026-07-28"];
|
|
2
|
+
export type ProtocolVersion = (typeof PROTOCOL_VERSIONS)[number];
|
|
3
|
+
export interface McpSnapshot {
|
|
4
|
+
serverInfo: {
|
|
5
|
+
name: string;
|
|
6
|
+
version: string;
|
|
7
|
+
};
|
|
8
|
+
tools: Record<string, unknown>[];
|
|
9
|
+
}
|
|
10
|
+
export declare const canonical: (value: unknown) => unknown;
|
|
11
|
+
export declare const mcpSnapshot: (url: string, version: ProtocolVersion) => Promise<McpSnapshot>;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export const PROTOCOL_VERSIONS = ['2025-11-25', '2026-07-28'];
|
|
2
|
+
const CLIENT = { name: 'agentage-snapshot', version: '1.0.0' };
|
|
3
|
+
// Key order never counts: the two eras serialize the same schema in different orders.
|
|
4
|
+
export const canonical = (value) => {
|
|
5
|
+
if (Array.isArray(value))
|
|
6
|
+
return value.map(canonical);
|
|
7
|
+
if (typeof value !== 'object' || value === null)
|
|
8
|
+
return value;
|
|
9
|
+
const entries = Object.entries(value).sort(([a], [b]) => a.localeCompare(b));
|
|
10
|
+
return Object.fromEntries(entries.map(([key, v]) => [key, canonical(v)]));
|
|
11
|
+
};
|
|
12
|
+
// A JSON answer or the stateless legacy leg's one-message SSE stream.
|
|
13
|
+
const payload = async (res) => {
|
|
14
|
+
const text = await res.text();
|
|
15
|
+
if (!res.ok)
|
|
16
|
+
throw new Error(`mcp answered ${res.status}: ${text.slice(0, 200)}`);
|
|
17
|
+
const data = (res.headers.get('content-type') ?? '').includes('text/event-stream')
|
|
18
|
+
? (text
|
|
19
|
+
.split('\n')
|
|
20
|
+
.find((line) => line.startsWith('data:'))
|
|
21
|
+
?.slice(5) ?? '{}')
|
|
22
|
+
: text;
|
|
23
|
+
const message = JSON.parse(data);
|
|
24
|
+
if (message['error'])
|
|
25
|
+
throw new Error(`mcp error: ${JSON.stringify(message['error'])}`);
|
|
26
|
+
return message['result'];
|
|
27
|
+
};
|
|
28
|
+
const META = {
|
|
29
|
+
'io.modelcontextprotocol/protocolVersion': '2026-07-28',
|
|
30
|
+
'io.modelcontextprotocol/clientInfo': CLIENT,
|
|
31
|
+
'io.modelcontextprotocol/clientCapabilities': {},
|
|
32
|
+
};
|
|
33
|
+
const post = async (url, version, method, params) => {
|
|
34
|
+
const modern = version === '2026-07-28';
|
|
35
|
+
const headers = {
|
|
36
|
+
'content-type': 'application/json',
|
|
37
|
+
accept: 'application/json, text/event-stream',
|
|
38
|
+
'mcp-protocol-version': version,
|
|
39
|
+
...(modern ? { 'mcp-method': method } : {}),
|
|
40
|
+
};
|
|
41
|
+
const body = {
|
|
42
|
+
jsonrpc: '2.0',
|
|
43
|
+
id: 1,
|
|
44
|
+
method,
|
|
45
|
+
params: modern ? { ...params, _meta: META } : params,
|
|
46
|
+
};
|
|
47
|
+
return payload(await fetch(url, { method: 'POST', headers, body: JSON.stringify(body) }));
|
|
48
|
+
};
|
|
49
|
+
const serverInfoOf = async (url, version) => {
|
|
50
|
+
if (version === '2025-11-25') {
|
|
51
|
+
const init = await post(url, version, 'initialize', {
|
|
52
|
+
protocolVersion: version,
|
|
53
|
+
capabilities: {},
|
|
54
|
+
clientInfo: CLIENT,
|
|
55
|
+
});
|
|
56
|
+
return init['serverInfo'];
|
|
57
|
+
}
|
|
58
|
+
const found = await post(url, version, 'server/discover', {});
|
|
59
|
+
return found['_meta']['io.modelcontextprotocol/serverInfo'];
|
|
60
|
+
};
|
|
61
|
+
// What a client of that protocol version sees: serverInfo and tools/list, canonical; the contract test diffs it.
|
|
62
|
+
export const mcpSnapshot = async (url, version) => {
|
|
63
|
+
const { name, version: serverVersion } = await serverInfoOf(url, version);
|
|
64
|
+
const listed = await post(url, version, 'tools/list', {});
|
|
65
|
+
return canonical({
|
|
66
|
+
serverInfo: { name, version: serverVersion },
|
|
67
|
+
tools: listed['tools'],
|
|
68
|
+
});
|
|
69
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type z } from 'zod';
|
|
2
|
+
import { type PartContext } from '../context/context.js';
|
|
3
|
+
type ObjectSchema = z.ZodObject;
|
|
4
|
+
export interface ToolSpec<I extends ObjectSchema, O extends ObjectSchema> {
|
|
5
|
+
description: string;
|
|
6
|
+
input: I;
|
|
7
|
+
output: O;
|
|
8
|
+
}
|
|
9
|
+
export type ToolHandler<I extends ObjectSchema, O extends ObjectSchema> = (args: z.output<I>) => z.input<O> | Promise<z.input<O>>;
|
|
10
|
+
export type ToolRegistrar = <I extends ObjectSchema, O extends ObjectSchema>(name: string, spec: ToolSpec<I, O>, handler: ToolHandler<I, O>) => void;
|
|
11
|
+
export interface McpContext extends PartContext {
|
|
12
|
+
readonly tool: ToolRegistrar;
|
|
13
|
+
}
|
|
14
|
+
export interface ToolEntry {
|
|
15
|
+
name: string;
|
|
16
|
+
spec: ToolSpec<ObjectSchema, ObjectSchema>;
|
|
17
|
+
handler: (args: Record<string, unknown>) => unknown;
|
|
18
|
+
}
|
|
19
|
+
export declare const createToolRegistry: () => {
|
|
20
|
+
tool: ToolRegistrar;
|
|
21
|
+
tools: ToolEntry[];
|
|
22
|
+
};
|
|
23
|
+
export declare const prefixed: (id: string, name: string) => string;
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import {} from 'zod';
|
|
2
|
+
import {} from '../context/context.js';
|
|
3
|
+
const TOOL_NAME = /^[a-z][a-z0-9_-]*$/;
|
|
4
|
+
export const createToolRegistry = () => {
|
|
5
|
+
const tools = [];
|
|
6
|
+
const tool = (name, spec, handler) => {
|
|
7
|
+
if (!TOOL_NAME.test(name))
|
|
8
|
+
throw new Error(`tool name ${name}: lowercase, digits, _ and -`);
|
|
9
|
+
if (tools.some((entry) => entry.name === name))
|
|
10
|
+
throw new Error(`tool ${name} is registered twice`);
|
|
11
|
+
if (!spec.description)
|
|
12
|
+
throw new Error(`tool ${name} needs a description: it routes the model`);
|
|
13
|
+
tools.push({ name, spec, handler: handler });
|
|
14
|
+
};
|
|
15
|
+
return { tool, tools };
|
|
16
|
+
};
|
|
17
|
+
export const prefixed = (id, name) => `${id}__${name}`;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type Express } from 'express';
|
|
2
|
+
import { type PartAppBase } from '../server/app.js';
|
|
3
|
+
import { type PagesSite } from './pages.js';
|
|
4
|
+
export interface PagesAppOptions extends PartAppBase {
|
|
5
|
+
site: PagesSite;
|
|
6
|
+
}
|
|
7
|
+
export declare const NOT_FOUND_PAGE = "<!doctype html>\n<html lang=\"en\"><head><meta charset=\"utf-8\"><title>Not found</title></head><body><p>Not found</p></body></html>\n";
|
|
8
|
+
export declare const createPagesApp: (options: PagesAppOptions) => Express;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import {} from 'express';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { createPartApp } from '../server/app.js';
|
|
4
|
+
import {} from './pages.js';
|
|
5
|
+
const SiteSchema = z.record(z.string().regex(/^\/[A-Za-z0-9._~/-]*$/), z.string());
|
|
6
|
+
export const NOT_FOUND_PAGE = '<!doctype html>\n<html lang="en"><head><meta charset="utf-8"><title>Not found</title></head><body><p>Not found</p></body></html>\n';
|
|
7
|
+
const serve = (site) => (req, res, next) => {
|
|
8
|
+
const html = site[req.path];
|
|
9
|
+
if (html === undefined)
|
|
10
|
+
return next();
|
|
11
|
+
res.type('html').setHeader('Cache-Control', 'no-cache');
|
|
12
|
+
res.send(html);
|
|
13
|
+
};
|
|
14
|
+
const missing = (_req, res) => {
|
|
15
|
+
res.status(404).type('html').send(NOT_FOUND_PAGE);
|
|
16
|
+
};
|
|
17
|
+
// Everything the gateway does not reserve comes here: a page from the site map, or an HTML 404.
|
|
18
|
+
export const createPagesApp = (options) => {
|
|
19
|
+
const site = SiteSchema.parse(options.site);
|
|
20
|
+
return createPartApp({
|
|
21
|
+
...options,
|
|
22
|
+
part: 'pages',
|
|
23
|
+
helmet: { contentSecurityPolicy: { directives: { upgradeInsecureRequests: null } } },
|
|
24
|
+
mount: (app) => {
|
|
25
|
+
app.get(/.*/, serve(site));
|
|
26
|
+
app.get(/.*/, missing);
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import {} from '../context/context.js';
|
|
2
|
+
const escape = (value) => value.replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`);
|
|
3
|
+
const STYLE = 'body{font:16px/1.5 system-ui,sans-serif;margin:3rem;color:#1d1d1f;background:#fff}' +
|
|
4
|
+
'@media (prefers-color-scheme:dark){body{color:#f5f5f7;background:#111}}code{font-size:.9em}';
|
|
5
|
+
// One static page that names what runs: "{id} {version} · {commit}".
|
|
6
|
+
export const versionPage = (ctx) => {
|
|
7
|
+
const { id, version, commit } = ctx.extension;
|
|
8
|
+
const title = escape(id);
|
|
9
|
+
return [
|
|
10
|
+
'<!doctype html>',
|
|
11
|
+
`<html lang="en"><head><meta charset="utf-8"><title>${title}</title>`,
|
|
12
|
+
`<meta name="viewport" content="width=device-width, initial-scale=1"><style>${STYLE}</style></head>`,
|
|
13
|
+
`<body><main><h1>${title}</h1><p>${title} ${escape(version)} · <code>${escape(commit)}</code></p></main></body></html>`,
|
|
14
|
+
'',
|
|
15
|
+
].join('\n');
|
|
16
|
+
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type BuildInfo } from '../discovery/discovery.js';
|
|
2
2
|
export type Env = Readonly<Record<string, string | undefined>>;
|
|
3
|
+
export declare const isoSeconds: (date: Date) => string;
|
|
3
4
|
export declare const buildInfoFromEnv: (env: Env, now?: Date) => BuildInfo;
|
|
4
5
|
export declare const portFromEnv: (env: Env, fallback?: number) => number;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import {} from '../discovery/discovery.js';
|
|
2
|
+
import { FULL_SHA } from '../manifest/primitives.schema.js';
|
|
2
3
|
const UNKNOWN_COMMIT = '0'.repeat(40);
|
|
3
|
-
const isoSeconds = (date) => `${date.toISOString().slice(0, 19)}Z`;
|
|
4
|
+
export const isoSeconds = (date) => `${date.toISOString().slice(0, 19)}Z`;
|
|
5
|
+
const commitOf = (sha = '') => (FULL_SHA.test(sha) ? sha : UNKNOWN_COMMIT);
|
|
4
6
|
// COMMIT_SHA and BUILD_TIME are baked into every image by the recipe; a bare dev run shows zeros.
|
|
5
7
|
export const buildInfoFromEnv = (env, now = new Date()) => ({
|
|
6
|
-
commit:
|
|
7
|
-
? env['COMMIT_SHA']
|
|
8
|
-
: UNKNOWN_COMMIT,
|
|
8
|
+
commit: commitOf(env['COMMIT_SHA']),
|
|
9
9
|
buildTime: env['BUILD_TIME'] || isoSeconds(now),
|
|
10
10
|
});
|
|
11
11
|
export const portFromEnv = (env, fallback = 3000) => {
|
package/dist/runtime/main.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { createLog } from '../log/log.js';
|
|
2
3
|
import { runPart } from './run-part.js';
|
|
3
4
|
// Image entry: `node node_modules/@agentage/extension/dist/runtime/main.js api` in /app.
|
|
5
|
+
const part = process.argv[2] ?? 'api';
|
|
4
6
|
try {
|
|
5
|
-
await runPart({ dir: process.cwd(), part
|
|
7
|
+
await runPart({ dir: process.cwd(), part, env: process.env });
|
|
6
8
|
}
|
|
7
9
|
catch (err) {
|
|
8
|
-
|
|
10
|
+
// The manifest may be what failed, so the id is not known here; the line keeps the stack.
|
|
11
|
+
createLog({ extension: 'unknown', part }).error(err);
|
|
9
12
|
process.exit(1);
|
|
10
13
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type Express } from 'express';
|
|
2
|
+
import { type PartContext } from '../context/context.js';
|
|
3
|
+
import { type ExtensionPackage } from '../manifest/manifest.schema.js';
|
|
4
|
+
import { type ImagePartKind } from '../manifest/primitives.schema.js';
|
|
5
|
+
import { type PartAppBase } from '../server/app.js';
|
|
6
|
+
export interface RunnerInput {
|
|
7
|
+
dir: string;
|
|
8
|
+
pkg: ExtensionPackage;
|
|
9
|
+
ctx: PartContext;
|
|
10
|
+
base: PartAppBase;
|
|
11
|
+
importModule?: (file: string) => Promise<unknown>;
|
|
12
|
+
}
|
|
13
|
+
export interface Running {
|
|
14
|
+
app: Express;
|
|
15
|
+
stop?: () => void;
|
|
16
|
+
}
|
|
17
|
+
type PartCreate = (ctx: never) => unknown;
|
|
18
|
+
export declare const createOf: (mod: unknown, file: string) => PartCreate;
|
|
19
|
+
export declare const RUNNERS: Record<ImagePartKind, (input: RunnerInput) => Promise<Running>>;
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { pathToFileURL } from 'node:url';
|
|
3
|
+
import {} from 'express';
|
|
4
|
+
import {} from '../context/context.js';
|
|
5
|
+
import { readOpenapi } from '../discovery/openapi.js';
|
|
6
|
+
import {} from '../manifest/manifest.schema.js';
|
|
7
|
+
import { entryOf, normalizeParts } from '../manifest/parts.js';
|
|
8
|
+
import {} from '../manifest/primitives.schema.js';
|
|
9
|
+
import { createMcpApp } from '../mcp/mcp-app.js';
|
|
10
|
+
import { createToolRegistry } from '../mcp/tools.js';
|
|
11
|
+
import { createPagesApp } from '../pages/pages-app.js';
|
|
12
|
+
import { createApiApp, createPartApp } from '../server/app.js';
|
|
13
|
+
import {} from '../server/part.types.js';
|
|
14
|
+
import { createUiApp } from '../ui/ui-app.js';
|
|
15
|
+
import { runLog } from '../worker/run-log.js';
|
|
16
|
+
import { memoryRuns, storeRuns } from '../worker/runs.js';
|
|
17
|
+
import { createScheduler } from '../worker/scheduler.js';
|
|
18
|
+
const importFile = (file) => import(pathToFileURL(file).href);
|
|
19
|
+
export const createOf = (mod, file) => {
|
|
20
|
+
const create = mod?.create;
|
|
21
|
+
if (typeof create !== 'function')
|
|
22
|
+
throw new Error(`${file} does not export create(ctx)`);
|
|
23
|
+
return create;
|
|
24
|
+
};
|
|
25
|
+
// Imports the part's entry and calls its create(ctx); ui has no code to run.
|
|
26
|
+
const callCreate = async (input, part, ctx) => {
|
|
27
|
+
const file = join(input.dir, entryOf(input.pkg, part));
|
|
28
|
+
const mod = await (input.importModule ?? importFile)(file);
|
|
29
|
+
return createOf(mod, file)(ctx);
|
|
30
|
+
};
|
|
31
|
+
const api = async (input) => {
|
|
32
|
+
const router = (await callCreate(input, 'api', input.ctx));
|
|
33
|
+
const openapi = await readOpenapi(input.dir);
|
|
34
|
+
return { app: createApiApp({ ...input.base, router, openapi }) };
|
|
35
|
+
};
|
|
36
|
+
const mcp = async (input) => {
|
|
37
|
+
const registry = createToolRegistry();
|
|
38
|
+
await callCreate(input, 'mcp', { ...input.ctx, tool: registry.tool });
|
|
39
|
+
const listing = input.ctx.settings['mcp.listing'] === 'token' ? 'token' : 'open';
|
|
40
|
+
return { app: createMcpApp({ ...input.base, tools: registry.tools, listing }) };
|
|
41
|
+
};
|
|
42
|
+
const pages = async (input) => {
|
|
43
|
+
const site = (await callCreate(input, 'pages', input.ctx));
|
|
44
|
+
return { app: createPagesApp({ ...input.base, site }) };
|
|
45
|
+
};
|
|
46
|
+
const ui = async (input) => ({
|
|
47
|
+
app: createUiApp({ ...input.base, dir: join(input.dir, 'dist', 'ui') }),
|
|
48
|
+
});
|
|
49
|
+
const worker = async (input) => {
|
|
50
|
+
const log = runLog(input.ctx.log);
|
|
51
|
+
const hasStore = normalizeParts(input.pkg.agentage.parts).store === 'postgres';
|
|
52
|
+
const runs = hasStore ? storeRuns(input.ctx.store) : memoryRuns();
|
|
53
|
+
const scheduler = createScheduler({ runs, log });
|
|
54
|
+
await callCreate(input, 'worker', { ...input.ctx, log, schedule: scheduler.schedule });
|
|
55
|
+
const checks = { ...input.base.checks, ...scheduler.checks() };
|
|
56
|
+
scheduler.start();
|
|
57
|
+
return {
|
|
58
|
+
app: createPartApp({ ...input.base, checks, part: 'worker', mount: () => undefined }),
|
|
59
|
+
stop: scheduler.stop,
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
export const RUNNERS = {
|
|
63
|
+
api,
|
|
64
|
+
mcp,
|
|
65
|
+
pages,
|
|
66
|
+
ui,
|
|
67
|
+
worker,
|
|
68
|
+
};
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { type Server } from 'node:http';
|
|
2
|
-
import { type
|
|
2
|
+
import { type ExtensionPackage } from '../manifest/manifest.schema.js';
|
|
3
|
+
import { type ImagePartKind } from '../manifest/primitives.schema.js';
|
|
3
4
|
import { type ServeOptions } from '../server/serve.js';
|
|
4
5
|
import { type Env } from './build-info.js';
|
|
6
|
+
export { createOf } from './part-runners.js';
|
|
5
7
|
export interface RunPartOptions {
|
|
6
8
|
dir: string;
|
|
7
9
|
part: string;
|
|
@@ -9,5 +11,5 @@ export interface RunPartOptions {
|
|
|
9
11
|
importModule?: (file: string) => Promise<unknown>;
|
|
10
12
|
serve?: Pick<ServeOptions, 'signals' | 'exit'>;
|
|
11
13
|
}
|
|
12
|
-
export declare const
|
|
14
|
+
export declare const partOf: (pkg: ExtensionPackage, part: string) => ImagePartKind;
|
|
13
15
|
export declare const runPart: (options: RunPartOptions) => Promise<Server>;
|
package/dist/runtime/run-part.js
CHANGED
|
@@ -1,22 +1,18 @@
|
|
|
1
|
-
import { join } from 'node:path';
|
|
2
|
-
import { pathToFileURL } from 'node:url';
|
|
3
1
|
import {} from 'node:http';
|
|
4
2
|
import { createContext } from '../context/context.js';
|
|
3
|
+
import { settingsFromEnv } from '../context/settings.js';
|
|
5
4
|
import { DEFAULT_RECORD_FILE, loadRecord } from '../discovery/discovery.js';
|
|
5
|
+
import {} from '../health/health.js';
|
|
6
6
|
import { createLog } from '../log/log.js';
|
|
7
|
-
import {
|
|
7
|
+
import {} from '../manifest/manifest.schema.js';
|
|
8
|
+
import { imageParts, normalizeParts } from '../manifest/parts.js';
|
|
9
|
+
import {} from '../manifest/primitives.schema.js';
|
|
8
10
|
import { readManifest } from '../manifest/read-manifest.js';
|
|
9
|
-
import { createApiApp } from '../server/app.js';
|
|
10
|
-
import {} from '../server/part.types.js';
|
|
11
11
|
import { serve } from '../server/serve.js';
|
|
12
|
+
import { createStore, storeCheck } from '../store/store.js';
|
|
12
13
|
import { buildInfoFromEnv, portFromEnv } from './build-info.js';
|
|
13
|
-
|
|
14
|
-
export
|
|
15
|
-
const create = mod?.create;
|
|
16
|
-
if (typeof create !== 'function')
|
|
17
|
-
throw new Error(`${file} does not export create(ctx)`);
|
|
18
|
-
return create;
|
|
19
|
-
};
|
|
14
|
+
import { RUNNERS } from './part-runners.js';
|
|
15
|
+
export { createOf } from './part-runners.js';
|
|
20
16
|
const recordFor = async (env, log) => {
|
|
21
17
|
const file = env['AGENTAGE_RECORD_FILE'] || DEFAULT_RECORD_FILE;
|
|
22
18
|
const record = await loadRecord(file);
|
|
@@ -24,34 +20,51 @@ const recordFor = async (env, log) => {
|
|
|
24
20
|
log.warn('no release record mounted; discovery lists this image by tag', { file });
|
|
25
21
|
return record;
|
|
26
22
|
};
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
if (
|
|
30
|
-
throw new Error(`part ${
|
|
23
|
+
export const partOf = (pkg, part) => {
|
|
24
|
+
const parts = imageParts(pkg);
|
|
25
|
+
if (!parts.includes(part))
|
|
26
|
+
throw new Error(`part ${part} is not an image part of this manifest`);
|
|
27
|
+
return part;
|
|
28
|
+
};
|
|
29
|
+
// ui serves files and gets no ctx; every other part gets the store when the manifest has postgres.
|
|
30
|
+
const storeFor = (pkg, part, env, log) => part !== 'ui' && normalizeParts(pkg.agentage.parts).store === 'postgres'
|
|
31
|
+
? createStore(env, log)
|
|
32
|
+
: undefined;
|
|
33
|
+
const inputFor = async (options, pkg, part, log) => {
|
|
31
34
|
const { env } = options;
|
|
32
|
-
const pkg = await readManifest(options.dir);
|
|
33
|
-
const log = createLog({ extension: pkg.agentage.id, part: 'api' });
|
|
34
|
-
const port = portFromEnv(env);
|
|
35
35
|
const origin = env['AGENTAGE_ORIGIN'] || undefined;
|
|
36
|
+
const build = buildInfoFromEnv(env);
|
|
37
|
+
const store = storeFor(pkg, part, env, log);
|
|
38
|
+
const settings = settingsFromEnv(pkg, env, log);
|
|
39
|
+
const ctxOrigin = origin ?? `http://127.0.0.1:${portFromEnv(env)}`;
|
|
40
|
+
const ctxPart = (part === 'ui' ? 'pages' : part);
|
|
36
41
|
const ctx = createContext({
|
|
37
42
|
pkg,
|
|
38
|
-
part:
|
|
39
|
-
origin:
|
|
43
|
+
part: ctxPart,
|
|
44
|
+
origin: ctxOrigin,
|
|
45
|
+
commit: build.commit,
|
|
40
46
|
log,
|
|
47
|
+
settings,
|
|
48
|
+
store,
|
|
41
49
|
});
|
|
42
|
-
const file = join(options.dir, entryOf(pkg, 'api'));
|
|
43
|
-
const mod = await (options.importModule ?? importFile)(file);
|
|
44
|
-
const router = await createOf(mod, file)(ctx);
|
|
45
|
-
const build = buildInfoFromEnv(env);
|
|
46
50
|
const record = await recordFor(env, log);
|
|
47
|
-
const
|
|
51
|
+
const checks = store ? { store: storeCheck(store) } : {};
|
|
52
|
+
const base = { pkg, build, log, record, origin, ownImage: env['AGENTAGE_IMAGE'], checks };
|
|
53
|
+
return {
|
|
54
|
+
dir: options.dir,
|
|
48
55
|
pkg,
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
56
|
+
ctx,
|
|
57
|
+
base,
|
|
58
|
+
importModule: options.importModule,
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
export const runPart = async (options) => {
|
|
62
|
+
const pkg = await readManifest(options.dir);
|
|
63
|
+
const part = partOf(pkg, options.part);
|
|
64
|
+
const log = createLog({ extension: pkg.agentage.id, part });
|
|
65
|
+
const { app, stop } = await RUNNERS[part](await inputFor(options, pkg, part, log));
|
|
66
|
+
if (stop)
|
|
67
|
+
for (const signal of options.serve?.signals ?? ['SIGTERM', 'SIGINT'])
|
|
68
|
+
process.once(signal, stop);
|
|
69
|
+
return serve({ app, port: portFromEnv(options.env), log, ...options.serve });
|
|
57
70
|
};
|
package/dist/server/app.d.ts
CHANGED
|
@@ -1,18 +1,32 @@
|
|
|
1
1
|
import { type CorsOptions } from 'cors';
|
|
2
2
|
import { type Express } from 'express';
|
|
3
|
+
import { type HelmetOptions } from 'helmet';
|
|
3
4
|
import { type BuildInfo } from '../discovery/discovery.js';
|
|
4
5
|
import { type ExtensionRecord } from '../discovery/discovery.schema.js';
|
|
6
|
+
import { type HealthCheck } from '../health/health.js';
|
|
5
7
|
import { type Log } from '../log/log.js';
|
|
6
8
|
import { type ExtensionPackage } from '../manifest/manifest.schema.js';
|
|
9
|
+
import { type ImagePartKind } from '../manifest/primitives.schema.js';
|
|
7
10
|
import { type ApiHandler } from './part.types.js';
|
|
8
|
-
export interface
|
|
11
|
+
export interface PartAppBase {
|
|
9
12
|
pkg: ExtensionPackage;
|
|
10
13
|
build: BuildInfo;
|
|
11
|
-
router: ApiHandler;
|
|
12
14
|
log: Log;
|
|
13
15
|
record?: ExtensionRecord | null;
|
|
14
16
|
ownImage?: string;
|
|
15
17
|
origin?: string;
|
|
18
|
+
checks?: Record<string, HealthCheck>;
|
|
19
|
+
}
|
|
20
|
+
export interface PartAppOptions extends PartAppBase {
|
|
21
|
+
part: ImagePartKind;
|
|
22
|
+
mount: (app: Express) => void;
|
|
23
|
+
helmet?: HelmetOptions;
|
|
24
|
+
}
|
|
25
|
+
export interface ApiAppOptions extends PartAppBase {
|
|
26
|
+
router: ApiHandler;
|
|
27
|
+
openapi?: Record<string, unknown> | null;
|
|
16
28
|
}
|
|
17
29
|
export declare const PUBLIC_CORS: CorsOptions;
|
|
30
|
+
export declare const BODY_LIMIT = "1mb";
|
|
31
|
+
export declare const createPartApp: (options: PartAppOptions) => Express;
|
|
18
32
|
export declare const createApiApp: (options: ApiAppOptions) => Express;
|