@agentage/extension 0.1.2 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/README.md +6 -5
  2. package/dist/agcid/current.d.ts +7 -0
  3. package/dist/agcid/current.js +4 -0
  4. package/dist/agcid/scope.d.ts +0 -1
  5. package/dist/agcid/scope.js +2 -4
  6. package/dist/auth/route-access.d.ts +2 -0
  7. package/dist/auth/route-access.js +21 -0
  8. package/dist/auth/token-check.d.ts +10 -0
  9. package/dist/auth/token-check.js +18 -0
  10. package/dist/command/args.d.ts +14 -0
  11. package/dist/command/args.js +60 -0
  12. package/dist/command/check-cli.d.ts +1 -0
  13. package/dist/command/check-cli.js +19 -0
  14. package/dist/command/command.types.d.ts +14 -0
  15. package/dist/command/command.types.js +2 -0
  16. package/dist/command/out.d.ts +19 -0
  17. package/dist/command/out.js +32 -0
  18. package/dist/command/run-command.d.ts +16 -0
  19. package/dist/command/run-command.js +43 -0
  20. package/dist/context/context.d.ts +8 -14
  21. package/dist/context/context.js +20 -26
  22. package/dist/context/settings.d.ts +5 -0
  23. package/dist/context/settings.js +32 -0
  24. package/dist/context/test-context.d.ts +44 -0
  25. package/dist/context/test-context.js +75 -0
  26. package/dist/discovery/discovery.d.ts +12 -1
  27. package/dist/discovery/discovery.js +15 -6
  28. package/dist/discovery/discovery.schema.d.ts +7 -0
  29. package/dist/discovery/discovery.schema.js +4 -2
  30. package/dist/discovery/openapi.d.ts +3 -0
  31. package/dist/discovery/openapi.js +19 -0
  32. package/dist/health/health.schema.js +1 -1
  33. package/dist/index.d.ts +16 -3
  34. package/dist/index.js +16 -3
  35. package/dist/log/log.js +1 -1
  36. package/dist/manifest/entry.d.ts +5 -0
  37. package/dist/manifest/entry.js +29 -0
  38. package/dist/manifest/manifest.schema.d.ts +18 -0
  39. package/dist/manifest/manifest.schema.js +1 -0
  40. package/dist/manifest/parts.d.ts +1 -0
  41. package/dist/manifest/parts.js +1 -0
  42. package/dist/mcp/mcp-app.d.ts +8 -0
  43. package/dist/mcp/mcp-app.js +94 -0
  44. package/dist/mcp/server.d.ts +8 -0
  45. package/dist/mcp/server.js +38 -0
  46. package/dist/mcp/snapshot.d.ts +11 -0
  47. package/dist/mcp/snapshot.js +69 -0
  48. package/dist/mcp/tools.d.ts +26 -0
  49. package/dist/mcp/tools.js +21 -0
  50. package/dist/pages/pages-app.d.ts +8 -0
  51. package/dist/pages/pages-app.js +29 -0
  52. package/dist/pages/pages.d.ts +4 -0
  53. package/dist/pages/pages.js +16 -0
  54. package/dist/runtime/part-runners.d.ts +20 -0
  55. package/dist/runtime/part-runners.js +63 -0
  56. package/dist/runtime/run-part.d.ts +7 -3
  57. package/dist/runtime/run-part.js +67 -29
  58. package/dist/server/app.d.ts +20 -2
  59. package/dist/server/app.js +39 -11
  60. package/dist/store/migrate.d.ts +2 -0
  61. package/dist/store/migrate.js +22 -0
  62. package/dist/store/migrations.d.ts +12 -0
  63. package/dist/store/migrations.js +81 -0
  64. package/dist/store/store.d.ts +16 -0
  65. package/dist/store/store.js +55 -0
  66. package/dist/store/store.types.d.ts +7 -0
  67. package/dist/store/store.types.js +1 -0
  68. package/dist/store/test-store.d.ts +5 -0
  69. package/dist/store/test-store.js +39 -0
  70. package/dist/testing/test-server.d.ts +6 -0
  71. package/dist/testing/test-server.js +31 -9
  72. package/dist/tool/build.d.ts +4 -0
  73. package/dist/tool/build.js +21 -26
  74. package/dist/tool/check.d.ts +16 -0
  75. package/dist/tool/check.js +30 -0
  76. package/dist/tool/docker.js +3 -2
  77. package/dist/tool/exec.d.ts +5 -0
  78. package/dist/tool/exec.js +48 -9
  79. package/dist/tool/index.d.ts +8 -0
  80. package/dist/tool/index.js +8 -0
  81. package/dist/tool/kit-version.d.ts +1 -0
  82. package/dist/tool/kit-version.js +3 -0
  83. package/dist/tool/main.d.ts +6 -0
  84. package/dist/tool/main.js +60 -35
  85. package/dist/tool/record-extras.d.ts +4 -0
  86. package/dist/tool/record-extras.js +56 -0
  87. package/dist/tool/stage.d.ts +4 -0
  88. package/dist/tool/stage.js +39 -21
  89. package/dist/tool/timeouts.d.ts +10 -0
  90. package/dist/tool/timeouts.js +19 -0
  91. package/dist/tool/up/compose-run.d.ts +15 -0
  92. package/dist/tool/up/compose-run.js +25 -0
  93. package/dist/tool/up/compose.d.ts +19 -0
  94. package/dist/tool/up/compose.js +80 -0
  95. package/dist/tool/up/dev-issuer-app.d.ts +22 -0
  96. package/dist/tool/up/dev-issuer-app.js +80 -0
  97. package/dist/tool/up/dev-issuer.d.ts +2 -0
  98. package/dist/tool/up/dev-issuer.js +6 -0
  99. package/dist/tool/up/down.d.ts +13 -0
  100. package/dist/tool/up/down.js +47 -0
  101. package/dist/tool/up/gateway.d.ts +36 -0
  102. package/dist/tool/up/gateway.js +60 -0
  103. package/dist/tool/up/lines.d.ts +2 -0
  104. package/dist/tool/up/lines.js +3 -0
  105. package/dist/tool/up/local-record.d.ts +44 -0
  106. package/dist/tool/up/local-record.js +28 -0
  107. package/dist/tool/up/ports.d.ts +2 -0
  108. package/dist/tool/up/ports.js +22 -0
  109. package/dist/tool/up/project.d.ts +4 -0
  110. package/dist/tool/up/project.js +20 -0
  111. package/dist/tool/up/store-line.d.ts +11 -0
  112. package/dist/tool/up/store-line.js +36 -0
  113. package/dist/tool/up/token.d.ts +6 -0
  114. package/dist/tool/up/token.js +25 -0
  115. package/dist/tool/up/up.d.ts +16 -0
  116. package/dist/tool/up/up.js +84 -0
  117. package/dist/tool/up/wait.d.ts +8 -0
  118. package/dist/tool/up/wait.js +26 -0
  119. package/dist/ui/ui-app.d.ts +8 -0
  120. package/dist/ui/ui-app.js +39 -0
  121. package/dist/worker/cron.d.ts +11 -0
  122. package/dist/worker/cron.js +73 -0
  123. package/dist/worker/job-options.d.ts +9 -0
  124. package/dist/worker/job-options.js +17 -0
  125. package/dist/worker/run-log.d.ts +9 -0
  126. package/dist/worker/run-log.js +38 -0
  127. package/dist/worker/runs.d.ts +20 -0
  128. package/dist/worker/runs.js +59 -0
  129. package/dist/worker/scheduler.d.ts +20 -0
  130. package/dist/worker/scheduler.js +106 -0
  131. package/dist/worker/worker.types.d.ts +5 -0
  132. package/dist/worker/worker.types.js +2 -0
  133. package/package.json +11 -2
  134. package/schemas/extension-discovery.schema.json +5 -0
  135. package/schemas/extension-package.schema.json +3 -0
  136. package/schemas/extension-record.schema.json +5 -0
  137. package/schemas/health.schema.json +1 -1
