@agentage/extension 0.2.0 → 0.3.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.
Files changed (87) hide show
  1. package/dist/agcid/scope.d.ts +0 -1
  2. package/dist/agcid/scope.js +0 -1
  3. package/dist/auth/part-auth.d.ts +18 -0
  4. package/dist/auth/part-auth.js +61 -0
  5. package/dist/auth/revoked.d.ts +9 -0
  6. package/dist/auth/revoked.js +34 -0
  7. package/dist/auth/route-access.d.ts +2 -0
  8. package/dist/auth/route-access.js +21 -0
  9. package/dist/auth/token-check.d.ts +13 -0
  10. package/dist/auth/token-check.js +26 -0
  11. package/dist/auth/verify-token.d.ts +18 -0
  12. package/dist/auth/verify-token.js +68 -0
  13. package/dist/command/check-cli.js +2 -7
  14. package/dist/command/run-command.d.ts +1 -0
  15. package/dist/command/run-command.js +5 -15
  16. package/dist/context/context.d.ts +4 -0
  17. package/dist/context/context.js +3 -1
  18. package/dist/context/test-context.js +23 -17
  19. package/dist/discovery/discovery.d.ts +8 -2
  20. package/dist/discovery/discovery.schema.d.ts +14 -2
  21. package/dist/discovery/discovery.schema.js +7 -3
  22. package/dist/envelope/envelope.d.ts +2 -0
  23. package/dist/envelope/envelope.js +1 -0
  24. package/dist/manifest/entry.d.ts +5 -0
  25. package/dist/manifest/entry.js +29 -0
  26. package/dist/manifest/parts.d.ts +1 -0
  27. package/dist/manifest/parts.js +1 -0
  28. package/dist/mcp/mcp-app.js +31 -7
  29. package/dist/mcp/server.js +20 -6
  30. package/dist/mcp/tools.d.ts +2 -0
  31. package/dist/mcp/tools.js +4 -0
  32. package/dist/runtime/part-runners.d.ts +6 -5
  33. package/dist/runtime/part-runners.js +20 -27
  34. package/dist/runtime/run-part.d.ts +2 -2
  35. package/dist/runtime/run-part.js +55 -28
  36. package/dist/server/app.d.ts +3 -0
  37. package/dist/server/app.js +15 -1
  38. package/dist/server/errors.js +2 -0
  39. package/dist/store/migrate.js +7 -3
  40. package/dist/store/migrations.d.ts +2 -0
  41. package/dist/store/migrations.js +32 -6
  42. package/dist/store/store.d.ts +7 -1
  43. package/dist/store/store.js +37 -7
  44. package/dist/tool/build.d.ts +0 -2
  45. package/dist/tool/build.js +5 -3
  46. package/dist/tool/check.d.ts +16 -0
  47. package/dist/tool/check.js +30 -0
  48. package/dist/tool/docker.js +3 -2
  49. package/dist/tool/exec.d.ts +1 -0
  50. package/dist/tool/exec.js +43 -16
  51. package/dist/tool/main.js +23 -24
  52. package/dist/tool/record-extras.d.ts +4 -1
  53. package/dist/tool/record-extras.js +16 -15
  54. package/dist/tool/stage.d.ts +1 -0
  55. package/dist/tool/stage.js +8 -10
  56. package/dist/tool/timeouts.d.ts +10 -0
  57. package/dist/tool/timeouts.js +19 -0
  58. package/dist/tool/up/compose-run.d.ts +2 -1
  59. package/dist/tool/up/compose-run.js +3 -2
  60. package/dist/tool/up/compose.d.ts +2 -1
  61. package/dist/tool/up/compose.js +32 -23
  62. package/dist/tool/up/dev-issuer-app.d.ts +9 -3
  63. package/dist/tool/up/dev-issuer-app.js +53 -24
  64. package/dist/tool/up/dev-issuer.js +2 -1
  65. package/dist/tool/up/down.d.ts +1 -0
  66. package/dist/tool/up/down.js +39 -30
  67. package/dist/tool/up/gateway.d.ts +9 -0
  68. package/dist/tool/up/gateway.js +11 -17
  69. package/dist/tool/up/lines.js +1 -1
  70. package/dist/tool/up/local-record.d.ts +4 -1
  71. package/dist/tool/up/ports.d.ts +2 -0
  72. package/dist/tool/up/ports.js +22 -0
  73. package/dist/tool/up/project.d.ts +4 -0
  74. package/dist/tool/up/project.js +20 -0
  75. package/dist/tool/up/store-line.d.ts +11 -0
  76. package/dist/tool/up/store-line.js +36 -0
  77. package/dist/tool/up/token.js +9 -2
  78. package/dist/tool/up/up.d.ts +3 -6
  79. package/dist/tool/up/up.js +49 -46
  80. package/dist/tool/up/wait.js +4 -2
  81. package/dist/ui/ui-app.d.ts +2 -0
  82. package/dist/ui/ui-app.js +18 -5
  83. package/dist/worker/runs.js +11 -11
  84. package/dist/worker/scheduler.js +14 -5
  85. package/package.json +2 -1
  86. package/schemas/extension-discovery.schema.json +17 -3
  87. package/schemas/extension-record.schema.json +17 -3
