anbaric-cloud-hosting 1.69.0 → 1.70.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/package.json +7 -7
- package/src/data-store/InMemorySkillStore.ts +31 -0
- package/src/data-store/PlatformMigrations.ts +14 -0
- package/src/data-store/PostgresSkillStore.ts +47 -0
- package/src/data-store/SkillStore.ts +38 -0
- package/src/hosting/HostingServer.ts +6 -1
- package/src/hosting/handlers/SkillsHandler.ts +81 -0
- package/src/main.ts +2 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "anbaric-cloud-hosting",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.70.0",
|
|
4
4
|
"description": "Anbaric Cloud hosting service: Postgres-backed job persistence and queuing exposed over an HTTP API",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "chris@anbaric.ai",
|
|
@@ -18,18 +18,18 @@
|
|
|
18
18
|
"@aws-sdk/client-s3": "^3.1110.0",
|
|
19
19
|
"@aws-sdk/client-secrets-manager": "^3.700.0",
|
|
20
20
|
"@aws-sdk/client-servicediscovery": "^3.1110.0",
|
|
21
|
-
"anbaric-data-store": "^1.
|
|
22
|
-
"anbaric-plugins": "^1.
|
|
23
|
-
"anbaric-tsapi": "^1.
|
|
24
|
-
"anbaric-web": "^1.
|
|
21
|
+
"anbaric-data-store": "^1.70.0",
|
|
22
|
+
"anbaric-plugins": "^1.70.0",
|
|
23
|
+
"anbaric-tsapi": "^1.70.0",
|
|
24
|
+
"anbaric-web": "^1.70.0",
|
|
25
25
|
"esbuild": "^0.28.2",
|
|
26
26
|
"pg": "^8.16.0"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"@types/node": "^26.2.0",
|
|
30
30
|
"@types/pg": "^8.15.0",
|
|
31
|
-
"anbaric-impl-cloud": "^1.
|
|
32
|
-
"anbaric-state-machine": "^1.
|
|
31
|
+
"anbaric-impl-cloud": "^1.70.0",
|
|
32
|
+
"anbaric-state-machine": "^1.70.0",
|
|
33
33
|
"tsx": "^4.20.0",
|
|
34
34
|
"typescript": "^7.0.2"
|
|
35
35
|
},
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import {Skill, SkillDraft, SkillStore} from "./SkillStore";
|
|
2
|
+
|
|
3
|
+
class InMemorySkillStore implements SkillStore {
|
|
4
|
+
|
|
5
|
+
private skills = new Map<string, Skill>();
|
|
6
|
+
|
|
7
|
+
async list() : Promise<Array<Skill>> {
|
|
8
|
+
return [...this.skills.values()]
|
|
9
|
+
.sort((left, right) => left.id.localeCompare(right.id))
|
|
10
|
+
.map(skill => ({ ...skill }));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async get(id : string) : Promise<Skill | undefined> {
|
|
14
|
+
const skill = this.skills.get(id);
|
|
15
|
+
return skill && { ...skill };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async save(draft : SkillDraft) : Promise<Skill> {
|
|
19
|
+
const now = new Date();
|
|
20
|
+
const saved : Skill = { ...draft, createdAt: this.skills.get(draft.id)?.createdAt ?? now, updatedAt: now };
|
|
21
|
+
this.skills.set(draft.id, saved);
|
|
22
|
+
return { ...saved };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async remove(id : string) : Promise<boolean> {
|
|
26
|
+
return this.skills.delete(id);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export { InMemorySkillStore };
|
|
@@ -273,6 +273,20 @@ const PLATFORM_MIGRATIONS : Array<Migration> = [
|
|
|
273
273
|
)`,
|
|
274
274
|
),
|
|
275
275
|
|
|
276
|
+
/* Guidance the tenant's builders write for the agents that work on it:
|
|
277
|
+
usage hints say when to reach for a skill, the body says what to do.
|
|
278
|
+
A platform table rather than workflow data, since apps never see it. */
|
|
279
|
+
statements("008-skills",
|
|
280
|
+
`CREATE TABLE IF NOT EXISTS anbaric_system.skills (
|
|
281
|
+
id TEXT PRIMARY KEY,
|
|
282
|
+
title TEXT NOT NULL,
|
|
283
|
+
usage_hints TEXT NOT NULL,
|
|
284
|
+
body TEXT NOT NULL,
|
|
285
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
286
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
287
|
+
)`,
|
|
288
|
+
),
|
|
289
|
+
|
|
276
290
|
];
|
|
277
291
|
|
|
278
292
|
export { PLATFORM_MIGRATIONS };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import {Pool} from "pg";
|
|
2
|
+
import {Skill, SkillDraft, SkillStore} from "./SkillStore";
|
|
3
|
+
|
|
4
|
+
const COLUMNS = "id, title, usage_hints, body, created_at, updated_at";
|
|
5
|
+
|
|
6
|
+
const rowToSkill = (row : any) : Skill => ({
|
|
7
|
+
id: row.id,
|
|
8
|
+
title: row.title,
|
|
9
|
+
usageHints: row.usage_hints,
|
|
10
|
+
body: row.body,
|
|
11
|
+
createdAt: row.created_at,
|
|
12
|
+
updatedAt: row.updated_at,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
class PostgresSkillStore implements SkillStore {
|
|
16
|
+
|
|
17
|
+
constructor(private pool : Pool) {}
|
|
18
|
+
|
|
19
|
+
async list() : Promise<Array<Skill>> {
|
|
20
|
+
const result = await this.pool.query(`SELECT ${COLUMNS} FROM anbaric_system.skills ORDER BY id`);
|
|
21
|
+
return result.rows.map(rowToSkill);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async get(id : string) : Promise<Skill | undefined> {
|
|
25
|
+
const result = await this.pool.query(`SELECT ${COLUMNS} FROM anbaric_system.skills WHERE id = $1`, [id]);
|
|
26
|
+
return result.rows[0] && rowToSkill(result.rows[0]);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async save(draft : SkillDraft) : Promise<Skill> {
|
|
30
|
+
const result = await this.pool.query(
|
|
31
|
+
`INSERT INTO anbaric_system.skills (id, title, usage_hints, body)
|
|
32
|
+
VALUES ($1, $2, $3, $4)
|
|
33
|
+
ON CONFLICT (id) DO UPDATE
|
|
34
|
+
SET title = EXCLUDED.title, usage_hints = EXCLUDED.usage_hints, body = EXCLUDED.body, updated_at = now()
|
|
35
|
+
RETURNING ${COLUMNS}`,
|
|
36
|
+
[draft.id, draft.title, draft.usageHints, draft.body]);
|
|
37
|
+
return rowToSkill(result.rows[0]);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async remove(id : string) : Promise<boolean> {
|
|
41
|
+
const result = await this.pool.query("DELETE FROM anbaric_system.skills WHERE id = $1", [id]);
|
|
42
|
+
return (result.rowCount ?? 0) > 0;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export { PostgresSkillStore };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/* A skill is guidance a tenant writes for the agents that build on it: when to
|
|
2
|
+
reach for it (the usage hints, which an agent reads before deciding) and
|
|
3
|
+
what to do (the body, which it reads once it has decided). Kept by the
|
|
4
|
+
platform, managed from the console, and never handed to deployed apps. */
|
|
5
|
+
type Skill = {
|
|
6
|
+
id : string,
|
|
7
|
+
title : string,
|
|
8
|
+
usageHints : string,
|
|
9
|
+
body : string,
|
|
10
|
+
createdAt : Date,
|
|
11
|
+
updatedAt : Date,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type SkillDraft = Omit<Skill, "createdAt" | "updatedAt">;
|
|
15
|
+
|
|
16
|
+
/* The id ends up as the tail of a tool name an agent sees, after a fixed
|
|
17
|
+
prefix, and tool names are capped at 64 characters: hence the shape and
|
|
18
|
+
the length. */
|
|
19
|
+
const SKILL_ID_SHAPE = /^[a-z0-9][a-z0-9-]{0,49}$/;
|
|
20
|
+
|
|
21
|
+
const isSkillId = (value : unknown) : value is string =>
|
|
22
|
+
typeof value === "string" && SKILL_ID_SHAPE.test(value);
|
|
23
|
+
|
|
24
|
+
interface SkillStore {
|
|
25
|
+
|
|
26
|
+
list() : Promise<Array<Skill>>;
|
|
27
|
+
|
|
28
|
+
get(id : string) : Promise<Skill | undefined>;
|
|
29
|
+
|
|
30
|
+
// Creates or replaces the skill with that id.
|
|
31
|
+
save(draft : SkillDraft) : Promise<Skill>;
|
|
32
|
+
|
|
33
|
+
remove(id : string) : Promise<boolean>;
|
|
34
|
+
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type { Skill, SkillDraft, SkillStore };
|
|
38
|
+
export { SKILL_ID_SHAPE, isSkillId };
|
|
@@ -14,6 +14,8 @@ import {MembersHandler} from "./handlers/auth/MembersHandler";
|
|
|
14
14
|
import {BillingHandler} from "./handlers/BillingHandler";
|
|
15
15
|
import {CentralBilling} from "../billing/CentralBilling";
|
|
16
16
|
import {AppSettingsStore} from "../data-store/AppSettingsStore";
|
|
17
|
+
import {SkillStore} from "../data-store/SkillStore";
|
|
18
|
+
import {SkillsHandler} from "./handlers/SkillsHandler";
|
|
17
19
|
import {Subdomains} from "../subdomains/Subdomains";
|
|
18
20
|
import {AuditsHandler} from "./handlers/AuditsHandler";
|
|
19
21
|
import {AuthorizeCliHandler} from "./handlers/auth/AuthorizeCliHandler";
|
|
@@ -83,7 +85,8 @@ class HostingServer {
|
|
|
83
85
|
fileStorageFor? : (appId : string) => FileStorage,
|
|
84
86
|
subdomains? : Subdomains,
|
|
85
87
|
billing? : CentralBilling,
|
|
86
|
-
appSettings? : AppSettingsStore
|
|
88
|
+
appSettings? : AppSettingsStore,
|
|
89
|
+
skills? : SkillStore) {
|
|
87
90
|
const pages = new PagesHandler();
|
|
88
91
|
const ping = new PingHandler(tenant);
|
|
89
92
|
const jobs = new JobsHandler(persistence);
|
|
@@ -119,6 +122,8 @@ class HostingServer {
|
|
|
119
122
|
if (memberships) publicRouter.registerApi("invitations", new InvitationsHandler(memberships, entitlements));
|
|
120
123
|
if (memberships) publicRouter.registerApi("members", new MembersHandler(memberships, entitlements));
|
|
121
124
|
if (billing) publicRouter.registerApi("billing", new BillingHandler(billing));
|
|
125
|
+
// People and their agents only: a deployed app has no business reading skills.
|
|
126
|
+
if (skills) publicRouter.registerApi("skills", new SkillsHandler(skills));
|
|
122
127
|
const prompts = promptManagerFor && new PromptsHandler(promptManagerFor);
|
|
123
128
|
if (prompts) publicRouter.registerApi("prompts", prompts);
|
|
124
129
|
if (cliAuthorizer) {
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import {deniesBuild} from "../../auth/TenantRole";
|
|
2
|
+
import {isSkillId, Skill, SkillStore} from "../../data-store/SkillStore";
|
|
3
|
+
import {Request} from "../Request";
|
|
4
|
+
import {RequestHandler} from "../RequestHandler";
|
|
5
|
+
|
|
6
|
+
/* The tenant's skills: guidance its builders write for the agents that work
|
|
7
|
+
on it. Anyone in the tenant may read them - an agent acting for a user
|
|
8
|
+
needs them as much as one acting for a builder - but writing them is a
|
|
9
|
+
builder's job. Served to people and their agents only, never to deployed
|
|
10
|
+
apps, which is why this is registered on the public entry point alone. */
|
|
11
|
+
class SkillsHandler implements RequestHandler {
|
|
12
|
+
|
|
13
|
+
constructor(private skills : SkillStore) {}
|
|
14
|
+
|
|
15
|
+
async handle(request : Request) : Promise<void> {
|
|
16
|
+
if (request.subresource) return request.notFound();
|
|
17
|
+
if (request.id) return this.handleSkill(request, request.id);
|
|
18
|
+
return this.handleCollection(request);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
private async handleCollection(request : Request) : Promise<void> {
|
|
22
|
+
if (request.method !== "GET") return request.notFound();
|
|
23
|
+
return request.reply(200, (await this.skills.list()).map(skill => this.publicSkill(skill)));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
private async handleSkill(request : Request, id : string) : Promise<void> {
|
|
27
|
+
switch (request.method) {
|
|
28
|
+
case "GET": {
|
|
29
|
+
const skill = await this.skills.get(id);
|
|
30
|
+
if (! skill) return request.reply(404, { error: `No skill with id "${id}"` });
|
|
31
|
+
return request.reply(200, this.publicSkill(skill));
|
|
32
|
+
}
|
|
33
|
+
case "PUT": {
|
|
34
|
+
if (deniesBuild(request.user?.tenantRole)) return this.refuse(request);
|
|
35
|
+
if (! isSkillId(id)) {
|
|
36
|
+
return request.reply(400, { error: `"${id}" is not a skill id: lower-case letters, digits and hyphens, up to 50 characters` });
|
|
37
|
+
}
|
|
38
|
+
const body = await request.body();
|
|
39
|
+
const draft = this.draftFrom(body);
|
|
40
|
+
if (! draft) return request.reply(400, { error: "Expected a body of { title : string, usageHints : string, body : string }, none empty" });
|
|
41
|
+
|
|
42
|
+
return request.reply(200, this.publicSkill(await this.skills.save({ id, ...draft })));
|
|
43
|
+
}
|
|
44
|
+
case "DELETE": {
|
|
45
|
+
if (deniesBuild(request.user?.tenantRole)) return this.refuse(request);
|
|
46
|
+
const removed = await this.skills.remove(id);
|
|
47
|
+
return removed ? request.reply(204) : request.reply(404, { error: `No skill with id "${id}"` });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
request.notFound();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private refuse(request : Request) : void {
|
|
54
|
+
request.reply(403, { error: "Your role in this tenant cannot manage skills" });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private draftFrom(body : unknown) : { title : string, usageHints : string, body : string } | undefined {
|
|
58
|
+
const fields = body as Record<string, unknown> | undefined;
|
|
59
|
+
const text = (value : unknown) : string | undefined =>
|
|
60
|
+
typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
|
|
61
|
+
|
|
62
|
+
const title = text(fields?.title);
|
|
63
|
+
const usageHints = text(fields?.usageHints);
|
|
64
|
+
const content = text(fields?.body);
|
|
65
|
+
return title && usageHints && content ? { title, usageHints, body: content } : undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private publicSkill(skill : Skill) {
|
|
69
|
+
return {
|
|
70
|
+
id: skill.id,
|
|
71
|
+
title: skill.title,
|
|
72
|
+
usageHints: skill.usageHints,
|
|
73
|
+
body: skill.body,
|
|
74
|
+
createdAt: skill.createdAt.toISOString(),
|
|
75
|
+
updatedAt: skill.updatedAt.toISOString(),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export { SkillsHandler };
|
package/src/main.ts
CHANGED
|
@@ -27,6 +27,7 @@ import {DocGenerator} from "./docs/DocGenerator";
|
|
|
27
27
|
import {GatewayDocModel} from "./docs/GatewayDocModel";
|
|
28
28
|
import {PostgresAppDocsStore} from "./data-store/PostgresAppDocsStore";
|
|
29
29
|
import {PostgresAppSettingsStore} from "./data-store/PostgresAppSettingsStore";
|
|
30
|
+
import {PostgresSkillStore} from "./data-store/PostgresSkillStore";
|
|
30
31
|
import {PostgresEntitlementStore} from "./data-store/PostgresEntitlementStore";
|
|
31
32
|
import {PostgresUserDirectory} from "./data-store/PostgresUserDirectory";
|
|
32
33
|
import {HttpMembershipService} from "./auth/HttpMembershipService";
|
|
@@ -156,7 +157,7 @@ const server = new HostingServer(jobs, queue, registry, buildLayer,
|
|
|
156
157
|
tokenAuthenticator, process.env.ANBARIC_TENANT, auditRecords, plugins,
|
|
157
158
|
new PostgresJobRunSchedulePersistence(pool), await loadNotifier(process.env.ANBARIC_NOTIFIER_MODULE),
|
|
158
159
|
new PostgresEntitlementStore(pool), new PostgresUserDirectory(pool), memberships,
|
|
159
|
-
(appId) => new PostgresPromptManager(pool, appId), fileStorageFor, subdomains, billing, appSettings);
|
|
160
|
+
(appId) => new PostgresPromptManager(pool, appId), fileStorageFor, subdomains, billing, appSettings, new PostgresSkillStore(pool));
|
|
160
161
|
const port = await server.listen(hostingPort);
|
|
161
162
|
const internal = await server.listenInternal(internalPort);
|
|
162
163
|
|