@app-vitals/agent-admin 1.5.0 → 1.7.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/openapi.platform.json +127 -0
- package/package.json +2 -2
- package/prisma/client/internal/class.ts +4 -4
- package/prisma/client/internal/prismaNamespace.ts +1 -0
- package/prisma/client/internal/prismaNamespaceBrowser.ts +1 -0
- package/prisma/client/models/Agent.ts +83 -1
- package/prisma/migrations/20261110000000_add_agent_baked_marketplaces/migration.sql +3 -0
- package/prisma/schema/schema.prisma +4 -0
- package/src/agents-api.smoke.test.ts +114 -0
- package/src/agents-api.ts +70 -1
- package/src/agents.baked-marketplaces.integration.test.ts +123 -0
- package/src/agents.ts +24 -0
- package/src/openapi-schemas.ts +20 -0
|
@@ -6485,3 +6485,117 @@ describe("admin API — PATCH /agents/:id specialty (SAL-1.2)", () => {
|
|
|
6485
6485
|
]);
|
|
6486
6486
|
});
|
|
6487
6487
|
});
|
|
6488
|
+
|
|
6489
|
+
// ─── PUT /agents/:id/baked-marketplaces (PMK-1.4) ─────────────────────────────
|
|
6490
|
+
|
|
6491
|
+
describe("admin API — baked marketplaces report", () => {
|
|
6492
|
+
const BAKED_URL = `/agents/${AGENT_ID}/baked-marketplaces`;
|
|
6493
|
+
let cookie: string;
|
|
6494
|
+
|
|
6495
|
+
beforeAll(async () => {
|
|
6496
|
+
cookie = await makeSessionCookie();
|
|
6497
|
+
});
|
|
6498
|
+
|
|
6499
|
+
/** Deps whose agentService stores/serves the baked list in memory. */
|
|
6500
|
+
function makeBakedDeps(tokenAgentId: string) {
|
|
6501
|
+
let baked: string[] = [];
|
|
6502
|
+
const base = makeDepsWithTokenValidation(async () => ({
|
|
6503
|
+
agentId: tokenAgentId,
|
|
6504
|
+
}));
|
|
6505
|
+
const deps: AdminDeps = {
|
|
6506
|
+
...base,
|
|
6507
|
+
agentService: {
|
|
6508
|
+
...base.agentService,
|
|
6509
|
+
getDetail: async (id: string) => {
|
|
6510
|
+
const d = await base.agentService.getDetail(id);
|
|
6511
|
+
return d ? { ...d, bakedMarketplaces: baked } : null;
|
|
6512
|
+
},
|
|
6513
|
+
setBakedMarketplaces: async (_id: string, names: string[]) => {
|
|
6514
|
+
baked = names;
|
|
6515
|
+
return names;
|
|
6516
|
+
},
|
|
6517
|
+
},
|
|
6518
|
+
};
|
|
6519
|
+
return { deps, stored: () => baked };
|
|
6520
|
+
}
|
|
6521
|
+
|
|
6522
|
+
const bearerPut = (app: ReturnType<typeof createAdminApp>, body: unknown) =>
|
|
6523
|
+
app.request(BAKED_URL, {
|
|
6524
|
+
method: "PUT",
|
|
6525
|
+
body: JSON.stringify(body),
|
|
6526
|
+
headers: {
|
|
6527
|
+
"Content-Type": "application/json",
|
|
6528
|
+
Authorization: `Bearer ${VALID_BEARER_TOKEN}`,
|
|
6529
|
+
},
|
|
6530
|
+
});
|
|
6531
|
+
const bearerGet = (app: ReturnType<typeof createAdminApp>) =>
|
|
6532
|
+
app.request(`/agents/${AGENT_ID}`, {
|
|
6533
|
+
headers: { Cookie: `admin_session=${cookie}` },
|
|
6534
|
+
});
|
|
6535
|
+
|
|
6536
|
+
it("stores the names for the agent's own key and GET shows them", async () => {
|
|
6537
|
+
const app = createAdminApp(makeBakedDeps(AGENT_ID).deps);
|
|
6538
|
+
const res = await bearerPut(app, { names: ["vitals-os", "shipwright"] });
|
|
6539
|
+
expect(res.status).toBe(200);
|
|
6540
|
+
expect((await res.json()).bakedMarketplaces).toEqual([
|
|
6541
|
+
"vitals-os",
|
|
6542
|
+
"shipwright",
|
|
6543
|
+
]);
|
|
6544
|
+
expect((await (await bearerGet(app)).json()).bakedMarketplaces).toEqual([
|
|
6545
|
+
"vitals-os",
|
|
6546
|
+
"shipwright",
|
|
6547
|
+
]);
|
|
6548
|
+
});
|
|
6549
|
+
|
|
6550
|
+
it("overwrites rather than appends", async () => {
|
|
6551
|
+
const app = createAdminApp(makeBakedDeps(AGENT_ID).deps);
|
|
6552
|
+
await bearerPut(app, { names: ["vitals-os", "shipwright"] });
|
|
6553
|
+
await bearerPut(app, { names: ["vitals-os"] });
|
|
6554
|
+
expect((await (await bearerGet(app)).json()).bakedMarketplaces).toEqual([
|
|
6555
|
+
"vitals-os",
|
|
6556
|
+
]);
|
|
6557
|
+
});
|
|
6558
|
+
|
|
6559
|
+
it("another agent's key is rejected (403) and stores nothing", async () => {
|
|
6560
|
+
const { deps, stored } = makeBakedDeps("other-agent");
|
|
6561
|
+
const res = await bearerPut(createAdminApp(deps), { names: ["x"] });
|
|
6562
|
+
expect(res.status).toBe(403);
|
|
6563
|
+
expect(stored()).toEqual([]);
|
|
6564
|
+
});
|
|
6565
|
+
|
|
6566
|
+
it("an admin session is rejected (403): only the agent's own key reports", async () => {
|
|
6567
|
+
const { deps, stored } = makeBakedDeps(AGENT_ID);
|
|
6568
|
+
const res = await createAdminApp(deps).request(BAKED_URL, {
|
|
6569
|
+
method: "PUT",
|
|
6570
|
+
body: JSON.stringify({ names: ["x"] }),
|
|
6571
|
+
headers: {
|
|
6572
|
+
"Content-Type": "application/json",
|
|
6573
|
+
Cookie: `admin_session=${cookie}`,
|
|
6574
|
+
},
|
|
6575
|
+
});
|
|
6576
|
+
expect(res.status).toBe(403);
|
|
6577
|
+
expect(stored()).toEqual([]);
|
|
6578
|
+
});
|
|
6579
|
+
|
|
6580
|
+
it("rejects unknown body fields (400)", async () => {
|
|
6581
|
+
const { deps } = makeBakedDeps(AGENT_ID);
|
|
6582
|
+
const res = await bearerPut(createAdminApp(deps), {
|
|
6583
|
+
names: [],
|
|
6584
|
+
extra: 1,
|
|
6585
|
+
});
|
|
6586
|
+
expect(res.status).toBe(400);
|
|
6587
|
+
});
|
|
6588
|
+
|
|
6589
|
+
it("PATCH /agents/:id cannot set the baked list", async () => {
|
|
6590
|
+
const { deps, stored } = makeBakedDeps(AGENT_ID);
|
|
6591
|
+
await createAdminApp(deps).request(`/agents/${AGENT_ID}`, {
|
|
6592
|
+
method: "PATCH",
|
|
6593
|
+
body: JSON.stringify({ bakedMarketplaces: ["evil"] }),
|
|
6594
|
+
headers: {
|
|
6595
|
+
"Content-Type": "application/json",
|
|
6596
|
+
Cookie: `admin_session=${cookie}`,
|
|
6597
|
+
},
|
|
6598
|
+
});
|
|
6599
|
+
expect(stored()).toEqual([]);
|
|
6600
|
+
});
|
|
6601
|
+
});
|
package/src/agents-api.ts
CHANGED
|
@@ -123,6 +123,7 @@ import {
|
|
|
123
123
|
AgentSummarySchema,
|
|
124
124
|
AgentTokenSchema,
|
|
125
125
|
AgentToolSchema,
|
|
126
|
+
BakedMarketplacesResultSchema,
|
|
126
127
|
ChatCredentialsWrapperSchema,
|
|
127
128
|
ChatTokenStatsSchema,
|
|
128
129
|
CreateAgentBodySchema,
|
|
@@ -154,6 +155,7 @@ import {
|
|
|
154
155
|
PatchAgentPluginBodySchema,
|
|
155
156
|
PatchAgentToolBodySchema,
|
|
156
157
|
PluginNameQuerySchema,
|
|
158
|
+
PutBakedMarketplacesBodySchema,
|
|
157
159
|
PutGitHubInstallationsBodySchema,
|
|
158
160
|
TokenIdParamSchema,
|
|
159
161
|
ToolIdParamSchema,
|
|
@@ -182,7 +184,10 @@ export interface AdminDeps {
|
|
|
182
184
|
// through the narrower Pick above.
|
|
183
185
|
| "updateFields"
|
|
184
186
|
| "runTransaction"
|
|
185
|
-
|
|
187
|
+
> &
|
|
188
|
+
// PMK-1.4: optional so existing AdminDeps consumers stay valid; the
|
|
189
|
+
// baked-marketplaces report route fails with a 500 when it is not wired.
|
|
190
|
+
Partial<Pick<AgentService, "setBakedMarketplaces">>;
|
|
186
191
|
agentEnvService: Pick<
|
|
187
192
|
AgentEnvService,
|
|
188
193
|
"upsert" | "patch" | "getByAgentId" | "deleteKey"
|
|
@@ -453,6 +458,15 @@ const GetAgentResultSchema = z
|
|
|
453
458
|
*/
|
|
454
459
|
trialExpiresAt: z.string().datetime().nullable().optional(),
|
|
455
460
|
trialExpiryWarnedAt: z.string().datetime().nullable().optional(),
|
|
461
|
+
/**
|
|
462
|
+
* PMK-1.4: names of the marketplaces baked into the agent's image, as last
|
|
463
|
+
* reported by the runtime at boot. Read-only: only the runtime-authenticated
|
|
464
|
+
* PUT /agents/:id/baked-marketplaces route writes it.
|
|
465
|
+
*/
|
|
466
|
+
bakedMarketplaces: z
|
|
467
|
+
.array(z.string())
|
|
468
|
+
.optional()
|
|
469
|
+
.openapi({ example: ["vitals-os", "shipwright"] }),
|
|
456
470
|
/** APM-1.3: the six real agent-policy fields (APM-1.1 DB columns). */
|
|
457
471
|
autoPostReviews: z.boolean().openapi({ example: true }),
|
|
458
472
|
allowSelfReview: z.boolean().openapi({ example: false }),
|
|
@@ -1341,6 +1355,37 @@ const putGitHubInstallationsRoute = createRoute({
|
|
|
1341
1355
|
},
|
|
1342
1356
|
});
|
|
1343
1357
|
|
|
1358
|
+
const putBakedMarketplacesRoute = createRoute({
|
|
1359
|
+
method: "put",
|
|
1360
|
+
path: "/agents/{id}/baked-marketplaces",
|
|
1361
|
+
summary: "Report the marketplaces baked into the agent's image",
|
|
1362
|
+
description:
|
|
1363
|
+
"Runtime-only report, authenticated with the agent's own key. Overwrites the stored list of baked marketplace names (the image's current state); it is exposed read-only as `bakedMarketplaces` on GET /agents/{id}. Other callers, including another agent's key, get 403.",
|
|
1364
|
+
request: {
|
|
1365
|
+
params: AgentIdParamSchema,
|
|
1366
|
+
body: {
|
|
1367
|
+
content: {
|
|
1368
|
+
"application/json": { schema: PutBakedMarketplacesBodySchema },
|
|
1369
|
+
},
|
|
1370
|
+
},
|
|
1371
|
+
},
|
|
1372
|
+
responses: {
|
|
1373
|
+
200: {
|
|
1374
|
+
description: "Baked marketplaces stored",
|
|
1375
|
+
content: {
|
|
1376
|
+
"application/json": { schema: BakedMarketplacesResultSchema },
|
|
1377
|
+
},
|
|
1378
|
+
},
|
|
1379
|
+
400: { description: "Bad request", ...jsonError },
|
|
1380
|
+
401: { description: "Unauthorized", ...jsonError },
|
|
1381
|
+
403: {
|
|
1382
|
+
description: "Forbidden — only the agent's own key may report",
|
|
1383
|
+
...jsonError,
|
|
1384
|
+
},
|
|
1385
|
+
404: { description: "Agent not found", ...jsonError },
|
|
1386
|
+
},
|
|
1387
|
+
});
|
|
1388
|
+
|
|
1344
1389
|
const getGitHubInstallationsRoute = createRoute({
|
|
1345
1390
|
method: "get",
|
|
1346
1391
|
path: "/agents/{id}/github-installations",
|
|
@@ -2747,6 +2792,28 @@ export function createAdminApp(deps: AdminDeps): OpenAPIHono<AdminAuthEnv> {
|
|
|
2747
2792
|
);
|
|
2748
2793
|
});
|
|
2749
2794
|
|
|
2795
|
+
// PUT /agents/:id/baked-marketplaces — runtime report (PMK-1.4), overwrite.
|
|
2796
|
+
// Restricted to the agent's own key: admins, members and other agents' keys
|
|
2797
|
+
// never write this value.
|
|
2798
|
+
app.openapi(putBakedMarketplacesRoute, async (c) => {
|
|
2799
|
+
const { id: agentId } = c.req.valid("param");
|
|
2800
|
+
const scope = c.get("scope") as AdminScope | undefined;
|
|
2801
|
+
if (scope?.kind !== "agent" || scope.agentId !== agentId) {
|
|
2802
|
+
throw new ForbiddenError(
|
|
2803
|
+
"Only the agent's own key may report baked marketplaces",
|
|
2804
|
+
);
|
|
2805
|
+
}
|
|
2806
|
+
if (!agentService.setBakedMarketplaces) {
|
|
2807
|
+
throw new Error("agentService.setBakedMarketplaces is not wired");
|
|
2808
|
+
}
|
|
2809
|
+
const { names } = c.req.valid("json");
|
|
2810
|
+
const stored = await agentService.setBakedMarketplaces(agentId, [
|
|
2811
|
+
...new Set(names),
|
|
2812
|
+
]);
|
|
2813
|
+
if (!stored) throw new NotFoundError(`agent ${agentId} not found`);
|
|
2814
|
+
return c.json({ bakedMarketplaces: stored }, 200);
|
|
2815
|
+
});
|
|
2816
|
+
|
|
2750
2817
|
// GET /agents/:id/github-installations — latest snapshot, 404 if none yet
|
|
2751
2818
|
app.openapi(getGitHubInstallationsRoute, async (c) => {
|
|
2752
2819
|
const { id: agentId } = c.req.valid("param");
|
|
@@ -2990,6 +3057,7 @@ function serializeAgent(
|
|
|
2990
3057
|
missingRequiredEnv?: string[];
|
|
2991
3058
|
trialExpiresAt?: Date | null;
|
|
2992
3059
|
trialExpiryWarnedAt?: Date | null;
|
|
3060
|
+
bakedMarketplaces?: string[];
|
|
2993
3061
|
autoPostReviews?: boolean;
|
|
2994
3062
|
allowSelfReview?: boolean;
|
|
2995
3063
|
minConfidence?: number;
|
|
@@ -3024,6 +3092,7 @@ function serializeAgent(
|
|
|
3024
3092
|
trialExpiryWarnedAt: agent.trialExpiryWarnedAt
|
|
3025
3093
|
? agent.trialExpiryWarnedAt.toISOString()
|
|
3026
3094
|
: null,
|
|
3095
|
+
bakedMarketplaces: agent.bakedMarketplaces ?? [],
|
|
3027
3096
|
autoPostReviews: agent.autoPostReviews ?? true,
|
|
3028
3097
|
allowSelfReview: agent.allowSelfReview ?? false,
|
|
3029
3098
|
minConfidence: agent.minConfidence ?? 75,
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PMK-1.4: integration tests (real Postgres) for the baked-marketplaces store:
|
|
3
|
+
* AgentService.setBakedMarketplaces, overwrite semantics, GET detail exposure,
|
|
4
|
+
* and the absence of any other write path.
|
|
5
|
+
*
|
|
6
|
+
* Requires DATABASE_URL_ADMIN_TEST; skips otherwise.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
|
10
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import type { PrismaClient } from "../prisma/client/client.ts";
|
|
13
|
+
import { AgentService } from "./agents.ts";
|
|
14
|
+
import { PatchAgentBodySchema } from "./openapi-schemas.ts";
|
|
15
|
+
import { createAdminPrismaClient } from "./prisma-client.ts";
|
|
16
|
+
|
|
17
|
+
const TEST_DB = process.env.DATABASE_URL_ADMIN_TEST;
|
|
18
|
+
const describeOrSkip = TEST_DB ? describe : describe.skip;
|
|
19
|
+
|
|
20
|
+
describeOrSkip("baked marketplaces store (integration)", () => {
|
|
21
|
+
let prisma: PrismaClient;
|
|
22
|
+
let service: AgentService;
|
|
23
|
+
|
|
24
|
+
beforeEach(async () => {
|
|
25
|
+
prisma = createAdminPrismaClient(TEST_DB as string);
|
|
26
|
+
await prisma.agentGitHubInstallationsSnapshot.deleteMany();
|
|
27
|
+
await prisma.agentWorkQueueSnapshot.deleteMany();
|
|
28
|
+
await prisma.agentChatTokenUsageDailyByModel.deleteMany();
|
|
29
|
+
await prisma.agentCronRun.deleteMany();
|
|
30
|
+
await prisma.agentToken.deleteMany();
|
|
31
|
+
await prisma.agentCronJob.deleteMany();
|
|
32
|
+
await prisma.agentTool.deleteMany();
|
|
33
|
+
await prisma.agentEnv.deleteMany();
|
|
34
|
+
await prisma.agentMember.deleteMany();
|
|
35
|
+
await prisma.agent.deleteMany();
|
|
36
|
+
service = new AgentService(prisma);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
afterEach(async () => {
|
|
40
|
+
await prisma.$disconnect();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("defaults to an empty list", async () => {
|
|
44
|
+
const { id } = await prisma.agent.create({ data: { name: "a" } });
|
|
45
|
+
expect((await service.getDetail(id))?.bakedMarketplaces).toEqual([]);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("stores the reported names and getDetail exposes them", async () => {
|
|
49
|
+
const { id } = await prisma.agent.create({ data: { name: "a" } });
|
|
50
|
+
const stored = await service.setBakedMarketplaces(id, [
|
|
51
|
+
"vitals-os",
|
|
52
|
+
"shipwright",
|
|
53
|
+
]);
|
|
54
|
+
expect(stored).toEqual(["vitals-os", "shipwright"]);
|
|
55
|
+
expect((await service.getDetail(id))?.bakedMarketplaces).toEqual([
|
|
56
|
+
"vitals-os",
|
|
57
|
+
"shipwright",
|
|
58
|
+
]);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("overwrites on every report instead of appending", async () => {
|
|
62
|
+
const { id } = await prisma.agent.create({ data: { name: "a" } });
|
|
63
|
+
await service.setBakedMarketplaces(id, ["vitals-os", "shipwright"]);
|
|
64
|
+
await service.setBakedMarketplaces(id, ["vitals-os"]);
|
|
65
|
+
expect((await service.getDetail(id))?.bakedMarketplaces).toEqual([
|
|
66
|
+
"vitals-os",
|
|
67
|
+
]);
|
|
68
|
+
await service.setBakedMarketplaces(id, []);
|
|
69
|
+
expect((await service.getDetail(id))?.bakedMarketplaces).toEqual([]);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("only touches the reporting agent's row", async () => {
|
|
73
|
+
const a = await prisma.agent.create({ data: { name: "a" } });
|
|
74
|
+
const b = await prisma.agent.create({ data: { name: "b" } });
|
|
75
|
+
await service.setBakedMarketplaces(a.id, ["x"]);
|
|
76
|
+
expect((await service.getDetail(b.id))?.bakedMarketplaces).toEqual([]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("returns null for an unknown agent", async () => {
|
|
80
|
+
expect(await service.setBakedMarketplaces("nope", ["x"])).toBeNull();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("updateSelfHosted (the PATCH writer) never writes the baked list", async () => {
|
|
84
|
+
const { id } = await prisma.agent.create({ data: { name: "a" } });
|
|
85
|
+
await service.setBakedMarketplaces(id, ["vitals-os"]);
|
|
86
|
+
await service.updateSelfHosted(id, {
|
|
87
|
+
selfHosted: true,
|
|
88
|
+
// biome-ignore lint/suspicious/noExplicitAny: proving an off-contract key is ignored
|
|
89
|
+
...({ bakedMarketplaces: ["evil"] } as any),
|
|
90
|
+
});
|
|
91
|
+
expect((await service.getDetail(id))?.bakedMarketplaces).toEqual([
|
|
92
|
+
"vitals-os",
|
|
93
|
+
]);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("PatchAgentBodySchema does not carry the field through", () => {
|
|
97
|
+
const parsed = PatchAgentBodySchema.safeParse({
|
|
98
|
+
bakedMarketplaces: ["evil"],
|
|
99
|
+
});
|
|
100
|
+
if (parsed.success) {
|
|
101
|
+
expect("bakedMarketplaces" in parsed.data).toBe(false);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("no admin source other than the report path writes bakedMarketplaces", () => {
|
|
106
|
+
const dir = import.meta.dir;
|
|
107
|
+
const writers = readdirSync(dir)
|
|
108
|
+
.filter((f) => f.endsWith(".ts") && !/\.test\.ts$/.test(f))
|
|
109
|
+
.filter((f) =>
|
|
110
|
+
/bakedMarketplaces/.test(readFileSync(join(dir, f), "utf8")),
|
|
111
|
+
)
|
|
112
|
+
.sort();
|
|
113
|
+
// agents.ts: writer + read select; agents-api.ts: report route + read
|
|
114
|
+
// serialization; openapi-schemas.ts: response/read schemas only.
|
|
115
|
+
expect(writers).toEqual([
|
|
116
|
+
"agents-api.ts",
|
|
117
|
+
"agents.ts",
|
|
118
|
+
"openapi-schemas.ts",
|
|
119
|
+
]);
|
|
120
|
+
const api = readFileSync(join(dir, "agents-api.ts"), "utf8");
|
|
121
|
+
expect(api.match(/setBakedMarketplaces\(/g)?.length).toBe(1);
|
|
122
|
+
});
|
|
123
|
+
});
|
package/src/agents.ts
CHANGED
|
@@ -125,6 +125,12 @@ export interface AgentDetail extends Partial<AgentAnthropicIdentity> {
|
|
|
125
125
|
*/
|
|
126
126
|
trialExpiresAt?: Date | null;
|
|
127
127
|
trialExpiryWarnedAt?: Date | null;
|
|
128
|
+
/**
|
|
129
|
+
* PMK-1.4: marketplace names baked into the agent's image, as last reported
|
|
130
|
+
* by the runtime. Read-only here; written only by setBakedMarketplaces().
|
|
131
|
+
* Optional so existing AgentDetail test doubles stay valid.
|
|
132
|
+
*/
|
|
133
|
+
bakedMarketplaces?: string[];
|
|
128
134
|
/**
|
|
129
135
|
* APM-1.3: the six real agent-policy fields (APM-1.1 DB columns,
|
|
130
136
|
* Prisma-column defaults noted per field below). Optional here (like
|
|
@@ -281,6 +287,7 @@ const DETAIL_SELECT = {
|
|
|
281
287
|
updatedAt: true,
|
|
282
288
|
trialExpiresAt: true,
|
|
283
289
|
trialExpiryWarnedAt: true,
|
|
290
|
+
bakedMarketplaces: true,
|
|
284
291
|
autoPostReviews: true,
|
|
285
292
|
allowSelfReview: true,
|
|
286
293
|
minConfidence: true,
|
|
@@ -454,6 +461,23 @@ export class AgentService {
|
|
|
454
461
|
return { ...row, missingRequiredEnv };
|
|
455
462
|
}
|
|
456
463
|
|
|
464
|
+
/**
|
|
465
|
+
* PMK-1.4: overwrite the agent's reported baked-marketplace names. Called
|
|
466
|
+
* only by the runtime-authenticated PUT /agents/:id/baked-marketplaces
|
|
467
|
+
* route; no PATCH field or UI control reaches it. Returns the stored list,
|
|
468
|
+
* or null when the agent does not exist.
|
|
469
|
+
*/
|
|
470
|
+
async setBakedMarketplaces(
|
|
471
|
+
id: string,
|
|
472
|
+
names: string[],
|
|
473
|
+
): Promise<string[] | null> {
|
|
474
|
+
const res = await this.prisma.agent.updateMany({
|
|
475
|
+
where: { id },
|
|
476
|
+
data: { bakedMarketplaces: names },
|
|
477
|
+
});
|
|
478
|
+
return res.count === 0 ? null : names;
|
|
479
|
+
}
|
|
480
|
+
|
|
457
481
|
/**
|
|
458
482
|
* Returns whether an agent with the given id exists.
|
|
459
483
|
*/
|
package/src/openapi-schemas.ts
CHANGED
|
@@ -1078,6 +1078,26 @@ export const AgentGitHubInstallationsSnapshotSchema = z
|
|
|
1078
1078
|
|
|
1079
1079
|
// ─── Path param schemas ───────────────────────────────────────────────────────
|
|
1080
1080
|
|
|
1081
|
+
/**
|
|
1082
|
+
* PUT /agents/:id/baked-marketplaces body (PMK-1.4). Runtime-only report:
|
|
1083
|
+
* `names` overwrites the stored list.
|
|
1084
|
+
*/
|
|
1085
|
+
export const PutBakedMarketplacesBodySchema = z
|
|
1086
|
+
.object({
|
|
1087
|
+
names: z
|
|
1088
|
+
.array(z.string().min(1).max(255))
|
|
1089
|
+
.max(100)
|
|
1090
|
+
.openapi({ example: ["vitals-os", "shipwright"] }),
|
|
1091
|
+
})
|
|
1092
|
+
.strict()
|
|
1093
|
+
.openapi("PutBakedMarketplacesBody");
|
|
1094
|
+
|
|
1095
|
+
export const BakedMarketplacesResultSchema = z
|
|
1096
|
+
.object({
|
|
1097
|
+
bakedMarketplaces: z.array(z.string()).openapi({ example: ["vitals-os"] }),
|
|
1098
|
+
})
|
|
1099
|
+
.openapi("BakedMarketplacesResult");
|
|
1100
|
+
|
|
1081
1101
|
export const AgentIdParamSchema = z.object({
|
|
1082
1102
|
id: z.string().openapi({ example: "clx1234567890" }),
|
|
1083
1103
|
});
|