@app-vitals/agent-admin 1.3.0 → 1.5.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.json +254 -0
- package/openapi.platform.json +254 -0
- package/package.json +2 -2
- package/prisma/client/browser.ts +10 -0
- package/prisma/client/client.ts +10 -0
- package/prisma/client/internal/class.ts +24 -4
- package/prisma/client/internal/prismaNamespace.ts +183 -2
- package/prisma/client/internal/prismaNamespaceBrowser.ts +32 -1
- package/prisma/client/models/Agent.ts +282 -0
- package/prisma/client/models/AgentMarketplace.ts +1421 -0
- package/prisma/client/models/GitHubEnterpriseApp.ts +1289 -0
- package/prisma/client/models.ts +2 -0
- package/prisma/migrations/20261101000000_add_agent_marketplace/migration.sql +19 -0
- package/prisma/migrations/20261109000000_add_github_enterprise_app/migration.sql +16 -0
- package/prisma/schema/schema.prisma +37 -0
- package/src/admin-boot.ts +5 -1
- package/src/agent-marketplaces-api.integration.test.ts +263 -0
- package/src/agent-marketplaces.integration.test.ts +82 -0
- package/src/agent-marketplaces.ts +120 -0
- package/src/agent-marketplaces.unit.test.ts +72 -0
- package/src/agents-api.ts +117 -1
- package/src/api.ts +29 -5
- package/src/github-enterprise-apps.integration.test.ts +152 -0
- package/src/github-enterprise-apps.ts +204 -0
- package/src/github-enterprise-apps.unit.test.ts +42 -0
- package/src/openapi-schemas.ts +56 -0
- package/src/openapi-schemas.unit.test.ts +29 -0
- package/src/token-crypto.ts +25 -0
- package/src/token-crypto.unit.test.ts +24 -1
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the GitHubEnterpriseApp serializer: metadata plus a
|
|
3
|
+
* "key set" flag only, never the private key (plain or encrypted).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, expect, it } from "bun:test";
|
|
7
|
+
import { toGitHubEnterpriseAppMetadata } from "./github-enterprise-apps.ts";
|
|
8
|
+
|
|
9
|
+
describe("toGitHubEnterpriseAppMetadata", () => {
|
|
10
|
+
const row = {
|
|
11
|
+
role: "installer",
|
|
12
|
+
appId: "123",
|
|
13
|
+
slug: "my-app",
|
|
14
|
+
enterpriseSlug: "acme",
|
|
15
|
+
privateKeyEncrypted: "aa:bb:cc",
|
|
16
|
+
lastValidationStatus: "ok",
|
|
17
|
+
lastValidatedAt: new Date("2026-01-01T00:00:00Z"),
|
|
18
|
+
lastValidationError: null,
|
|
19
|
+
createdAt: new Date("2026-01-01T00:00:00Z"),
|
|
20
|
+
updatedAt: new Date("2026-01-02T00:00:00Z"),
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
it("returns metadata and keySet, with no key material", () => {
|
|
24
|
+
const out = toGitHubEnterpriseAppMetadata(row);
|
|
25
|
+
expect(out.keySet).toBe(true);
|
|
26
|
+
expect(out).toMatchObject({
|
|
27
|
+
role: "installer",
|
|
28
|
+
appId: "123",
|
|
29
|
+
slug: "my-app",
|
|
30
|
+
});
|
|
31
|
+
expect(Object.keys(out).some((k) => /key/i.test(k) && k !== "keySet")).toBe(
|
|
32
|
+
false,
|
|
33
|
+
);
|
|
34
|
+
expect(JSON.stringify(out)).not.toContain("aa:bb:cc");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("keySet is false when no ciphertext is stored", () => {
|
|
38
|
+
expect(
|
|
39
|
+
toGitHubEnterpriseAppMetadata({ ...row, privateKeyEncrypted: "" }).keySet,
|
|
40
|
+
).toBe(false);
|
|
41
|
+
});
|
|
42
|
+
});
|
package/src/openapi-schemas.ts
CHANGED
|
@@ -895,6 +895,43 @@ export const PatchAgentPluginBodySchema = z
|
|
|
895
895
|
})
|
|
896
896
|
.openapi("PatchAgentPluginBody");
|
|
897
897
|
|
|
898
|
+
// ─── AgentMarketplace ─────────────────────────────────────────────────────────
|
|
899
|
+
|
|
900
|
+
export const AgentMarketplaceSchema = z
|
|
901
|
+
.object({
|
|
902
|
+
id: z.string().openapi({ example: "clx1234567890" }),
|
|
903
|
+
agentId: z.string().openapi({ example: "clx1234567890" }),
|
|
904
|
+
name: z.string().openapi({ example: "shipwright" }),
|
|
905
|
+
source: z.string().openapi({ example: "app-vitals/shipwright" }),
|
|
906
|
+
ref: z.string().nullable().openapi({ example: "main" }),
|
|
907
|
+
createdAt: z
|
|
908
|
+
.string()
|
|
909
|
+
.datetime()
|
|
910
|
+
.openapi({ example: "2026-01-01T00:00:00.000Z" }),
|
|
911
|
+
updatedAt: z
|
|
912
|
+
.string()
|
|
913
|
+
.datetime()
|
|
914
|
+
.openapi({ example: "2026-01-01T00:00:00.000Z" }),
|
|
915
|
+
})
|
|
916
|
+
.openapi("AgentMarketplace");
|
|
917
|
+
|
|
918
|
+
export type AgentMarketplace = z.infer<typeof AgentMarketplaceSchema>;
|
|
919
|
+
|
|
920
|
+
/**
|
|
921
|
+
* `source` / `name` / `ref` are validated by AgentMarketplaceService (422), not
|
|
922
|
+
* here — the Zod layer answers 400.
|
|
923
|
+
*/
|
|
924
|
+
export const UpsertAgentMarketplaceBodySchema = z
|
|
925
|
+
.object({
|
|
926
|
+
name: z.string().openapi({ example: "shipwright" }),
|
|
927
|
+
source: z.string().openapi({
|
|
928
|
+
description: "GitHub `owner/repo` or an https git URL",
|
|
929
|
+
example: "app-vitals/shipwright",
|
|
930
|
+
}),
|
|
931
|
+
ref: z.string().nullable().optional().openapi({ example: "main" }),
|
|
932
|
+
})
|
|
933
|
+
.openapi("UpsertAgentMarketplaceBody");
|
|
934
|
+
|
|
898
935
|
// ─── Pipeline phases ────────────────────────────────────────────────────────────
|
|
899
936
|
//
|
|
900
937
|
// The six pipeline phases a methodology override can target — the keys of the
|
|
@@ -1084,6 +1121,11 @@ export const TokenIdParamSchema = z.object({
|
|
|
1084
1121
|
tokenId: z.string().openapi({ example: "clx0987654321" }),
|
|
1085
1122
|
});
|
|
1086
1123
|
|
|
1124
|
+
export const MarketplaceIdParamSchema = z.object({
|
|
1125
|
+
id: z.string().openapi({ example: "clx1234567890" }),
|
|
1126
|
+
marketplaceId: z.string().openapi({ example: "clx0987654321" }),
|
|
1127
|
+
});
|
|
1128
|
+
|
|
1087
1129
|
export const EnvKeyParamSchema = z.object({
|
|
1088
1130
|
id: z.string().openapi({ example: "clx1234567890" }),
|
|
1089
1131
|
key: z.string().openapi({ example: "MY_VAR" }),
|
|
@@ -1298,6 +1340,14 @@ const AgentConfigPluginSchema = z
|
|
|
1298
1340
|
})
|
|
1299
1341
|
.openapi("AgentConfigPlugin");
|
|
1300
1342
|
|
|
1343
|
+
const AgentConfigMarketplaceSchema = z
|
|
1344
|
+
.object({
|
|
1345
|
+
name: z.string().openapi({ example: "shipwright" }),
|
|
1346
|
+
source: z.string().openapi({ example: "app-vitals/shipwright" }),
|
|
1347
|
+
ref: z.string().nullable().openapi({ example: "main" }),
|
|
1348
|
+
})
|
|
1349
|
+
.openapi("AgentConfigMarketplace");
|
|
1350
|
+
|
|
1301
1351
|
export const AgentConfigResponseSchema = z
|
|
1302
1352
|
.object({
|
|
1303
1353
|
env: z
|
|
@@ -1305,6 +1355,12 @@ export const AgentConfigResponseSchema = z
|
|
|
1305
1355
|
.openapi({ example: { SLACK_BOT_TOKEN: "xoxb-..." } }),
|
|
1306
1356
|
allowedTools: z.array(z.string()).openapi({ example: ["Read", "Write"] }),
|
|
1307
1357
|
plugins: z.array(AgentConfigPluginSchema),
|
|
1358
|
+
/**
|
|
1359
|
+
* PMK-1.2: remote plugin marketplaces to register before plugin install.
|
|
1360
|
+
* Optional so existing fixtures/doubles keep compiling; the handler
|
|
1361
|
+
* always populates it (empty array when the agent has none).
|
|
1362
|
+
*/
|
|
1363
|
+
marketplaces: z.array(AgentConfigMarketplaceSchema).optional(),
|
|
1308
1364
|
repos: z.array(z.string()).openapi({ example: ["org/repo1", "org/repo2"] }),
|
|
1309
1365
|
reviewAuthorAllowlist: z
|
|
1310
1366
|
.array(z.string())
|
|
@@ -10,6 +10,7 @@ import { type AgentRuntimeDeps, createAgentRuntimeApp } from "./api.ts";
|
|
|
10
10
|
import {
|
|
11
11
|
type AgentCronJob,
|
|
12
12
|
AgentCronJobSchema,
|
|
13
|
+
AgentMarketplaceSchema,
|
|
13
14
|
type AgentPlugin,
|
|
14
15
|
AgentPluginSchema,
|
|
15
16
|
type AgentToken,
|
|
@@ -18,6 +19,7 @@ import {
|
|
|
18
19
|
AgentToolSchema,
|
|
19
20
|
ErrorSchema,
|
|
20
21
|
PatchAgentBodySchema,
|
|
22
|
+
UpsertAgentMarketplaceBodySchema,
|
|
21
23
|
} from "./openapi-schemas.ts";
|
|
22
24
|
|
|
23
25
|
// ─── Fixtures ─────────────────────────────────────────────────────────────────
|
|
@@ -381,3 +383,30 @@ describe("PatchAgentBodySchema.podServiceAccountName (COS-49.1)", () => {
|
|
|
381
383
|
}
|
|
382
384
|
});
|
|
383
385
|
});
|
|
386
|
+
|
|
387
|
+
// ─── AgentMarketplace (PMK-1.2) ───────────────────────────────────────────────
|
|
388
|
+
|
|
389
|
+
describe("AgentMarketplace schemas", () => {
|
|
390
|
+
test("AgentMarketplaceSchema accepts a row with a null ref", () => {
|
|
391
|
+
const result = AgentMarketplaceSchema.safeParse({
|
|
392
|
+
id: "m1",
|
|
393
|
+
agentId: "a1",
|
|
394
|
+
name: "shipwright",
|
|
395
|
+
source: "app-vitals/shipwright",
|
|
396
|
+
ref: null,
|
|
397
|
+
createdAt: "2026-01-01T00:00:00.000Z",
|
|
398
|
+
updatedAt: "2026-01-01T00:00:00.000Z",
|
|
399
|
+
});
|
|
400
|
+
expect(result.success).toBe(true);
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
test("UpsertAgentMarketplaceBodySchema leaves source validation to the service (422)", () => {
|
|
404
|
+
expect(
|
|
405
|
+
UpsertAgentMarketplaceBodySchema.safeParse({ name: "m", source: "bad" })
|
|
406
|
+
.success,
|
|
407
|
+
).toBe(true);
|
|
408
|
+
expect(
|
|
409
|
+
UpsertAgentMarketplaceBodySchema.safeParse({ name: "m" }).success,
|
|
410
|
+
).toBe(false);
|
|
411
|
+
});
|
|
412
|
+
});
|
package/src/token-crypto.ts
CHANGED
|
@@ -58,3 +58,28 @@ export function makeTokenCrypto(
|
|
|
58
58
|
},
|
|
59
59
|
};
|
|
60
60
|
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Strict TokenCrypto for credentials that must never be stored or passed on as
|
|
64
|
+
* plain text (e.g. GitHub Enterprise App private keys).
|
|
65
|
+
*
|
|
66
|
+
* Differences from makeTokenCrypto(): returns null when no key is configured
|
|
67
|
+
* (callers must refuse to save, not fall back to identity), and decrypt()
|
|
68
|
+
* throws on any failure instead of returning the stored value as legacy
|
|
69
|
+
* plain text. A malformed key still throws at construction. Error messages
|
|
70
|
+
* never include the input value.
|
|
71
|
+
*/
|
|
72
|
+
export function makeStrictTokenCrypto(
|
|
73
|
+
key = process.env.SHIPWRIGHT_ENCRYPTION_KEY,
|
|
74
|
+
): TokenCrypto | null {
|
|
75
|
+
if (!key) return null;
|
|
76
|
+
if (!isValidKeyHex(key)) {
|
|
77
|
+
throw new Error(
|
|
78
|
+
`SHIPWRIGHT_ENCRYPTION_KEY must be a 64-character hex string (32 bytes for AES-256-GCM), but got ${key.length} character(s). Generate one with: openssl rand -hex 32`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
encrypt: (token) => encrypt(token, key),
|
|
83
|
+
decrypt: (encrypted) => decrypt(encrypted, key),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
@@ -6,7 +6,11 @@
|
|
|
6
6
|
|
|
7
7
|
import { afterEach, describe, expect, it } from "bun:test";
|
|
8
8
|
import { decrypt, encrypt, isValidKeyHex } from "./crypto.ts";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
identityCrypto,
|
|
11
|
+
makeStrictTokenCrypto,
|
|
12
|
+
makeTokenCrypto,
|
|
13
|
+
} from "./token-crypto.ts";
|
|
10
14
|
|
|
11
15
|
// ─── Test key ─────────────────────────────────────────────────────────────────
|
|
12
16
|
// A known 64-char hex key (32 bytes) for deterministic tests.
|
|
@@ -180,3 +184,22 @@ describe("identityCrypto", () => {
|
|
|
180
184
|
expect(identityCrypto.decrypt("hello")).toBe("hello");
|
|
181
185
|
});
|
|
182
186
|
});
|
|
187
|
+
|
|
188
|
+
describe("makeStrictTokenCrypto", () => {
|
|
189
|
+
const KEY = "c".repeat(64);
|
|
190
|
+
|
|
191
|
+
it("returns null when no key is configured (no identity fallback)", () => {
|
|
192
|
+
expect(makeStrictTokenCrypto(undefined)).toBeNull();
|
|
193
|
+
expect(makeStrictTokenCrypto("")).toBeNull();
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("throws on a malformed key", () => {
|
|
197
|
+
expect(() => makeStrictTokenCrypto("short")).toThrow(/64-character/);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("round-trips and throws on non-ciphertext instead of passing it through", () => {
|
|
201
|
+
const c = makeStrictTokenCrypto(KEY);
|
|
202
|
+
expect(c?.decrypt(c.encrypt("secret"))).toBe("secret");
|
|
203
|
+
expect(() => c?.decrypt("legacy-plain-text")).toThrow();
|
|
204
|
+
});
|
|
205
|
+
});
|