@@ -33,6 +33,4 @@ export interface BuildResult {
33
33
  }
34
34
  export declare const imageTag: (registry: string, pkg: ExtensionPackage, part: ImagePartKind) => string;
35
35
  export declare const recordFor: (dir: string, pkg: ExtensionPackage, info: BuildInfo, images: BuiltImage[]) => Promise<ExtensionRecord>;
36
- declare const writeRecord: (dir: string, record: ExtensionRecord, file?: string) => Promise<string>;
37
36
  export declare const build: (options?: BuildOptions, deps?: BuildDeps) => Promise<BuildResult>;
38
- export { writeRecord };
@@ -8,17 +8,19 @@ import {} from '../manifest/primitives.schema.js';
8
8
  import { readManifest } from '../manifest/read-manifest.js';
9
9
  import {} from '../runtime/build-info.js';
10
10
  import { resolveBuildInfo } from './build-info.js';
11
+ import { assertConforms } from './check.js';
11
12
  import { createDocker } from './docker.js';
12
13
  import { mustRun, runCommand } from './exec.js';
13
14
  import { labelsOf } from './labels.js';
14
15
  import { recordExtras } from './record-extras.js';
15
16
  import { RECORD_FILE, stageContext } from './stage.js';
17
+ import { TIMEOUTS } from './timeouts.js';
16
18
  export const DEFAULT_REGISTRY = 'ghcr.io/agentage';
17
19
  export const imageTag = (registry, pkg, part) => `${registry}/${pkg.agentage.id}-${part}:${pkg.version}`;
18
20
  const compile = async (dir, pkg, run) => {
19
21
  const scripts = pkg.scripts;
20
22
  if (typeof scripts?.['build'] === 'string')
21
- await mustRun(run, 'npm', ['run', 'build'], { cwd: dir });
23
+ await mustRun(run, 'npm', ['run', 'build'], { cwd: dir, timeoutMs: TIMEOUTS.compile });
22
24
  };
23
25
  const buildImage = async (job, docker, tmpRoot) => {
24
26
  const context = await stageContext(job.dir, job.pkg, job.part, tmpRoot);
@@ -60,16 +62,16 @@ const buildParts = async (base, options, deps) => {
60
62
  }
61
63
  return images;
62
64
  };
63
- // Validate, compile, one image per part from the kit recipe; --push pushes and writes the record.
65
+ // Validate, compile, check, one image per part from the kit recipe; --push pushes, writes the record.
64
66
  export const build = async (options = {}, deps = { run: runCommand }) => {
65
67
  const dir = resolve(options.dir ?? '.');
66
68
  const pkg = await readManifest(dir);
67
69
  const info = await resolveBuildInfo(dir, options.env ?? process.env, deps.run);
68
70
  await compile(dir, pkg, deps.run);
71
+ await assertConforms({ dir, pkg });
69
72
  const images = await buildParts({ dir, pkg, info }, options, deps);
70
73
  const result = { id: pkg.agentage.id, version: pkg.version, ...info, images };
71
74
  if (!options.push)
72
75
  return result;
73
76
  return { ...result, recordFile: await writeRecord(dir, await recordFor(dir, pkg, info, images)) };
74
77
  };
75
- export { writeRecord };
@@ -0,0 +1,16 @@
1
+ import { type ExtensionPackage } from '../manifest/manifest.schema.js';
2
+ export interface CheckInput {
3
+ dir: string;
4
+ pkg: ExtensionPackage;
5
+ }
6
+ export interface CheckRule {
7
+ name: string;
8
+ run(input: CheckInput): Promise<string[]>;
9
+ }
10
+ export interface CheckProblem {
11
+ rule: string;
12
+ problem: string;
13
+ }
14
+ export declare const RULES: readonly CheckRule[];
15
+ export declare const check: (input: CheckInput, rules?: readonly CheckRule[]) => Promise<CheckProblem[]>;
16
+ export declare const assertConforms: (input: CheckInput, rules?: readonly CheckRule[]) => Promise<void>;
@@ -0,0 +1,30 @@
1
+ import { checkCli } from '../command/check-cli.js';
2
+ import {} from '../manifest/manifest.schema.js';
3
+ import { imageParts, normalizeParts } from '../manifest/parts.js';
4
+ import { assertBuilt } from './stage.js';
5
+ const problemsOf = (work) => work.then(() => [], (err) => [err.message]);
6
+ // Every image part built where its image looks for it; the cli handlers are exactly the commands.
7
+ const entries = {
8
+ name: 'entries',
9
+ run: async ({ dir, pkg }) => {
10
+ const built = imageParts(pkg).map((part) => problemsOf(assertBuilt(dir, pkg, part)));
11
+ const cli = normalizeParts(pkg.agentage.parts).code.cli ? await checkCli(dir) : null;
12
+ return [...(await Promise.all(built)).flat(), ...(cli ? [`cli part: ${cli}`] : [])];
13
+ },
14
+ };
15
+ // Kit 0.2 has the entries rule; M4 adds manifest, surface, stack, store and sealing beside it.
16
+ export const RULES = [entries];
17
+ export const check = async (input, rules = RULES) => {
18
+ const found = [];
19
+ for (const rule of rules)
20
+ for (const problem of await rule.run(input))
21
+ found.push({ rule: rule.name, problem });
22
+ return found;
23
+ };
24
+ // What build runs after the compile and before any image: one failure stops everything.
25
+ export const assertConforms = async (input, rules = RULES) => {
26
+ const problems = await check(input, rules);
27
+ if (problems.length === 0)
28
+ return;
29
+ throw new Error(`check failed: ${problems.map((p) => `${p.rule}: ${p.problem}`).join('; ')}`);
30
+ };
@@ -1,4 +1,5 @@
1
1
  import { mustRun } from './exec.js';
