@agentage/extension 0.1.2 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -5
- package/dist/agcid/current.d.ts +7 -0
- package/dist/agcid/current.js +4 -0
- package/dist/agcid/scope.d.ts +0 -1
- package/dist/agcid/scope.js +2 -4
- package/dist/auth/route-access.d.ts +2 -0
- package/dist/auth/route-access.js +21 -0
- package/dist/auth/token-check.d.ts +10 -0
- package/dist/auth/token-check.js +18 -0
- 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 +19 -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 +16 -0
- package/dist/command/run-command.js +43 -0
- package/dist/context/context.d.ts +8 -14
- package/dist/context/context.js +20 -26
- 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 +75 -0
- package/dist/discovery/discovery.d.ts +12 -1
- package/dist/discovery/discovery.js +15 -6
- package/dist/discovery/discovery.schema.d.ts +7 -0
- package/dist/discovery/discovery.schema.js +4 -2
- package/dist/discovery/openapi.d.ts +3 -0
- package/dist/discovery/openapi.js +19 -0
- package/dist/health/health.schema.js +1 -1
- package/dist/index.d.ts +16 -3
- package/dist/index.js +16 -3
- package/dist/log/log.js +1 -1
- package/dist/manifest/entry.d.ts +5 -0
- package/dist/manifest/entry.js +29 -0
- package/dist/manifest/manifest.schema.d.ts +18 -0
- package/dist/manifest/manifest.schema.js +1 -0
- package/dist/manifest/parts.d.ts +1 -0
- package/dist/manifest/parts.js +1 -0
- package/dist/mcp/mcp-app.d.ts +8 -0
- package/dist/mcp/mcp-app.js +94 -0
- package/dist/mcp/server.d.ts +8 -0
- package/dist/mcp/server.js +38 -0
- package/dist/mcp/snapshot.d.ts +11 -0
- package/dist/mcp/snapshot.js +69 -0
- package/dist/mcp/tools.d.ts +26 -0
- package/dist/mcp/tools.js +21 -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/part-runners.d.ts +20 -0
- package/dist/runtime/part-runners.js +63 -0
- package/dist/runtime/run-part.d.ts +7 -3
- package/dist/runtime/run-part.js +67 -29
- package/dist/server/app.d.ts +20 -2
- package/dist/server/app.js +39 -11
- package/dist/store/migrate.d.ts +2 -0
- package/dist/store/migrate.js +22 -0
- package/dist/store/migrations.d.ts +12 -0
- package/dist/store/migrations.js +81 -0
- package/dist/store/store.d.ts +16 -0
- package/dist/store/store.js +55 -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.d.ts +4 -0
- package/dist/tool/build.js +21 -26
- package/dist/tool/check.d.ts +16 -0
- package/dist/tool/check.js +30 -0
- package/dist/tool/docker.js +3 -2
- package/dist/tool/exec.d.ts +5 -0
- package/dist/tool/exec.js +48 -9
- package/dist/tool/index.d.ts +8 -0
- package/dist/tool/index.js +8 -0
- package/dist/tool/kit-version.d.ts +1 -0
- package/dist/tool/kit-version.js +3 -0
- package/dist/tool/main.d.ts +6 -0
- package/dist/tool/main.js +60 -35
- package/dist/tool/record-extras.d.ts +4 -0
- package/dist/tool/record-extras.js +56 -0
- package/dist/tool/stage.d.ts +4 -0
- package/dist/tool/stage.js +39 -21
- package/dist/tool/timeouts.d.ts +10 -0
- package/dist/tool/timeouts.js +19 -0
- package/dist/tool/up/compose-run.d.ts +15 -0
- package/dist/tool/up/compose-run.js +25 -0
- package/dist/tool/up/compose.d.ts +19 -0
- package/dist/tool/up/compose.js +80 -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 +6 -0
- package/dist/tool/up/down.d.ts +13 -0
- package/dist/tool/up/down.js +47 -0
- package/dist/tool/up/gateway.d.ts +36 -0
- package/dist/tool/up/gateway.js +60 -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/ports.d.ts +2 -0
- package/dist/tool/up/ports.js +22 -0
- package/dist/tool/up/project.d.ts +4 -0
- package/dist/tool/up/project.js +20 -0
- package/dist/tool/up/store-line.d.ts +11 -0
- package/dist/tool/up/store-line.js +36 -0
- package/dist/tool/up/token.d.ts +6 -0
- package/dist/tool/up/token.js +25 -0
- package/dist/tool/up/up.d.ts +16 -0
- package/dist/tool/up/up.js +84 -0
- package/dist/tool/up/wait.d.ts +8 -0
- package/dist/tool/up/wait.js +26 -0
- package/dist/ui/ui-app.d.ts +8 -0
- package/dist/ui/ui-app.js +39 -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 +106 -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/stage.js
CHANGED
|
@@ -1,11 +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 } from '../manifest/parts.js';
|
|
5
|
+
import { entryOf, hasPostgres } from '../manifest/parts.js';
|
|
6
6
|
import {} from '../manifest/primitives.schema.js';
|
|
7
|
+
import { OPENAPI_FILE } from '../discovery/openapi.js';
|
|
7
8
|
import { serverPartRecipe } from './recipe.js';
|
|
8
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";
|
|
9
12
|
const dependencySpecs = (pkg) => {
|
|
10
13
|
const deps = pkg.dependencies;
|
|
11
14
|
if (typeof deps !== 'object' || deps === null)
|
|
@@ -22,44 +25,59 @@ export const localTarballs = (pkg) => dependencySpecs(pkg)
|
|
|
22
25
|
}
|
|
23
26
|
return path;
|
|
24
27
|
});
|
|
28
|
+
export const exists = (path) => access(path).then(() => true, () => false);
|
|
25
29
|
const copyInto = async (from, to) => {
|
|
26
30
|
await mkdir(dirname(to), { recursive: true });
|
|
27
31
|
await cp(from, to, { recursive: true });
|
|
28
32
|
};
|
|
29
33
|
const copyDeps = async (dir, root, pkg) => {
|
|
30
|
-
await
|
|
34
|
+
if (!(await exists(join(dir, 'package-lock.json')))) {
|
|
31
35
|
throw new Error('package-lock.json is missing: the image installs with npm ci');
|
|
32
|
-
}
|
|
36
|
+
}
|
|
33
37
|
await copyInto(join(dir, 'package.json'), join(root, 'deps', 'package.json'));
|
|
34
38
|
await copyInto(join(dir, 'package-lock.json'), join(root, 'deps', 'package-lock.json'));
|
|
35
39
|
for (const path of localTarballs(pkg))
|
|
36
40
|
await copyInto(join(dir, path), join(root, 'deps', path));
|
|
37
41
|
};
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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.
|
|
45
|
+
export const appPaths = async (dir, pkg, part) => {
|
|
46
|
+
const paths = ['package.json', join('dist', part)];
|
|
47
|
+
if (part === 'api' && (await exists(join(dir, OPENAPI_FILE))))
|
|
48
|
+
paths.push(OPENAPI_FILE);
|
|
49
|
+
if (await exists(join(dir, 'dist', 'lib')))
|
|
50
|
+
paths.push(join('dist', 'lib'));
|
|
51
|
+
if (migrates(pkg, part) && (await exists(join(dir, 'migrations'))))
|
|
52
|
+
paths.push('migrations');
|
|
53
|
+
return paths;
|
|
54
|
+
};
|
|
55
|
+
const copyApp = async (dir, root, pkg, part) => {
|
|
56
|
+
for (const path of await appPaths(dir, pkg, part))
|
|
57
|
+
await copyInto(join(dir, path), join(root, 'app', path));
|
|
58
|
+
if (migrates(pkg, part)) {
|
|
59
|
+
await mkdir(join(root, 'app', 'dist', 'store'), { recursive: true });
|
|
60
|
+
await writeFile(join(root, 'app', 'dist', 'store', 'migrate.js'), MIGRATE_SHIM);
|
|
61
|
+
}
|
|
44
62
|
};
|
|
63
|
+
// ui is a bundle, not an entry: the widget bundler writes dist/ui/widgets.json beside it.
|
|
64
|
+
const builtFile = (pkg, part) => normalize(part === 'ui' ? './dist/ui/widgets.json' : entryOf(pkg, part));
|
|
45
65
|
// The image carries dist/ only, so an entry outside it, or never built, would not start.
|
|
46
|
-
const
|
|
47
|
-
const
|
|
48
|
-
if (!
|
|
49
|
-
throw new Error(`the ${part} entry ${
|
|
50
|
-
try {
|
|
51
|
-
await access(join(dir, entry));
|
|
66
|
+
export const assertBuilt = async (dir, pkg, part) => {
|
|
67
|
+
const file = builtFile(pkg, part);
|
|
68
|
+
if (!file.startsWith(`dist${sep}${part}${sep}`)) {
|
|
69
|
+
throw new Error(`the ${part} entry ${file} is not in dist/${part}/`);
|
|
52
70
|
}
|
|
53
|
-
|
|
54
|
-
throw new Error(`${
|
|
71
|
+
if (!(await exists(join(dir, file)))) {
|
|
72
|
+
throw new Error(`${file} not found: the package build must write the ${part} part there`);
|
|
55
73
|
}
|
|
56
74
|
};
|
|
57
|
-
// A clean build context per part:
|
|
75
|
+
// A clean build context per part: the manifest, the lockfile, local tarballs and the part's files.
|
|
58
76
|
export const stageContext = async (dir, pkg, part, tmpRoot = tmpdir()) => {
|
|
59
|
-
await
|
|
77
|
+
await assertBuilt(dir, pkg, part);
|
|
60
78
|
const root = await mkdtemp(join(tmpRoot, `agentage-${pkg.agentage.id}-${part}-`));
|
|
61
79
|
await copyDeps(dir, root, pkg);
|
|
62
|
-
await copyApp(dir, root);
|
|
80
|
+
await copyApp(dir, root, pkg, part);
|
|
63
81
|
await writeFile(join(root, 'Dockerfile'), serverPartRecipe(part));
|
|
64
82
|
return root;
|
|
65
83
|
};
|
|
@@ -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;
|
|
@@ -0,0 +1,15 @@
|
|
|
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 composeArgs: (target: ComposeTarget, args: string[]) => string[];
|
|
13
|
+
export declare const compose: (run: Run, target: ComposeTarget, args: string[], timeoutMs?: number) => Promise<string>;
|
|
14
|
+
export declare const parsePs: (stdout: string) => ServiceState[];
|
|
15
|
+
export declare const states: (run: Run, target: ComposeTarget) => Promise<ServiceState[]>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { mustRun } from '../exec.js';
|
|
2
|
+
export const composeArgs = (target, args) => [
|
|
3
|
+
'compose',
|
|
4
|
+
...(target.file ? ['-f', target.file] : []),
|
|
5
|
+
'-p',
|
|
6
|
+
target.project,
|
|
7
|
+
...args,
|
|
8
|
+
];
|
|
9
|
+
export const compose = (run, target, args, timeoutMs) => mustRun(run, 'docker', composeArgs(target, args), { timeoutMs });
|
|
10
|
+
// `compose ps --format json` prints one object per line (older releases: one array).
|
|
11
|
+
export const parsePs = (stdout) => {
|
|
12
|
+
const text = stdout.trim();
|
|
13
|
+
if (text === '')
|
|
14
|
+
return [];
|
|
15
|
+
const rows = text.startsWith('[')
|
|
16
|
+
? JSON.parse(text)
|
|
17
|
+
: text.split('\n').map((line) => JSON.parse(line));
|
|
18
|
+
return rows.map((row) => ({
|
|
19
|
+
service: row.Service ?? '',
|
|
20
|
+
state: row.State ?? '',
|
|
21
|
+
health: row.Health ?? '',
|
|
22
|
+
exitCode: row.ExitCode ?? 0,
|
|
23
|
+
}));
|
|
24
|
+
};
|
|
25
|
+
export const states = async (run, target) => parsePs(await compose(run, target, ['ps', '-a', '--format', 'json']));
|
|
@@ -0,0 +1,19 @@
|
|
|
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
|
+
project: string;
|
|
6
|
+
tags: Record<string, string>;
|
|
7
|
+
recordFile: string;
|
|
8
|
+
gatewayFile: string;
|
|
9
|
+
seedsDir?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare const composeFile: (input: ComposeInput) => {
|
|
12
|
+
name: string;
|
|
13
|
+
services: Record<string, unknown>;
|
|
14
|
+
volumes: {
|
|
15
|
+
store: {};
|
|
16
|
+
} | {
|
|
17
|
+
store?: undefined;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import {} from '../../manifest/manifest.schema.js';
|
|
2
|
+
import { hasPostgres, imageParts } from '../../manifest/parts.js';
|
|
3
|
+
import {} from '../../manifest/primitives.schema.js';
|
|
4
|
+
import { DEV_ISSUER } from './dev-issuer-app.js';
|
|
5
|
+
import { GATEWAY_PORT, ISSUER_PORT, PART_PORT, TRAEFIK_COMMAND, TRAEFIK_IMAGE } from './gateway.js';
|
|
6
|
+
export const STORE_IMAGE = 'postgres:18-alpine';
|
|
7
|
+
const RECORD_TARGET = '/run/agentage/agentage-extension.json';
|
|
8
|
+
const ISSUER_ENTRY = 'node_modules/@agentage/extension/dist/tool/up/dev-issuer.js';
|
|
9
|
+
// A local-only credential: the store has no published port, only the parts' network reaches it.
|
|
10
|
+
const STORE_PASSWORD = 'agentage-local';
|
|
11
|
+
const storeUrl = (id) => `postgres://${id}:${STORE_PASSWORD}@store:5432/${id}`;
|
|
12
|
+
// Only the ui under `up` names the dev issuer: its CSP then lets the harness ask it for a token.
|
|
13
|
+
const partEnv = (pkg, part) => ({
|
|
14
|
+
AGENTAGE_ORIGIN: `http://localhost:${GATEWAY_PORT}`,
|
|
15
|
+
AGENTAGE_RECORD_FILE: RECORD_TARGET,
|
|
16
|
+
...(hasPostgres(pkg) && part !== 'ui' ? { AGENTAGE_STORE_URL: storeUrl(pkg.agentage.id) } : {}),
|
|
17
|
+
...(part === 'ui' ? { AGENTAGE_DEV_ISSUER: DEV_ISSUER } : {}),
|
|
18
|
+
});
|
|
19
|
+
const storeService = (id) => ({
|
|
20
|
+
image: STORE_IMAGE,
|
|
21
|
+
environment: { POSTGRES_USER: id, POSTGRES_PASSWORD: STORE_PASSWORD, POSTGRES_DB: id },
|
|
22
|
+
volumes: ['store:/var/lib/postgresql'],
|
|
23
|
+
healthcheck: {
|
|
24
|
+
test: ['CMD-SHELL', `pg_isready -U ${id} -d ${id}`],
|
|
25
|
+
interval: '2s',
|
|
26
|
+
timeout: '3s',
|
|
27
|
+
retries: 30,
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
const partService = (input, part) => ({
|
|
31
|
+
image: input.tags[part],
|
|
32
|
+
environment: partEnv(input.pkg, part),
|
|
33
|
+
volumes: [`${input.recordFile}:${RECORD_TARGET}:ro`],
|
|
34
|
+
...(hasPostgres(input.pkg) && part !== 'ui'
|
|
35
|
+
? { depends_on: { store: { condition: 'service_healthy' } } }
|
|
36
|
+
: {}),
|
|
37
|
+
});
|
|
38
|
+
const fixedServices = (input, anyImage) => ({
|
|
39
|
+
gateway: {
|
|
40
|
+
image: TRAEFIK_IMAGE,
|
|
41
|
+
command: TRAEFIK_COMMAND,
|
|
42
|
+
ports: [`127.0.0.1:${GATEWAY_PORT}:80`],
|
|
43
|
+
volumes: [`${input.gatewayFile}:/etc/traefik/dynamic.yml:ro`],
|
|
44
|
+
healthcheck: {
|
|
45
|
+
test: ['CMD', 'wget', '-qO-', 'http://127.0.0.1:8082/ping'],
|
|
46
|
+
interval: '2s',
|
|
47
|
+
timeout: '3s',
|
|
48
|
+
retries: 30,
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
issuer: {
|
|
52
|
+
image: anyImage,
|
|
53
|
+
command: ['node', ISSUER_ENTRY],
|
|
54
|
+
ports: [`127.0.0.1:${ISSUER_PORT}:${PART_PORT}`],
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
const migrateService = (input) => ({
|
|
58
|
+
image: input.tags['api'],
|
|
59
|
+
command: ['node', 'dist/store/migrate.js'],
|
|
60
|
+
environment: partEnv(input.pkg, 'migrate'),
|
|
61
|
+
...(input.seedsDir ? { volumes: [`${input.seedsDir}:/app/seeds:ro`] } : {}),
|
|
62
|
+
depends_on: { store: { condition: 'service_healthy' } },
|
|
63
|
+
profiles: ['jobs'],
|
|
64
|
+
restart: 'no',
|
|
65
|
+
});
|
|
66
|
+
// One compose file: gateway, dev issuer, the store and its migrate job, one service per part.
|
|
67
|
+
export const composeFile = (input) => {
|
|
68
|
+
const parts = imageParts(input.pkg);
|
|
69
|
+
const services = {};
|
|
70
|
+
for (const part of parts)
|
|
71
|
+
services[part] = partService(input, part);
|
|
72
|
+
Object.assign(services, fixedServices(input, input.tags[parts[0] ?? 'api'] ?? ''));
|
|
73
|
+
if (hasPostgres(input.pkg)) {
|
|
74
|
+
services['store'] = storeService(input.pkg.agentage.id);
|
|
75
|
+
if (input.tags['api'])
|
|
76
|
+
services['migrate'] = migrateService(input);
|
|
77
|
+
}
|
|
78
|
+
const volumes = hasPostgres(input.pkg) ? { store: {} } : {};
|
|
79
|
+
return { name: input.project, services, volumes };
|
|
80
|
+
};
|
|
@@ -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,6 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { portFromEnv } from '../../runtime/build-info.js';
|
|
3
|
+
import { createIssuer } from './dev-issuer-app.js';
|
|
4
|
+
// The dev issuer of `up`: run from a part image, on the image's port so its HEALTHCHECK answers.
|
|
5
|
+
const port = portFromEnv(process.env);
|
|
6
|
+
createIssuer().listen(port, () => process.stderr.write(`dev issuer on :${port}\n`));
|
|
@@ -0,0 +1,13 @@
|
|
|
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
|
+
project: string;
|
|
9
|
+
containers: number;
|
|
10
|
+
networks: number;
|
|
11
|
+
volumes: number;
|
|
12
|
+
}
|
|
13
|
+
export declare const down: (options?: DownOptions, run?: Run) => Promise<DownResult>;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { readManifest } from '../../manifest/read-manifest.js';
|
|
3
|
+
import { quietly, runCommand } from '../exec.js';
|
|
4
|
+
import { composeArgs } from './compose-run.js';
|
|
5
|
+
import { plural, stepLine } from './lines.js';
|
|
6
|
+
import { LOCAL_DIR } from './local-record.js';
|
|
7
|
+
import { legacyProject, projectName, renderedProject, workingCopy } from './project.js';
|
|
8
|
+
const count = (log, kind) => log.split('\n').filter((line) => new RegExp(`\\b${kind}\\b.*\\bRemoved\\b`).test(line)).length;
|
|
9
|
+
const removedOf = (log) => ({
|
|
10
|
+
containers: count(log, 'Container'),
|
|
11
|
+
networks: count(log, 'Network'),
|
|
12
|
+
volumes: count(log, 'Volume'),
|
|
13
|
+
});
|
|
14
|
+
const summary = (removed, keepData) => {
|
|
15
|
+
if (removed.containers + removed.networks + removed.volumes === 0)
|
|
16
|
+
return 'nothing was running';
|
|
17
|
+
const counts = [
|
|
18
|
+
plural(removed.containers, 'container'),
|
|
19
|
+
plural(removed.networks, 'network'),
|
|
20
|
+
plural(removed.volumes, 'volume'),
|
|
21
|
+
];
|
|
22
|
+
return counts.join(' · ') + (keepData ? ' (the store volume kept)' : '');
|
|
23
|
+
};
|
|
24
|
+
// This folder's own project; a copied compose.json naming another copy's project is ignored.
|
|
25
|
+
const targetOf = async (dir) => {
|
|
26
|
+
const { id } = (await readManifest(dir)).agentage;
|
|
27
|
+
const file = join(dir, LOCAL_DIR, 'compose.json');
|
|
28
|
+
const rendered = await renderedProject(file);
|
|
29
|
+
const project = projectName(id, dir);
|
|
30
|
+
if (rendered === project)
|
|
31
|
+
return { file, project };
|
|
32
|
+
return { project: rendered === legacyProject(id) ? rendered : project };
|
|
33
|
+
};
|
|
34
|
+
// Everything `up` started in this working copy goes, and only that; --keep-data keeps the store.
|
|
35
|
+
export const down = async (options = {}, run = runCommand) => {
|
|
36
|
+
const dir = await workingCopy(options.dir);
|
|
37
|
+
const target = await targetOf(dir);
|
|
38
|
+
const volumes = options.keepData ? [] : ['--volumes'];
|
|
39
|
+
const args = composeArgs(target, ['--profile', 'jobs', 'down', '--remove-orphans', ...volumes]);
|
|
40
|
+
const result = await quietly(run)('docker', args);
|
|
41
|
+
const log = result.stderr ?? '';
|
|
42
|
+
if (result.code !== 0)
|
|
43
|
+
throw new Error(`docker compose down failed: ${log.trim()}`);
|
|
44
|
+
const removed = removedOf(log);
|
|
45
|
+
options.onLine?.(stepLine('removed', summary(removed, options.keepData), target.project));
|
|
46
|
+
return { project: target.project, ...removed };
|
|
47
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
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 PART_PATHS: {
|
|
7
|
+
pages: string;
|
|
8
|
+
ui: string;
|
|
9
|
+
api: string;
|
|
10
|
+
mcp: string;
|
|
11
|
+
};
|
|
12
|
+
type RoutedPart = keyof typeof PART_PATHS;
|
|
13
|
+
export declare const partPath: (part: RoutedPart, version: string) => string;
|
|
14
|
+
export declare const gatewayConfig: (parts: ImagePartKind[], version: string) => {
|
|
15
|
+
http: {
|
|
16
|
+
middlewares: {
|
|
17
|
+
'gateway-headers': {
|
|
18
|
+
headers: {
|
|
19
|
+
contentTypeNosniff: boolean;
|
|
20
|
+
referrerPolicy: string;
|
|
21
|
+
frameDeny: boolean;
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
services: Record<string, unknown>;
|
|
26
|
+
routers: Record<string, {
|
|
27
|
+
rule: string;
|
|
28
|
+
priority: number;
|
|
29
|
+
service: string;
|
|
30
|
+
entryPoints: string[];
|
|
31
|
+
middlewares: string[];
|
|
32
|
+
}>;
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
export declare const TRAEFIK_COMMAND: string[];
|
|
36
|
+
export {};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import {} from '../../manifest/primitives.schema.js';
|
|
2
|
+
export const GATEWAY_PORT = 4280;
|
|
3
|
+
export const ISSUER_PORT = 4281;
|
|
4
|
+
export const PART_PORT = 3000;
|
|
5
|
+
export const TRAEFIK_IMAGE = 'traefik:v3.7.17@sha256:68ae5fe5b64ca166704b746a9e8afcd385e36b3f569a5691401bf652d57b3748';
|
|
6
|
+
// The URL layout of api-standards section 1, as the estate's gateway renders it.
|
|
7
|
+
const RESERVED = 'PathPrefix(`/api/`) || Path(`/mcp`) || PathPrefix(`/mcp/`) || PathPrefix(`/ui/`) || PathPrefix(`/.well-known/`) || PathPrefix(`/git/`) || Path(`/health`)';
|
|
8
|
+
// Each routed part's path, in the order `up` prints them; /mcp is exact, the rest are prefixes.
|
|
9
|
+
export const PART_PATHS = { pages: '/', ui: '/ui/{version}/', api: '/api/v1/', mcp: '/mcp' };
|
|
10
|
+
// In this order, so the first present serves /.well-known/ (api-standards section 1).
|
|
11
|
+
const ROUTED = ['api', 'mcp', 'pages', 'ui'];
|
|
12
|
+
export const partPath = (part, version) => PART_PATHS[part].replace('{version}', version);
|
|
13
|
+
const ruleOf = (part, version) => part === 'mcp' ? 'Path(`/mcp`)' : `PathPrefix(\`${partPath(part, version)}\`)`;
|
|
14
|
+
const router = (rule, priority, service) => ({
|
|
15
|
+
rule,
|
|
16
|
+
priority,
|
|
17
|
+
service,
|
|
18
|
+
entryPoints: ['web'],
|
|
19
|
+
middlewares: ['gateway-headers'],
|
|
20
|
+
});
|
|
21
|
+
const partRouters = (routed, version) => {
|
|
22
|
+
const routers = {};
|
|
23
|
+
for (const part of routed)
|
|
24
|
+
routers[part] = router(ruleOf(part, version), part === 'pages' ? 1 : 100, part);
|
|
25
|
+
if (routed[0])
|
|
26
|
+
routers['well-known'] = router('PathPrefix(`/.well-known/`)', 100, routed[0]);
|
|
27
|
+
return routers;
|
|
28
|
+
};
|
|
29
|
+
// Traefik's file provider: a reserved-prefix miss is Traefik's own 404, like the estate gateway.
|
|
30
|
+
export const gatewayConfig = (parts, version) => {
|
|
31
|
+
const routed = ROUTED.filter((part) => parts.includes(part));
|
|
32
|
+
const services = {
|
|
33
|
+
'gateway-miss': { loadBalancer: { servers: [{ url: 'http://127.0.0.1:8081' }] } },
|
|
34
|
+
};
|
|
35
|
+
for (const part of routed)
|
|
36
|
+
services[part] = { loadBalancer: { servers: [{ url: `http://${part}:${PART_PORT}` }] } };
|
|
37
|
+
const routers = {
|
|
38
|
+
reserved: router(RESERVED, 50, 'gateway-miss'),
|
|
39
|
+
...partRouters(routed, version),
|
|
40
|
+
};
|
|
41
|
+
if (!routed.includes('pages'))
|
|
42
|
+
routers['host'] = router('PathPrefix(`/`)', 1, 'gateway-miss');
|
|
43
|
+
const headers = {
|
|
44
|
+
contentTypeNosniff: true,
|
|
45
|
+
referrerPolicy: 'strict-origin-when-cross-origin',
|
|
46
|
+
frameDeny: true,
|
|
47
|
+
};
|
|
48
|
+
return { http: { middlewares: { 'gateway-headers': { headers } }, services, routers } };
|
|
49
|
+
};
|
|
50
|
+
export const TRAEFIK_COMMAND = [
|
|
51
|
+
'--global.checkNewVersion=false',
|
|
52
|
+
'--global.sendAnonymousUsage=false',
|
|
53
|
+
'--log.format=json',
|
|
54
|
+
'--providers.file.filename=/etc/traefik/dynamic.yml',
|
|
55
|
+
'--entryPoints.web.address=:80',
|
|
56
|
+
// No routers listen here, so every request it receives gets Traefik's own 404.
|
|
57
|
+
'--entryPoints.miss.address=:8081',
|
|
58
|
+
'--entryPoints.ping.address=127.0.0.1:8082',
|
|
59
|
+
'--ping.entryPoint=ping',
|
|
60
|
+
];
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
// What `up` and `down` print: a label, what it is, the detail; the columns line up as in a terminal.
|
|
2
|
+
export const stepLine = (label, what, detail = '') => `${label.padEnd(9)}${what.padEnd(Math.max(45, what.length + 2))}${detail}`.trimEnd();
|
|
3
|
+
export const plural = (n, word) => `${n} ${word}${n === 1 ? '' : 's'}`;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { type ExtensionRecord } from '../../discovery/discovery.schema.js';
|
|
2
|
+
import { type ExtensionPackage } from '../../manifest/manifest.schema.js';
|
|
3
|
+
import { type BuildResult } from '../build.js';
|
|
4
|
+
import { type Run } from '../exec.js';
|
|
5
|
+
export declare const LOCAL_DIR = ".agentage";
|
|
6
|
+
export declare const assertDiscoverable: (record: ExtensionRecord, origin: string) => void;
|
|
7
|
+
export declare const writeLocalRecord: (dir: string, pkg: ExtensionPackage, built: BuildResult, run: Run) => Promise<{
|
|
8
|
+
file: string;
|
|
9
|
+
record: {
|
|
10
|
+
id: string;
|
|
11
|
+
version: string;
|
|
12
|
+
commit: string;
|
|
13
|
+
buildTime: string;
|
|
14
|
+
parts: {
|
|
15
|
+
pages?: {
|
|
16
|
+
image: string;
|
|
17
|
+
} | undefined;
|
|
18
|
+
ui?: {
|
|
19
|
+
image: string;
|
|
20
|
+
} | undefined;
|
|
21
|
+
api?: {
|
|
22
|
+
image: string;
|
|
23
|
+
} | undefined;
|
|
24
|
+
mcp?: {
|
|
25
|
+
image: string;
|
|
26
|
+
} | undefined;
|
|
27
|
+
worker?: {
|
|
28
|
+
image: string;
|
|
29
|
+
} | undefined;
|
|
30
|
+
store?: {
|
|
31
|
+
kind: "postgres" | "git" | "signoz";
|
|
32
|
+
migrations?: number | undefined;
|
|
33
|
+
} | undefined;
|
|
34
|
+
};
|
|
35
|
+
openapi?: string | undefined;
|
|
36
|
+
tools?: string[] | undefined;
|
|
37
|
+
widgets?: {
|
|
38
|
+
name: string;
|
|
39
|
+
element: string;
|
|
40
|
+
module: string;
|
|
41
|
+
}[] | undefined;
|
|
42
|
+
commands?: string[] | undefined;
|
|
43
|
+
};
|
|
44
|
+
}>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { discoveryDocument } from '../../discovery/discovery.js';
|
|
4
|
+
import { DiscoveryDocumentSchema } from '../../discovery/discovery.schema.js';
|
|
5
|
+
import { formatIssues } from '../../manifest/read-manifest.js';
|
|
6
|
+
import {} from '../../manifest/manifest.schema.js';
|
|
7
|
+
import { recordFor } from '../build.js';
|
|
8
|
+
import { mustRun } from '../exec.js';
|
|
9
|
+
export const LOCAL_DIR = '.agentage';
|
|
10
|
+
// A local build has no registry digest; the image id from the daemon stands in for it (Q4).
|
|
11
|
+
const localImage = async (run, built) => {
|
|
12
|
+
const id = (await mustRun(run, 'docker', ['image', 'inspect', '-f', '{{.Id}}', built.tag])).trim();
|
|
13
|
+
const repository = built.tag.slice(0, built.tag.lastIndexOf(':'));
|
|
14
|
+
return { ...built, digest: id, image: `${repository}@${id}` };
|
|
15
|
+
};
|
|
16
|
+
export const assertDiscoverable = (record, origin) => {
|
|
17
|
+
const result = DiscoveryDocumentSchema.safeParse(discoveryDocument(record, origin));
|
|
18
|
+
if (!result.success)
|
|
19
|
+
throw new Error(`discovery would be invalid: ${formatIssues(result.error.issues)}`);
|
|
20
|
+
};
|
|
21
|
+
export const writeLocalRecord = async (dir, pkg, built, run) => {
|
|
22
|
+
const images = await Promise.all(built.images.map((image) => localImage(run, image)));
|
|
23
|
+
const record = await recordFor(dir, pkg, built, images);
|
|
24
|
+
await mkdir(join(dir, LOCAL_DIR), { recursive: true });
|
|
25
|
+
const file = join(dir, LOCAL_DIR, 'record.json');
|
|
26
|
+
await writeFile(file, `${JSON.stringify(record, null, 2)}\n`);
|
|
27
|
+
return { file, record };
|
|
28
|
+
};
|