@agentage/extension 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agcid/scope.d.ts +0 -1
- package/dist/agcid/scope.js +0 -1
- 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/check-cli.js +2 -7
- package/dist/command/run-command.d.ts +1 -0
- package/dist/command/run-command.js +5 -15
- package/dist/context/test-context.js +20 -17
- package/dist/discovery/discovery.schema.d.ts +5 -0
- package/dist/discovery/discovery.schema.js +1 -1
- package/dist/manifest/entry.d.ts +5 -0
- package/dist/manifest/entry.js +29 -0
- package/dist/manifest/parts.d.ts +1 -0
- package/dist/manifest/parts.js +1 -0
- package/dist/mcp/mcp-app.js +27 -7
- package/dist/mcp/server.js +3 -1
- package/dist/mcp/tools.d.ts +2 -0
- package/dist/mcp/tools.js +4 -0
- package/dist/runtime/part-runners.d.ts +5 -5
- package/dist/runtime/part-runners.js +19 -24
- package/dist/runtime/run-part.d.ts +4 -2
- package/dist/runtime/run-part.js +43 -27
- package/dist/server/app.d.ts +2 -0
- package/dist/server/app.js +12 -1
- package/dist/store/migrate.js +7 -3
- package/dist/store/migrations.d.ts +2 -0
- package/dist/store/migrations.js +32 -6
- package/dist/store/store.d.ts +7 -1
- package/dist/store/store.js +37 -7
- package/dist/tool/build.d.ts +0 -2
- package/dist/tool/build.js +5 -3
- 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 +1 -0
- package/dist/tool/exec.js +43 -16
- package/dist/tool/main.js +23 -24
- package/dist/tool/record-extras.js +7 -11
- package/dist/tool/stage.d.ts +1 -0
- package/dist/tool/stage.js +8 -10
- package/dist/tool/timeouts.d.ts +10 -0
- package/dist/tool/timeouts.js +19 -0
- package/dist/tool/up/compose-run.d.ts +2 -1
- package/dist/tool/up/compose-run.js +3 -2
- package/dist/tool/up/compose.d.ts +2 -1
- package/dist/tool/up/compose.js +13 -10
- package/dist/tool/up/dev-issuer.js +2 -1
- package/dist/tool/up/down.d.ts +1 -0
- package/dist/tool/up/down.js +39 -30
- package/dist/tool/up/gateway.d.ts +9 -0
- package/dist/tool/up/gateway.js +11 -17
- package/dist/tool/up/lines.js +1 -1
- 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.js +9 -2
- package/dist/tool/up/up.d.ts +3 -6
- package/dist/tool/up/up.js +49 -46
- package/dist/tool/up/wait.js +4 -2
- package/dist/ui/ui-app.d.ts +2 -0
- package/dist/ui/ui-app.js +18 -5
- package/dist/worker/runs.js +11 -11
- package/dist/worker/scheduler.js +14 -5
- package/package.json +1 -1
package/dist/server/app.js
CHANGED
|
@@ -2,6 +2,8 @@ import cors, {} from 'cors';
|
|
|
2
2
|
import express, {} from 'express';
|
|
3
3
|
import helmet, {} from 'helmet';
|
|
4
4
|
import { agcidMiddleware } from '../agcid/scope.js';
|
|
5
|
+
import { routeAccess } from '../auth/route-access.js';
|
|
6
|
+
import { setCaller } from '../auth/token-check.js';
|
|
5
7
|
import { discoveryDocument, recordOf, } from '../discovery/discovery.js';
|
|
6
8
|
import {} from '../discovery/discovery.schema.js';
|
|
7
9
|
import { createHealthHandler } from '../health/health.js';
|
|
@@ -51,6 +53,14 @@ export const createPartApp = (options) => {
|
|
|
51
53
|
};
|
|
52
54
|
// The committed OpenAPI document, bare JSON and anonymous like discovery (api-standards section 1).
|
|
53
55
|
const openapiHandler = (doc) => (_req, res) => void res.json(doc);
|
|
56
|
+
// Before the body is read: each request checked for the access its OpenAPI operation declares.
|
|
57
|
+
const routeGuard = (options) => async (req, _res, next) => {
|
|
58
|
+
if (!options.tokenCheck)
|
|
59
|
+
return next();
|
|
60
|
+
const access = routeAccess(options.openapi, req.method, `/api/v1${req.path}`);
|
|
61
|
+
setCaller(req, await options.tokenCheck(req.get('authorization'), access));
|
|
62
|
+
next();
|
|
63
|
+
};
|
|
54
64
|
export const createApiApp = (options) => createPartApp({
|
|
55
65
|
...options,
|
|
56
66
|
part: 'api',
|
|
@@ -58,6 +68,7 @@ export const createApiApp = (options) => createPartApp({
|
|
|
58
68
|
app.use('/api/v1', cors(PUBLIC_CORS));
|
|
59
69
|
if (options.openapi)
|
|
60
70
|
app.get('/api/v1/openapi.json', openapiHandler(options.openapi));
|
|
61
|
-
|
|
71
|
+
const json = express.json({ limit: BODY_LIMIT });
|
|
72
|
+
app.use('/api/v1', routeGuard(options), json, options.router);
|
|
62
73
|
},
|
|
63
74
|
});
|
package/dist/store/migrate.js
CHANGED
|
@@ -3,12 +3,16 @@ import { createLog } from '../log/log.js';
|
|
|
3
3
|
import { readManifest } from '../manifest/read-manifest.js';
|
|
4
4
|
import { migrate } from './migrations.js';
|
|
5
5
|
import { createStore } from './store.js';
|
|
6
|
+
// A migration may rewrite a table: minutes, not a request's seconds; the job itself has no retry.
|
|
7
|
+
const MIGRATE_TIMEOUTS = { connectMs: 10_000, statementMs: 300_000 };
|
|
6
8
|
// The api image's migrate entry (`node dist/store/migrate.js` in /app), run as a job before a rollout.
|
|
7
|
-
|
|
9
|
+
// Until the manifest is read the id is unknown; every line after it carries the extension.
|
|
10
|
+
let log = createLog({ extension: 'unknown', part: 'migrate' });
|
|
8
11
|
try {
|
|
9
12
|
const pkg = await readManifest(process.cwd());
|
|
10
|
-
|
|
11
|
-
const
|
|
13
|
+
log = createLog({ extension: pkg.agentage.id, part: 'migrate' });
|
|
14
|
+
const store = createStore(process.env, log, MIGRATE_TIMEOUTS);
|
|
15
|
+
const result = await migrate(store, process.cwd(), log);
|
|
12
16
|
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
13
17
|
await store.end();
|
|
14
18
|
}
|
|
@@ -5,6 +5,8 @@ export interface MigrateResult {
|
|
|
5
5
|
migrations: number;
|
|
6
6
|
applied: number;
|
|
7
7
|
seeds: number;
|
|
8
|
+
seedsBefore: number;
|
|
8
9
|
}
|
|
9
10
|
export declare const sqlFiles: (dir: string) => Promise<string[]>;
|
|
11
|
+
export declare const SEEDS_OWED = "seeds:owed";
|
|
10
12
|
export declare const migrate: (store: Store, dir: string, log: Log) => Promise<MigrateResult>;
|
package/dist/store/migrations.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readdir, readFile } from 'node:fs/promises';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import {} from '../log/log.js';
|
|
3
|
+
import { messageOf } from '../log/log.js';
|
|
4
4
|
import {} from './store.types.js';
|
|
5
5
|
export const SQL_FILE = /^[0-9]{3}-[a-z0-9-]+\.sql$/;
|
|
6
6
|
// Filename order; anything else in the folder is refused, so a typo never silently skips a file.
|
|
@@ -23,6 +23,14 @@ const LEDGER = `create table if not exists _migrations (
|
|
|
23
23
|
name text primary key,
|
|
24
24
|
applied_at timestamptz not null default now()
|
|
25
25
|
)`;
|
|
26
|
+
const SEED = 'seed:';
|
|
27
|
+
// Written by the first run on an empty store, removed once the seeds ran: a failed first run still seeds later.
|
|
28
|
+
export const SEEDS_OWED = 'seeds:owed';
|
|
29
|
+
// Tables other than the kit's own: a store holding any (a restored dump) never gets the seeds.
|
|
30
|
+
const USER_TABLES = `select count(*)::int as n from information_schema.tables
|
|
31
|
+
where table_schema not in ('pg_catalog', 'information_schema')
|
|
32
|
+
and table_name not in ('_migrations', 'agentage_job_runs')`;
|
|
33
|
+
const isEmpty = async (store, done) => done.size === 0 && ((await store.query(USER_TABLES)).rows[0]?.n ?? 0) === 0;
|
|
26
34
|
const appliedNames = async (store) => {
|
|
27
35
|
await store.query(LEDGER);
|
|
28
36
|
const { rows } = await store.query('select name from _migrations');
|
|
@@ -31,7 +39,14 @@ const appliedNames = async (store) => {
|
|
|
31
39
|
// One string, one round trip: the file and its ledger row commit together or not at all.
|
|
32
40
|
const applyFile = async (store, file, name) => {
|
|
33
41
|
const sql = await readFile(file, 'utf8');
|
|
34
|
-
|
|
42
|
+
try {
|
|
43
|
+
await store.query(`begin;\n${sql}\n;insert into _migrations (name) values ('${name}');\ncommit;`);
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
await store.query('rollback').catch(() => undefined);
|
|
47
|
+
const kind = name.startsWith(SEED) ? 'seed' : 'migration';
|
|
48
|
+
throw new Error(`${kind} ${name.replace(SEED, '')} failed: ${messageOf(err)}`, { cause: err });
|
|
49
|
+
}
|
|
35
50
|
};
|
|
36
51
|
const applyAll = async (store, dir, prefix, done) => {
|
|
37
52
|
let count = 0;
|
|
@@ -44,12 +59,23 @@ const applyAll = async (store, dir, prefix, done) => {
|
|
|
44
59
|
}
|
|
45
60
|
return count;
|
|
46
61
|
};
|
|
47
|
-
|
|
62
|
+
const seedOnce = async (store, dir, done) => {
|
|
63
|
+
const seeds = await applyAll(store, join(dir, 'seeds'), SEED, done);
|
|
64
|
+
await store.query(`delete from _migrations where name = '${SEEDS_OWED}'`);
|
|
65
|
+
return seeds;
|
|
66
|
+
};
|
|
67
|
+
// Migrations in order, each once; seeds once, only on a store with no ledger row and no user table.
|
|
48
68
|
export const migrate = async (store, dir, log) => {
|
|
49
69
|
const done = await appliedNames(store);
|
|
70
|
+
const empty = await isEmpty(store, done);
|
|
71
|
+
if (empty)
|
|
72
|
+
await store.query(`insert into _migrations (name) values ('${SEEDS_OWED}')`);
|
|
73
|
+
const owed = empty || done.has(SEEDS_OWED);
|
|
50
74
|
const migrations = (await sqlFiles(join(dir, 'migrations'))).length;
|
|
51
75
|
const applied = await applyAll(store, join(dir, 'migrations'), '', done);
|
|
52
|
-
const seeds =
|
|
53
|
-
|
|
54
|
-
|
|
76
|
+
const seeds = owed ? await seedOnce(store, dir, done) : 0;
|
|
77
|
+
const seedsBefore = [...done].filter((name) => name.startsWith(SEED)).length;
|
|
78
|
+
const result = { migrations, applied, seeds, seedsBefore };
|
|
79
|
+
log.info('migrated', { ...result });
|
|
80
|
+
return result;
|
|
55
81
|
};
|
package/dist/store/store.d.ts
CHANGED
|
@@ -5,6 +5,12 @@ import { type Store } from './store.types.js';
|
|
|
5
5
|
export interface PoolStore extends Store {
|
|
6
6
|
end(): Promise<void>;
|
|
7
7
|
}
|
|
8
|
+
export interface StoreTimeouts {
|
|
9
|
+
connectMs: number;
|
|
10
|
+
statementMs: number;
|
|
11
|
+
}
|
|
12
|
+
export declare const PART_TIMEOUTS: StoreTimeouts;
|
|
8
13
|
export declare const storeUrl: (env: Env, read?: (file: string) => string) => string | undefined;
|
|
9
|
-
export declare const
|
|
14
|
+
export declare const isOutage: (err: unknown) => boolean;
|
|
15
|
+
export declare const createStore: (env: Env, log: Log, timeouts?: StoreTimeouts) => PoolStore;
|
|
10
16
|
export declare const storeCheck: (store: Store) => HealthCheck;
|
package/dist/store/store.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import pg from 'pg';
|
|
3
|
+
import { fail } from '../envelope/envelope.js';
|
|
3
4
|
import {} from '../health/health.js';
|
|
4
|
-
import {} from '../log/log.js';
|
|
5
|
+
import { messageOf } from '../log/log.js';
|
|
5
6
|
import {} from '../runtime/build-info.js';
|
|
6
7
|
import {} from './store.types.js';
|
|
8
|
+
// A part's query is bounded by the server and, if the network itself hangs, by the client too.
|
|
9
|
+
export const PART_TIMEOUTS = { connectMs: 5_000, statementMs: 10_000 };
|
|
10
|
+
const CLIENT_SLACK_MS = 2_000;
|
|
7
11
|
// The host sets the connection: AGENTAGE_STORE_URL, a secret file named by _FILE, else libpq PG* env.
|
|
8
12
|
export const storeUrl = (env, read = (file) => readFileSync(file, 'utf8')) => {
|
|
9
13
|
const file = env['AGENTAGE_STORE_URL_FILE'];
|
|
@@ -11,13 +15,39 @@ export const storeUrl = (env, read = (file) => readFileSync(file, 'utf8')) => {
|
|
|
11
15
|
return read(file).trim();
|
|
12
16
|
return env['AGENTAGE_STORE_URL'] || undefined;
|
|
13
17
|
};
|
|
14
|
-
|
|
15
|
-
|
|
18
|
+
// Connection classes 08 and 57P01-03 (shutdown), 57014 (statement timeout), socket errors, pg's own timeouts.
|
|
19
|
+
const OUTAGE_SQLSTATE = /^(08|57P0[1-3]$|57014$)/;
|
|
20
|
+
const OUTAGE_SOCKET = /^(ECONNREFUSED|ECONNRESET|ETIMEDOUT|ENOTFOUND|EAI_AGAIN|EHOSTUNREACH|EPIPE)$/;
|
|
21
|
+
const OUTAGE_MESSAGE = /timeout exceeded when trying to connect|Query read timeout|Connection terminated/;
|
|
22
|
+
export const isOutage = (err) => {
|
|
23
|
+
const code = err?.code;
|
|
24
|
+
if (typeof code === 'string' && (OUTAGE_SQLSTATE.test(code) || OUTAGE_SOCKET.test(code)))
|
|
25
|
+
return true;
|
|
26
|
+
return OUTAGE_MESSAGE.test(messageOf(err));
|
|
27
|
+
};
|
|
28
|
+
// A store that cannot answer is UNAVAILABLE (503, Retry-After), never an INTERNAL 500; no retry here.
|
|
29
|
+
const guarded = (pool, log) => async function query(text, params) {
|
|
30
|
+
try {
|
|
31
|
+
return await pool.query(text, params);
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
if (!isOutage(err))
|
|
35
|
+
throw err;
|
|
36
|
+
log.warn('store unavailable', { reason: messageOf(err) });
|
|
37
|
+
throw fail('UNAVAILABLE', 'The store is unavailable');
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
export const createStore = (env, log, timeouts = PART_TIMEOUTS) => {
|
|
41
|
+
const pool = new pg.Pool({
|
|
42
|
+
connectionString: storeUrl(env),
|
|
43
|
+
max: 5,
|
|
44
|
+
idleTimeoutMillis: 30_000,
|
|
45
|
+
connectionTimeoutMillis: timeouts.connectMs,
|
|
46
|
+
statement_timeout: timeouts.statementMs,
|
|
47
|
+
query_timeout: timeouts.statementMs + CLIENT_SLACK_MS,
|
|
48
|
+
});
|
|
16
49
|
pool.on('error', (err) => log.error(err, { store: 'pool' }));
|
|
17
|
-
return {
|
|
18
|
-
query: async (text, params) => pool.query(text, params),
|
|
19
|
-
end: () => pool.end(),
|
|
20
|
-
};
|
|
50
|
+
return { query: guarded(pool, log), end: () => pool.end() };
|
|
21
51
|
};
|
|
22
52
|
export const storeCheck = (store) => async () => {
|
|
23
53
|
await store.query('select 1');
|
package/dist/tool/build.d.ts
CHANGED
|
@@ -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 };
|
package/dist/tool/build.js
CHANGED
|
@@ -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
|
|
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
|
+
};
|
package/dist/tool/docker.js
CHANGED
|
@@ -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
|
});
|
package/dist/tool/exec.d.ts
CHANGED
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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 (
|
|
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
|
-
|
|
21
|
-
|
|
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
|
|
18
|
-
up
|
|
19
|
-
down
|
|
20
|
-
token
|
|
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
|
|
52
|
+
return EXIT_OK;
|
|
53
53
|
}
|
|
54
54
|
catch (err) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
62
|
-
export const main = async (argv, deps = defaultDeps) => {
|
|
63
|
-
let parsed;
|
|
60
|
+
const parsedOrUsage = (argv, deps) => {
|
|
64
61
|
try {
|
|
65
|
-
|
|
62
|
+
return parse(argv);
|
|
66
63
|
}
|
|
67
64
|
catch (err) {
|
|
68
65
|
deps.err(`${err.message}\n${USAGE}`);
|
|
69
|
-
return
|
|
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,19 +1,18 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
|
-
import { pathToFileURL } from 'node:url';
|
|
3
2
|
import { z } from 'zod';
|
|
4
3
|
import { createContext } from '../context/context.js';
|
|
5
4
|
import {} from '../discovery/discovery.js';
|
|
5
|
+
import { WidgetSchema } from '../discovery/discovery.schema.js';
|
|
6
6
|
import { OPENAPI_PATH, readOpenapi } from '../discovery/openapi.js';
|
|
7
7
|
import { createLog } from '../log/log.js';
|
|
8
|
+
import { loadCreate } from '../manifest/entry.js';
|
|
8
9
|
import { readJsonIfExists } from '../manifest/read-manifest.js';
|
|
9
10
|
import {} from '../manifest/manifest.schema.js';
|
|
10
|
-
import {
|
|
11
|
+
import { hasPostgres, normalizeParts } from '../manifest/parts.js';
|
|
11
12
|
import { createToolRegistry, prefixed } from '../mcp/tools.js';
|
|
12
13
|
import { sqlFiles } from '../store/migrations.js';
|
|
13
14
|
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);
|
|
15
|
+
const WidgetsSchema = z.array(WidgetSchema).min(1);
|
|
17
16
|
const noStore = {
|
|
18
17
|
query: async () => {
|
|
19
18
|
throw new Error('the store is not open while the build lists tools');
|
|
@@ -31,10 +30,7 @@ export const toolNames = async (dir, pkg) => {
|
|
|
31
30
|
log: silent,
|
|
32
31
|
store: noStore,
|
|
33
32
|
});
|
|
34
|
-
|
|
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 });
|
|
33
|
+
await (await loadCreate(dir, pkg, 'mcp'))({ ...base, tool: registry.tool });
|
|
38
34
|
return registry.tools.map((tool) => prefixed(pkg.agentage.id, tool.name));
|
|
39
35
|
};
|
|
40
36
|
const widgetsOf = async (dir) => {
|
|
@@ -43,7 +39,7 @@ const widgetsOf = async (dir) => {
|
|
|
43
39
|
};
|
|
44
40
|
// What the code serves, for the record: tools, widgets and the migration count.
|
|
45
41
|
export const recordExtras = async (dir, pkg) => {
|
|
46
|
-
const { code
|
|
42
|
+
const { code } = normalizeParts(pkg.agentage.parts);
|
|
47
43
|
const extras = {};
|
|
48
44
|
if (code.mcp) {
|
|
49
45
|
const tools = await toolNames(dir, pkg);
|
|
@@ -54,7 +50,7 @@ export const recordExtras = async (dir, pkg) => {
|
|
|
54
50
|
extras.widgets = await widgetsOf(dir);
|
|
55
51
|
if (code.api && (await readOpenapi(dir)))
|
|
56
52
|
extras.openapi = OPENAPI_PATH;
|
|
57
|
-
if (
|
|
53
|
+
if (hasPostgres(pkg))
|
|
58
54
|
extras.migrations = (await sqlFiles(join(dir, 'migrations'))).length;
|
|
59
55
|
return extras;
|
|
60
56
|
};
|
package/dist/tool/stage.d.ts
CHANGED
|
@@ -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>;
|
package/dist/tool/stage.js
CHANGED
|
@@ -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,
|
|
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
|
-
//
|
|
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
|
-
|
|
50
|
-
|
|
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 (
|
|
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
|
|
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[]>;
|