@app-vitals/agent-runtime 1.6.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.
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Baked marketplace reporter — at boot, lists the marketplace directories
3
+ * baked into the image (BAKED_MARKETPLACES_ROOT) and reports their names to
4
+ * admin: PUT /agents/:agentId/baked-marketplaces (overwrite). Read-only
5
+ * visibility: admin stores the list so the UI can show what the image ships.
6
+ *
7
+ * Fire-and-forget: never throws; non-2xx and thrown errors are swallowed with
8
+ * console.warn. The payload is the directory basenames only.
9
+ */
10
+
11
+ import { basename } from "node:path";
12
+ import { BAKED_MARKETPLACES_ROOT, discoverBakedMarketplaces } from "./setup.ts";
13
+
14
+ const REQUEST_TIMEOUT_MS = 10_000;
15
+
16
+ interface ReportBakedMarketplacesOptions {
17
+ apiUrl: string;
18
+ agentId: string;
19
+ apiKey: string;
20
+ root?: string;
21
+ fetchFn?: typeof fetch;
22
+ }
23
+
24
+ export async function reportBakedMarketplaces(
25
+ opts: ReportBakedMarketplacesOptions,
26
+ ): Promise<void> {
27
+ const url = `${opts.apiUrl}/agents/${opts.agentId}/baked-marketplaces`;
28
+ try {
29
+ const names = discoverBakedMarketplaces(
30
+ opts.root ?? BAKED_MARKETPLACES_ROOT,
31
+ ).map((dir) => basename(dir));
32
+ const res = await (opts.fetchFn ?? fetch)(url, {
33
+ method: "PUT",
34
+ headers: {
35
+ "Content-Type": "application/json",
36
+ Authorization: `Bearer ${opts.apiKey}`,
37
+ },
38
+ body: JSON.stringify({ names }),
39
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
40
+ });
41
+ if (!res.ok) {
42
+ console.warn(
43
+ `[baked-marketplaces-reporter] PUT ${url} returned ${res.status} — swallowing`,
44
+ );
45
+ }
46
+ } catch (err) {
47
+ console.warn(
48
+ `[baked-marketplaces-reporter] PUT ${url} failed: ${String(err)} — swallowing`,
49
+ );
50
+ }
51
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Unit tests for reportBakedMarketplaces: reads the baked marketplace dirs,
3
+ * PUTs the names once, and never throws.
4
+ */
5
+
6
+ import { afterEach, describe, expect, test } from "bun:test";
7
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
8
+ import { tmpdir } from "node:os";
9
+ import { join } from "node:path";
10
+ import { reportBakedMarketplaces } from "./baked-marketplaces-reporter.ts";
11
+
12
+ const roots: string[] = [];
13
+ afterEach(() => {
14
+ for (const r of roots.splice(0)) rmSync(r, { recursive: true, force: true });
15
+ });
16
+
17
+ function bakedRoot(names: string[]): string {
18
+ const root = mkdtempSync(join(tmpdir(), "baked-"));
19
+ roots.push(root);
20
+ for (const n of names) {
21
+ mkdirSync(join(root, n, ".claude-plugin"), { recursive: true });
22
+ writeFileSync(join(root, n, ".claude-plugin", "marketplace.json"), "{}");
23
+ }
24
+ return root;
25
+ }
26
+
27
+ function harness(response: Response | Error = new Response("{}")) {
28
+ const calls: { url: string; init: RequestInit | undefined }[] = [];
29
+ const fetchFn = (async (url: string | URL | Request, init?: RequestInit) => {
30
+ calls.push({ url: String(url), init });
31
+ if (response instanceof Error) throw response;
32
+ return response;
33
+ }) as typeof fetch;
34
+ return { calls, fetchFn };
35
+ }
36
+
37
+ const base = { apiUrl: "http://admin", agentId: "a1", apiKey: "k" };
38
+
39
+ describe("reportBakedMarketplaces", () => {
40
+ test("PUTs the basenames once with the agent key", async () => {
41
+ const root = bakedRoot(["vitals-os", "shipwright"]);
42
+ const { calls, fetchFn } = harness();
43
+ await reportBakedMarketplaces({ ...base, root, fetchFn });
44
+ expect(calls).toHaveLength(1);
45
+ expect(calls[0].url).toBe("http://admin/agents/a1/baked-marketplaces");
46
+ expect(calls[0].init?.method).toBe("PUT");
47
+ expect(new Headers(calls[0].init?.headers).get("authorization")).toBe(
48
+ "Bearer k",
49
+ );
50
+ const body = JSON.parse(calls[0].init?.body as string);
51
+ expect([...body.names].sort()).toEqual(["shipwright", "vitals-os"]);
52
+ });
53
+
54
+ test("reports an empty list when the root is missing", async () => {
55
+ const { calls, fetchFn } = harness();
56
+ await reportBakedMarketplaces({
57
+ ...base,
58
+ root: "/nonexistent-baked-root",
59
+ fetchFn,
60
+ });
61
+ expect(JSON.parse(calls[0].init?.body as string)).toEqual({ names: [] });
62
+ });
63
+
64
+ test("swallows a non-2xx response", async () => {
65
+ const { fetchFn } = harness(new Response("no", { status: 500 }));
66
+ await expect(
67
+ reportBakedMarketplaces({ ...base, root: bakedRoot(["x"]), fetchFn }),
68
+ ).resolves.toBeUndefined();
69
+ });
70
+
71
+ test("swallows a thrown fetch error", async () => {
72
+ const { fetchFn } = harness(new Error("boom"));
73
+ await expect(
74
+ reportBakedMarketplaces({ ...base, root: bakedRoot(["x"]), fetchFn }),
75
+ ).resolves.toBeUndefined();
76
+ });
77
+ });
package/boot.ts CHANGED
@@ -39,6 +39,7 @@ import {
39
39
  } from "./agent-slack-membership-ref.ts";
40
40
  import { agentTrialExpiryRef } from "./agent-trial-expiry-ref.ts";
41
41
  import { applyEnvToProcess } from "./anthropic-federation-guard.ts";
42
+ import { reportBakedMarketplaces } from "./baked-marketplaces-reporter.ts";
42
43
  import { createChatPoller } from "./chat-poller.ts";
43
44
  import {
44
45
  HttpChatTokenReporter,
@@ -259,6 +260,20 @@ export async function startAgent(
259
260
  })
260
261
  : new NoopGitHubInstallationsReporter();
261
262
 
263
+ // PMK-1.4: report the image's baked marketplace names to admin once at boot
264
+ // (overwrite, read-only visibility). Never throws; skipped when not wired.
265
+ if (
266
+ config.shipwright.apiUrl &&
267
+ config.shipwright.apiKey &&
268
+ config.shipwright.agentId
269
+ ) {
270
+ void reportBakedMarketplaces({
271
+ apiUrl: config.shipwright.apiUrl,
272
+ agentId: config.shipwright.agentId,
273
+ apiKey: config.shipwright.apiKey,
274
+ });
275
+ }
276
+
262
277
  // Always-loaded-context stamp (context-stamp.ts): fingerprints the workspace
263
278
  // CLAUDE.md + its imports + no-`paths` rules + the plugin version, read once
264
279
  // per run so cron-run rows can be grouped by what the model was given. The
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@app-vitals/agent-runtime",
3
- "version": "1.6.0",
3
+ "version": "1.7.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/app-vitals/agent.git",
@@ -40,8 +40,8 @@
40
40
  "./workspace-paths": "./workspace-paths.ts"
41
41
  },
42
42
  "dependencies": {
43
- "@app-vitals/agent-admin": "1.6.0",
44
- "@app-vitals/agent-lib": "1.6.0",
43
+ "@app-vitals/agent-admin": "1.7.0",
44
+ "@app-vitals/agent-lib": "1.7.0",
45
45
  "@octokit/auth-app": "^8.2.0",
46
46
  "@sentry/bun": "^10.63.0",
47
47
  "@slack/bolt": "^5.0.0",