2
+ import { TIMEOUTS } from './timeouts.js';
2
3
  const PLATFORM = 'linux/amd64';
3
4
  const flags = (flag, values) => Object.entries(values).flatMap(([key, value]) => [flag, `${key}=${value}`]);
4
5
  // No provenance attestation: the pushed digest is then the image itself, as the record expects.
@@ -21,7 +22,7 @@ export const digestOf = (pushOutput) => {
21
22
  };
22
23
  export const createDocker = (run) => ({
23
24
  build: async (options) => {
24
- await mustRun(run, 'docker', buildArgsOf(options));
25
+ await mustRun(run, 'docker', buildArgsOf(options), { timeoutMs: TIMEOUTS.build });
25
26
  },
26
- push: async (tag) => digestOf(await mustRun(run, 'docker', ['push', tag], { echo: true })),
27
+ push: async (tag) => digestOf(await mustRun(run, 'docker', ['push', tag], { echo: true, timeoutMs: TIMEOUTS.push })),
27
28
  });
@@ -3,6 +3,7 @@ export interface RunOptions {
3
3
  echo?: boolean;
4
4
  quiet?: boolean;
5
5
  env?: Record<string, string | undefined>;
6
+ timeoutMs?: number;
6
7
  }
7
8
  export interface RunResult {
8
9
  code: number;
package/dist/tool/exec.js CHANGED
@@ -1,24 +1,51 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import {} from 'node:stream';
3
- // stdout is captured (and echoed to stderr with `echo`); stderr streams through as progress.
4
- export const runCommand = (command, args, options = {}) => new Promise((resolve, reject) => {
5
- const stdio = ['ignore', 'pipe', options.quiet ? 'pipe' : 'inherit'];
6
- const env = options.env ? { ...process.env, ...options.env } : process.env;
7
- const child = spawn(command, args, {
8
- cwd: options.cwd,
9
- stdio: [...stdio],
10
- env,
11
- });
12
- let stdout = '';
13
- let stderr = '';
3
+ import { KILL_GRACE_MS, TIMEOUTS } from './timeouts.js';
4
+ const spawnFor = (command, args, options) => spawn(command, args, {
5
+ cwd: options.cwd,
6
+ stdio: ['ignore', 'pipe', options.quiet ? 'pipe' : 'inherit'],
7
+ env: options.env ? { ...process.env, ...options.env } : process.env,
8
+ });
9
+ // Resolves true once the command is past its time and has been told to stop.
10
+ const expiry = (child, ms) => {
11
+ let timer;
12
+ const expired = { value: false };
13
+ timer = setTimeout(() => {
14
+ expired.value = true;
15
+ child.kill('SIGTERM');
16
+ timer = setTimeout(() => child.kill('SIGKILL'), KILL_GRACE_MS).unref();
17
+ }, ms).unref();
18
+ return { expired, clear: () => clearTimeout(timer) };
19
+ };
20
+ const timedOut = (command, args, ms) => new Error(`${command} ${args[0] ?? ''} did not finish in ${Math.round(ms / 1000)} s`);
21
+ // stdout is captured (and echoed to stderr with `echo`); a piped stderr is captured too.
22
+ const collect = (child, echo) => {
23
+ const out = { stdout: '', stderr: '' };
14
24
  child.stdout.on('data', (chunk) => {
15
- stdout += chunk.toString();
16
- if (options.echo)
25
+ out.stdout += chunk.toString();
26
+ if (echo)
17
27
  process.stderr.write(chunk);
18
28
  });
19
- child.stderr?.on('data', (chunk) => void (stderr += chunk.toString()));
20
- child.on('error', (err) => reject(err.code === 'ENOENT' ? new Error(`${command} is not installed or not on PATH`) : err));
21
- child.on('close', (code) => resolve({ code: code ?? 1, stdout, ...(options.quiet ? { stderr } : {}) }));
29
+ child.stderr?.on('data', (chunk) => void (out.stderr += chunk.toString()));
30
+ return out;
31
+ };
32
+ // stderr streams through as progress unless `quiet`; past its timeout the command is killed.
33
+ export const runCommand = (command, args, options = {}) => new Promise((resolve, reject) => {
34
+ const child = spawnFor(command, args, options);
35
+ const ms = options.timeoutMs ?? TIMEOUTS.command;
36
+ const timer = expiry(child, ms);
37
+ const out = collect(child, options.echo);
38
+ child.on('error', (err) => {
39
+ timer.clear();
40
+ reject(err.code === 'ENOENT' ? new Error(`${command} is not installed or not on PATH`) : err);
41
+ });
42
+ child.on('close', (code) => {
43
+ timer.clear();
44
+ if (timer.expired.value)
45
+ return reject(timedOut(command, args, ms));
46
+ const stderr = options.quiet ? { stderr: out.stderr } : {};
47
+ resolve({ code: code ?? 1, stdout: out.stdout, ...stderr });
48
+ });
22
49
  });
23
50
  const tail = (text) => text ? `\n${text.trim().split('\n').slice(-15).join('\n')}` : '';
24
51
  export const mustRun = async (run, command, args, options) => {
package/dist/tool/main.js CHANGED
@@ -1,4 +1,6 @@
1
1
  import { parseArgs } from 'node:util';
2
+ import { UsageError } from '../command/args.js';
3
+ import { EXIT_FAILED, EXIT_OK, EXIT_USAGE } from '../command/run-command.js';
2
4
  import { build } from './build.js';
3
5
  import { buildLines } from './format.js';
4
6
  import { down } from './up/down.js';
@@ -14,10 +16,10 @@ const USAGE = `usage: agentage-extension <build|up|down|token> [options]
14
16
  token a dev token: --scope <id>:<verb> [--issuer <url>]
15
17
  --dir the extension package folder (default: the current folder)`;
16
18
  const defaultDeps = {
17
- build: (options) => build(options),
18
- up: (options) => up(options),
19
- down: (options) => down(options),
20
- token: (options) => token(options),
19
+ build,
20
+ up,
21
+ down,
22
+ token,
21
23
  out: (line) => void process.stdout.write(`${line}\n`),
22
24
  err: (line) => void process.stderr.write(`${line}\n`),
23
25
  };
@@ -31,8 +33,6 @@ const OPTIONS = {
31
33
  help: { type: 'boolean', short: 'h' },
32
34
  };
33
35
  const parse = (argv) => parseArgs({ args: argv, allowPositionals: true, options: OPTIONS });
34
- class UsageError extends Error {
35
- }
36
36
  const COMMANDS = {
37
37
  build: async (v, deps) => {
38
38
  for (const line of buildLines(await deps.build({ push: v.push, dir: v.dir, registry: v.registry })))
@@ -49,34 +49,33 @@ const COMMANDS = {
49
49
  const runOne = async (name, values, deps) => {
50
50
  try {
51
51
  await COMMANDS[name]?.(values, deps);
52
- return 0;
52
+ return EXIT_OK;
53
53
  }
54
54
  catch (err) {
55
- deps.err(err instanceof UsageError
56
- ? `${err.message}\n${USAGE}`
57
- : `${name} failed: ${err.message}`);
58
- return err instanceof UsageError ? 2 : 1;
55
+ const usage = err instanceof UsageError;
56
+ deps.err(usage ? `${err.message}\n${USAGE}` : `${name} failed: ${err.message}`);
57
+ return usage ? EXIT_USAGE : EXIT_FAILED;
59
58
  }
60
59
  };
61
- // Exit codes: 0 ok, 1 the step failed, 2 usage.
62
- export const main = async (argv, deps = defaultDeps) => {
63
- let parsed;
60
+ const parsedOrUsage = (argv, deps) => {
64
61
  try {
65
- parsed = parse(argv);
62
+ return parse(argv);
66
63
  }
67
64
  catch (err) {
68
65
  deps.err(`${err.message}\n${USAGE}`);
69
- return 2;
66
+ return null;
70
67
  }
68
+ };
69
+ // Exit codes: 0 ok, 1 the step failed, 2 usage.
70
+ export const main = async (argv, deps = defaultDeps) => {
71
+ const parsed = parsedOrUsage(argv, deps);
72
+ if (!parsed)
73
+ return EXIT_USAGE;
71
74
  const { positionals, values } = parsed;
72
- if (values.help) {
73
- deps.out(USAGE);
74
- return 0;
75
- }
75
+ if (values.help)
76
+ return (deps.out(USAGE), EXIT_OK);
76
77
  const name = positionals[0] ?? '';
77
- if (!(name in COMMANDS) || positionals.length !== 1) {
78
- deps.err(USAGE);
79
- return 2;
80
- }
78
+ if (!(name in COMMANDS) || positionals.length !== 1)
79
+ return (deps.err(USAGE), EXIT_USAGE);
81
80
  return runOne(name, values, deps);
82
81
  };
@@ -1,4 +1,7 @@
1
1
  import { type RecordExtras } from '../discovery/discovery.js';
2
2
  import { type ExtensionPackage } from '../manifest/manifest.schema.js';
3
- export declare const toolNames: (dir: string, pkg: ExtensionPackage) => Promise<string[]>;
3
+ export declare const toolsOf: (dir: string, pkg: ExtensionPackage) => Promise<{
4
+ name: string;
5
+ scope: string;
6
+ }[]>;
4
7
  export declare const recordExtras: (dir: string, pkg: ExtensionPackage) => Promise<RecordExtras>;
@@ -1,27 +1,27 @@
1
1
  import { join } from 'node:path';
2
- import { pathToFileURL } from 'node:url';
3
2
  import { z } from 'zod';
3
+ import { scopeOf } from '../auth/token-check.js';
4
4
  import { createContext } from '../context/context.js';
5
5
  import {} from '../discovery/discovery.js';
6
+ import { WidgetSchema } from '../discovery/discovery.schema.js';
6
7
  import { OPENAPI_PATH, readOpenapi } from '../discovery/openapi.js';
7
8
  import { createLog } from '../log/log.js';
9
+ import { loadCreate } from '../manifest/entry.js';
8
10
  import { readJsonIfExists } from '../manifest/read-manifest.js';
9
11
  import {} from '../manifest/manifest.schema.js';
10
- import { entryOf, normalizeParts } from '../manifest/parts.js';
12
+ import { hasPostgres, normalizeParts } from '../manifest/parts.js';
11
13
  import { createToolRegistry, prefixed } from '../mcp/tools.js';
12
14
  import { sqlFiles } from '../store/migrations.js';
13
15
  import {} from '../store/store.types.js';
14
- const WidgetsSchema = z
15
- .array(z.strictObject({ name: z.string(), element: z.string(), module: z.string() }))
16
- .min(1);
16
+ const WidgetsSchema = z.array(WidgetSchema).min(1);
17
17
  const noStore = {
18
18
  query: async () => {
19
19
  throw new Error('the store is not open while the build lists tools');
20
20
  },
21
21
  };
22
22
  const silent = createLog({ extension: 'build', part: 'mcp' }, () => undefined);
23
- // The tool names the mcp part registers, read by running its create(ctx) with a recording ctx.
24
- export const toolNames = async (dir, pkg) => {
23
+ // The tools the mcp part registers and their scopes, read by running its create(ctx) on a recorder.
24
+ export const toolsOf = async (dir, pkg) => {
25
25
  const registry = createToolRegistry();
26
26
  const base = createContext({
27
27
  pkg,
@@ -31,11 +31,12 @@ export const toolNames = async (dir, pkg) => {
31
31
  log: silent,
32
32
  store: noStore,
33
33
  });
34
- const mod = (await import(pathToFileURL(join(dir, entryOf(pkg, 'mcp'))).href));
35
- if (typeof mod.create !== 'function')
36
- throw new Error('the mcp entry does not export create(ctx)');
37
- await mod.create({ ...base, tool: registry.tool });
38
- return registry.tools.map((tool) => prefixed(pkg.agentage.id, tool.name));
34
+ await (await loadCreate(dir, pkg, 'mcp'))({ ...base, tool: registry.tool });
35
+ const { id } = pkg.agentage;
36
+ return registry.tools.map((tool) => ({
37
+ name: prefixed(id, tool.name),
38
+ scope: scopeOf(id, tool.spec.scope),
39
+ }));
39
40
  };
40
41
  const widgetsOf = async (dir) => {
41
42
  const value = await readJsonIfExists(join(dir, 'dist', 'ui', 'widgets.json'));
@@ -43,10 +44,10 @@ const widgetsOf = async (dir) => {
43
44
  };
44
45
  // What the code serves, for the record: tools, widgets and the migration count.
45
46
  export const recordExtras = async (dir, pkg) => {
46
- const { code, store } = normalizeParts(pkg.agentage.parts);
47
+ const { code } = normalizeParts(pkg.agentage.parts);
47
48
  const extras = {};
48
49
  if (code.mcp) {
49
- const tools = await toolNames(dir, pkg);
50
+ const tools = await toolsOf(dir, pkg);
50
51
  if (tools.length > 0)
51
52
  extras.tools = tools;
52
53
  }
@@ -54,7 +55,7 @@ export const recordExtras = async (dir, pkg) => {
54
55
  extras.widgets = await widgetsOf(dir);
55
56
  if (code.api && (await readOpenapi(dir)))
56
57
  extras.openapi = OPENAPI_PATH;
57
- if (store === 'postgres')
58
+ if (hasPostgres(pkg))
58
59
  extras.migrations = (await sqlFiles(join(dir, 'migrations'))).length;
59
60
  return extras;
60
61
  };
@@ -3,6 +3,7 @@ import { type ImagePartKind } from '../manifest/primitives.schema.js';
3
3
  export declare const RECORD_FILE = "agentage-extension.json";
4
4
  export declare const MIGRATE_SHIM = "// Generated by @agentage/extension build.\nimport '@agentage/extension/migrate';\n";
5
5
  export declare const localTarballs: (pkg: ExtensionPackage) => string[];
6
+ export declare const exists: (path: string) => Promise<boolean>;
6
7
  export declare const appPaths: (dir: string, pkg: ExtensionPackage, part: ImagePartKind) => Promise<string[]>;
7
8
  export declare const assertBuilt: (dir: string, pkg: ExtensionPackage, part: ImagePartKind) => Promise<void>;
8
9
  export declare const stageContext: (dir: string, pkg: ExtensionPackage, part: ImagePartKind, tmpRoot?: string) => Promise<string>;
@@ -2,7 +2,7 @@ import { access, cp, mkdir, mkdtemp, writeFile } from 'node:fs/promises';
2
2
  import { tmpdir } from 'node:os';
3
3
  import { dirname, isAbsolute, join, normalize, sep } from 'node:path';
4
4
  import {} from '../manifest/manifest.schema.js';
5
- import { entryOf, normalizeParts } from '../manifest/parts.js';
5
+ import { entryOf, hasPostgres } from '../manifest/parts.js';
6
6
  import {} from '../manifest/primitives.schema.js';
7
7
  import { OPENAPI_FILE } from '../discovery/openapi.js';
8
8
  import { serverPartRecipe } from './recipe.js';
@@ -25,7 +25,7 @@ export const localTarballs = (pkg) => dependencySpecs(pkg)
25
25
  }
26
26
  return path;
27
27
  });
28
- const exists = (path) => access(path).then(() => true, () => false);
28
+ export const exists = (path) => access(path).then(() => true, () => false);
29
29
  const copyInto = async (from, to) => {
30
30
  await mkdir(dirname(to), { recursive: true });
31
31
  await cp(from, to, { recursive: true });
@@ -39,25 +39,23 @@ const copyDeps = async (dir, root, pkg) => {
39
39
  for (const path of localTarballs(pkg))
40
40
  await copyInto(join(dir, path), join(root, 'deps', path));
41
41
  };
42
- // What a part image carries: its own dist/{part}, the shared dist/lib, and for api the store files.
42
+ // The api image runs the migrate job, so it alone carries the migrations; seeds never ship.
43
+ const migrates = (pkg, part) => part === 'api' && hasPostgres(pkg);
44
+ // What a part image carries: its own dist/{part}, the shared dist/lib, and for api the migrations.
43
45
  export const appPaths = async (dir, pkg, part) => {
44
46
  const paths = ['package.json', join('dist', part)];
45
47
  if (part === 'api' && (await exists(join(dir, OPENAPI_FILE))))
46
48
  paths.push(OPENAPI_FILE);
47
49
  if (await exists(join(dir, 'dist', 'lib')))
48
50
  paths.push(join('dist', 'lib'));
49
- const postgres = normalizeParts(pkg.agentage.parts).store === 'postgres';
50
- if (part === 'api' && postgres) {
51
- for (const folder of ['migrations', 'seeds'])
52
- if (await exists(join(dir, folder)))
53
- paths.push(folder);
54
- }
51
+ if (migrates(pkg, part) && (await exists(join(dir, 'migrations'))))
52
+ paths.push('migrations');
55
53
  return paths;
56
54
  };
57
55
  const copyApp = async (dir, root, pkg, part) => {
58
56
  for (const path of await appPaths(dir, pkg, part))
59
57
  await copyInto(join(dir, path), join(root, 'app', path));
60
- if (part === 'api' && normalizeParts(pkg.agentage.parts).store === 'postgres') {
58
+ if (migrates(pkg, part)) {
61
59
  await mkdir(join(root, 'app', 'dist', 'store'), { recursive: true });
62
60
  await writeFile(join(root, 'app', 'dist', 'store', 'migrate.js'), MIGRATE_SHIM);
63
61
  }
@@ -0,0 +1,10 @@
1
+ export declare const TIMEOUTS: {
2
+ readonly command: 120000;
3
+ readonly compile: 600000;
4
+ readonly build: 1200000;
5
+ readonly push: 600000;
6
+ readonly start: 600000;
7
+ readonly migrate: 300000;
8
+ readonly issuer: 5000;
9
+ };
10
+ export declare const KILL_GRACE_MS = 5000;
@@ -0,0 +1,19 @@
1
+ // Every outbound call the tool makes and its bound; none retries, since a re-run is safe.
2
+ export const TIMEOUTS = {
3
+ // git rev-parse, docker image inspect, compose ps, logs and down, the port check
4
+ command: 120_000,
5
+ // the package's own npm run build
6
+ compile: 600_000,
7
+ // docker build: a cold cache pulls the node base image and runs npm ci
8
+ build: 1_200_000,
9
+ // docker push to the registry
10
+ push: 600_000,
11
+ // compose up: the store and gateway images are pulled on first use
12
+ start: 600_000,
13
+ // compose run migrate: the store's migrations and seeds
14
+ migrate: 300_000,
15
+ // POST /token to the dev issuer
16
+ issuer: 5_000,
17
+ };
18
+ // SIGTERM first; a command that ignores it is killed this long after.
19
+ export const KILL_GRACE_MS = 5_000;
@@ -9,6 +9,7 @@ export interface ServiceState {
9
9
  health: string;
10
10
  exitCode: number;
11
11
  }
12
- export declare const compose: (run: Run, target: ComposeTarget, args: string[]) => Promise<string>;
12
+ export declare const composeArgs: (target: ComposeTarget, args: string[]) => string[];
13
+ export declare const compose: (run: Run, target: ComposeTarget, args: string[], timeoutMs?: number) => Promise<string>;
13
14
  export declare const parsePs: (stdout: string) => ServiceState[];
14
15
  export declare const states: (run: Run, target: ComposeTarget) => Promise<ServiceState[]>;
@@ -1,11 +1,12 @@
1
1
  import { mustRun } from '../exec.js';
2
- export const compose = (run, target, args) => mustRun(run, 'docker', [
2
+ export const composeArgs = (target, args) => [
3
3
  'compose',
4
4
  ...(target.file ? ['-f', target.file] : []),
5
5
  '-p',
6
6
  target.project,
7
7
  ...args,
8
- ]);
8
+ ];
9
+ export const compose = (run, target, args, timeoutMs) => mustRun(run, 'docker', composeArgs(target, args), { timeoutMs });
9
10
  // `compose ps --format json` prints one object per line (older releases: one array).
10
11
  export const parsePs = (stdout) => {
11
12
  const text = stdout.trim();
@@ -2,11 +2,12 @@ import { type ExtensionPackage } from '../../manifest/manifest.schema.js';
2
2
  export declare const STORE_IMAGE = "postgres:18-alpine";
3
3
  export interface ComposeInput {
4
4
  pkg: ExtensionPackage;
5
+ project: string;
5
6
  tags: Record<string, string>;
6
7
  recordFile: string;
7
8
  gatewayFile: string;
9
+ seedsDir?: string;
8
10
  }
9
- export declare const projectName: (pkg: ExtensionPackage) => string;
10
11
  export declare const composeFile: (input: ComposeInput) => {
11
12
  name: string;
12
13
  services: Record<string, unknown>;
@@ -1,38 +1,48 @@
1
1
  import {} from '../../manifest/manifest.schema.js';
2
- import { imageParts, normalizeParts } from '../../manifest/parts.js';
2
+ import { hasPostgres, imageParts } from '../../manifest/parts.js';
3
3
  import {} from '../../manifest/primitives.schema.js';
4
+ import { DEV_AUDIENCE, DEV_ISSUER, UP_ISSUER } from './dev-issuer-app.js';
4
5
  import { GATEWAY_PORT, ISSUER_PORT, PART_PORT, TRAEFIK_COMMAND, TRAEFIK_IMAGE } from './gateway.js';
5
6
  export const STORE_IMAGE = 'postgres:18-alpine';
6
7
  const RECORD_TARGET = '/run/agentage/agentage-extension.json';
7
8
  const ISSUER_ENTRY = 'node_modules/@agentage/extension/dist/tool/up/dev-issuer.js';
8
- export const projectName = (pkg) => `agentage-${pkg.agentage.id}`;
9
9
  // A local-only credential: the store has no published port, only the parts' network reaches it.
10
- const storeUrl = (id) => `postgres://${id}:agentage-local@store:5432/${id}`;
11
- const hasStore = (pkg) => normalizeParts(pkg.agentage.parts).store === 'postgres';
10
+ const STORE_PASSWORD = 'agentage-local';
11
+ const storeUrl = (id) => `postgres://${id}:${STORE_PASSWORD}@store:5432/${id}`;
12
+ const TOKENS = { AGENTAGE_ISSUER: UP_ISSUER, AGENTAGE_AUDIENCE: DEV_AUDIENCE };
13
+ // Only the ui under `up` names the dev issuer: its CSP then lets the harness ask it for a token.
12
14
  const partEnv = (pkg, part) => ({
13
15
  AGENTAGE_ORIGIN: `http://localhost:${GATEWAY_PORT}`,
16
+ ...TOKENS,
14
17
  AGENTAGE_RECORD_FILE: RECORD_TARGET,
15
- ...(hasStore(pkg) && part !== 'ui' ? { AGENTAGE_STORE_URL: storeUrl(pkg.agentage.id) } : {}),
18
+ ...(hasPostgres(pkg) && part !== 'ui' ? { AGENTAGE_STORE_URL: storeUrl(pkg.agentage.id) } : {}),
19
+ ...(part === 'ui' ? { AGENTAGE_DEV_ISSUER: DEV_ISSUER } : {}),
16
20
  });
21
+ const QUICK_CHECK = { interval: '2s', timeout: '3s', retries: 30 };
17
22
  const storeService = (id) => ({
18
23
  image: STORE_IMAGE,
19
- environment: { POSTGRES_USER: id, POSTGRES_PASSWORD: 'agentage-local', POSTGRES_DB: id },
24
+ environment: { POSTGRES_USER: id, POSTGRES_PASSWORD: STORE_PASSWORD, POSTGRES_DB: id },
20
25
  volumes: ['store:/var/lib/postgresql'],
21
26
  healthcheck: {
22
27
  test: ['CMD-SHELL', `pg_isready -U ${id} -d ${id}`],
23
- interval: '2s',
24
- timeout: '3s',
25
- retries: 30,
28
+ ...QUICK_CHECK,
26
29
  },
27
30
  });
28
- const partService = (input, part) => ({
29
- image: input.tags[part],
30
- environment: partEnv(input.pkg, part),
31
- volumes: [`${input.recordFile}:${RECORD_TARGET}:ro`],
32
- ...(hasStore(input.pkg) && part !== 'ui'
33
- ? { depends_on: { store: { condition: 'service_healthy' } } }
34
- : {}),
31
+ const HEALTHY = { condition: 'service_healthy' };
32
+ // api and mcp read the issuer's keys and revoked list before they serve.
33
+ const dependsOn = (pkg, part) => ({
34
+ ...(hasPostgres(pkg) && part !== 'ui' ? { store: HEALTHY } : {}),
35
+ ...(part === 'api' || part === 'mcp' ? { issuer: HEALTHY } : {}),
35
36
  });
37
+ const partService = (input, part) => {
38
+ const depends_on = dependsOn(input.pkg, part);
39
+ return {
40
+ image: input.tags[part],
41
+ environment: partEnv(input.pkg, part),
42
+ volumes: [`${input.recordFile}:${RECORD_TARGET}:ro`],
43
+ ...(Object.keys(depends_on).length > 0 ? { depends_on } : {}),
44
+ };
45
+ };
36
46
  const fixedServices = (input, anyImage) => ({
37
47
  gateway: {
38
48
  image: TRAEFIK_IMAGE,
@@ -41,9 +51,7 @@ const fixedServices = (input, anyImage) => ({
41
51
  volumes: [`${input.gatewayFile}:/etc/traefik/dynamic.yml:ro`],
42
52
  healthcheck: {
43
53
  test: ['CMD', 'wget', '-qO-', 'http://127.0.0.1:8082/ping'],
44
- interval: '2s',
45
- timeout: '3s',
46
- retries: 30,
54
+ ...QUICK_CHECK,
47
55
  },
48
56
  },
49
57
  issuer: {
@@ -56,7 +64,8 @@ const migrateService = (input) => ({
56
64
  image: input.tags['api'],
57
65
  command: ['node', 'dist/store/migrate.js'],
58
66
  environment: partEnv(input.pkg, 'migrate'),
59
- depends_on: { store: { condition: 'service_healthy' } },
67
+ ...(input.seedsDir ? { volumes: [`${input.seedsDir}:/app/seeds:ro`] } : {}),
68
+ depends_on: { store: HEALTHY },
60
69
  profiles: ['jobs'],
61
70
  restart: 'no',
62
71
  });
@@ -67,11 +76,11 @@ export const composeFile = (input) => {
67
76
  for (const part of parts)
68
77
  services[part] = partService(input, part);
69
78
  Object.assign(services, fixedServices(input, input.tags[parts[0] ?? 'api'] ?? ''));
70
- if (hasStore(input.pkg)) {
79
+ if (hasPostgres(input.pkg)) {
71
80
  services['store'] = storeService(input.pkg.agentage.id);
72
81
  if (input.tags['api'])
73
82
  services['migrate'] = migrateService(input);
74
83
  }
75
- const volumes = hasStore(input.pkg) ? { store: {} } : {};
76
- return { name: projectName(input.pkg), services, volumes };
84
+ const volumes = hasPostgres(input.pkg) ? { store: {} } : {};
85
+ return { name: input.project, services, volumes };
77
86
  };
@@ -2,11 +2,17 @@ import { type KeyObject } from 'node:crypto';
2
2
  import { type Server } from 'node:http';
3
3
  import { z } from 'zod';
4
4
  export declare const DEV_ISSUER = "http://localhost:4281";
5
- export declare const DEV_AUDIENCE = "*.agentage.io";
5
+ export declare const UP_ISSUER = "http://issuer:3000";
6
+ export declare const DEV_AUDIENCE = "localhost";
7
+ export type IssuerSettings = {
8
+ issuer: string;
9
+ audience: string;
10
+ };
6
11
  declare const TokenRequestSchema: z.ZodObject<{
7
12
  scope: z.ZodString;
8
13
  sub: z.ZodOptional<z.ZodString>;
9
14
  ttlSeconds: z.ZodOptional<z.ZodInt>;
15
+ aud: z.ZodOptional<z.ZodString>;
10
16
  }, z.core.$strict>;
11
17
  interface Keys {
12
18
  kid: string;
@@ -14,9 +20,9 @@ interface Keys {
14
20
  jwk: Record<string, unknown>;
15
21
  }
16
22
  export declare const newKeys: () => Keys;
17
- export declare const mint: (keys: Keys, request: z.infer<typeof TokenRequestSchema>, now?: number) => {
23
+ export declare const mint: (keys: Keys, request: z.infer<typeof TokenRequestSchema>, settings: IssuerSettings, now?: number) => {
18
24
  token: string;
19
25
  expiresAt: string;
20
26
  };
21
- export declare const createIssuer: (keys?: Keys) => Server;
27
+ export declare const createIssuer: (settings?: IssuerSettings, keys?: Keys) => Server;
22
28
  export {};