@@ -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 ImportModule } from '../manifest/entry.js';
5
+ import { type ImagePartKind } from '../manifest/primitives.schema.js';
6
+ import { type PartAppBase } from '../server/app.js';
7
+ import { type Env } from './build-info.js';
8
+ export interface RunnerInput {
9
+ dir: string;
10
+ pkg: ExtensionPackage;
11
+ env: Env;
12
+ ctx: () => PartContext;
13
+ base: PartAppBase;
14
+ importModule?: ImportModule;
15
+ }
16
+ export interface Running {
17
+ app: Express;
18
+ stop?: () => void;
19
+ }
20
+ export declare const RUNNERS: Record<ImagePartKind, (input: RunnerInput) => Promise<Running>>;
@@ -0,0 +1,63 @@
1
+ import { join } from 'node:path';
2
+ import {} from 'express';
3
+ import {} from '../context/context.js';
4
+ import { readOpenapi } from '../discovery/openapi.js';
5
+ import {} from '../manifest/manifest.schema.js';
6
+ import { loadCreate } from '../manifest/entry.js';
7
+ import { hasPostgres } 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 {} from './build-info.js';
15
+ import { createUiApp } from '../ui/ui-app.js';
16
+ import { runLog } from '../worker/run-log.js';
17
+ import { memoryRuns, storeRuns } from '../worker/runs.js';
18
+ import { createScheduler } from '../worker/scheduler.js';
19
+ // ui has no code to run; every other part is its create(ctx).
20
+ const callCreate = async (input, part, ctx) => (await loadCreate(input.dir, input.pkg, part, input.importModule))(ctx);
21
+ const api = async (input) => {
22
+ const router = (await callCreate(input, 'api', input.ctx()));
23
+ const openapi = await readOpenapi(input.dir);
24
+ return { app: createApiApp({ ...input.base, router, openapi }) };
25
+ };
26
+ const mcp = async (input) => {
27
+ const registry = createToolRegistry();
28
+ const ctx = input.ctx();
29
+ await callCreate(input, 'mcp', { ...ctx, tool: registry.tool });
30
+ const listing = ctx.settings['mcp.listing'] === 'token' ? 'token' : 'open';
31
+ return { app: createMcpApp({ ...input.base, tools: registry.tools, listing }) };
32
+ };
33
+ const pages = async (input) => {
34
+ const site = (await callCreate(input, 'pages', input.ctx()));
35
+ return { app: createPagesApp({ ...input.base, site }) };
36
+ };
37
+ const ui = async (input) => ({
38
+ app: createUiApp({
39
+ ...input.base,
40
+ dir: join(input.dir, 'dist', 'ui'),
41
+ devIssuer: input.env['AGENTAGE_DEV_ISSUER'],
42
+ }),
43
+ });
44
+ const worker = async (input) => {
45
+ const ctx = input.ctx();
46
+ const log = runLog(ctx.log);
47
+ const runs = hasPostgres(input.pkg) ? storeRuns(ctx.store) : memoryRuns();
48
+ const scheduler = createScheduler({ runs, log });
49
+ await callCreate(input, 'worker', { ...ctx, log, schedule: scheduler.schedule });
50
+ const checks = { ...input.base.checks, ...scheduler.checks() };
51
+ scheduler.start();
52
+ return {
53
+ app: createPartApp({ ...input.base, checks, part: 'worker', mount: () => undefined }),
54
+ stop: scheduler.stop,
55
+ };
56
+ };
57
+ export const RUNNERS = {
58
+ api,
59
+ mcp,
60
+ pages,
61
+ ui,
62
+ worker,
63
+ };
@@ -1,13 +1,17 @@
1
1
  import { type Server } from 'node:http';
