anbaric-cloud-hosting 1.35.0 → 1.37.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/app-management/DockerBuildLayer.ts +1 -0
- package/src/app-management/FargateBuildLayer.ts +1 -0
- package/src/data-store/PostgresPromptManager.ts +87 -0
- package/src/data-store/Schema.ts +12 -0
- package/src/hosting/HostingServer.ts +7 -2
- package/src/hosting/handlers/PromptsHandler.ts +46 -0
- package/src/hosting/pages/platform-ui.html +1 -1
- package/src/index.ts +2 -0
- package/src/main.ts +3 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "anbaric-cloud-hosting",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.37.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.37.0",
|
|
22
|
+
"anbaric-plugins": "^1.37.0",
|
|
23
|
+
"anbaric-tsapi": "^1.37.0",
|
|
24
|
+
"anbaric-web": "^1.37.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.37.0",
|
|
32
|
+
"anbaric-state-machine": "^1.37.0",
|
|
33
33
|
"tsx": "^4.20.0",
|
|
34
34
|
"typescript": "^7.0.2"
|
|
35
35
|
},
|
|
@@ -108,6 +108,7 @@ class DockerBuildLayer extends BaseBuildLayer {
|
|
|
108
108
|
"--env", "ANBARIC_AUDITOR_TYPE=cloud",
|
|
109
109
|
"--env", "ANBARIC_SESSION_RESOLVER_TYPE=cloud",
|
|
110
110
|
"--env", "ANBARIC_ENTITLEMENTS_TYPE=cloud",
|
|
111
|
+
"--env", "ANBARIC_PROMPT_MANAGER_TYPE=cloud",
|
|
111
112
|
"--env", "ANBARIC_NOTIFIER_TYPE=cloud",
|
|
112
113
|
"--env", `ANBARIC_CONSUMER_PORT=${deployment.consumerPort}`,
|
|
113
114
|
"--env", `ANBARIC_CONSUMER_URL=http://${container}:${deployment.consumerPort}`,
|
|
@@ -198,6 +198,7 @@ class FargateBuildLayer extends BaseBuildLayer {
|
|
|
198
198
|
{ name: "ANBARIC_AUDITOR_TYPE", value: "cloud" },
|
|
199
199
|
{ name: "ANBARIC_SESSION_RESOLVER_TYPE", value: "cloud" },
|
|
200
200
|
{ name: "ANBARIC_ENTITLEMENTS_TYPE", value: "cloud" },
|
|
201
|
+
{ name: "ANBARIC_PROMPT_MANAGER_TYPE", value: "cloud" },
|
|
201
202
|
{ name: "ANBARIC_NOTIFIER_TYPE", value: "cloud" },
|
|
202
203
|
{ name: "ANBARIC_CONSUMER_PORT", value: String(deployment.consumerPort) },
|
|
203
204
|
{ name: "ANBARIC_CONSUMER_URL", value: `http://${deployment.appHost}:${deployment.consumerPort}` },
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import {Pool} from "pg";
|
|
2
|
+
import {JsonSchema, Prompt, PromptManager, samePromptContent} from "anbaric-tsapi";
|
|
3
|
+
|
|
4
|
+
const COLUMNS = "app_id, prompt_id, version, instructions, input_schema, output_schema, created_at";
|
|
5
|
+
|
|
6
|
+
const rowToPrompt = (row : any) : Prompt => ({
|
|
7
|
+
appId: row.app_id,
|
|
8
|
+
promptId: row.prompt_id,
|
|
9
|
+
version: row.version,
|
|
10
|
+
instructions: row.instructions,
|
|
11
|
+
inputSchema: row.input_schema ?? undefined,
|
|
12
|
+
outputSchema: row.output_schema ?? undefined,
|
|
13
|
+
createdAt: row.created_at instanceof Date ? row.created_at.toISOString() : String(row.created_at),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
/* Saving reads the latest version and writes the next inside one transaction,
|
|
17
|
+
with the latest row locked, so two saves of the same prompt cannot both
|
|
18
|
+
claim the same version number. */
|
|
19
|
+
class PostgresPromptManager implements PromptManager {
|
|
20
|
+
|
|
21
|
+
constructor(private pool : Pool, private appId : string) {}
|
|
22
|
+
|
|
23
|
+
async save(promptId : string, instructions : string, inputSchema? : JsonSchema, outputSchema? : JsonSchema) : Promise<Prompt> {
|
|
24
|
+
const client = await this.pool.connect();
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
await client.query("BEGIN");
|
|
28
|
+
const latest = await client.query(
|
|
29
|
+
`SELECT ${COLUMNS} FROM prompts WHERE app_id = $1 AND prompt_id = $2
|
|
30
|
+
ORDER BY version DESC LIMIT 1 FOR UPDATE`, [this.appId, promptId]);
|
|
31
|
+
const current = latest.rows[0] && rowToPrompt(latest.rows[0]);
|
|
32
|
+
|
|
33
|
+
if (current && samePromptContent(current, instructions, inputSchema, outputSchema)) {
|
|
34
|
+
await client.query("COMMIT");
|
|
35
|
+
return current;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const inserted = await client.query(
|
|
39
|
+
`INSERT INTO prompts (app_id, prompt_id, version, instructions, input_schema, output_schema)
|
|
40
|
+
VALUES ($1, $2, $3, $4, $5, $6)
|
|
41
|
+
RETURNING ${COLUMNS}`,
|
|
42
|
+
[this.appId, promptId, (current?.version ?? 0) + 1, instructions,
|
|
43
|
+
inputSchema === undefined ? null : JSON.stringify(inputSchema),
|
|
44
|
+
outputSchema === undefined ? null : JSON.stringify(outputSchema)]);
|
|
45
|
+
await client.query("COMMIT");
|
|
46
|
+
return rowToPrompt(inserted.rows[0]);
|
|
47
|
+
} catch (error) {
|
|
48
|
+
await client.query("ROLLBACK").catch(() => {});
|
|
49
|
+
throw error;
|
|
50
|
+
} finally {
|
|
51
|
+
client.release();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async retrieve(promptId : string, version? : number) : Promise<Prompt> {
|
|
56
|
+
const result = version === undefined
|
|
57
|
+
? await this.pool.query(
|
|
58
|
+
`SELECT ${COLUMNS} FROM prompts WHERE app_id = $1 AND prompt_id = $2 ORDER BY version DESC LIMIT 1`,
|
|
59
|
+
[this.appId, promptId])
|
|
60
|
+
: await this.pool.query(
|
|
61
|
+
`SELECT ${COLUMNS} FROM prompts WHERE app_id = $1 AND prompt_id = $2 AND version = $3`,
|
|
62
|
+
[this.appId, promptId, version]);
|
|
63
|
+
if (! result.rows[0]) {
|
|
64
|
+
throw new Error(version === undefined
|
|
65
|
+
? `No prompt found with id "${promptId}"`
|
|
66
|
+
: `No prompt found with id "${promptId}" at version ${version}`);
|
|
67
|
+
}
|
|
68
|
+
return rowToPrompt(result.rows[0]);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async list() : Promise<Array<Prompt>> {
|
|
72
|
+
const result = await this.pool.query(
|
|
73
|
+
`SELECT DISTINCT ON (prompt_id) ${COLUMNS} FROM prompts
|
|
74
|
+
WHERE app_id = $1 ORDER BY prompt_id, version DESC`, [this.appId]);
|
|
75
|
+
return result.rows.map(rowToPrompt);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async history(promptId : string) : Promise<Array<Prompt>> {
|
|
79
|
+
const result = await this.pool.query(
|
|
80
|
+
`SELECT ${COLUMNS} FROM prompts WHERE app_id = $1 AND prompt_id = $2 ORDER BY version DESC`,
|
|
81
|
+
[this.appId, promptId]);
|
|
82
|
+
return result.rows.map(rowToPrompt);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export { PostgresPromptManager }
|
package/src/data-store/Schema.ts
CHANGED
|
@@ -144,6 +144,18 @@ const ensureSchema = async (pool : Pool) : Promise<void> => {
|
|
|
144
144
|
VALUES (NULL, 'access', 'Global access')
|
|
145
145
|
ON CONFLICT (COALESCE(app_id, ''), entitlement_id) DO NOTHING
|
|
146
146
|
`);
|
|
147
|
+
await pool.query(`
|
|
148
|
+
CREATE TABLE IF NOT EXISTS prompts (
|
|
149
|
+
app_id TEXT NOT NULL,
|
|
150
|
+
prompt_id TEXT NOT NULL,
|
|
151
|
+
version INTEGER NOT NULL,
|
|
152
|
+
instructions TEXT NOT NULL,
|
|
153
|
+
input_schema JSONB,
|
|
154
|
+
output_schema JSONB,
|
|
155
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
156
|
+
PRIMARY KEY (app_id, prompt_id, version)
|
|
157
|
+
)
|
|
158
|
+
`);
|
|
147
159
|
await pool.query("CREATE SCHEMA IF NOT EXISTS anbaric_system");
|
|
148
160
|
await pool.query(`
|
|
149
161
|
CREATE TABLE IF NOT EXISTS anbaric_system.cli_keys (
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {JobPersistence, JobRunSchedulePersistence, JsonStore, Notifier, SecretStore} from "anbaric-tsapi";
|
|
1
|
+
import {JobPersistence, JobRunSchedulePersistence, JsonStore, Notifier, PromptManager, SecretStore} from "anbaric-tsapi";
|
|
2
2
|
import {BuildLayer} from "../app-management/BuildLayer";
|
|
3
3
|
import {AuditRecordStore} from "../auditing/AuditRecordStore";
|
|
4
4
|
import {Authenticator} from "../auth/Authenticator";
|
|
@@ -15,6 +15,7 @@ import {KeysHandler} from "./handlers/auth/KeysHandler";
|
|
|
15
15
|
import {SessionsHandler} from "./handlers/auth/SessionsHandler";
|
|
16
16
|
import {UsersHandler} from "./handlers/auth/UsersHandler";
|
|
17
17
|
import {InvitationsHandler} from "./handlers/auth/InvitationsHandler";
|
|
18
|
+
import {PromptsHandler} from "./handlers/PromptsHandler";
|
|
18
19
|
import {MembershipService} from "../auth/MembershipService";
|
|
19
20
|
import {WhoamiHandler} from "./handlers/auth/WhoamiHandler";
|
|
20
21
|
import {UserDirectory} from "../auth/UserDirectory";
|
|
@@ -68,7 +69,8 @@ class HostingServer {
|
|
|
68
69
|
notifier? : Notifier,
|
|
69
70
|
entitlements? : EntitlementStore,
|
|
70
71
|
userDirectory? : UserDirectory,
|
|
71
|
-
memberships? : MembershipService
|
|
72
|
+
memberships? : MembershipService,
|
|
73
|
+
promptManagerFor? : (appId : string) => PromptManager) {
|
|
72
74
|
const pages = new PagesHandler();
|
|
73
75
|
const ping = new PingHandler(tenant);
|
|
74
76
|
const jobs = new JobsHandler(persistence);
|
|
@@ -100,6 +102,8 @@ class HostingServer {
|
|
|
100
102
|
if (entitlements) publicRouter.registerApi("entitlements", new EntitlementsAdminHandler(entitlements));
|
|
101
103
|
if (userDirectory) publicRouter.registerApi("users", new UsersHandler(userDirectory));
|
|
102
104
|
if (memberships) publicRouter.registerApi("invitations", new InvitationsHandler(memberships));
|
|
105
|
+
const prompts = promptManagerFor && new PromptsHandler(promptManagerFor);
|
|
106
|
+
if (prompts) publicRouter.registerApi("prompts", prompts);
|
|
103
107
|
if (cliAuthorizer) {
|
|
104
108
|
publicRouter.register("authorize-cli", new AuthorizeCliHandler(cliAuthorizer, pages, tenant));
|
|
105
109
|
publicRouter.registerApi("keys", new KeysHandler(cliAuthorizer));
|
|
@@ -125,6 +129,7 @@ class HostingServer {
|
|
|
125
129
|
if (jobRunSchedules) internalRouter.registerApi("job-run-schedules", new JobRunSchedulesHandler(jobRunSchedules));
|
|
126
130
|
if (notifier) internalRouter.registerApi("notifications", new NotificationsHandler(notifier));
|
|
127
131
|
if (entitlements) internalRouter.registerApi("entitlements", new EntitlementsHandler(entitlements));
|
|
132
|
+
if (prompts) internalRouter.registerApi("prompts", prompts);
|
|
128
133
|
|
|
129
134
|
// The favicon is requested by the browser before anyone has signed in,
|
|
130
135
|
// and by deployed apps' pages, so gating it behind a session would send
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import {PromptManager} from "anbaric-tsapi";
|
|
2
|
+
import {Request} from "../Request";
|
|
3
|
+
import {RequestHandler} from "../RequestHandler";
|
|
4
|
+
|
|
5
|
+
/* Prompts belong to the calling app (the ambient app header): save a version,
|
|
6
|
+
fetch the latest or a named one, list, and read a prompt's history. */
|
|
7
|
+
class PromptsHandler implements RequestHandler {
|
|
8
|
+
|
|
9
|
+
constructor(private managerFor : (appId : string) => PromptManager) {}
|
|
10
|
+
|
|
11
|
+
async handle(request : Request) : Promise<void> {
|
|
12
|
+
const appId = request.appId;
|
|
13
|
+
if (! appId) return request.reply(400, { error: "Missing the x-anbaric-app header" });
|
|
14
|
+
|
|
15
|
+
const manager = this.managerFor(appId);
|
|
16
|
+
const { method, id, subresource } = request;
|
|
17
|
+
|
|
18
|
+
if (method === "GET" && ! id) return request.reply(200, await manager.list());
|
|
19
|
+
if (method === "PUT" && id && ! subresource) return this.save(request, manager, id);
|
|
20
|
+
if (method === "GET" && id && ! subresource) return this.retrieve(request, manager, id);
|
|
21
|
+
if (method === "GET" && id && subresource === "history") return request.reply(200, await manager.history(id));
|
|
22
|
+
request.notFound();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
private async save(request : Request, manager : PromptManager, promptId : string) : Promise<void> {
|
|
26
|
+
const body = await request.body() as { instructions? : string, inputSchema? : object, outputSchema? : object } | undefined;
|
|
27
|
+
if (typeof body?.instructions !== "string") return request.reply(400, { error: "instructions is required" });
|
|
28
|
+
|
|
29
|
+
request.reply(200, await manager.save(promptId, body.instructions, body.inputSchema, body.outputSchema));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private async retrieve(request : Request, manager : PromptManager, promptId : string) : Promise<void> {
|
|
33
|
+
const raw = request.query("version");
|
|
34
|
+
const version = raw === undefined ? undefined : Number(raw);
|
|
35
|
+
if (version !== undefined && ! Number.isInteger(version)) return request.reply(400, { error: `"${raw}" is not a version` });
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
request.reply(200, await manager.retrieve(promptId, version));
|
|
39
|
+
} catch (error) {
|
|
40
|
+
request.reply(404, { error: error instanceof Error ? error.message : String(error) });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export { PromptsHandler }
|