@agentage/extension 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -5
- package/dist/agcid/agcid.d.ts +1 -0
- package/dist/agcid/agcid.js +2 -2
- package/dist/agcid/current.d.ts +7 -0
- package/dist/agcid/current.js +4 -0
- package/dist/agcid/scope.d.ts +1 -1
- package/dist/agcid/scope.js +3 -4
- package/dist/command/args.d.ts +14 -0
- package/dist/command/args.js +60 -0
- package/dist/command/check-cli.d.ts +1 -0
- package/dist/command/check-cli.js +24 -0
- package/dist/command/command.types.d.ts +14 -0
- package/dist/command/command.types.js +2 -0
- package/dist/command/out.d.ts +19 -0
- package/dist/command/out.js +32 -0
- package/dist/command/run-command.d.ts +15 -0
- package/dist/command/run-command.js +53 -0
- package/dist/context/context.d.ts +8 -14
- package/dist/context/context.js +19 -21
- package/dist/context/settings.d.ts +5 -0
- package/dist/context/settings.js +32 -0
- package/dist/context/test-context.d.ts +44 -0
- package/dist/context/test-context.js +72 -0
- package/dist/discovery/discovery.d.ts +12 -1
- package/dist/discovery/discovery.js +21 -27
- package/dist/discovery/discovery.schema.d.ts +2 -26
- package/dist/discovery/discovery.schema.js +6 -4
- package/dist/discovery/openapi.d.ts +3 -0
- package/dist/discovery/openapi.js +19 -0
- package/dist/envelope/envelope.js +1 -0
- package/dist/envelope/envelope.schema.d.ts +0 -4
- package/dist/envelope/envelope.schema.js +3 -2
- package/dist/health/health.d.ts +5 -2
- package/dist/health/health.js +23 -8
- package/dist/health/health.schema.d.ts +2 -9
- package/dist/health/health.schema.js +3 -3
- package/dist/index.d.ts +19 -12
- package/dist/index.js +19 -12
- package/dist/log/log.d.ts +1 -0
- package/dist/log/log.js +7 -8
- package/dist/manifest/manifest.schema.d.ts +5 -75
- package/dist/manifest/manifest.schema.js +8 -4
- package/dist/manifest/primitives.schema.d.ts +1 -0
- package/dist/manifest/primitives.schema.js +3 -1
- package/dist/manifest/read-manifest.d.ts +1 -0
- package/dist/manifest/read-manifest.js +26 -3
- package/dist/mcp/mcp-app.d.ts +8 -0
- package/dist/mcp/mcp-app.js +74 -0
- package/dist/mcp/server.d.ts +8 -0
- package/dist/mcp/server.js +36 -0
- package/dist/mcp/snapshot.d.ts +11 -0
- package/dist/mcp/snapshot.js +69 -0
- package/dist/mcp/tools.d.ts +24 -0
- package/dist/mcp/tools.js +17 -0
- package/dist/pages/pages-app.d.ts +8 -0
- package/dist/pages/pages-app.js +29 -0
- package/dist/pages/pages.d.ts +4 -0
- package/dist/pages/pages.js +16 -0
- package/dist/runtime/build-info.d.ts +1 -0
- package/dist/runtime/build-info.js +4 -4
- package/dist/runtime/main.js +5 -2
- package/dist/runtime/part-runners.d.ts +20 -0
- package/dist/runtime/part-runners.js +68 -0
- package/dist/runtime/run-part.d.ts +4 -2
- package/dist/runtime/run-part.js +47 -34
- package/dist/server/app.d.ts +16 -2
- package/dist/server/app.js +28 -12
- package/dist/server/errors.d.ts +0 -2
- package/dist/server/errors.js +1 -1
- package/dist/server/serve.d.ts +0 -1
- package/dist/server/serve.js +2 -1
- package/dist/store/migrate.d.ts +2 -0
- package/dist/store/migrate.js +18 -0
- package/dist/store/migrations.d.ts +10 -0
- package/dist/store/migrations.js +55 -0
- package/dist/store/store.d.ts +10 -0
- package/dist/store/store.js +25 -0
- package/dist/store/store.types.d.ts +7 -0
- package/dist/store/store.types.js +1 -0
- package/dist/store/test-store.d.ts +5 -0
- package/dist/store/test-store.js +39 -0
- package/dist/testing/test-server.d.ts +6 -0
- package/dist/testing/test-server.js +31 -9
- package/dist/tool/build-info.js +2 -3
- package/dist/tool/build.d.ts +5 -2
- package/dist/tool/build.js +16 -24
- package/dist/tool/docker.d.ts +0 -1
- package/dist/tool/docker.js +2 -1
- package/dist/tool/exec.d.ts +4 -1
- package/dist/tool/exec.js +13 -5
- package/dist/tool/index.d.ts +9 -10
- package/dist/tool/index.js +9 -10
- package/dist/tool/kit-version.d.ts +1 -0
- package/dist/tool/kit-version.js +3 -0
- package/dist/tool/main.d.ts +6 -1
- package/dist/tool/main.js +58 -30
- package/dist/tool/recipe.d.ts +0 -1
- package/dist/tool/recipe.js +7 -3
- package/dist/tool/record-extras.d.ts +4 -0
- package/dist/tool/record-extras.js +60 -0
- package/dist/tool/stage.d.ts +3 -0
- package/dist/tool/stage.js +45 -12
- package/dist/tool/up/compose-run.d.ts +14 -0
- package/dist/tool/up/compose-run.js +24 -0
- package/dist/tool/up/compose.d.ts +18 -0
- package/dist/tool/up/compose.js +77 -0
- package/dist/tool/up/dev-issuer-app.d.ts +22 -0
- package/dist/tool/up/dev-issuer-app.js +80 -0
- package/dist/tool/up/dev-issuer.d.ts +2 -0
- package/dist/tool/up/dev-issuer.js +5 -0
- package/dist/tool/up/down.d.ts +12 -0
- package/dist/tool/up/down.js +38 -0
- package/dist/tool/up/gateway.d.ts +27 -0
- package/dist/tool/up/gateway.js +66 -0
- package/dist/tool/up/lines.d.ts +2 -0
- package/dist/tool/up/lines.js +3 -0
- package/dist/tool/up/local-record.d.ts +44 -0
- package/dist/tool/up/local-record.js +28 -0
- package/dist/tool/up/token.d.ts +6 -0
- package/dist/tool/up/token.js +18 -0
- package/dist/tool/up/up.d.ts +19 -0
- package/dist/tool/up/up.js +81 -0
- package/dist/tool/up/wait.d.ts +8 -0
- package/dist/tool/up/wait.js +24 -0
- package/dist/ui/ui-app.d.ts +6 -0
- package/dist/ui/ui-app.js +26 -0
- package/dist/worker/cron.d.ts +11 -0
- package/dist/worker/cron.js +73 -0
- package/dist/worker/job-options.d.ts +9 -0
- package/dist/worker/job-options.js +17 -0
- package/dist/worker/run-log.d.ts +9 -0
- package/dist/worker/run-log.js +38 -0
- package/dist/worker/runs.d.ts +20 -0
- package/dist/worker/runs.js +59 -0
- package/dist/worker/scheduler.d.ts +20 -0
- package/dist/worker/scheduler.js +97 -0
- package/dist/worker/worker.types.d.ts +5 -0
- package/dist/worker/worker.types.js +2 -0
- package/package.json +11 -2
- package/schemas/extension-discovery.schema.json +5 -0
- package/schemas/extension-package.schema.json +3 -0
- package/schemas/extension-record.schema.json +5 -0
- package/schemas/health.schema.json +1 -1
package/dist/tool/main.d.ts
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { type BuildOptions, type BuildResult } from './build.js';
|
|
2
|
+
import { type DownOptions } from './up/down.js';
|
|
3
|
+
import { type TokenOptions } from './up/token.js';
|
|
4
|
+
import { type UpOptions } from './up/up.js';
|
|
2
5
|
export interface MainDeps {
|
|
3
6
|
build: (options: BuildOptions) => Promise<BuildResult>;
|
|
7
|
+
up: (options: UpOptions) => Promise<unknown>;
|
|
8
|
+
down: (options: DownOptions) => Promise<unknown>;
|
|
9
|
+
token: (options: TokenOptions) => Promise<string>;
|
|
4
10
|
out: (line: string) => void;
|
|
5
11
|
err: (line: string) => void;
|
|
6
12
|
}
|
|
7
|
-
export declare const USAGE = "usage: agentage-extension build [--push] [--dir <path>] [--registry <host/owner>]\n\n build one image per server part from the kit recipe\n --push push the images and write dist/agentage-extension.json with their digests\n --dir the extension package folder (default: the current folder)\n --registry image registry and owner (default: ghcr.io/agentage)";
|
|
8
13
|
export declare const main: (argv: string[], deps?: MainDeps) => Promise<number>;
|
package/dist/tool/main.js
CHANGED
|
@@ -1,28 +1,64 @@
|
|
|
1
1
|
import { parseArgs } from 'node:util';
|
|
2
2
|
import { build } from './build.js';
|
|
3
3
|
import { buildLines } from './format.js';
|
|
4
|
-
|
|
4
|
+
import { down } from './up/down.js';
|
|
5
|
+
import { token } from './up/token.js';
|
|
6
|
+
import { up } from './up/up.js';
|
|
7
|
+
const USAGE = `usage: agentage-extension <build|up|down|token> [options]
|
|
5
8
|
|
|
6
|
-
build one image per
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
build one image per part from the kit recipe
|
|
10
|
+
--push push the images and write dist/agentage-extension.json with their digests
|
|
11
|
+
--registry image registry and owner (default: ghcr.io/agentage)
|
|
12
|
+
up the release images on Docker: store, dev issuer, gateway on localhost:4280
|
|
13
|
+
down remove what up started; --keep-data keeps the store volume
|
|
14
|
+
token a dev token: --scope <id>:<verb> [--issuer <url>]
|
|
15
|
+
--dir the extension package folder (default: the current folder)`;
|
|
10
16
|
const defaultDeps = {
|
|
11
17
|
build: (options) => build(options),
|
|
18
|
+
up: (options) => up(options),
|
|
19
|
+
down: (options) => down(options),
|
|
20
|
+
token: (options) => token(options),
|
|
12
21
|
out: (line) => void process.stdout.write(`${line}\n`),
|
|
13
22
|
err: (line) => void process.stderr.write(`${line}\n`),
|
|
14
23
|
};
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
24
|
+
const OPTIONS = {
|
|
25
|
+
push: { type: 'boolean' },
|
|
26
|
+
dir: { type: 'string' },
|
|
27
|
+
registry: { type: 'string' },
|
|
28
|
+
'keep-data': { type: 'boolean' },
|
|
29
|
+
scope: { type: 'string' },
|
|
30
|
+
issuer: { type: 'string' },
|
|
31
|
+
help: { type: 'boolean', short: 'h' },
|
|
32
|
+
};
|
|
33
|
+
const parse = (argv) => parseArgs({ args: argv, allowPositionals: true, options: OPTIONS });
|
|
34
|
+
class UsageError extends Error {
|
|
35
|
+
}
|
|
36
|
+
const COMMANDS = {
|
|
37
|
+
build: async (v, deps) => {
|
|
38
|
+
for (const line of buildLines(await deps.build({ push: v.push, dir: v.dir, registry: v.registry })))
|
|
39
|
+
deps.out(line);
|
|
23
40
|
},
|
|
24
|
-
})
|
|
25
|
-
|
|
41
|
+
up: async (v, deps) => void (await deps.up({ dir: v.dir, onLine: deps.out })),
|
|
42
|
+
down: async (v, deps) => void (await deps.down({ dir: v.dir, keepData: v['keep-data'], onLine: deps.out })),
|
|
43
|
+
token: async (v, deps) => {
|
|
44
|
+
if (!v.scope)
|
|
45
|
+
throw new UsageError('token needs --scope <id>:<verb>');
|
|
46
|
+
deps.out(await deps.token({ scope: v.scope, issuer: v.issuer }));
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
const runOne = async (name, values, deps) => {
|
|
50
|
+
try {
|
|
51
|
+
await COMMANDS[name]?.(values, deps);
|
|
52
|
+
return 0;
|
|
53
|
+
}
|
|
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;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
// Exit codes: 0 ok, 1 the step failed, 2 usage.
|
|
26
62
|
export const main = async (argv, deps = defaultDeps) => {
|
|
27
63
|
let parsed;
|
|
28
64
|
try {
|
|
@@ -33,22 +69,14 @@ export const main = async (argv, deps = defaultDeps) => {
|
|
|
33
69
|
return 2;
|
|
34
70
|
}
|
|
35
71
|
const { positionals, values } = parsed;
|
|
36
|
-
if (values.help)
|
|
37
|
-
|
|
38
|
-
if (positionals[0] !== 'build' || positionals.length !== 1)
|
|
39
|
-
return (deps.err(USAGE), 2);
|
|
40
|
-
try {
|
|
41
|
-
const result = await deps.build({
|
|
42
|
-
push: values.push,
|
|
43
|
-
dir: values.dir,
|
|
44
|
-
registry: values.registry,
|
|
45
|
-
});
|
|
46
|
-
for (const line of buildLines(result))
|
|
47
|
-
deps.out(line);
|
|
72
|
+
if (values.help) {
|
|
73
|
+
deps.out(USAGE);
|
|
48
74
|
return 0;
|
|
49
75
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
76
|
+
const name = positionals[0] ?? '';
|
|
77
|
+
if (!(name in COMMANDS) || positionals.length !== 1) {
|
|
78
|
+
deps.err(USAGE);
|
|
79
|
+
return 2;
|
|
53
80
|
}
|
|
81
|
+
return runOne(name, values, deps);
|
|
54
82
|
};
|
package/dist/tool/recipe.d.ts
CHANGED
package/dist/tool/recipe.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import {} from '../manifest/primitives.schema.js';
|
|
2
|
-
|
|
2
|
+
const PART_PORT = 3000;
|
|
3
|
+
// 503 passes: a restart cannot fix a dependency outage, so only a dead or 404 part is unhealthy.
|
|
3
4
|
const HEALTH_PROBE = `fetch('http://127.0.0.1:${PART_PORT}/health')` +
|
|
4
5
|
'.then((r) => process.exit(r.status === 200 || r.status === 503 ? 0 : 1), () => process.exit(1))';
|
|
5
|
-
|
|
6
|
-
export const serverPartRecipe = (part) => [
|
|
6
|
+
const RECIPE = [
|
|
7
7
|
'# syntax=docker/dockerfile:1',
|
|
8
8
|
'# Generated by @agentage/extension build. Extension authors never write a Dockerfile.',
|
|
9
9
|
'FROM node:26-alpine AS deps',
|
|
@@ -27,6 +27,10 @@ export const serverPartRecipe = (part) => [
|
|
|
27
27
|
'USER node',
|
|
28
28
|
`EXPOSE ${PART_PORT}`,
|
|
29
29
|
`HEALTHCHECK --interval=10s --timeout=3s --start-period=15s --retries=3 CMD ["node", "-e", "${HEALTH_PROBE}"]`,
|
|
30
|
+
];
|
|
31
|
+
// The one recipe for a Node server part; the staged context holds deps/ (npm ci) and app/ (code).
|
|
32
|
+
export const serverPartRecipe = (part) => [
|
|
33
|
+
...RECIPE,
|
|
30
34
|
`CMD ["node", "node_modules/@agentage/extension/dist/runtime/main.js", "${part}"]`,
|
|
31
35
|
'',
|
|
32
36
|
].join('\n');
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type RecordExtras } from '../discovery/discovery.js';
|
|
2
|
+
import { type ExtensionPackage } from '../manifest/manifest.schema.js';
|
|
3
|
+
export declare const toolNames: (dir: string, pkg: ExtensionPackage) => Promise<string[]>;
|
|
4
|
+
export declare const recordExtras: (dir: string, pkg: ExtensionPackage) => Promise<RecordExtras>;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { pathToFileURL } from 'node:url';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { createContext } from '../context/context.js';
|
|
5
|
+
import {} from '../discovery/discovery.js';
|
|
6
|
+
import { OPENAPI_PATH, readOpenapi } from '../discovery/openapi.js';
|
|
7
|
+
import { createLog } from '../log/log.js';
|
|
8
|
+
import { readJsonIfExists } from '../manifest/read-manifest.js';
|
|
9
|
+
import {} from '../manifest/manifest.schema.js';
|
|
10
|
+
import { entryOf, normalizeParts } from '../manifest/parts.js';
|
|
11
|
+
import { createToolRegistry, prefixed } from '../mcp/tools.js';
|
|
12
|
+
import { sqlFiles } from '../store/migrations.js';
|
|
13
|
+
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);
|
|
17
|
+
const noStore = {
|
|
18
|
+
query: async () => {
|
|
19
|
+
throw new Error('the store is not open while the build lists tools');
|
|
20
|
+
},
|
|
21
|
+
};
|
|
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) => {
|
|
25
|
+
const registry = createToolRegistry();
|
|
26
|
+
const base = createContext({
|
|
27
|
+
pkg,
|
|
28
|
+
part: 'mcp',
|
|
29
|
+
origin: 'http://127.0.0.1',
|
|
30
|
+
commit: '0'.repeat(40),
|
|
31
|
+
log: silent,
|
|
32
|
+
store: noStore,
|
|
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));
|
|
39
|
+
};
|
|
40
|
+
const widgetsOf = async (dir) => {
|
|
41
|
+
const value = await readJsonIfExists(join(dir, 'dist', 'ui', 'widgets.json'));
|
|
42
|
+
return value === undefined ? undefined : WidgetsSchema.parse(value);
|
|
43
|
+
};
|
|
44
|
+
// What the code serves, for the record: tools, widgets and the migration count.
|
|
45
|
+
export const recordExtras = async (dir, pkg) => {
|
|
46
|
+
const { code, store } = normalizeParts(pkg.agentage.parts);
|
|
47
|
+
const extras = {};
|
|
48
|
+
if (code.mcp) {
|
|
49
|
+
const tools = await toolNames(dir, pkg);
|
|
50
|
+
if (tools.length > 0)
|
|
51
|
+
extras.tools = tools;
|
|
52
|
+
}
|
|
53
|
+
if (code.ui)
|
|
54
|
+
extras.widgets = await widgetsOf(dir);
|
|
55
|
+
if (code.api && (await readOpenapi(dir)))
|
|
56
|
+
extras.openapi = OPENAPI_PATH;
|
|
57
|
+
if (store === 'postgres')
|
|
58
|
+
extras.migrations = (await sqlFiles(join(dir, 'migrations'))).length;
|
|
59
|
+
return extras;
|
|
60
|
+
};
|
package/dist/tool/stage.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { type ExtensionPackage } from '../manifest/manifest.schema.js';
|
|
2
2
|
import { type ImagePartKind } from '../manifest/primitives.schema.js';
|
|
3
3
|
export declare const RECORD_FILE = "agentage-extension.json";
|
|
4
|
+
export declare const MIGRATE_SHIM = "// Generated by @agentage/extension build.\nimport '@agentage/extension/migrate';\n";
|
|
4
5
|
export declare const localTarballs: (pkg: ExtensionPackage) => string[];
|
|
6
|
+
export declare const appPaths: (dir: string, pkg: ExtensionPackage, part: ImagePartKind) => Promise<string[]>;
|
|
7
|
+
export declare const assertBuilt: (dir: string, pkg: ExtensionPackage, part: ImagePartKind) => Promise<void>;
|
|
5
8
|
export declare const stageContext: (dir: string, pkg: ExtensionPackage, part: ImagePartKind, tmpRoot?: string) => Promise<string>;
|
package/dist/tool/stage.js
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { access, cp, mkdir, mkdtemp, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { tmpdir } from 'node:os';
|
|
3
|
-
import {
|
|
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
6
|
import {} from '../manifest/primitives.schema.js';
|
|
7
|
+
import { OPENAPI_FILE } from '../discovery/openapi.js';
|
|
6
8
|
import { serverPartRecipe } from './recipe.js';
|
|
7
9
|
export const RECORD_FILE = 'agentage-extension.json';
|
|
10
|
+
// The api image's migrate entry: `node dist/store/migrate.js` in /app runs the kit's migration runner.
|
|
11
|
+
export const MIGRATE_SHIM = "// Generated by @agentage/extension build.\nimport '@agentage/extension/migrate';\n";
|
|
8
12
|
const dependencySpecs = (pkg) => {
|
|
9
13
|
const deps = pkg.dependencies;
|
|
10
14
|
if (typeof deps !== 'object' || deps === null)
|
|
@@ -21,32 +25,61 @@ export const localTarballs = (pkg) => dependencySpecs(pkg)
|
|
|
21
25
|
}
|
|
22
26
|
return path;
|
|
23
27
|
});
|
|
28
|
+
const exists = (path) => access(path).then(() => true, () => false);
|
|
24
29
|
const copyInto = async (from, to) => {
|
|
25
30
|
await mkdir(dirname(to), { recursive: true });
|
|
26
31
|
await cp(from, to, { recursive: true });
|
|
27
32
|
};
|
|
28
33
|
const copyDeps = async (dir, root, pkg) => {
|
|
29
|
-
await
|
|
34
|
+
if (!(await exists(join(dir, 'package-lock.json')))) {
|
|
30
35
|
throw new Error('package-lock.json is missing: the image installs with npm ci');
|
|
31
|
-
}
|
|
36
|
+
}
|
|
32
37
|
await copyInto(join(dir, 'package.json'), join(root, 'deps', 'package.json'));
|
|
33
38
|
await copyInto(join(dir, 'package-lock.json'), join(root, 'deps', 'package-lock.json'));
|
|
34
39
|
for (const path of localTarballs(pkg))
|
|
35
40
|
await copyInto(join(dir, path), join(root, 'deps', path));
|
|
36
41
|
};
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
// What a part image carries: its own dist/{part}, the shared dist/lib, and for api the store files.
|
|
43
|
+
export const appPaths = async (dir, pkg, part) => {
|
|
44
|
+
const paths = ['package.json', join('dist', part)];
|
|
45
|
+
if (part === 'api' && (await exists(join(dir, OPENAPI_FILE))))
|
|
46
|
+
paths.push(OPENAPI_FILE);
|
|
47
|
+
if (await exists(join(dir, 'dist', 'lib')))
|
|
48
|
+
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
|
+
}
|
|
55
|
+
return paths;
|
|
56
|
+
};
|
|
57
|
+
const copyApp = async (dir, root, pkg, part) => {
|
|
58
|
+
for (const path of await appPaths(dir, pkg, part))
|
|
59
|
+
await copyInto(join(dir, path), join(root, 'app', path));
|
|
60
|
+
if (part === 'api' && normalizeParts(pkg.agentage.parts).store === 'postgres') {
|
|
61
|
+
await mkdir(join(root, 'app', 'dist', 'store'), { recursive: true });
|
|
62
|
+
await writeFile(join(root, 'app', 'dist', 'store', 'migrate.js'), MIGRATE_SHIM);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
// ui is a bundle, not an entry: the widget bundler writes dist/ui/widgets.json beside it.
|
|
66
|
+
const builtFile = (pkg, part) => normalize(part === 'ui' ? './dist/ui/widgets.json' : entryOf(pkg, part));
|
|
67
|
+
// The image carries dist/ only, so an entry outside it, or never built, would not start.
|
|
68
|
+
export const assertBuilt = async (dir, pkg, part) => {
|
|
69
|
+
const file = builtFile(pkg, part);
|
|
70
|
+
if (!file.startsWith(`dist${sep}${part}${sep}`)) {
|
|
71
|
+
throw new Error(`the ${part} entry ${file} is not in dist/${part}/`);
|
|
72
|
+
}
|
|
73
|
+
if (!(await exists(join(dir, file)))) {
|
|
74
|
+
throw new Error(`${file} not found: the package build must write the ${part} part there`);
|
|
75
|
+
}
|
|
44
76
|
};
|
|
45
|
-
// A clean build context per part:
|
|
77
|
+
// A clean build context per part: the manifest, the lockfile, local tarballs and the part's files.
|
|
46
78
|
export const stageContext = async (dir, pkg, part, tmpRoot = tmpdir()) => {
|
|
79
|
+
await assertBuilt(dir, pkg, part);
|
|
47
80
|
const root = await mkdtemp(join(tmpRoot, `agentage-${pkg.agentage.id}-${part}-`));
|
|
48
81
|
await copyDeps(dir, root, pkg);
|
|
49
|
-
await copyApp(dir, root);
|
|
82
|
+
await copyApp(dir, root, pkg, part);
|
|
50
83
|
await writeFile(join(root, 'Dockerfile'), serverPartRecipe(part));
|
|
51
84
|
return root;
|
|
52
85
|
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type Run } from '../exec.js';
|
|
2
|
+
export interface ComposeTarget {
|
|
3
|
+
file?: string;
|
|
4
|
+
project: string;
|
|
5
|
+
}
|
|
6
|
+
export interface ServiceState {
|
|
7
|
+
service: string;
|
|
8
|
+
state: string;
|
|
9
|
+
health: string;
|
|
10
|
+
exitCode: number;
|
|
11
|
+
}
|
|
12
|
+
export declare const compose: (run: Run, target: ComposeTarget, args: string[]) => Promise<string>;
|
|
13
|
+
export declare const parsePs: (stdout: string) => ServiceState[];
|
|
14
|
+
export declare const states: (run: Run, target: ComposeTarget) => Promise<ServiceState[]>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { mustRun } from '../exec.js';
|
|
2
|
+
export const compose = (run, target, args) => mustRun(run, 'docker', [
|
|
3
|
+
'compose',
|
|
4
|
+
...(target.file ? ['-f', target.file] : []),
|
|
5
|
+
'-p',
|
|
6
|
+
target.project,
|
|
7
|
+
...args,
|
|
8
|
+
]);
|
|
9
|
+
// `compose ps --format json` prints one object per line (older releases: one array).
|
|
10
|
+
export const parsePs = (stdout) => {
|
|
11
|
+
const text = stdout.trim();
|
|
12
|
+
if (text === '')
|
|
13
|
+
return [];
|
|
14
|
+
const rows = text.startsWith('[')
|
|
15
|
+
? JSON.parse(text)
|
|
16
|
+
: text.split('\n').map((line) => JSON.parse(line));
|
|
17
|
+
return rows.map((row) => ({
|
|
18
|
+
service: row.Service ?? '',
|
|
19
|
+
state: row.State ?? '',
|
|
20
|
+
health: row.Health ?? '',
|
|
21
|
+
exitCode: row.ExitCode ?? 0,
|
|
22
|
+
}));
|
|
23
|
+
};
|
|
24
|
+
export const states = async (run, target) => parsePs(await compose(run, target, ['ps', '-a', '--format', 'json']));
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type ExtensionPackage } from '../../manifest/manifest.schema.js';
|
|
2
|
+
export declare const STORE_IMAGE = "postgres:18-alpine";
|
|
3
|
+
export interface ComposeInput {
|
|
4
|
+
pkg: ExtensionPackage;
|
|
5
|
+
tags: Record<string, string>;
|
|
6
|
+
recordFile: string;
|
|
7
|
+
gatewayFile: string;
|
|
8
|
+
}
|
|
9
|
+
export declare const projectName: (pkg: ExtensionPackage) => string;
|
|
10
|
+
export declare const composeFile: (input: ComposeInput) => {
|
|
11
|
+
name: string;
|
|
12
|
+
services: Record<string, unknown>;
|
|
13
|
+
volumes: {
|
|
14
|
+
store: {};
|
|
15
|
+
} | {
|
|
16
|
+
store?: undefined;
|
|
17
|
+
};
|
|
18
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import {} from '../../manifest/manifest.schema.js';
|
|
2
|
+
import { imageParts, normalizeParts } from '../../manifest/parts.js';
|
|
3
|
+
import {} from '../../manifest/primitives.schema.js';
|
|
4
|
+
import { GATEWAY_PORT, ISSUER_PORT, PART_PORT, TRAEFIK_COMMAND, TRAEFIK_IMAGE } from './gateway.js';
|
|
5
|
+
export const STORE_IMAGE = 'postgres:18-alpine';
|
|
6
|
+
const RECORD_TARGET = '/run/agentage/agentage-extension.json';
|
|
7
|
+
const ISSUER_ENTRY = 'node_modules/@agentage/extension/dist/tool/up/dev-issuer.js';
|
|
8
|
+
export const projectName = (pkg) => `agentage-${pkg.agentage.id}`;
|
|
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';
|
|
12
|
+
const partEnv = (pkg, part) => ({
|
|
13
|
+
AGENTAGE_ORIGIN: `http://localhost:${GATEWAY_PORT}`,
|
|
14
|
+
AGENTAGE_RECORD_FILE: RECORD_TARGET,
|
|
15
|
+
...(hasStore(pkg) && part !== 'ui' ? { AGENTAGE_STORE_URL: storeUrl(pkg.agentage.id) } : {}),
|
|
16
|
+
});
|
|
17
|
+
const storeService = (id) => ({
|
|
18
|
+
image: STORE_IMAGE,
|
|
19
|
+
environment: { POSTGRES_USER: id, POSTGRES_PASSWORD: 'agentage-local', POSTGRES_DB: id },
|
|
20
|
+
volumes: ['store:/var/lib/postgresql'],
|
|
21
|
+
healthcheck: {
|
|
22
|
+
test: ['CMD-SHELL', `pg_isready -U ${id} -d ${id}`],
|
|
23
|
+
interval: '2s',
|
|
24
|
+
timeout: '3s',
|
|
25
|
+
retries: 30,
|
|
26
|
+
},
|
|
27
|
+
});
|
|
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
|
+
: {}),
|
|
35
|
+
});
|
|
36
|
+
const fixedServices = (input, anyImage) => ({
|
|
37
|
+
gateway: {
|
|
38
|
+
image: TRAEFIK_IMAGE,
|
|
39
|
+
command: TRAEFIK_COMMAND,
|
|
40
|
+
ports: [`127.0.0.1:${GATEWAY_PORT}:80`],
|
|
41
|
+
volumes: [`${input.gatewayFile}:/etc/traefik/dynamic.yml:ro`],
|
|
42
|
+
healthcheck: {
|
|
43
|
+
test: ['CMD', 'wget', '-qO-', 'http://127.0.0.1:8082/ping'],
|
|
44
|
+
interval: '2s',
|
|
45
|
+
timeout: '3s',
|
|
46
|
+
retries: 30,
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
issuer: {
|
|
50
|
+
image: anyImage,
|
|
51
|
+
command: ['node', ISSUER_ENTRY],
|
|
52
|
+
ports: [`127.0.0.1:${ISSUER_PORT}:${PART_PORT}`],
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
const migrateService = (input) => ({
|
|
56
|
+
image: input.tags['api'],
|
|
57
|
+
command: ['node', 'dist/store/migrate.js'],
|
|
58
|
+
environment: partEnv(input.pkg, 'migrate'),
|
|
59
|
+
depends_on: { store: { condition: 'service_healthy' } },
|
|
60
|
+
profiles: ['jobs'],
|
|
61
|
+
restart: 'no',
|
|
62
|
+
});
|
|
63
|
+
// One compose file: gateway, dev issuer, the store and its migrate job, one service per part.
|
|
64
|
+
export const composeFile = (input) => {
|
|
65
|
+
const parts = imageParts(input.pkg);
|
|
66
|
+
const services = {};
|
|
67
|
+
for (const part of parts)
|
|
68
|
+
services[part] = partService(input, part);
|
|
69
|
+
Object.assign(services, fixedServices(input, input.tags[parts[0] ?? 'api'] ?? ''));
|
|
70
|
+
if (hasStore(input.pkg)) {
|
|
71
|
+
services['store'] = storeService(input.pkg.agentage.id);
|
|
72
|
+
if (input.tags['api'])
|
|
73
|
+
services['migrate'] = migrateService(input);
|
|
74
|
+
}
|
|
75
|
+
const volumes = hasStore(input.pkg) ? { store: {} } : {};
|
|
76
|
+
return { name: projectName(input.pkg), services, volumes };
|
|
77
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type KeyObject } from 'node:crypto';
|
|
2
|
+
import { type Server } from 'node:http';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
export declare const DEV_ISSUER = "http://localhost:4281";
|
|
5
|
+
export declare const DEV_AUDIENCE = "*.agentage.io";
|
|
6
|
+
declare const TokenRequestSchema: z.ZodObject<{
|
|
7
|
+
scope: z.ZodString;
|
|
8
|
+
sub: z.ZodOptional<z.ZodString>;
|
|
9
|
+
ttlSeconds: z.ZodOptional<z.ZodInt>;
|
|
10
|
+
}, z.core.$strict>;
|
|
11
|
+
interface Keys {
|
|
12
|
+
kid: string;
|
|
13
|
+
privateKey: KeyObject;
|
|
14
|
+
jwk: Record<string, unknown>;
|
|
15
|
+
}
|
|
16
|
+
export declare const newKeys: () => Keys;
|
|
17
|
+
export declare const mint: (keys: Keys, request: z.infer<typeof TokenRequestSchema>, now?: number) => {
|
|
18
|
+
token: string;
|
|
19
|
+
expiresAt: string;
|
|
20
|
+
};
|
|
21
|
+
export declare const createIssuer: (keys?: Keys) => Server;
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { generateKeyPairSync, randomUUID, sign } from 'node:crypto';
|
|
2
|
+
import { createServer } from 'node:http';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
export const DEV_ISSUER = 'http://localhost:4281';
|
|
5
|
+
export const DEV_AUDIENCE = '*.agentage.io';
|
|
6
|
+
const TokenRequestSchema = z.strictObject({
|
|
7
|
+
scope: z
|
|
8
|
+
.string()
|
|
9
|
+
.max(512)
|
|
10
|
+
.regex(/^[a-z][a-z0-9-]*:(read|write|admin)( [a-z][a-z0-9-]*:(read|write|admin))*$/),
|
|
11
|
+
sub: z
|
|
12
|
+
.string()
|
|
13
|
+
.regex(/^[A-Za-z0-9_-]{1,64}$/)
|
|
14
|
+
.optional(),
|
|
15
|
+
ttlSeconds: z.int().min(60).max(86_400).optional(),
|
|
16
|
+
});
|
|
17
|
+
export const newKeys = () => {
|
|
18
|
+
const { privateKey, publicKey } = generateKeyPairSync('ed25519');
|
|
19
|
+
const kid = `dev-${randomUUID().slice(0, 8)}`;
|
|
20
|
+
return {
|
|
21
|
+
kid,
|
|
22
|
+
privateKey,
|
|
23
|
+
jwk: { ...publicKey.export({ format: 'jwk' }), kid, alg: 'EdDSA', use: 'sig' },
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
const b64 = (value) => Buffer.from(JSON.stringify(value)).toString('base64url');
|
|
27
|
+
// The api-standards token shape: aga_<jwt>, typ at+jwt, EdDSA, scope {id}:{verb}, a jti.
|
|
28
|
+
export const mint = (keys, request, now = Date.now()) => {
|
|
29
|
+
const iat = Math.floor(now / 1000);
|
|
30
|
+
const exp = iat + (request.ttlSeconds ?? 3600);
|
|
31
|
+
const claims = {
|
|
32
|
+
iss: DEV_ISSUER,
|
|
33
|
+
sub: request.sub ?? 'usr_dev',
|
|
34
|
+
aud: DEV_AUDIENCE,
|
|
35
|
+
scope: request.scope,
|
|
36
|
+
iat,
|
|
37
|
+
exp,
|
|
38
|
+
jti: `tok_${randomUUID()}`,
|
|
39
|
+
};
|
|
40
|
+
const input = `${b64({ alg: 'EdDSA', typ: 'at+jwt', kid: keys.kid })}.${b64(claims)}`;
|
|
41
|
+
const signature = sign(null, Buffer.from(input), keys.privateKey).toString('base64url');
|
|
42
|
+
return { token: `aga_${input}.${signature}`, expiresAt: new Date(exp * 1000).toISOString() };
|
|
43
|
+
};
|
|
44
|
+
const reply = (res, status, body) => {
|
|
45
|
+
res.writeHead(status, {
|
|
46
|
+
'content-type': 'application/json',
|
|
47
|
+
'cache-control': 'no-store',
|
|
48
|
+
'access-control-allow-origin': '*',
|
|
49
|
+
'access-control-allow-headers': 'content-type',
|
|
50
|
+
'access-control-allow-methods': 'GET, POST, OPTIONS',
|
|
51
|
+
});
|
|
52
|
+
res.end(body === undefined ? undefined : JSON.stringify(body));
|
|
53
|
+
};
|
|
54
|
+
const readBody = async (req) => {
|
|
55
|
+
let text = '';
|
|
56
|
+
for await (const chunk of req)
|
|
57
|
+
text += String(chunk);
|
|
58
|
+
if (text.length > 4096)
|
|
59
|
+
throw new Error('body too large');
|
|
60
|
+
return JSON.parse(text || '{}');
|
|
61
|
+
};
|
|
62
|
+
const tokenRoute = async (keys, req, res) => {
|
|
63
|
+
const parsed = TokenRequestSchema.safeParse(await readBody(req).catch(() => null));
|
|
64
|
+
if (!parsed.success)
|
|
65
|
+
return reply(res, 400, { error: 'scope must be {id}:{read|write|admin}, space-separated' });
|
|
66
|
+
reply(res, 200, mint(keys, parsed.data));
|
|
67
|
+
};
|
|
68
|
+
// Mints only: the kit does not verify tokens yet. Never part of a deployed estate.
|
|
69
|
+
export const createIssuer = (keys = newKeys()) => createServer((req, res) => {
|
|
70
|
+
const path = (req.url ?? '/').split('?')[0];
|
|
71
|
+
if (req.method === 'OPTIONS')
|
|
72
|
+
return reply(res, 204, undefined);
|
|
73
|
+
if (req.method === 'GET' && path === '/health')
|
|
74
|
+
return reply(res, 200, { status: 'ok' });
|
|
75
|
+
if (req.method === 'GET' && path === '/.well-known/jwks.json')
|
|
76
|
+
return reply(res, 200, { keys: [keys.jwk] });
|
|
77
|
+
if (req.method === 'POST' && path === '/token')
|
|
78
|
+
return void tokenRoute(keys, req, res);
|
|
79
|
+
reply(res, 404, { error: 'not found' });
|
|
80
|
+
});
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createIssuer } from './dev-issuer-app.js';
|
|
3
|
+
// The dev issuer of `up`: run from a part image, on the image's port so its HEALTHCHECK answers.
|
|
4
|
+
const port = Number(process.env['PORT'] ?? 3000);
|
|
5
|
+
createIssuer().listen(port, () => process.stderr.write(`dev issuer on :${port}\n`));
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type Run } from '../exec.js';
|
|
2
|
+
export interface DownOptions {
|
|
3
|
+
dir?: string;
|
|
4
|
+
keepData?: boolean;
|
|
5
|
+
onLine?: (line: string) => void;
|
|
6
|
+
}
|
|
7
|
+
export interface DownResult {
|
|
8
|
+
containers: number;
|
|
9
|
+
networks: number;
|
|
10
|
+
volumes: number;
|
|
11
|
+
}
|
|
12
|
+
export declare const down: (options?: DownOptions, run?: Run) => Promise<DownResult>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { access } from 'node:fs/promises';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
|
+
import { readManifest } from '../../manifest/read-manifest.js';
|
|
4
|
+
import { quietly, runCommand } from '../exec.js';
|
|
5
|
+
import { projectName } from './compose.js';
|
|
6
|
+
import { plural, stepLine } from './lines.js';
|
|
7
|
+
import { LOCAL_DIR } from './local-record.js';
|
|
8
|
+
const count = (log, kind) => log.split('\n').filter((line) => new RegExp(`\\b${kind}\\b.*\\bRemoved\\b`).test(line)).length;
|
|
9
|
+
// Everything `up` started goes; --keep-data keeps the store volume for the next `up`.
|
|
10
|
+
export const down = async (options = {}, run = runCommand) => {
|
|
11
|
+
const dir = resolve(options.dir ?? '.');
|
|
12
|
+
const pkg = await readManifest(dir);
|
|
13
|
+
const file = join(dir, LOCAL_DIR, 'compose.json');
|
|
14
|
+
const withFile = await access(file).then(() => ['-f', file], () => []);
|
|
15
|
+
const args = [
|
|
16
|
+
'compose',
|
|
17
|
+
...withFile,
|
|
18
|
+
'-p',
|
|
19
|
+
projectName(pkg),
|
|
20
|
+
'--profile',
|
|
21
|
+
'jobs',
|
|
22
|
+
'down',
|
|
23
|
+
'--remove-orphans',
|
|
24
|
+
];
|
|
25
|
+
const result = await quietly(run)('docker', options.keepData ? args : [...args, '--volumes']);
|
|
26
|
+
if (result.code !== 0)
|
|
27
|
+
throw new Error(`docker compose down failed: ${(result.stderr ?? '').trim()}`);
|
|
28
|
+
const log = result.stderr ?? '';
|
|
29
|
+
const removed = {
|
|
30
|
+
containers: count(log, 'Container'),
|
|
31
|
+
networks: count(log, 'Network'),
|
|
32
|
+
volumes: count(log, 'Volume'),
|
|
33
|
+
};
|
|
34
|
+
const kept = options.keepData ? ' (the store volume kept)' : '';
|
|
35
|
+
const what = `${plural(removed.containers, 'container')} · ${plural(removed.networks, 'network')} · ${plural(removed.volumes, 'volume')}${kept}`;
|
|
36
|
+
options.onLine?.(stepLine('removed', what));
|
|
37
|
+
return removed;
|
|
38
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type ImagePartKind } from '../../manifest/primitives.schema.js';
|
|
2
|
+
export declare const GATEWAY_PORT = 4280;
|
|
3
|
+
export declare const ISSUER_PORT = 4281;
|
|
4
|
+
export declare const PART_PORT = 3000;
|
|
5
|
+
export declare const TRAEFIK_IMAGE = "traefik:v3.7.17@sha256:68ae5fe5b64ca166704b746a9e8afcd385e36b3f569a5691401bf652d57b3748";
|
|
6
|
+
export declare const gatewayConfig: (parts: ImagePartKind[], version: string) => {
|
|
7
|
+
http: {
|
|
8
|
+
middlewares: {
|
|
9
|
+
'gateway-headers': {
|
|
10
|
+
headers: {
|
|
11
|
+
contentTypeNosniff: boolean;
|
|
12
|
+
referrerPolicy: string;
|
|
13
|
+
frameDeny: boolean;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
services: Record<string, unknown>;
|
|
18
|
+
routers: Record<string, {
|
|
19
|
+
rule: string;
|
|
20
|
+
priority: number;
|
|
21
|
+
service: string;
|
|
22
|
+
entryPoints: string[];
|
|
23
|
+
middlewares: string[];
|
|
24
|
+
}>;
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
export declare const TRAEFIK_COMMAND: string[];
|