2
- import { type ApiCreate } from '../server/part.types.js';
2
+ import { type TokenCheck } from '../auth/token-check.js';
3
+ import { type ImportModule } from '../manifest/entry.js';
4
+ import { type ExtensionPackage } from '../manifest/manifest.schema.js';
5
+ import { type ImagePartKind } from '../manifest/primitives.schema.js';
3
6
  import { type ServeOptions } from '../server/serve.js';
4
7
  import { type Env } from './build-info.js';
5
8
  export interface RunPartOptions {
6
9
  dir: string;
7
10
  part: string;
8
11
  env: Env;
9
- importModule?: (file: string) => Promise<unknown>;
12
+ importModule?: ImportModule;
10
13
  serve?: Pick<ServeOptions, 'signals' | 'exit'>;
14
+ tokenCheck?: TokenCheck;
11
15
  }
12
- export declare const createOf: (mod: unknown, file: string) => ApiCreate;
16
+ export declare const partOf: (pkg: ExtensionPackage, part: string) => ImagePartKind;
13
17
  export declare const runPart: (options: RunPartOptions) => Promise<Server>;
@@ -1,23 +1,20 @@
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 '../auth/token-check.js';
6
+ import {} from '../health/health.js';
6
7
  import { createLog } from '../log/log.js';
7
- import { entryOf } from '../manifest/parts.js';
8
+ import {} from '../manifest/entry.js';
8
9
  import {} from '../manifest/manifest.schema.js';
10
+ import { hasPostgres, imageParts } from '../manifest/parts.js';
11
+ import {} from '../manifest/primitives.schema.js';
9
12
  import { readManifest } from '../manifest/read-manifest.js';
10
- import { createApiApp } from '../server/app.js';
11
- import {} from '../server/part.types.js';
12
13
  import { serve } from '../server/serve.js';
14
+ import { createStore, storeCheck } from '../store/store.js';
15
+ import {} from '../store/store.types.js';
13
16
  import { buildInfoFromEnv, portFromEnv } from './build-info.js';
