@agentage/extension 0.2.0 → 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 (69) hide show
  1. package/dist/agcid/scope.d.ts +0 -1
  2. package/dist/agcid/scope.js +0 -1
  3. package/dist/auth/route-access.d.ts +2 -0
  4. package/dist/auth/route-access.js +21 -0
  5. package/dist/auth/token-check.d.ts +10 -0
  6. package/dist/auth/token-check.js +18 -0
  7. package/dist/command/check-cli.js +2 -7
  8. package/dist/command/run-command.d.ts +1 -0
  9. package/dist/command/run-command.js +5 -15
  10. package/dist/context/test-context.js +20 -17
  11. package/dist/discovery/discovery.schema.d.ts +5 -0
  12. package/dist/discovery/discovery.schema.js +1 -1
  13. package/dist/manifest/entry.d.ts +5 -0
  14. package/dist/manifest/entry.js +29 -0
  15. package/dist/manifest/parts.d.ts +1 -0
  16. package/dist/manifest/parts.js +1 -0
  17. package/dist/mcp/mcp-app.js +27 -7
  18. package/dist/mcp/server.js +3 -1
  19. package/dist/mcp/tools.d.ts +2 -0
  20. package/dist/mcp/tools.js +4 -0
  21. package/dist/runtime/part-runners.d.ts +5 -5
  22. package/dist/runtime/part-runners.js +19 -24
  23. package/dist/runtime/run-part.d.ts +4 -2
  24. package/dist/runtime/run-part.js +43 -27
  25. package/dist/server/app.d.ts +2 -0
  26. package/dist/server/app.js +12 -1
  27. package/dist/store/migrate.js +7 -3
  28. package/dist/store/migrations.d.ts +2 -0
  29. package/dist/store/migrations.js +32 -6
  30. package/dist/store/store.d.ts +7 -1
  31. package/dist/store/store.js +37 -7
  32. package/dist/tool/build.d.ts +0 -2
  33. package/dist/tool/build.js +5 -3
  34. package/dist/tool/check.d.ts +16 -0
  35. package/dist/tool/check.js +30 -0
  36. package/dist/tool/docker.js +3 -2
  37. package/dist/tool/exec.d.ts +1 -0
  38. package/dist/tool/exec.js +43 -16
  39. package/dist/tool/main.js +23 -24
  40. package/dist/tool/record-extras.js +7 -11
  41. package/dist/tool/stage.d.ts +1 -0
  42. package/dist/tool/stage.js +8 -10
  43. package/dist/tool/timeouts.d.ts +10 -0
  44. package/dist/tool/timeouts.js +19 -0
  45. package/dist/tool/up/compose-run.d.ts +2 -1
  46. package/dist/tool/up/compose-run.js +3 -2
  47. package/dist/tool/up/compose.d.ts +2 -1
  48. package/dist/tool/up/compose.js +13 -10
  49. package/dist/tool/up/dev-issuer.js +2 -1
  50. package/dist/tool/up/down.d.ts +1 -0
  51. package/dist/tool/up/down.js +39 -30
  52. package/dist/tool/up/gateway.d.ts +9 -0
  53. package/dist/tool/up/gateway.js +11 -17
  54. package/dist/tool/up/lines.js +1 -1
  55. package/dist/tool/up/ports.d.ts +2 -0
  56. package/dist/tool/up/ports.js +22 -0
  57. package/dist/tool/up/project.d.ts +4 -0
  58. package/dist/tool/up/project.js +20 -0
  59. package/dist/tool/up/store-line.d.ts +11 -0
  60. package/dist/tool/up/store-line.js +36 -0
  61. package/dist/tool/up/token.js +9 -2
  62. package/dist/tool/up/up.d.ts +3 -6
  63. package/dist/tool/up/up.js +49 -46
  64. package/dist/tool/up/wait.js +4 -2
  65. package/dist/ui/ui-app.d.ts +2 -0
  66. package/dist/ui/ui-app.js +18 -5
  67. package/dist/worker/runs.js +11 -11
  68. package/dist/worker/scheduler.js +14 -5
  69. package/package.json +1 -1
@@ -1,4 +1,3 @@
1
1
  import { type RequestHandler, type Response } from 'express';
2
- export { currentAgcid } from './current.js';
3
2
  export declare const agcidMiddleware: RequestHandler;
4
3
  export declare const agcidOfResponse: (res: Response) => string;
@@ -1,7 +1,6 @@
1
1
  import {} from 'express';