14
- const importFile = (file) => import(pathToFileURL(file).href);
15
- export const createOf = (mod, file) => {
16
- const create = mod?.create;
17
- if (typeof create !== 'function')
18
- throw new Error(`${file} does not export create(ctx)`);
19
- return create;
20
- };
17
+ import { RUNNERS } from './part-runners.js';
21
18
  const recordFor = async (env, log) => {
22
19
  const file = env['AGENTAGE_RECORD_FILE'] || DEFAULT_RECORD_FILE;
23
20
  const record = await loadRecord(file);
@@ -25,24 +22,65 @@ const recordFor = async (env, log) => {
25
22
  log.warn('no release record mounted; discovery lists this image by tag', { file });
26
23
  return record;
27
24
  };
28
- const createRouter = async (options, ctx, pkg) => {
29
- const file = join(options.dir, entryOf(pkg, 'api'));
30
- const mod = await (options.importModule ?? importFile)(file);
31
- return createOf(mod, file)(ctx);
25
+ export const partOf = (pkg, part) => {
26
+ const parts = imageParts(pkg);
27
+ if (!parts.includes(part))
28
+ throw new Error(`part ${part} is not an image part of this manifest`);
29
+ return part;
30
+ };
31
+ // ui serves files and gets no ctx; every other part gets the store when the manifest has postgres.
32
+ const storeFor = ({ pkg, part, env, log }) => part !== 'ui' && hasPostgres(pkg) ? createStore(env, log) : undefined;
33
+ const contextFor = (start, part, store) => createContext({
34
+ pkg: start.pkg,
35
+ part,
36
+ origin: start.env['AGENTAGE_ORIGIN'] || `http://127.0.0.1:${portFromEnv(start.env)}`,
37
+ commit: buildInfoFromEnv(start.env).commit,
38
+ log: start.log,
39
+ settings: settingsFromEnv(start.pkg, start.env, start.log),
40
+ store,
41
+ });
42
+ // Built on first use, so the ui runner, which never asks, has no ctx at all.
43
+ const lazyContext = (start, store) => {
44
+ let ctx;
45
+ return () => {
46
+ if (start.part === 'ui')
47
+ throw new Error('the ui part gets no ctx');
48
+ return (ctx ??= contextFor(start, start.part, store));
49
+ };
50
+ };
51
+ const baseFor = async (options, start, store) => {
52
+ const { pkg, env, log } = start;
53
+ const checks = store ? { store: storeCheck(store) } : {};
54
+ return {
55
+ pkg,
56
+ log,
57
+ checks,
58
+ build: buildInfoFromEnv(env),
59
+ record: await recordFor(env, log),
60
+ origin: env['AGENTAGE_ORIGIN'] || undefined,
61
+ ownImage: env['AGENTAGE_IMAGE'],
62
+ tokenCheck: options.tokenCheck,
63
+ };
64
+ };
65
+ const inputFor = async (options, start) => {
66
+ const store = storeFor(start);
67
+ return {
68
+ dir: options.dir,
69
+ pkg: start.pkg,
70
+ env: start.env,
71
+ ctx: lazyContext(start, store),
72
+ base: await baseFor(options, start, store),
73
+ importModule: options.importModule,
74
+ };
32
75
  };
33
- const contextFor = (pkg, origin, port, log) => createContext({ pkg, part: 'api', origin: origin ?? `http://127.0.0.1:${port}`, log });
34
76
  export const runPart = async (options) => {
35
- if (options.part !== 'api')
36
- throw new Error(`part ${options.part} is not served by kit 0.1`);
37
- const { env } = options;
38
77
  const pkg = await readManifest(options.dir);
39
- const log = createLog({ extension: pkg.agentage.id, part: 'api' });
40
- const port = portFromEnv(env);
41
- const origin = env['AGENTAGE_ORIGIN'] || undefined;
42
- const router = await createRouter(options, contextFor(pkg, origin, port, log), pkg);
43
- const build = buildInfoFromEnv(env);
44
- const record = await recordFor(env, log);
45
- const ownImage = env['AGENTAGE_IMAGE'];
46
- const app = createApiApp({ pkg, build, router, log, record, origin, ownImage });
47
- return serve({ app, port, log, ...options.serve });
78
+ const part = partOf(pkg, options.part);
79
+ const log = createLog({ extension: pkg.agentage.id, part });
80
+ const start = { pkg, part, env: options.env, log };
81
+ const { app, stop } = await RUNNERS[part](await inputFor(options, start));
82
+ if (stop)
83
+ for (const signal of options.serve?.signals ?? ['SIGTERM', 'SIGINT'])
84
+ process.once(signal, stop);
85
+ return serve({ app, port: portFromEnv(options.env), log, ...options.serve });
48
86
  };
@@ -1,16 +1,34 @@
1
+ import { type CorsOptions } from 'cors';
1
2
  import { type Express } from 'express';
3
+ import { type HelmetOptions } from 'helmet';
4
+ import { type TokenCheck } from '../auth/token-check.js';
2
5
  import { type BuildInfo } from '../discovery/discovery.js';
3
6
  import { type ExtensionRecord } from '../discovery/discovery.schema.js';
7
+ import { type HealthCheck } from '../health/health.js';
4
8
  import { type Log } from '../log/log.js';
5
9
  import { type ExtensionPackage } from '../manifest/manifest.schema.js';
10
+ import { type ImagePartKind } from '../manifest/primitives.schema.js';
6
11
  import { type ApiHandler } from './part.types.js';
7
- export interface ApiAppOptions {
12
+ export interface PartAppBase {
8
13
  pkg: ExtensionPackage;
9
14
  build: BuildInfo;
10
- router: ApiHandler;
11
15
  log: Log;
12
16
  record?: ExtensionRecord | null;
13
17
  ownImage?: string;
14
18
  origin?: string;
19
+ checks?: Record<string, HealthCheck>;
20
+ tokenCheck?: TokenCheck;
21
+ }
22
+ export interface PartAppOptions extends PartAppBase {
23
+ part: ImagePartKind;
24
+ mount: (app: Express) => void;
25
+ helmet?: HelmetOptions;
26
+ }
27
+ export interface ApiAppOptions extends PartAppBase {
28
+ router: ApiHandler;
29
+ openapi?: Record<string, unknown> | null;
15
30
  }
31
+ export declare const PUBLIC_CORS: CorsOptions;
32
+ export declare const BODY_LIMIT = "1mb";
33
+ export declare const createPartApp: (options: PartAppOptions) => Express;
16
34
  export declare const createApiApp: (options: ApiAppOptions) => Express;
@@ -1,25 +1,28 @@
1
1
  import cors, {} from 'cors';
2
2
  import express, {} from 'express';
3
- import helmet from 'helmet';
3
+ import helmet, {} from 'helmet';
4
4
  import { agcidMiddleware } from '../agcid/scope.js';
5
+ import { routeAccess } from '../auth/route-access.js';
6
+ import { setCaller } from '../auth/token-check.js';
5
7
  import { discoveryDocument, recordOf, } from '../discovery/discovery.js';
6
8
  import {} from '../discovery/discovery.schema.js';
7
9
  import { createHealthHandler } from '../health/health.js';
8
10
  import {} from '../log/log.js';
9
11
  import {} from '../manifest/manifest.schema.js';
12
+ import {} from '../manifest/primitives.schema.js';
10
13
  import { errorHandler, notFound } from './errors.js';
11
14
  import {} from './part.types.js';
12
- // api-standards section 6: CORS * without credentials on /.well-known/ and /api/.
13
- const PUBLIC_CORS = {
15
+ // api-standards section 6: CORS * without credentials on /.well-known/, /api/, /ui/ and /mcp.
16
+ export const PUBLIC_CORS = {
14
17
  origin: '*',
15
18
  credentials: false,
16
19
  allowedHeaders: ['Authorization', 'Content-Type', 'agcid', 'traceparent', 'If-Match'],
17
20
  exposedHeaders: ['agcid', 'ETag', 'Retry-After', 'RateLimit', 'WWW-Authenticate'],
18
21
  };
19
- const BODY_LIMIT = '1mb';
22
+ export const BODY_LIMIT = '1mb';
20
23
  const requestOrigin = (req) => `${req.protocol}://${req.get('host') ?? ''}`;
21
24
  const discoveryHandler = (options) => {
22
- const images = options.ownImage ? { api: options.ownImage } : {};
25
+ const images = options.ownImage ? { [options.part]: options.ownImage } : {};
23
26
  const record = options.record ?? recordOf(options.pkg, options.build, images);
24
27
  return (req, res) => {
25
28
  res.json(discoveryDocument(record, options.origin ?? requestOrigin(req)));
@@ -27,20 +30,45 @@ const discoveryHandler = (options) => {
27
30
  };
28
31
  const healthHandler = (options) => createHealthHandler({
29
32
  extension: options.pkg.agentage.id,
30
- part: 'api',
33
+ part: options.part,
31
34
  version: options.pkg.version,
32
35
  build: options.build,
36
+ checks: options.checks,
33
37
  });
34
- export const createApiApp = (options) => {
38
+ // The kit app every part image runs: agcid, headers, /health, discovery, the part, the envelope.
39
+ export const createPartApp = (options) => {
35
40
  const app = express();
36
41
  // The gateway calls parts over a private network; its X-Forwarded-Proto then sets req.protocol.
37
42
  app.set('trust proxy', 'loopback, linklocal, uniquelocal');
38
- app.use(agcidMiddleware, helmet());
43
+ app.use(agcidMiddleware, helmet(options.helmet));
39
44
  app.get('/health', healthHandler(options));
40
- app.use('/.well-known', cors(PUBLIC_CORS));
41
- app.get('/.well-known/agentage-extension.json', discoveryHandler(options));
42
- app.use('/api/v1', cors(PUBLIC_CORS), express.json({ limit: BODY_LIMIT }), options.router);
45
+ if (options.part !== 'worker') {
46
+ app.use('/.well-known', cors(PUBLIC_CORS));
47
+ app.get('/.well-known/agentage-extension.json', discoveryHandler(options));
48
+ }
49
+ options.mount(app);
43
50
  app.use(notFound);
44
51
  app.use(errorHandler(options.log));
45
52
  return app;
46
53
  };
54
+ // The committed OpenAPI document, bare JSON and anonymous like discovery (api-standards section 1).
55
+ const openapiHandler = (doc) => (_req, res) => void res.json(doc);
56
+ // Before the body is read: each request checked for the access its OpenAPI operation declares.
57
+ const routeGuard = (options) => async (req, _res, next) => {
58
+ if (!options.tokenCheck)
59
+ return next();
60
+ const access = routeAccess(options.openapi, req.method, `/api/v1${req.path}`);
61
+ setCaller(req, await options.tokenCheck(req.get('authorization'), access));
62
+ next();
63
+ };
64
+ export const createApiApp = (options) => createPartApp({
65
+ ...options,
66
+ part: 'api',
67
+ mount: (app) => {
68
+ app.use('/api/v1', cors(PUBLIC_CORS));
69
+ if (options.openapi)
70
+ app.get('/api/v1/openapi.json', openapiHandler(options.openapi));
71
+ const json = express.json({ limit: BODY_LIMIT });
72
+ app.use('/api/v1', routeGuard(options), json, options.router);
73
+ },
74
+ });
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env node
2
+ import { createLog } from '../log/log.js';
3
+ import { readManifest } from '../manifest/read-manifest.js';
4
+ import { migrate } from './migrations.js';
5
+ import { createStore } from './store.js';
6
+ // A migration may rewrite a table: minutes, not a request's seconds; the job itself has no retry.
7
+ const MIGRATE_TIMEOUTS = { connectMs: 10_000, statementMs: 300_000 };
8
+ // The api image's migrate entry (`node dist/store/migrate.js` in /app), run as a job before a rollout.
9
+ // Until the manifest is read the id is unknown; every line after it carries the extension.
10
+ let log = createLog({ extension: 'unknown', part: 'migrate' });
11
+ try {
12
+ const pkg = await readManifest(process.cwd());
13
+ log = createLog({ extension: pkg.agentage.id, part: 'migrate' });
14
+ const store = createStore(process.env, log, MIGRATE_TIMEOUTS);
15
+ const result = await migrate(store, process.cwd(), log);
16
+ process.stdout.write(`${JSON.stringify(result)}\n`);
17
+ await store.end();
18
+ }
19
+ catch (err) {
20
+ log.error(err);
21
+ process.exit(1);
22
+ }
@@ -0,0 +1,12 @@
1
+ import { type Log } from '../log/log.js';
2
+ import { type Store } from './store.types.js';
3
+ export declare const SQL_FILE: RegExp;
4
+ export interface MigrateResult {
5
+ migrations: number;
6
+ applied: number;
7
+ seeds: number;
8
+ seedsBefore: number;
9
+ }
10
+ export declare const sqlFiles: (dir: string) => Promise<string[]>;
11
+ export declare const SEEDS_OWED = "seeds:owed";
12
+ export declare const migrate: (store: Store, dir: string, log: Log) => Promise<MigrateResult>;
@@ -0,0 +1,81 @@
1
+ import { readdir, readFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { messageOf } from '../log/log.js';
4
+ import {} from './store.types.js';
5
+ export const SQL_FILE = /^[0-9]{3}-[a-z0-9-]+\.sql$/;
6
+ // Filename order; anything else in the folder is refused, so a typo never silently skips a file.
7
+ export const sqlFiles = async (dir) => {
8
+ let names;
9
+ try {
10
+ names = await readdir(dir);
11
+ }
12
+ catch (err) {
13
+ if (err.code === 'ENOENT')
14
+ return [];
15
+ throw err;
16
+ }
17
+ const bad = names.filter((name) => !SQL_FILE.test(name));
18
+ if (bad.length > 0)
19
+ throw new Error(`${dir}: not NNN-name.sql: ${bad.join(', ')}`);
20
+ return names.sort();
21
+ };
22
+ const LEDGER = `create table if not exists _migrations (
23
+ name text primary key,
24
+ applied_at timestamptz not null default now()
25
+ )`;
26
+ const SEED = 'seed:';
27
+ // Written by the first run on an empty store, removed once the seeds ran: a failed first run still seeds later.
28
+ export const SEEDS_OWED = 'seeds:owed';
29
+ // Tables other than the kit's own: a store holding any (a restored dump) never gets the seeds.
30
+ const USER_TABLES = `select count(*)::int as n from information_schema.tables
31
+ where table_schema not in ('pg_catalog', 'information_schema')
32
+ and table_name not in ('_migrations', 'agentage_job_runs')`;
33
+ const isEmpty = async (store, done) => done.size === 0 && ((await store.query(USER_TABLES)).rows[0]?.n ?? 0) === 0;
34
+ const appliedNames = async (store) => {
35
+ await store.query(LEDGER);
36
+ const { rows } = await store.query('select name from _migrations');
37
+ return new Set(rows.map((row) => row.name));
38
+ };
39
+ // One string, one round trip: the file and its ledger row commit together or not at all.
40
+ const applyFile = async (store, file, name) => {
41
+ const sql = await readFile(file, 'utf8');
42
+ try {
43
+ await store.query(`begin;\n${sql}\n;insert into _migrations (name) values ('${name}');\ncommit;`);
44
+ }
45
+ catch (err) {
46
+ await store.query('rollback').catch(() => undefined);
47
+ const kind = name.startsWith(SEED) ? 'seed' : 'migration';
48
+ throw new Error(`${kind} ${name.replace(SEED, '')} failed: ${messageOf(err)}`, { cause: err });
49
+ }
50
+ };
51
+ const applyAll = async (store, dir, prefix, done) => {
52
+ let count = 0;
53
+ for (const file of await sqlFiles(dir)) {
54
+ const name = `${prefix}${file}`;
55
+ if (done.has(name))
56
+ continue;
57
+ await applyFile(store, join(dir, file), name);
58
+ count += 1;
59
+ }
60
+ return count;
61
+ };
62
+ const seedOnce = async (store, dir, done) => {
63
+ const seeds = await applyAll(store, join(dir, 'seeds'), SEED, done);
64
+ await store.query(`delete from _migrations where name = '${SEEDS_OWED}'`);
65
+ return seeds;
66
+ };
67
+ // Migrations in order, each once; seeds once, only on a store with no ledger row and no user table.
68
+ export const migrate = async (store, dir, log) => {
69
+ const done = await appliedNames(store);
70
+ const empty = await isEmpty(store, done);
71
+ if (empty)
72
+ await store.query(`insert into _migrations (name) values ('${SEEDS_OWED}')`);
73
+ const owed = empty || done.has(SEEDS_OWED);
74
+ const migrations = (await sqlFiles(join(dir, 'migrations'))).length;
75
+ const applied = await applyAll(store, join(dir, 'migrations'), '', done);
76
+ const seeds = owed ? await seedOnce(store, dir, done) : 0;
77
+ const seedsBefore = [...done].filter((name) => name.startsWith(SEED)).length;
78
+ const result = { migrations, applied, seeds, seedsBefore };
79
+ log.info('migrated', { ...result });
80
+ return result;
81
+ };
@@ -0,0 +1,16 @@
1
+ import { type HealthCheck } from '../health/health.js';
2
+ import { type Log } from '../log/log.js';
3
+ import { type Env } from '../runtime/build-info.js';
4
+ import { type Store } from './store.types.js';
5
+ export interface PoolStore extends Store {
6
+ end(): Promise<void>;
7
+ }
8
+ export interface StoreTimeouts {
9
+ connectMs: number;
10
+ statementMs: number;
11
+ }
12
+ export declare const PART_TIMEOUTS: StoreTimeouts;
13
+ export declare const storeUrl: (env: Env, read?: (file: string) => string) => string | undefined;
14
+ export declare const isOutage: (err: unknown) => boolean;
15
+ export declare const createStore: (env: Env, log: Log, timeouts?: StoreTimeouts) => PoolStore;
16
+ export declare const storeCheck: (store: Store) => HealthCheck;
@@ -0,0 +1,55 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import pg from 'pg';
3
+ import { fail } from '../envelope/envelope.js';
4
+ import {} from '../health/health.js';
5
+ import { messageOf } from '../log/log.js';
6
+ import {} from '../runtime/build-info.js';
7
+ import {} from './store.types.js';
8
+ // A part's query is bounded by the server and, if the network itself hangs, by the client too.
9
+ export const PART_TIMEOUTS = { connectMs: 5_000, statementMs: 10_000 };
10
+ const CLIENT_SLACK_MS = 2_000;
11
+ // The host sets the connection: AGENTAGE_STORE_URL, a secret file named by _FILE, else libpq PG* env.
12
+ export const storeUrl = (env, read = (file) => readFileSync(file, 'utf8')) => {
13
+ const file = env['AGENTAGE_STORE_URL_FILE'];
14
+ if (file)
15
+ return read(file).trim();
16
+ return env['AGENTAGE_STORE_URL'] || undefined;
17
+ };
18
+ // Connection classes 08 and 57P01-03 (shutdown), 57014 (statement timeout), socket errors, pg's own timeouts.
19
+ const OUTAGE_SQLSTATE = /^(08|57P0[1-3]$|57014$)/;
20
+ const OUTAGE_SOCKET = /^(ECONNREFUSED|ECONNRESET|ETIMEDOUT|ENOTFOUND|EAI_AGAIN|EHOSTUNREACH|EPIPE)$/;
21
+ const OUTAGE_MESSAGE = /timeout exceeded when trying to connect|Query read timeout|Connection terminated/;
22
+ export const isOutage = (err) => {
23
+ const code = err?.code;
24
+ if (typeof code === 'string' && (OUTAGE_SQLSTATE.test(code) || OUTAGE_SOCKET.test(code)))
25
+ return true;
26
+ return OUTAGE_MESSAGE.test(messageOf(err));
27
+ };
28
+ // A store that cannot answer is UNAVAILABLE (503, Retry-After), never an INTERNAL 500; no retry here.
29
+ const guarded = (pool, log) => async function query(text, params) {
30
+ try {
31
+ return await pool.query(text, params);
32
+ }
33
+ catch (err) {
34
+ if (!isOutage(err))
35
+ throw err;
36
+ log.warn('store unavailable', { reason: messageOf(err) });
37
+ throw fail('UNAVAILABLE', 'The store is unavailable');
38
+ }
39
+ };
40
+ export const createStore = (env, log, timeouts = PART_TIMEOUTS) => {
41
+ const pool = new pg.Pool({
42
+ connectionString: storeUrl(env),
43
+ max: 5,
44
+ idleTimeoutMillis: 30_000,
45
+ connectionTimeoutMillis: timeouts.connectMs,
46
+ statement_timeout: timeouts.statementMs,
47
+ query_timeout: timeouts.statementMs + CLIENT_SLACK_MS,
48
+ });
49
+ pool.on('error', (err) => log.error(err, { store: 'pool' }));
50
+ return { query: guarded(pool, log), end: () => pool.end() };
51
+ };
52
+ export const storeCheck = (store) => async () => {
53
+ await store.query('select 1');
54
+ return 'ok';
55
+ };
@@ -0,0 +1,7 @@
1
+ export type Row = Record<string, unknown>;
2
+ export interface QueryResult<R extends Row = Row> {
3
+ rows: R[];
4
+ }
5
+ export interface Store {
6
+ query<R extends Row = Row>(text: string, params?: unknown[]): Promise<QueryResult<R>>;
7
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,5 @@
1
+ import { type Store } from './store.types.js';
2
+ export interface TestStore extends Store {
3
+ close(): Promise<void>;
4
+ }
5
+ export declare const createTestStore: (prepare: (store: Store) => Promise<unknown>) => TestStore;
@@ -0,0 +1,39 @@
1
+ import {} from './store.types.js';
2
+ const open = async () => {
3
+ try {
4
+ const { PGlite } = (await import('@electric-sql/pglite'));
5
+ return new PGlite();
6
+ }
7
+ catch {
8
+ throw new Error('a test store needs @electric-sql/pglite: npm install -D @electric-sql/pglite');
9
+ }
10
+ };
11
+ // Like pg: a parameterless query may hold several statements (migrations), the last one answers.
12
+ const bound = (db) => ({
13
+ query: async (text, params) => {
14
+ if (params && params.length > 0)
15
+ return db.query(text, params);
16
+ const results = await db.exec(text);
17
+ return { rows: (results.at(-1)?.rows ?? []) };
18
+ },
19
+ });
20
+ // In-process Postgres (WASM): a throwaway store of the same kind, prepared once on first use.
21
+ export const createTestStore = (prepare) => {
22
+ let ready;
23
+ const start = async () => {
24
+ const db = await open();
25
+ const store = bound(db);
26
+ await prepare(store);
27
+ return { ...store, db };
28
+ };
29
+ return {
30
+ query: async (text, params) => {
31
+ ready ??= start();
32
+ return (await ready).query(text, params);
33
+ },
34
+ close: async () => {
35
+ if (ready)
36
+ await (await ready).db.close();
37
+ },
38
+ };
39
+ };
@@ -1,12 +1,18 @@
1
1
  import { type PartContext } from '../context/context.js';
2
+ import { type McpContext } from '../mcp/tools.js';
2
3
  import { type ApiCreate } from '../server/part.types.js';
3
4
  export interface TestServerOptions<C extends PartContext> {
4
5
  create: ApiCreate<C>;
5
6
  ctx?: C;
6
7
  }
8
+ export interface TestMcpOptions {
9
+ create: (ctx: McpContext) => unknown;
10
+ ctx?: PartContext;
11
+ }
7
12
  export interface TestServer {
8
13
  url: string;
9
14
  fetch(path: string, init?: RequestInit): Promise<Response>;
10
15
  close(): Promise<void>;
11
16
  }
12
17
  export declare const startTestServer: <C extends PartContext = PartContext>(options: TestServerOptions<C>) => Promise<TestServer>;
18
+ export declare const startTestMcp: (options: TestMcpOptions) => Promise<TestServer>;