2
2
  import { AGCID_HEADER, agcidOf } from './agcid.js';
3
3
  import { requestScope } from './current.js';
4
- export { currentAgcid } from './current.js';
5
4
  // Keeps a valid incoming agcid, else mints one; echoed on every response and tags every log line.
6
5
  export const agcidMiddleware = (req, res, next) => {
7
6
  const agcid = agcidOf(req.get(AGCID_HEADER));
@@ -0,0 +1,2 @@
1
+ import { type Access } from './token-check.js';
2
+ export declare const routeAccess: (doc: Record<string, unknown> | null | undefined, method: string, path: string) => Access;
@@ -0,0 +1,21 @@
1
+ import { accessOfSecurity } from './token-check.js';
2
+ const PARAM = /^\{.+\}$/;
3
+ const sameShape = (template, path) => {
4
+ const want = template.split('/');
5
+ const got = path.split('/');
6
+ return want.length === got.length && want.every((seg, i) => PARAM.test(seg) || seg === got[i]);
7
+ };
8
+ const params = (template) => template.split('/').filter((seg) => PARAM.test(seg)).length;
9
+ // As OpenAPI matches: a concrete path before a template, then the template with the fewest params.
10
+ const mostSpecific = (templates, path) => templates
11
+ .filter((template) => sameShape(template, path))
12
+ .sort((a, b) => params(a) - params(b))[0];
13
+ // The access the committed OpenAPI declares for a request: its operation's, else the document's.
14
+ export const routeAccess = (doc, method, path) => {
15
+ const paths = (doc?.['paths'] ?? {});
16
+ const template = mostSpecific(Object.keys(paths), path);
17
+ const operation = template ? paths[template]?.[method.toLowerCase()] : undefined;
18
+ if (!operation)
19
+ return 'read';
20
+ return accessOfSecurity(operation.security ?? doc?.['security']);
21
+ };
@@ -0,0 +1,10 @@
1
+ export type Access = 'public' | 'read' | 'write' | 'admin';
2
+ export interface Caller {
3
+ sub: string;
4
+ scopes: readonly string[];
5
+ }
6
+ export type TokenCheck = (authorization: string | undefined, access: Access) => Promise<Caller | null>;
7
+ export declare const setCaller: (request: object, caller: Caller | null) => void;
8
+ export declare const callerOf: (request: object) => Caller | null;
9
+ export declare const strongest: (needed: readonly Access[]) => Access;
10
+ export declare const accessOfSecurity: (security: unknown) => Access;
@@ -0,0 +1,18 @@
1
+ // Kit-private until ctx.auth.caller(request) exists (M3): part code never reads it from the request.
2
+ const callers = new WeakMap();
3
+ export const setCaller = (request, caller) => void callers.set(request, caller);
4
+ export const callerOf = (request) => callers.get(request) ?? null;
5
+ const RANK = { public: 0, read: 1, write: 2, admin: 3 };
6
+ export const strongest = (needed) => needed.reduce((top, access) => (RANK[access] > RANK[top] ? access : top), 'public');
7
+ const VERB = /:(read|write|admin)$/;
8
+ // OpenAPI security: [] is public, a scope names the verb; a route that declares nothing needs read.
9
+ export const accessOfSecurity = (security) => {
10
+ if (Array.isArray(security) && security.length === 0)
11
+ return 'public';
12
+ const scopes = (Array.isArray(security) ? security : [])
13
+ .flatMap((requirement) => Object.values(requirement ?? {}))
14
+ .flat()
15
+ .filter((scope) => typeof scope === 'string');
16
+ const verbs = scopes.map((scope) => VERB.exec(scope)?.[1] ?? 'read');
17
+ return verbs.length === 0 ? 'read' : strongest(verbs);
18
+ };
@@ -1,6 +1,6 @@
1
1
  import { readManifest } from '../manifest/read-manifest.js';
2
- import { loadHandlers } from './run-command.js';
3
2
  import { createOut } from './out.js';
3
+ import { contextFor, loadHandlers } from './run-command.js';
4
4
  const quiet = () => undefined;
5
5
  // null when the cli entry exports create and its handlers are exactly the declared commands.
6
6
  export const checkCli = async (dir) => {
@@ -8,12 +8,7 @@ export const checkCli = async (dir) => {
8
8
  const pkg = await readManifest(dir);
9
9
  const declared = (pkg.agentage.commands ?? []).map((command) => command.name).sort();
10
10
  const out = createOut({ json: false, stdout: quiet, stderr: quiet });
11
- const ctx = {
12
- extension: { id: pkg.agentage.id, version: pkg.version, part: 'cli' },
13
- args: {},
14
- out,
15
- };
16
- const handlers = Object.keys(await loadHandlers(dir, pkg, ctx)).sort();
11
+ const handlers = Object.keys(await loadHandlers(dir, pkg, contextFor(pkg, {}, out))).sort();
17
12
  if (JSON.stringify(handlers) === JSON.stringify(declared))
18
13
  return null;
19
14
  return `cli handlers [${handlers.join(', ')}] differ from commands [${declared.join(', ')}]`;
@@ -12,4 +12,5 @@ export declare const EXIT_OK = 0;
12
12
  export declare const EXIT_FAILED = 1;
13
13
  export declare const EXIT_USAGE = 2;
14
14
  export declare const loadHandlers: (dir: string, pkg: ExtensionPackage, ctx: CliContext) => Promise<Record<string, CommandHandler>>;
15
+ export declare const contextFor: (pkg: ExtensionPackage, args: CliContext["args"], out: CliContext["out"]) => CliContext;
15
16
  export declare const runCommand: (o: RunCommandOptions) => Promise<number>;
@@ -1,7 +1,5 @@
1
- import { join } from 'node:path';
2
- import { pathToFileURL } from 'node:url';
1
+ import { loadCreate } from '../manifest/entry.js';
3
2
  import {} from '../manifest/manifest.schema.js';
4
- import { entryOf } from '../manifest/parts.js';
5
3
  import { readManifest } from '../manifest/read-manifest.js';
6
4
  import { parseCommandArgs, usage, UsageError } from './args.js';
7
5
  import {} from './command.types.js';
@@ -11,14 +9,8 @@ export const EXIT_FAILED = 1;
11
9
  export const EXIT_USAGE = 2;
12
10
  const writeOut = (text) => void process.stdout.write(text);
13
11
  const writeErr = (text) => void process.stderr.write(text);
14
- export const loadHandlers = async (dir, pkg, ctx) => {
15
- const file = join(dir, entryOf(pkg, 'cli'));
16
- const mod = (await import(pathToFileURL(file).href));
17
- if (typeof mod.create !== 'function')
18
- throw new Error(`${file} does not export create(ctx)`);
19
- return mod.create(ctx);
20
- };
21
- const contextFor = (pkg, args, out) => ({
12
+ export const loadHandlers = async (dir, pkg, ctx) => (await loadCreate(dir, pkg, 'cli'))(ctx);
13
+ export const contextFor = (pkg, args, out) => ({
22
14
  extension: Object.freeze({ id: pkg.agentage.id, version: pkg.version, part: 'cli' }),
23
15
  args: Object.freeze(args),
24
16
  out,
@@ -28,10 +20,8 @@ const execute = async (o, pkg, stdout, stderr) => {
28
20
  if (!spec)
29
21
  throw new UsageError(`${pkg.agentage.id} has no command ${o.command}`);
30
22
  const parsed = parseCommandArgs(spec, o.argv);
31
- if (parsed.help) {
32
- stdout(`${usage(spec)}\n`);
33
- return EXIT_OK;
34
- }
23
+ if (parsed.help)
24
+ return (stdout(`${usage(spec)}\n`), EXIT_OK);
35
25
  const out = createOut({ json: parsed.json, stdout, stderr });
36
26
  const handler = (await loadHandlers(o.dir, pkg, contextFor(pkg, parsed.args, out)))[o.command];
37
27
  if (typeof handler !== 'function')
@@ -24,33 +24,36 @@ const testStore = (option, log) => {
24
24
  const dir = option === true ? process.cwd() : option.dir;
25
25
  return createTestStore((store) => migrate(store, dir, log));
26
26
  };
27
+ const TEST_EXTENSION = {
28
+ id: 'test',
29
+ version: '0.0.0',
30
+ part: 'api',
31
+ origin: 'http://127.0.0.1',
32
+ commit: '0'.repeat(40),
33
+ };
34
+ // Jobs run by hand with runJob, tools recorded as the mcp part registers them.
35
+ const recorders = (log) => {
36
+ const scheduler = createScheduler({ runs: memoryRuns(), log });
37
+ return { ...createToolRegistry(), schedule: scheduler.schedule, runJob: scheduler.run };
38
+ };
27
39
  const serverContext = (options) => {
28
40
  const logs = [];
29
41
  const log = runLog(recordingLog(logs));
30
- const extension = {
31
- id: 'test',
32
- version: '0.0.0',
33
- part: 'api',
34
- origin: 'http://127.0.0.1',
35
- commit: '0'.repeat(40),
36
- ...options.extension,
37
- };
38
42
  const store = testStore(options.store, recordingLog([]));
39
- const scheduler = createScheduler({ runs: memoryRuns(), log });
40
- const { tool, tools } = createToolRegistry();
41
- const base = { extension, log, settings: { ...options.settings }, logs, tool, tools };
42
- const close = async () => store?.close();
43
43
  return {
44
- ...base,
44
+ extension: { ...TEST_EXTENSION, ...options.extension },
45
+ log,
46
+ logs,
47
+ settings: { ...options.settings },
45
48
  store: store ?? NO_STORE,
46
- close,
47
- schedule: scheduler.schedule,
48
- runJob: scheduler.run,
49
+ close: async () => store?.close(),
50
+ ...recorders(log),
49
51
  };
50
52
  };
53
+ // One recorded line per line written, the trailing newline dropped.
54
+ const lines = (into) => (text) => void into.push(...text.replace(/\n$/, '').split('\n'));
51
55
  const commandContext = (options) => {
52
56
  const output = { stdout: [], stderr: [] };
53
- const lines = (into) => (text) => void into.push(...text.replace(/\n$/, '').split('\n'));
54
57
  const out = createOut({
55
58
  json: options.json ?? false,
56
59
  stdout: lines(output.stdout),
@@ -1,4 +1,9 @@
1
1
  import { z } from 'zod';
2
+ export declare const WidgetSchema: z.ZodObject<{
3
+ name: z.ZodString;
4
+ element: z.ZodString;
5
+ module: z.ZodString;
6
+ }, z.core.$strict>;
2
7
  export declare const ExtensionRecordSchema: z.ZodObject<{
3
8
  id: z.ZodString;
4
9
  version: z.ZodString;
@@ -24,7 +24,7 @@ const OriginSchema = z
24
24
  .string()
25
25
  .max(270)
26
26
  .regex(/^(https:\/\/[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)+|http:\/\/(127\.0\.0\.1|localhost))(:[0-9]{1,5})?$/);
27
- const WidgetSchema = z.strictObject({
27
+ export const WidgetSchema = z.strictObject({
28
28
  name: z.string().regex(/^[a-z][a-z0-9-]*$/),
29
29
  element: z.string().regex(/^[a-z][a-z0-9]*-[a-z0-9-]+$/),
30
30
  module: z.string().regex(/^\/ui\/[^/]+\/[^/]+\.m?js$/),
@@ -0,0 +1,5 @@
1
+ import { type ExtensionPackage } from './manifest.schema.js';
2
+ import { type ImagePartKind } from './primitives.schema.js';
3
+ export type ImportModule = (file: string) => Promise<unknown>;
4
+ export declare const createOf: (mod: unknown, file: string) => ((ctx: never) => unknown);
5
+ export declare const loadCreate: <C>(dir: string, pkg: ExtensionPackage, part: ImagePartKind | "cli", importModule?: ImportModule) => Promise<(ctx: C) => unknown>;
@@ -0,0 +1,29 @@
1
+ import { join } from 'node:path';
2
+ import { pathToFileURL } from 'node:url';
3
+ import {} from './manifest.schema.js';
4
+ import { entryOf } from './parts.js';
5
+ import {} from './primitives.schema.js';
6
+ const importFile = (file) => import(pathToFileURL(file).href);
7
+ export const createOf = (mod, file) => {
8
+ const create = mod?.create;
9
+ if (typeof create !== 'function')
10
+ throw new Error(`${file} does not export create(ctx)`);
11
+ return create;
12
+ };
13
+ // The entry itself missing (not a module it imports) means the package was never built.
14
+ const notBuilt = (err, file) => {
15
+ const missing = err?.code === 'ERR_MODULE_NOT_FOUND';
16
+ const message = String(err?.message);
17
+ const named = [file, pathToFileURL(file).href].some((name) => message.includes(`'${name}'`));
18
+ if (!missing || !named)
19
+ return err;
20
+ return new Error(`${file} not found: build the package first (npm run build)`);
21
+ };
22
+ // The one way the kit reaches part code: import the part's entry, take its create(ctx).
23
+ export const loadCreate = async (dir, pkg, part, importModule = importFile) => {
24
+ const file = join(dir, entryOf(pkg, part));
25
+ const mod = await importModule(file).catch((err) => {
26
+ throw notBuilt(err, file);
27
+ });
28
+ return createOf(mod, file);
29
+ };
@@ -9,3 +9,4 @@ export interface NormalizedParts {
9
9
  export declare const normalizeParts: (parts: Parts) => NormalizedParts;
10
10
  export declare const imageParts: (pkg: ExtensionPackage) => ImagePartKind[];
11
11
  export declare const entryOf: (pkg: ExtensionPackage, part: ImagePartKind | "cli") => string;
12
+ export declare const hasPostgres: (pkg: ExtensionPackage) => boolean;
@@ -22,3 +22,4 @@ export const imageParts = (pkg) => {
22
22
  return IMAGE_PART_KINDS.filter((kind) => code[kind] !== undefined);
23
23
  };
24
24
  export const entryOf = (pkg, part) => normalizeParts(pkg.agentage.parts).code[part]?.entry ?? `./dist/${part}/index.js`;
25
+ export const hasPostgres = (pkg) => normalizeParts(pkg.agentage.parts).store === 'postgres';
@@ -3,10 +3,11 @@ import { pipeline } from 'node:stream/promises';
3
3
  import cors from 'cors';
4
4
  import express, {} from 'express';
5
5
  import { createMcpHandler } from '@modelcontextprotocol/server';
6
+ import { strongest } from '../auth/token-check.js';
6
7
  import { fail } from '../envelope/envelope.js';
7
8
  import { BODY_LIMIT, createPartApp, PUBLIC_CORS } from '../server/app.js';
8
9
  import { serverFactory } from './server.js';
9
- import {} from './tools.js';
10
+ import { prefixed } from './tools.js';
10
11
  const MCP_HEADERS = ['Mcp-Protocol-Version', 'Mcp-Method', 'Mcp-Name', 'Accept'];
11
12
  const MCP_CORS = {
12
13
  ...PUBLIC_CORS,
@@ -20,7 +21,28 @@ const parsed = (body) => {
20
21
  return undefined;
21
22
  }
22
23
  };
23
- const isListing = (body) => (Array.isArray(body) ? body : [body]).some((message) => message?.method === 'tools/list');
24
+ const messagesOf = (body) => (Array.isArray(body) ? body : [body]);
25
+ const isListing = (body) => messagesOf(body).some((message) => message?.method === 'tools/list');
26
+ // Each tools/call needs its tool's declared scope (read when it declares none).
27
+ const callAccess = (options, body) => messagesOf(body)
28
+ .filter((message) => message?.method === 'tools/call')
29
+ .map((message) => {
30
+ const id = options.pkg.agentage.id;
31
+ const entry = options.tools.find((t) => prefixed(id, t.name) === message?.params?.name);
32
+ return entry?.spec.scope ?? 'read';
33
+ });
34
+ // Tokens off: calls stay anonymous and a closed listing refuses every tools/list (nothing can verify).
35
+ const guard = async (options, req, body) => {
36
+ const listing = options.listing === 'token' && isListing(body) ? ['read'] : [];
37
+ const needed = [...listing, ...callAccess(options, body)];
38
+ if (!options.tokenCheck) {
39
+ if (listing.length > 0)
40
+ throw fail('UNAUTHORIZED', 'tools/list needs a token');
41
+ return;
42
+ }
43
+ if (needed.length > 0)
44
+ await options.tokenCheck(req.get('authorization'), strongest(needed));
45
+ };
24
46
  const webRequest = (req, body) => {
25
47
  const headers = new Headers();
26
48
  for (const [key, value] of Object.entries(req.headers)) {
@@ -40,12 +62,10 @@ const send = async (response, res) => {
40
62
  return void res.end();
41
63
  await pipeline(Readable.fromWeb(response.body), res);
42
64
  };
43
- // The kit verifies no token yet, so a closed listing refuses every tools/list.
44
- const handle = (handler, listing) => async (req, res) => {
65
+ const handle = (handler, options) => async (req, res) => {
45
66
  const body = Buffer.isBuffer(req.body) ? req.body : Buffer.alloc(0);
46
67
  const message = parsed(body);
47
- if (listing === 'token' && isListing(message))
48
- throw fail('UNAUTHORIZED', 'tools/list needs a token');
68
+ await guard(options, req, message);
49
69
  await send(await handler.fetch(webRequest(req, body), { parsedBody: message }), res);
50
70
  };
51
71
  const methodNotAllowed = (_req, res) => {
@@ -67,7 +87,7 @@ export const createMcpApp = (options) => {
67
87
  part: 'mcp',
68
88
  mount: (app) => {
69
89
  app.use('/mcp', cors(MCP_CORS));
70
- app.post('/mcp', raw, handle(handler, options.listing));
90
+ app.post('/mcp', raw, handle(handler, options));
71
91
  app.all('/mcp', methodNotAllowed);
72
92
  },
73
93
  });
@@ -1,15 +1,17 @@
1
1
  import { McpServer } from '@modelcontextprotocol/server';
2
+ import { agcidOf } from '../agcid/agcid.js';
2
3
  import { currentAgcid } from '../agcid/current.js';
3
4
  import { fail, isFailure } from '../envelope/envelope.js';
4
5
  import {} from '../log/log.js';
5
6
  import { prefixed } from './tools.js';
6
7
  const text = (value) => [{ type: 'text', text: JSON.stringify(value) }];
7
8
  // A failed tool is isError with the REST error shape; anything unexpected is a generic INTERNAL.
9
+ // Outside a request (no agcid in scope) the error still carries a valid one, minted here.
8
10
  const errorResult = (err, log) => {
9
11
  const failure = isFailure(err) ? err : fail('INTERNAL', 'Internal error');
10
12
  if (failure.status >= 500)
11
13
  log.error(err);
12
- const error = { code: failure.code, message: failure.message, agcid: currentAgcid() ?? '' };
14
+ const error = { code: failure.code, message: failure.message, agcid: agcidOf(currentAgcid()) };
13
15
  return { isError: true, content: text({ error }), structuredContent: { error } };
14
16
  };
15
17
  const callback = (entry, log) => async (args) => {
@@ -1,10 +1,12 @@
1
1
  import { type z } from 'zod';
2
+ import { type Access } from '../auth/token-check.js';
2
3
  import { type PartContext } from '../context/context.js';
3
4
  type ObjectSchema = z.ZodObject;
4
5
  export interface ToolSpec<I extends ObjectSchema, O extends ObjectSchema> {
5
6
  description: string;
6
7
  input: I;
7
8
  output: O;
9
+ scope?: Access;
8
10
  }
9
11
  export type ToolHandler<I extends ObjectSchema, O extends ObjectSchema> = (args: z.output<I>) => z.input<O> | Promise<z.input<O>>;
10
12
  export type ToolRegistrar = <I extends ObjectSchema, O extends ObjectSchema>(name: string, spec: ToolSpec<I, O>, handler: ToolHandler<I, O>) => void;
package/dist/mcp/tools.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import {} from 'zod';
2
+ import {} from '../auth/token-check.js';
2
3
  import {} from '../context/context.js';
3
4
  const TOOL_NAME = /^[a-z][a-z0-9_-]*$/;
5
+ const SCOPES = ['public', 'read', 'write', 'admin'];
4
6
  export const createToolRegistry = () => {
5
7
  const tools = [];
6
8
  const tool = (name, spec, handler) => {
@@ -10,6 +12,8 @@ export const createToolRegistry = () => {
10
12
  throw new Error(`tool ${name} is registered twice`);
11
13
  if (!spec.description)
12
14
  throw new Error(`tool ${name} needs a description: it routes the model`);
15
+ if (spec.scope !== undefined && !SCOPES.includes(spec.scope))
16
+ throw new Error(`tool ${name} scope ${String(spec.scope)}: one of ${SCOPES.join(', ')}`);
13
17
  tools.push({ name, spec, handler: handler });
14
18
  };
15
19
  return { tool, tools };
@@ -1,20 +1,20 @@
1
1
  import { type Express } from 'express';
2
2
  import { type PartContext } from '../context/context.js';
3
3
  import { type ExtensionPackage } from '../manifest/manifest.schema.js';
4
+ import { type ImportModule } from '../manifest/entry.js';
4
5
  import { type ImagePartKind } from '../manifest/primitives.schema.js';
5
6
  import { type PartAppBase } from '../server/app.js';
7
+ import { type Env } from './build-info.js';
6
8
  export interface RunnerInput {
7
9
  dir: string;
8
10
  pkg: ExtensionPackage;
9
- ctx: PartContext;
11
+ env: Env;
12
+ ctx: () => PartContext;
10
13
  base: PartAppBase;
11
- importModule?: (file: string) => Promise<unknown>;
14
+ importModule?: ImportModule;
12
15
  }
13
16
  export interface Running {
14
17
  app: Express;
15
18
  stop?: () => void;
16
19
  }
17
- type PartCreate = (ctx: never) => unknown;
18
- export declare const createOf: (mod: unknown, file: string) => PartCreate;
19
20
  export declare const RUNNERS: Record<ImagePartKind, (input: RunnerInput) => Promise<Running>>;
20
- export {};
@@ -1,57 +1,52 @@
1
1
  import { join } from 'node:path';
2
- import { pathToFileURL } from 'node:url';
3
2
  import {} from 'express';
4
3
  import {} from '../context/context.js';
5
4
  import { readOpenapi } from '../discovery/openapi.js';
6
5
  import {} from '../manifest/manifest.schema.js';
7
- import { entryOf, normalizeParts } from '../manifest/parts.js';
6
+ import { loadCreate } from '../manifest/entry.js';
7
+ import { hasPostgres } from '../manifest/parts.js';
8
8
  import {} from '../manifest/primitives.schema.js';
9
9
  import { createMcpApp } from '../mcp/mcp-app.js';
10
10
  import { createToolRegistry } from '../mcp/tools.js';
11
11
  import { createPagesApp } from '../pages/pages-app.js';
12
12
  import { createApiApp, createPartApp } from '../server/app.js';
13
13
  import {} from '../server/part.types.js';
14
+ import {} from './build-info.js';
14
15
  import { createUiApp } from '../ui/ui-app.js';
15
16
  import { runLog } from '../worker/run-log.js';
16
17
  import { memoryRuns, storeRuns } from '../worker/runs.js';
17
18
  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
- };
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);
31
21
  const api = async (input) => {
32
- const router = (await callCreate(input, 'api', input.ctx));
22
+ const router = (await callCreate(input, 'api', input.ctx()));
33
23
  const openapi = await readOpenapi(input.dir);
34
24
  return { app: createApiApp({ ...input.base, router, openapi }) };
35
25
  };
36
26
  const mcp = async (input) => {
37
27
  const registry = createToolRegistry();
38
- await callCreate(input, 'mcp', { ...input.ctx, tool: registry.tool });
39
- const listing = input.ctx.settings['mcp.listing'] === 'token' ? 'token' : 'open';
28
+ const ctx = input.ctx();
29
+ await callCreate(input, 'mcp', { ...ctx, tool: registry.tool });
30
+ const listing = ctx.settings['mcp.listing'] === 'token' ? 'token' : 'open';
40
31
  return { app: createMcpApp({ ...input.base, tools: registry.tools, listing }) };
41
32
  };
42
33
  const pages = async (input) => {
43
- const site = (await callCreate(input, 'pages', input.ctx));
34
+ const site = (await callCreate(input, 'pages', input.ctx()));
44
35
  return { app: createPagesApp({ ...input.base, site }) };
45
36
  };
46
37
  const ui = async (input) => ({
47
- app: createUiApp({ ...input.base, dir: join(input.dir, 'dist', 'ui') }),
38
+ app: createUiApp({
39
+ ...input.base,
40
+ dir: join(input.dir, 'dist', 'ui'),
41
+ devIssuer: input.env['AGENTAGE_DEV_ISSUER'],
42
+ }),
48
43
  });
49
44
  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();
45
+ const ctx = input.ctx();
46
+ const log = runLog(ctx.log);
47
+ const runs = hasPostgres(input.pkg) ? storeRuns(ctx.store) : memoryRuns();
53
48
  const scheduler = createScheduler({ runs, log });
54
- await callCreate(input, 'worker', { ...input.ctx, log, schedule: scheduler.schedule });
49
+ await callCreate(input, 'worker', { ...ctx, log, schedule: scheduler.schedule });
55
50
  const checks = { ...input.base.checks, ...scheduler.checks() };
56
51
  scheduler.start();
57
52
  return {
@@ -1,15 +1,17 @@
1
1
  import { type Server } from 'node:http';
2
+ import { type TokenCheck } from '../auth/token-check.js';
3
+ import { type ImportModule } from '../manifest/entry.js';
2
4
  import { type ExtensionPackage } from '../manifest/manifest.schema.js';
3
5
  import { type ImagePartKind } from '../manifest/primitives.schema.js';
4
6
  import { type ServeOptions } from '../server/serve.js';
5
7
  import { type Env } from './build-info.js';
6
- export { createOf } from './part-runners.js';
7
8
  export interface RunPartOptions {
8
9
  dir: string;
9
10
  part: string;
10
11
  env: Env;
11
- importModule?: (file: string) => Promise<unknown>;
12
+ importModule?: ImportModule;
12
13
  serve?: Pick<ServeOptions, 'signals' | 'exit'>;
14
+ tokenCheck?: TokenCheck;
13
15
  }
14
16
  export declare const partOf: (pkg: ExtensionPackage, part: string) => ImagePartKind;
15
17
  export declare const runPart: (options: RunPartOptions) => Promise<Server>;
@@ -2,17 +2,19 @@ import {} from 'node:http';
2
2
  import { createContext } from '../context/context.js';
3
3
  import { settingsFromEnv } from '../context/settings.js';
4
4
  import { DEFAULT_RECORD_FILE, loadRecord } from '../discovery/discovery.js';
5
+ import {} from '../auth/token-check.js';
5
6
  import {} from '../health/health.js';
6
7
  import { createLog } from '../log/log.js';
8
+ import {} from '../manifest/entry.js';
7
9
  import {} from '../manifest/manifest.schema.js';
8
- import { imageParts, normalizeParts } from '../manifest/parts.js';
10
+ import { hasPostgres, imageParts } from '../manifest/parts.js';
9
11
  import {} from '../manifest/primitives.schema.js';
10
12
  import { readManifest } from '../manifest/read-manifest.js';
11
13
  import { serve } from '../server/serve.js';
12
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
17
  import { RUNNERS } from './part-runners.js';
15
- export { createOf } from './part-runners.js';
16
18
  const recordFor = async (env, log) => {
17
19
  const file = env['AGENTAGE_RECORD_FILE'] || DEFAULT_RECORD_FILE;
18
20
  const record = await loadRecord(file);
@@ -27,34 +29,47 @@ export const partOf = (pkg, part) => {
27
29
  return part;
28
30
  };
29
31
  // 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) => {
34
- const { env } = options;
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);
41
- const ctx = createContext({
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 {
42
55
  pkg,
43
- part: ctxPart,
44
- origin: ctxOrigin,
45
- commit: build.commit,
46
56
  log,
47
- settings,
48
- store,
49
- });
50
- const record = await recordFor(env, log);
51
- const checks = store ? { store: storeCheck(store) } : {};
52
- const base = { pkg, build, log, record, origin, ownImage: env['AGENTAGE_IMAGE'], checks };
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);
53
67
  return {
54
68
  dir: options.dir,
55
- pkg,
56
- ctx,
57
- base,
69
+ pkg: start.pkg,
70
+ env: start.env,
71
+ ctx: lazyContext(start, store),
72
+ base: await baseFor(options, start, store),
58
73
  importModule: options.importModule,
59
74
  };
60
75
  };
@@ -62,7 +77,8 @@ export const runPart = async (options) => {
62
77
  const pkg = await readManifest(options.dir);
63
78
  const part = partOf(pkg, options.part);
64
79
  const log = createLog({ extension: pkg.agentage.id, part });
65
- const { app, stop } = await RUNNERS[part](await inputFor(options, pkg, part, log));
80
+ const start = { pkg, part, env: options.env, log };
81
+ const { app, stop } = await RUNNERS[part](await inputFor(options, start));
66
82
  if (stop)
67
83
  for (const signal of options.serve?.signals ?? ['SIGTERM', 'SIGINT'])
68
84
  process.once(signal, stop);
@@ -1,6 +1,7 @@
1
1
  import { type CorsOptions } from 'cors';
2
2
  import { type Express } from 'express';
3
3
  import { type HelmetOptions } from 'helmet';
4
+ import { type TokenCheck } from '../auth/token-check.js';
4
5
  import { type BuildInfo } from '../discovery/discovery.js';
5
6
  import { type ExtensionRecord } from '../discovery/discovery.schema.js';
6
7
  import { type HealthCheck } from '../health/health.js';
@@ -16,6 +17,7 @@ export interface PartAppBase {
16
17
  ownImage?: string;
17
18
  origin?: string;
18
19
  checks?: Record<string, HealthCheck>;
20
+ tokenCheck?: TokenCheck;
19
21
  }
20
22
  export interface PartAppOptions extends PartAppBase {
21
23
  part: ImagePartKind;