@wardby/cli 0.3.0 → 0.4.1

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.
Files changed (275) hide show
  1. package/.env.example +22 -3
  2. package/README.md +11 -9
  3. package/dist/cli-help.d.ts +1 -1
  4. package/dist/cli-help.js +1 -0
  5. package/dist/cli.js +68 -10
  6. package/dist/coding/base-commit.d.ts +6 -0
  7. package/dist/coding/base-commit.js +12 -0
  8. package/dist/coding/protocol.d.ts +17 -1
  9. package/dist/coding/protocol.js +17 -6
  10. package/dist/coding/provider.d.ts +6 -1
  11. package/dist/coding/provider.js +16 -9
  12. package/dist/config/providers.d.ts +25 -0
  13. package/dist/config/providers.js +71 -0
  14. package/dist/core/attribution.d.ts +101 -0
  15. package/dist/core/attribution.js +208 -0
  16. package/dist/core/budget-groups.d.ts +17 -10
  17. package/dist/core/budget-groups.js +15 -12
  18. package/dist/core/coding-queue.d.ts +3 -0
  19. package/dist/core/coding-queue.js +6 -2
  20. package/dist/core/coding-service-status.d.ts +11 -0
  21. package/dist/core/coding-service-status.js +17 -0
  22. package/dist/core/cost-report.d.ts +88 -0
  23. package/dist/core/cost-report.js +248 -0
  24. package/dist/core/dispatch.d.ts +42 -2
  25. package/dist/core/dispatch.js +144 -26
  26. package/dist/core/engine-native.js +17 -4
  27. package/dist/core/glob.d.ts +10 -0
  28. package/dist/core/glob.js +33 -0
  29. package/dist/core/host-events.d.ts +24 -1
  30. package/dist/core/host-events.js +341 -1
  31. package/dist/core/host-status.d.ts +19 -2
  32. package/dist/core/host-status.js +47 -30
  33. package/dist/core/issue-bridge.d.ts +60 -0
  34. package/dist/core/issue-bridge.js +189 -0
  35. package/dist/core/issue-dedupe.d.ts +70 -0
  36. package/dist/core/issue-dedupe.js +255 -0
  37. package/dist/core/issue-events.d.ts +42 -0
  38. package/dist/core/issue-events.js +155 -0
  39. package/dist/core/issue-status.d.ts +29 -0
  40. package/dist/core/issue-status.js +241 -0
  41. package/dist/core/issue-tracker-tools.d.ts +64 -0
  42. package/dist/core/issue-tracker-tools.js +850 -0
  43. package/dist/core/model-usage.d.ts +10 -0
  44. package/dist/core/model-usage.js +24 -0
  45. package/dist/core/reconciler.d.ts +8 -4
  46. package/dist/core/reconciler.js +15 -4
  47. package/dist/core/review-host-tools.js +10 -3
  48. package/dist/core/run-pricing.d.ts +61 -0
  49. package/dist/core/run-pricing.js +56 -0
  50. package/dist/core/runner.d.ts +5 -2
  51. package/dist/core/runner.js +178 -27
  52. package/dist/core/scheduler.d.ts +4 -1
  53. package/dist/core/scheduler.js +3 -2
  54. package/dist/core/self-defects.d.ts +80 -0
  55. package/dist/core/self-defects.js +180 -0
  56. package/dist/core/tool-names.js +3 -0
  57. package/dist/core/webhooks.d.ts +9 -1
  58. package/dist/core/webhooks.js +19 -1
  59. package/dist/env.js +6 -1
  60. package/dist/generated/prisma/browser.d.ts +66 -0
  61. package/dist/generated/prisma/client.d.ts +66 -0
  62. package/dist/generated/prisma/commonInputTypes.d.ts +122 -52
  63. package/dist/generated/prisma/enums.d.ts +7 -0
  64. package/dist/generated/prisma/enums.js +6 -0
  65. package/dist/generated/prisma/internal/class.d.ts +99 -0
  66. package/dist/generated/prisma/internal/class.js +4 -4
  67. package/dist/generated/prisma/internal/prismaNamespace.d.ts +826 -1
  68. package/dist/generated/prisma/internal/prismaNamespace.js +135 -2
  69. package/dist/generated/prisma/internal/prismaNamespaceBrowser.d.ts +142 -0
  70. package/dist/generated/prisma/internal/prismaNamespaceBrowser.js +135 -2
  71. package/dist/generated/prisma/models/Agent.d.ts +389 -1
  72. package/dist/generated/prisma/models/AgentIssueProject.d.ts +1838 -0
  73. package/dist/generated/prisma/models/AgentIssueProject.js +1 -0
  74. package/dist/generated/prisma/models/AgentRepository.d.ts +1 -1
  75. package/dist/generated/prisma/models/AuthUser.d.ts +1 -1
  76. package/dist/generated/prisma/models/CodingProxySession.d.ts +73 -1
  77. package/dist/generated/prisma/models/CodingRun.d.ts +130 -1
  78. package/dist/generated/prisma/models/CodingRunServiceStatus.d.ts +1404 -0
  79. package/dist/generated/prisma/models/CodingRunServiceStatus.js +1 -0
  80. package/dist/generated/prisma/models/IssueFingerprint.d.ts +1183 -0
  81. package/dist/generated/prisma/models/IssueFingerprint.js +1 -0
  82. package/dist/generated/prisma/models/IssuePullRequest.d.ts +1255 -0
  83. package/dist/generated/prisma/models/IssuePullRequest.js +1 -0
  84. package/dist/generated/prisma/models/ModelCatalogEntry.d.ts +1322 -0
  85. package/dist/generated/prisma/models/ModelCatalogEntry.js +1 -0
  86. package/dist/generated/prisma/models/Run.d.ts +933 -1
  87. package/dist/generated/prisma/models/RunAttribution.d.ts +1259 -0
  88. package/dist/generated/prisma/models/RunAttribution.js +1 -0
  89. package/dist/generated/prisma/models/RunIssueStatus.d.ts +1199 -0
  90. package/dist/generated/prisma/models/RunIssueStatus.js +1 -0
  91. package/dist/generated/prisma/models/RunModelUsage.d.ts +1316 -0
  92. package/dist/generated/prisma/models/RunModelUsage.js +1 -0
  93. package/dist/generated/prisma/models/WorkItem.d.ts +1408 -0
  94. package/dist/generated/prisma/models/WorkItem.js +1 -0
  95. package/dist/generated/prisma/models.d.ts +9 -0
  96. package/dist/help-index.json +355 -16
  97. package/dist/import/neutral-schema.d.ts +16 -16
  98. package/dist/knowledge/check.d.ts +13 -0
  99. package/dist/knowledge/check.js +69 -0
  100. package/dist/knowledge/cli.d.ts +14 -0
  101. package/dist/knowledge/cli.js +67 -0
  102. package/dist/knowledge/concept.d.ts +54 -0
  103. package/dist/knowledge/concept.js +78 -0
  104. package/dist/knowledge/note.d.ts +11 -0
  105. package/dist/knowledge/note.js +39 -0
  106. package/dist/knowledge/relevance.d.ts +11 -0
  107. package/dist/knowledge/relevance.js +14 -0
  108. package/dist/knowledge/span-hash.d.ts +3 -0
  109. package/dist/knowledge/span-hash.js +16 -0
  110. package/dist/mcp/auth/access.d.ts +4 -2
  111. package/dist/mcp/auth/ownership.d.ts +9 -9
  112. package/dist/mcp/auth/resource-server.d.ts +3 -1
  113. package/dist/mcp/auth/resource-server.js +18 -3
  114. package/dist/mcp/auth/self-hosted/credentials.d.ts +3 -3
  115. package/dist/mcp/auth/self-hosted/session.d.ts +5 -5
  116. package/dist/mcp/context.d.ts +3 -0
  117. package/dist/mcp/host-events/deliveries.d.ts +9 -0
  118. package/dist/mcp/host-events/deliveries.js +17 -0
  119. package/dist/mcp/host-events/github-ingress.d.ts +4 -2
  120. package/dist/mcp/host-events/github-ingress.js +4 -13
  121. package/dist/mcp/host-events/jira-ingress.d.ts +29 -0
  122. package/dist/mcp/host-events/jira-ingress.js +92 -0
  123. package/dist/mcp/index.d.ts +2 -0
  124. package/dist/mcp/index.js +87 -9
  125. package/dist/mcp/server.js +5 -2
  126. package/dist/mcp/tools/agents.js +74 -3
  127. package/dist/mcp/tools/cost-report.d.ts +8 -0
  128. package/dist/mcp/tools/cost-report.js +60 -0
  129. package/dist/mcp/tools/issue-projects.d.ts +2 -0
  130. package/dist/mcp/tools/issue-projects.js +238 -0
  131. package/dist/mcp/tools/model-catalog.d.ts +22 -0
  132. package/dist/mcp/tools/model-catalog.js +423 -0
  133. package/dist/mcp/tools/repositories.js +2 -1
  134. package/dist/mcp/tools/tools.d.ts +2 -2
  135. package/dist/mcp/tools/trigger.js +33 -5
  136. package/dist/mcp/transport/streamable-http.d.ts +5 -0
  137. package/dist/mcp/transport/streamable-http.js +23 -1
  138. package/dist/mcp/webhooks/ingress.d.ts +2 -1
  139. package/dist/mcp/webhooks/ingress.js +9 -2
  140. package/dist/providers/auth/self-hosted.d.ts +8 -1
  141. package/dist/providers/auth/self-hosted.js +39 -2
  142. package/dist/providers/coding-proxy/memory-ledger.d.ts +1 -1
  143. package/dist/providers/coding-proxy/memory-ledger.js +10 -1
  144. package/dist/providers/coding-proxy/metering.d.ts +2 -1
  145. package/dist/providers/coding-proxy/metering.js +13 -2
  146. package/dist/providers/coding-proxy/prisma-ledger.js +59 -6
  147. package/dist/providers/coding-proxy/proxy.d.ts +12 -2
  148. package/dist/providers/coding-proxy/proxy.js +92 -30
  149. package/dist/providers/coding-proxy/types.d.ts +18 -1
  150. package/dist/providers/coding-proxy/types.js +12 -1
  151. package/dist/providers/engine/types.d.ts +19 -0
  152. package/dist/providers/executor/composition.js +9 -1
  153. package/dist/providers/executor/container.d.ts +30 -2
  154. package/dist/providers/executor/container.js +98 -17
  155. package/dist/providers/executor/dbos.d.ts +2 -0
  156. package/dist/providers/executor/dbos.js +7 -5
  157. package/dist/providers/executor/routing.d.ts +6 -0
  158. package/dist/providers/executor/routing.js +5 -0
  159. package/dist/providers/executor/types.d.ts +12 -0
  160. package/dist/providers/issue-tracker/adf.d.ts +31 -0
  161. package/dist/providers/issue-tracker/adf.js +181 -0
  162. package/dist/providers/issue-tracker/index.d.ts +5 -0
  163. package/dist/providers/issue-tracker/index.js +12 -0
  164. package/dist/providers/issue-tracker/jira-client.d.ts +41 -0
  165. package/dist/providers/issue-tracker/jira-client.js +151 -0
  166. package/dist/providers/issue-tracker/jira-events.d.ts +3 -0
  167. package/dist/providers/issue-tracker/jira-events.js +98 -0
  168. package/dist/providers/issue-tracker/jira.d.ts +116 -0
  169. package/dist/providers/issue-tracker/jira.js +502 -0
  170. package/dist/providers/issue-tracker/types.d.ts +269 -0
  171. package/dist/providers/issue-tracker/types.js +16 -0
  172. package/dist/providers/jobs/docker.d.ts +5 -1
  173. package/dist/providers/jobs/docker.js +61 -33
  174. package/dist/providers/jobs/kubernetes.d.ts +3 -0
  175. package/dist/providers/jobs/kubernetes.js +44 -4
  176. package/dist/providers/jobs/service-state.d.ts +22 -0
  177. package/dist/providers/jobs/service-state.js +17 -0
  178. package/dist/providers/llm/anthropic.d.ts +3 -3
  179. package/dist/providers/llm/anthropic.js +3 -9
  180. package/dist/providers/llm/bedrock.d.ts +3 -3
  181. package/dist/providers/llm/bedrock.js +3 -9
  182. package/dist/providers/llm/catalog-lookup.d.ts +10 -0
  183. package/dist/providers/llm/catalog-lookup.js +15 -0
  184. package/dist/providers/llm/catalog-shipped.d.ts +18 -0
  185. package/dist/providers/llm/catalog-shipped.js +197 -0
  186. package/dist/providers/llm/catalog-store.d.ts +58 -0
  187. package/dist/providers/llm/catalog-store.js +138 -0
  188. package/dist/providers/llm/catalog-types.d.ts +66 -0
  189. package/dist/providers/llm/catalog-types.js +64 -0
  190. package/dist/providers/llm/catalog.d.ts +61 -0
  191. package/dist/providers/llm/catalog.js +147 -0
  192. package/dist/providers/llm/claude-provider.d.ts +10 -14
  193. package/dist/providers/llm/claude-provider.js +11 -6
  194. package/dist/providers/llm/index.d.ts +9 -6
  195. package/dist/providers/llm/index.js +8 -5
  196. package/dist/providers/llm/openai.d.ts +14 -5
  197. package/dist/providers/llm/openai.js +24 -14
  198. package/dist/providers/llm/pricing-core.d.ts +5 -3
  199. package/dist/providers/llm/registration.js +8 -12
  200. package/dist/providers/llm/routing.d.ts +18 -17
  201. package/dist/providers/llm/routing.js +40 -24
  202. package/dist/providers/review-host/github-events.js +47 -1
  203. package/dist/providers/review-host/github.js +7 -6
  204. package/dist/providers/review-host/types.d.ts +25 -0
  205. package/dist/providers/vcs/git.js +2 -22
  206. package/dist/providers/vcs/github.d.ts +20 -0
  207. package/dist/providers/vcs/github.js +28 -2
  208. package/dist/providers/vcs/types.d.ts +6 -0
  209. package/dist/quickstart/index.d.ts +8 -0
  210. package/dist/quickstart/index.js +34 -34
  211. package/dist/serve.js +8 -2
  212. package/dist/viewer/api-schema.d.ts +2757 -0
  213. package/dist/viewer/api-schema.js +165 -0
  214. package/dist/viewer/build-schemas.d.ts +2 -0
  215. package/dist/viewer/build-schemas.js +18 -0
  216. package/dist/viewer/event-bus.d.ts +38 -0
  217. package/dist/viewer/event-bus.js +232 -0
  218. package/dist/viewer/graph.d.ts +40 -0
  219. package/dist/viewer/graph.js +243 -0
  220. package/dist/viewer/http.d.ts +30 -0
  221. package/dist/viewer/http.js +133 -0
  222. package/dist/viewer/run-detail.d.ts +4 -0
  223. package/dist/viewer/run-detail.js +61 -0
  224. package/dist/wardby-bin.js +5 -0
  225. package/docs/README.md +10 -0
  226. package/docs/agent-recipes.md +383 -0
  227. package/docs/code-review-agents.md +29 -2
  228. package/docs/coding-agent-setup.md +3 -0
  229. package/docs/coding-worker-isolation.md +39 -5
  230. package/docs/getting-started-gke.md +28 -11
  231. package/docs/getting-started-identity-provider.md +49 -38
  232. package/docs/getting-started.md +14 -0
  233. package/docs/jira-agents.md +649 -0
  234. package/docs/knowledge.md +387 -0
  235. package/docs/models.md +221 -0
  236. package/docs/security-deployment.md +19 -9
  237. package/docs/viewer-api.md +142 -0
  238. package/help/admin-viewer.md +39 -0
  239. package/help/agent-recipes.md +173 -0
  240. package/help/architecture-agent.md +189 -0
  241. package/help/builder-agent.md +80 -0
  242. package/help/code-review-agents.md +6 -0
  243. package/help/cost-attribution.md +67 -0
  244. package/help/creating-agents.md +22 -0
  245. package/help/deploy-gke.md +6 -0
  246. package/help/errors/model-unavailable.md +63 -0
  247. package/help/getting-started.md +1 -0
  248. package/help/github.md +18 -0
  249. package/help/identity-and-access.md +8 -3
  250. package/help/jira.md +135 -0
  251. package/help/knowledge.md +47 -0
  252. package/help/models.md +90 -0
  253. package/help/operating-agents.md +7 -1
  254. package/help/troubleshooting/budgets.md +6 -0
  255. package/package.json +5 -2
  256. package/prisma/migrations/20260930000000_jira_issue_projects/migration.sql +34 -0
  257. package/prisma/migrations/20261001000000_jira_phase2_allowlists/migration.sql +3 -0
  258. package/prisma/migrations/20261001010000_jira_link_types_allowlist/migration.sql +2 -0
  259. package/prisma/migrations/20261002000000_jira_coding_bridge/migration.sql +28 -0
  260. package/prisma/migrations/20261002010000_jira_issue_creation/migration.sql +25 -0
  261. package/prisma/migrations/20261003000000_issue_cost_attribution/migration.sql +56 -0
  262. package/prisma/migrations/20261003010000_coding_run_service_status/migration.sql +23 -0
  263. package/prisma/migrations/20261003020000_viewer_notify/migration.sql +54 -0
  264. package/prisma/migrations/20261003030000_viewer_notify_fixes/migration.sql +47 -0
  265. package/prisma/migrations/20261003040000_viewer_indexes/migration.sql +12 -0
  266. package/prisma/migrations/20261004000000_model_catalog/migration.sql +26 -0
  267. package/prisma/schema.prisma +258 -2
  268. package/dist/mcp/tools/models.d.ts +0 -8
  269. package/dist/mcp/tools/models.js +0 -15
  270. package/dist/providers/llm/pricing-anthropic.d.ts +0 -14
  271. package/dist/providers/llm/pricing-anthropic.js +0 -48
  272. package/dist/providers/llm/pricing-bedrock-claude.d.ts +0 -20
  273. package/dist/providers/llm/pricing-bedrock-claude.js +0 -46
  274. package/dist/providers/llm/pricing.d.ts +0 -30
  275. package/dist/providers/llm/pricing.js +0 -74
@@ -21,6 +21,12 @@ export const SCOPES_SUPPORTED = [
21
21
  "agents:admin",
22
22
  // set_agent_memory / delete_agent_memory (reading memory is agents:read).
23
23
  "memory:write",
24
+ // Read-only, deployment-wide view of runs for the admin viewer (docs/viewer-api.md).
25
+ // Privileged: it shows every owner's runs, so only the admin role grants it.
26
+ "admin:view",
27
+ // Add, override, disable and reset model catalog entries (see docs/models.md).
28
+ // Reading the catalog (list_models, get_model) is agents:read.
29
+ "models:admin",
24
30
  ];
25
31
  /**
26
32
  * Scopes whose operations reach beyond the caller's own (or public)
@@ -28,20 +34,29 @@ export const SCOPES_SUPPORTED = [
28
34
  * agents:admin reassigns ANY agent's owner (make_owner) and sets a BYO
29
35
  * workerImageRef; packages:approve widens coding agents' package allowlists;
30
36
  * services:manage changes the coding-run service catalog, which decides what
31
- * runs next to every coding run that names an entry.
37
+ * runs next to every coding run that names an entry; admin:view reads every
38
+ * owner's runs (the admin viewer API); models:admin changes the model catalog,
39
+ * which prices every principal's runs.
32
40
  * A token scope only DELEGATES — it never authorizes on its own:
33
41
  * requireScope/requireAnyScope honour one only when one of the caller's
34
42
  * roles (McpRequestContext.roles, resolved live per request) grants it.
35
43
  */
36
- export const PRIVILEGED_SCOPES = ["agents:admin", "packages:approve", "services:manage"];
44
+ export const PRIVILEGED_SCOPES = [
45
+ "agents:admin",
46
+ "packages:approve",
47
+ "services:manage",
48
+ "admin:view",
49
+ "models:admin",
50
+ ];
37
51
  /**
38
52
  * The built-in roles and the permissions (privileged scope names) each
39
53
  * grants. No roles = member: every non-privileged scope, nothing privileged.
40
54
  */
41
55
  export const ROLE_PERMISSIONS = {
42
- admin: ["agents:admin", "packages:approve", "services:manage"],
56
+ admin: ["agents:admin", "packages:approve", "services:manage", "admin:view", "models:admin"],
43
57
  "package-approver": ["packages:approve"],
44
58
  "service-manager": ["services:manage"],
59
+ "model-manager": ["models:admin"],
45
60
  };
46
61
  export const ROLE_NAMES = Object.keys(ROLE_PERMISSIONS);
47
62
  export function isRoleName(name) {
@@ -18,18 +18,18 @@ export declare class Credentials {
18
18
  }
19
19
  export declare function lockUser(tx: AuthDb, id: string): Promise<{
20
20
  principal: {
21
- id: string;
22
21
  createdAt: Date;
22
+ id: string;
23
23
  subject: string;
24
24
  };
25
25
  } & {
26
26
  status: string;
27
- id: string;
28
27
  createdAt: Date;
29
28
  updatedAt: Date;
30
- principalId: string;
29
+ id: string;
31
30
  roles: string[];
32
31
  displayName: string | null;
32
+ principalId: string;
33
33
  }>;
34
34
  export declare class IdentityService {
35
35
  readonly db: PrismaClient;
@@ -11,27 +11,27 @@ export declare class Sessions {
11
11
  get(token: string, tx?: AuthDb): Promise<{
12
12
  user: {
13
13
  principal: {
14
- id: string;
15
14
  createdAt: Date;
15
+ id: string;
16
16
  subject: string;
17
17
  };
18
18
  } & {
19
19
  status: string;
20
- id: string;
21
20
  createdAt: Date;
22
21
  updatedAt: Date;
23
- principalId: string;
22
+ id: string;
24
23
  roles: string[];
25
24
  displayName: string | null;
25
+ principalId: string;
26
26
  };
27
27
  } & {
28
28
  createdAt: Date;
29
+ lastSeenAt: Date;
30
+ sessionId: string;
29
31
  expiresAt: Date;
30
32
  userId: string;
31
33
  secretHash: string;
32
34
  revokedAt: Date | null;
33
- sessionId: string;
34
- lastSeenAt: Date;
35
35
  }>;
36
36
  logout(token: string, challenge: string): Promise<void>;
37
37
  }
@@ -11,9 +11,12 @@ import type { Principal, PrismaClient } from "#prisma";
11
11
  import type { RequestStateAccessor } from "@modelcontextprotocol/server";
12
12
  import type { ProviderRegistry } from "../providers/index.js";
13
13
  import type { RepoAccessGate } from "../core/repo-access.js";
14
+ import type { IssueTrackerRegistry } from "../providers/issue-tracker/types.js";
14
15
  import type { HostUserAuthorizerRegistry, ReviewHostRegistry } from "../providers/review-host/types.js";
15
16
  export type McpProviders = Pick<ProviderRegistry, "llm" | "engine" | "datastore" | "secrets" | "executor" | "memory"> & {
16
17
  reviewHosts?: ReviewHostRegistry;
18
+ /** Issue trackers (Jira); empty or absent = no issue-tracker integration. */
19
+ issueTrackers?: IssueTrackerRegistry;
17
20
  /** Identity linking (link_host_account); empty or absent = linking disabled. */
18
21
  hostUserAuthorizers?: HostUserAuthorizerRegistry;
19
22
  /**
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Shared housekeeping for the HostEventDelivery dedupe table. Every host
3
+ * ingress (GitHub, Jira) calls this after recording a delivery; one process
4
+ * clock keeps the prune to at most once an hour regardless of provider.
5
+ */
6
+ import type { PrismaClient } from "#prisma";
7
+ export declare function maybePruneHostEventDeliveries(db: Pick<PrismaClient, "hostEventDelivery">, now: Date): Promise<void>;
8
+ /** Test-only: reset the prune clock. */
9
+ export declare function resetPruneClockForTests(): void;
@@ -0,0 +1,17 @@
1
+ import { logger } from "../../core/logger.js";
2
+ const log = logger.child({ module: "host-event-deliveries" });
3
+ const DELIVERY_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
4
+ const PRUNE_INTERVAL_MS = 60 * 60 * 1000;
5
+ let lastPruneAt = 0;
6
+ export async function maybePruneHostEventDeliveries(db, now) {
7
+ if (now.getTime() - lastPruneAt <= PRUNE_INTERVAL_MS)
8
+ return;
9
+ lastPruneAt = now.getTime();
10
+ await db.hostEventDelivery
11
+ .deleteMany({ where: { receivedAt: { lt: new Date(now.getTime() - DELIVERY_RETENTION_MS) } } })
12
+ .catch((err) => log.warn({ err }, "delivery prune failed"));
13
+ }
14
+ /** Test-only: reset the prune clock. */
15
+ export function resetPruneClockForTests() {
16
+ lastPruneAt = 0;
17
+ }
@@ -7,6 +7,7 @@
7
7
  import { type PrismaClient } from "#prisma";
8
8
  import type { HostEventDb } from "../../core/host-events.js";
9
9
  import type { Executor } from "../../providers/executor/types.js";
10
+ import type { IssueTrackerRegistry } from "../../providers/issue-tracker/types.js";
10
11
  import type { ReviewHostRegistry } from "../../providers/review-host/types.js";
11
12
  import type { RepoAccessGate } from "../../core/repo-access.js";
12
13
  export interface GitHubIngressDeps {
@@ -15,6 +16,8 @@ export interface GitHubIngressDeps {
15
16
  hosts: ReviewHostRegistry;
16
17
  /** Repository authorization for every dispatch, and the mention author's permission. */
17
18
  repoAccess: RepoAccessGate;
19
+ /** For a merged/closed PR's linked issue; absent → closed PRs are ignored. */
20
+ issueTrackers?: IssueTrackerRegistry;
18
21
  webhookSecret: string | undefined;
19
22
  appIdentity: () => Promise<{
20
23
  id: number;
@@ -32,5 +35,4 @@ export declare function handleGitHubEventIngress(req: {
32
35
  headers: Record<string, string | undefined>;
33
36
  rawBody: string;
34
37
  }, deps: GitHubIngressDeps): Promise<GitHubIngressResult>;
35
- /** Test-only: reset the prune clock. */
36
- export declare function resetPruneClockForTests(): void;
38
+ export { resetPruneClockForTests } from "./deliveries.js";
@@ -5,14 +5,12 @@
5
5
  * Nothing from the body is logged.
6
6
  */
7
7
  import { Prisma } from "#prisma";
8
+ import { maybePruneHostEventDeliveries } from "./deliveries.js";
8
9
  import { routeHostEvent } from "../../core/host-events.js";
9
10
  import { logger } from "../../core/logger.js";
10
11
  import { normalizeGitHubEvent, verifyGitHubSignature } from "../../providers/review-host/github-events.js";
11
12
  const log = logger.child({ module: "github-ingress" });
12
- const DELIVERY_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
13
- const PRUNE_INTERVAL_MS = 60 * 60 * 1000;
14
13
  const SAFE_DELIVERY = /^[A-Za-z0-9-]{1,100}$/;
15
- let lastPruneAt = 0;
16
14
  export async function handleGitHubEventIngress(req, deps) {
17
15
  if (!deps.webhookSecret)
18
16
  return { status: 404, body: { error: "not_found" } };
@@ -49,12 +47,7 @@ export async function handleGitHubEventIngress(req, deps) {
49
47
  }
50
48
  throw err;
51
49
  }
52
- if (now.getTime() - lastPruneAt > PRUNE_INTERVAL_MS) {
53
- lastPruneAt = now.getTime();
54
- await deps.db.hostEventDelivery
55
- .deleteMany({ where: { receivedAt: { lt: new Date(now.getTime() - DELIVERY_RETENTION_MS) } } })
56
- .catch((err) => log.warn({ err }, "delivery prune failed"));
57
- }
50
+ await maybePruneHostEventDeliveries(deps.db, now);
58
51
  // The delivery row is recorded before routing so a duplicate can never
59
52
  // race past it, but that means a transient failure inside routeHostEvent
60
53
  // must not leave a delivery permanently marked "done" — GitHub's redelivery
@@ -67,6 +60,7 @@ export async function handleGitHubEventIngress(req, deps) {
67
60
  executor: deps.executor,
68
61
  hosts: deps.hosts,
69
62
  repoAccess: deps.repoAccess,
63
+ ...(deps.issueTrackers ? { issueTrackers: deps.issueTrackers } : {}),
70
64
  mentionHandle: app.slug,
71
65
  });
72
66
  log.info({ event: event.kind, repository: event.repository, deliveryId, runIds: routed.runIds }, "host event routed");
@@ -86,7 +80,4 @@ export async function handleGitHubEventIngress(req, deps) {
86
80
  throw err;
87
81
  }
88
82
  }
89
- /** Test-only: reset the prune clock. */
90
- export function resetPruneClockForTests() {
91
- lastPruneAt = 0;
92
- }
83
+ export { resetPruneClockForTests } from "./deliveries.js";
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Jira Cloud webhook ingress — framework-agnostic, mounted unauthenticated
3
+ * (by OAuth) in streamable-http.ts at /hosts/jira/events. Authenticated by the
4
+ * webhook secret (X-Hub-Signature, WebSub style) over the raw body. The
5
+ * delivery id (X-Atlassian-Webhook-Identifier) is stable across Jira's
6
+ * retries, so it is the dedupe key. Nothing from the body is logged.
7
+ */
8
+ import { type PrismaClient } from "#prisma";
9
+ import { type IssueEventDb } from "../../core/issue-events.js";
10
+ import type { Executor } from "../../providers/executor/types.js";
11
+ import type { IssueTrackerRegistry } from "../../providers/issue-tracker/types.js";
12
+ export interface JiraIngressDeps {
13
+ db: IssueEventDb & Pick<PrismaClient, "hostEventDelivery">;
14
+ executor: Executor;
15
+ trackers: IssueTrackerRegistry;
16
+ webhookSecret: string;
17
+ /** Epoch-ms clock; injected by tests. */
18
+ now?: () => number;
19
+ }
20
+ export interface JiraIngressResult {
21
+ status: number;
22
+ body: Record<string, unknown>;
23
+ /** Cosmetic follow-ups to run after the response is sent. */
24
+ afterResponse?: () => Promise<void>;
25
+ }
26
+ export declare function handleJiraEventIngress(req: {
27
+ headers: Record<string, string | undefined>;
28
+ rawBody: string;
29
+ }, deps: JiraIngressDeps): Promise<JiraIngressResult>;
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Jira Cloud webhook ingress — framework-agnostic, mounted unauthenticated
3
+ * (by OAuth) in streamable-http.ts at /hosts/jira/events. Authenticated by the
4
+ * webhook secret (X-Hub-Signature, WebSub style) over the raw body. The
5
+ * delivery id (X-Atlassian-Webhook-Identifier) is stable across Jira's
6
+ * retries, so it is the dedupe key. Nothing from the body is logged.
7
+ */
8
+ import { Prisma } from "#prisma";
9
+ import { maybePruneHostEventDeliveries } from "./deliveries.js";
10
+ import { routeIssueEvent } from "../../core/issue-events.js";
11
+ import { logger } from "../../core/logger.js";
12
+ import { normalizeJiraEvent, verifyJiraSignature } from "../../providers/issue-tracker/jira-events.js";
13
+ const log = logger.child({ module: "jira-ingress" });
14
+ const SAFE_DELIVERY = /^[A-Za-z0-9._:-]{1,200}$/;
15
+ /** Jira retries a delivery up to 5 times, 5-15 min apart, so 2 h comfortably covers a legitimate replay. */
16
+ const MAX_AGE_MS = 2 * 60 * 60 * 1000;
17
+ const MAX_FUTURE_MS = 5 * 60 * 1000;
18
+ /**
19
+ * Whether the payload's top-level `timestamp` is inside the replay window.
20
+ * Assumed to be epoch milliseconds; a value below 1e12 (before 2001 in ms)
21
+ * is read as seconds. The unit is unverified against a live delivery.
22
+ * A missing or non-numeric timestamp is not fresh.
23
+ */
24
+ function isFresh(payload, nowMs) {
25
+ const raw = payload?.timestamp;
26
+ if (typeof raw !== "number" || !Number.isFinite(raw) || raw <= 0)
27
+ return false;
28
+ const ts = raw < 1e12 ? raw * 1000 : raw;
29
+ return ts >= nowMs - MAX_AGE_MS && ts <= nowMs + MAX_FUTURE_MS;
30
+ }
31
+ export async function handleJiraEventIngress(req, deps) {
32
+ const tracker = deps.trackers.jira;
33
+ if (!tracker)
34
+ return { status: 404, body: { error: "not_found" } };
35
+ if (!verifyJiraSignature(req.rawBody, req.headers["x-hub-signature"], deps.webhookSecret)) {
36
+ return { status: 401, body: { error: "invalid_signature" } };
37
+ }
38
+ // wardby must not act as a person. A lookup failure throws (5xx), so Jira retries the delivery.
39
+ const identity = await tracker.identity();
40
+ if (identity.accountType === "atlassian") {
41
+ log.error("Jira token belongs to a person; refusing deliveries (use a service account)");
42
+ return { status: 503, body: { error: "jira_personal_account" } };
43
+ }
44
+ const deliveryId = req.headers["x-atlassian-webhook-identifier"];
45
+ if (!deliveryId || !SAFE_DELIVERY.test(deliveryId)) {
46
+ return { status: 400, body: { error: "missing_delivery_headers" } };
47
+ }
48
+ let payload;
49
+ try {
50
+ payload = JSON.parse(req.rawBody);
51
+ }
52
+ catch {
53
+ return { status: 400, body: { error: "invalid_json" } };
54
+ }
55
+ // Replay protection: the signature covers the body, so an old captured delivery would otherwise verify forever.
56
+ if (!isFresh(payload, (deps.now ?? Date.now)()))
57
+ return { status: 202, body: { ignored: "stale" } };
58
+ const event = normalizeJiraEvent(payload, identity.accountId);
59
+ // An event we don't act on must never claim the delivery id (see the GitHub ingress).
60
+ if (!event)
61
+ return { status: 202, body: { ignored: true } };
62
+ try {
63
+ await deps.db.hostEventDelivery.create({ data: { provider: "jira", deliveryId } });
64
+ }
65
+ catch (err) {
66
+ if (err instanceof Prisma.PrismaClientKnownRequestError && err.code === "P2002") {
67
+ return { status: 202, body: { duplicate: true } };
68
+ }
69
+ throw err;
70
+ }
71
+ await maybePruneHostEventDeliveries(deps.db, new Date());
72
+ // A routing failure must not leave the delivery permanently marked done:
73
+ // un-record it and rethrow so Jira's retry is routed for real.
74
+ try {
75
+ const routed = await routeIssueEvent(event, { db: deps.db, executor: deps.executor, trackers: deps.trackers });
76
+ log.info({ kinds: event.kinds, deliveryId, runIds: routed.runIds }, "jira event routed");
77
+ return {
78
+ status: 202,
79
+ body: { runIds: routed.runIds },
80
+ afterResponse: async () => {
81
+ for (const followUp of routed.followUps)
82
+ await followUp();
83
+ },
84
+ };
85
+ }
86
+ catch (err) {
87
+ await deps.db.hostEventDelivery
88
+ .deleteMany({ where: { provider: "jira", deliveryId } })
89
+ .catch((delErr) => log.warn({ err: delErr }, "could not roll back the delivery record after a routing failure"));
90
+ throw err;
91
+ }
92
+ }
@@ -12,6 +12,8 @@ export declare function localOperatorContext(principal: Principal, providers: Mc
12
12
  export declare function registerAllTools(mcp: WardbyMcpServer, opts: {
13
13
  secretElicitationUrl: SecretElicitationUrlBuilder;
14
14
  secretElicitationProtocol: boolean;
15
+ /** Rebuilds the process's installed model CatalogStore after set_model/disable_model/reset_model. */
16
+ refreshModelCatalog?: () => Promise<void>;
15
17
  }): void;
16
18
  export interface McpProviderComposition {
17
19
  providers: McpProviders;
package/dist/mcp/index.js CHANGED
@@ -13,15 +13,17 @@
13
13
  * missing `AUTH_JWKS_URI`/`AUTH_SIGNING_KEY` a deployment running stdio-only
14
14
  * never needed to set.
15
15
  */
16
- import { loadProviderConfig, loadMcpConfig, loadShutdownDrainSeconds, loadAuthConfig, loadGitHubEventConfig, loadGitHubVcsConfig, } from "../config/providers.js";
16
+ import { loadProviderConfig, loadMcpConfig, loadShutdownDrainSeconds, loadAuthConfig, loadGitHubEventConfig, loadJiraConfig, loadGitHubVcsConfig, } from "../config/providers.js";
17
17
  import { prisma } from "../core/db.js";
18
18
  import { waitForInFlightRuns } from "../core/in-flight-runs.js";
19
19
  import { NativeEngine } from "../core/engine-native.js";
20
20
  import { resolveLlmRegistrations, RoutingLlmProvider } from "../providers/llm/index.js";
21
+ import { startModelCatalog } from "../providers/llm/catalog-store.js";
21
22
  import { PostgresDatastore } from "../providers/datastore/index.js";
22
23
  import { PostgresAgentMemory } from "../providers/memory/index.js";
23
24
  import { buildConfiguredExecutor, buildExecutor } from "../providers/executor/index.js";
24
25
  import { buildSecretCipher } from "../providers/secrets/index.js";
26
+ import { buildIssueTrackers } from "../providers/issue-tracker/index.js";
25
27
  import { buildHostUserAuthorizers, buildReviewHosts } from "../providers/review-host/index.js";
26
28
  import { createRepoAccessGate } from "../core/repo-access.js";
27
29
  import { userCallbackPath } from "../core/host-identity-links.js";
@@ -32,21 +34,25 @@ import { countUnattendedSchedules, unattendedSchedulesWarning } from "./unattend
32
34
  import { runStdioServer } from "./transport/stdio.js";
33
35
  import { startHttpServer } from "./transport/streamable-http.js";
34
36
  import { resolvePrincipal } from "./auth/principal.js";
35
- import { ROLE_NAMES, SCOPES_SUPPORTED } from "./auth/resource-server.js";
37
+ import { ROLE_NAMES, SCOPES_SUPPORTED, authenticate } from "./auth/resource-server.js";
36
38
  import { canonicalUrl } from "./transport/http-limits.js";
39
+ import { createViewerApi } from "../viewer/http.js";
40
+ import { createViewerEventBus } from "../viewer/event-bus.js";
37
41
  import { registerAgentTools } from "./tools/agents.js";
38
42
  import { registerServiceTools } from "./tools/services.js";
39
43
  import { registerBudgetGroupTools } from "./tools/budget-groups.js";
40
- import { registerModelTools } from "./tools/models.js";
44
+ import { registerModelCatalogTools } from "./tools/model-catalog.js";
41
45
  import { registerHelpTools } from "./tools/help.js";
42
46
  import { registerTriggerTool } from "./tools/trigger.js";
43
47
  import { registerToolAuthoringTools } from "./tools/tools.js";
44
48
  import { registerSchedulingTools } from "./tools/scheduling.js";
45
49
  import { registerRunTools } from "./tools/runs.js";
50
+ import { registerCostReportTools } from "./tools/cost-report.js";
46
51
  import { registerDatastoreTools } from "./tools/datastore.js";
47
52
  import { registerSubAgentTools } from "./tools/subagents.js";
48
53
  import { registerGrantTools } from "./tools/grants.js";
49
54
  import { registerRepositoryTools } from "./tools/repositories.js";
55
+ import { registerIssueProjectTools } from "./tools/issue-projects.js";
50
56
  import { registerHostAccountTools } from "./tools/host-accounts.js";
51
57
  import { registerMemoryTools } from "./tools/memory.js";
52
58
  import { registerSecretsTools } from "./tools/secrets.js";
@@ -97,16 +103,18 @@ export function registerAllTools(mcp, opts) {
97
103
  registerAgentTools(mcp);
98
104
  registerServiceTools(mcp);
99
105
  registerBudgetGroupTools(mcp);
100
- registerModelTools(mcp);
106
+ registerModelCatalogTools(mcp, { refresh: opts.refreshModelCatalog });
101
107
  registerHelpTools(mcp);
102
108
  registerTriggerTool(mcp);
103
109
  registerToolAuthoringTools(mcp);
104
110
  registerSchedulingTools(mcp);
105
111
  registerRunTools(mcp);
112
+ registerCostReportTools(mcp);
106
113
  registerDatastoreTools(mcp);
107
114
  registerSubAgentTools(mcp);
108
115
  registerGrantTools(mcp);
109
116
  registerRepositoryTools(mcp);
117
+ registerIssueProjectTools(mcp);
110
118
  registerHostAccountTools(mcp);
111
119
  registerMemoryTools(mcp);
112
120
  registerSecretsTools(mcp, {
@@ -128,6 +136,7 @@ export function buildMcpProviders() {
128
136
  const datastore = new PostgresDatastore(prisma, secrets);
129
137
  const memory = new PostgresAgentMemory(prisma);
130
138
  const reviewHosts = buildReviewHosts();
139
+ const issueTrackers = buildIssueTrackers();
131
140
  const hostUserAuthorizers = buildHostUserAuthorizers();
132
141
  // One gate (and one cache) for the whole process: set-time checks in the
133
142
  // MCP tools, repo_* calls in native runs, coding runs, and host events.
@@ -140,12 +149,32 @@ export function buildMcpProviders() {
140
149
  // through the same composed executor everything else uses. There's no
141
150
  // way to hand the native executor a reference to its own wrapping
142
151
  // RoutingExecutor before that wrapper is constructed.
143
- const nativeProviders = { llm, engine, datastore, secrets, memory, reviewHosts, repoAccess };
152
+ const nativeProviders = {
153
+ llm,
154
+ engine,
155
+ datastore,
156
+ secrets,
157
+ memory,
158
+ reviewHosts,
159
+ issueTrackers,
160
+ repoAccess,
161
+ };
144
162
  const nativeExecutor = buildExecutor(providerConfig, nativeProviders, prisma);
145
163
  const executor = buildConfiguredExecutor({ native: nativeExecutor, db: prisma, providerConfig, repoAccess });
146
164
  nativeProviders.executor = executor;
147
165
  return {
148
- providers: { llm, engine, datastore, secrets, executor, memory, reviewHosts, hostUserAuthorizers, repoAccess },
166
+ providers: {
167
+ llm,
168
+ engine,
169
+ datastore,
170
+ secrets,
171
+ executor,
172
+ memory,
173
+ reviewHosts,
174
+ issueTrackers,
175
+ hostUserAuthorizers,
176
+ repoAccess,
177
+ },
149
178
  };
150
179
  }
151
180
  /** Awaits `promise` (if any), logging and swallowing a rejection instead of propagating it. */
@@ -162,6 +191,14 @@ export async function startMcp(options = {}) {
162
191
  const mcpConfig = loadMcpConfig();
163
192
  // Parsed up front so a malformed value fails at startup, not at shutdown.
164
193
  const drainSeconds = loadShutdownDrainSeconds();
194
+ // Loads the model catalog before anything else: running on the shipped
195
+ // catalog alone (the fallback currentModelCatalog() uses before a store
196
+ // is installed) would quietly re-enable models an admin turned off.
197
+ // `modelCatalog` is handed to registerAllTools as `refreshModelCatalog`,
198
+ // which the model catalog tools (set_model, disable_model, reset_model)
199
+ // call via `modelCatalog.refreshNow()` after each write — there is no
200
+ // separate refresh tool.
201
+ const modelCatalog = await startModelCatalog(prisma);
165
202
  const providers = options.providers ?? buildMcpProviders().providers;
166
203
  await providers.executor.launch?.();
167
204
  if (!options.schedulerAttached)
@@ -176,6 +213,7 @@ export async function startMcp(options = {}) {
176
213
  registerAllTools(mcp, {
177
214
  secretElicitationUrl: (token) => secretElicitationHost.urlFor(token),
178
215
  secretElicitationProtocol: mcpConfig.secretElicitationProtocol,
216
+ refreshModelCatalog: () => modelCatalog.refreshNow(),
179
217
  });
180
218
  // stdio never carries per-call AuthInfo — one fixed, fully-trusted
181
219
  // local identity for the whole connection, per the design's own
@@ -187,6 +225,7 @@ export async function startMcp(options = {}) {
187
225
  close: async () => {
188
226
  await closeQuietly(stdio.close(), "stdio transport close");
189
227
  await closeQuietly(providers.executor.close?.(), "executor close");
228
+ modelCatalog.close();
190
229
  },
191
230
  };
192
231
  }
@@ -209,17 +248,19 @@ export async function startMcp(options = {}) {
209
248
  registerAllTools(mcp, {
210
249
  secretElicitationUrl: (token) => Promise.resolve(`${httpOrigin}${SECRET_ELICITATION_PATH}?t=${encodeURIComponent(token)}`),
211
250
  secretElicitationProtocol: mcpConfig.secretElicitationProtocol,
251
+ refreshModelCatalog: () => modelCatalog.refreshNow(),
212
252
  });
213
253
  const eventConfig = loadGitHubEventConfig();
214
254
  const githubConfig = loadGitHubVcsConfig();
215
255
  const reviewHosts = providers.reviewHosts;
216
- const hostEvents = eventConfig.webhookSecret && reviewHosts?.github && githubConfig.appId && githubConfig.privateKey
256
+ const githubEvents = eventConfig.webhookSecret && reviewHosts?.github && githubConfig.appId && githubConfig.privateKey
217
257
  ? {
218
258
  github: {
219
259
  db: prisma,
220
260
  executor: providers.executor,
221
261
  hosts: reviewHosts,
222
262
  repoAccess: providers.repoAccess ?? createRepoAccessGate({ db: prisma, hosts: reviewHosts }),
263
+ ...(providers.issueTrackers ? { issueTrackers: providers.issueTrackers } : {}),
223
264
  webhookSecret: eventConfig.webhookSecret,
224
265
  appIdentity: (() => {
225
266
  const client = new GitHubAppClient({
@@ -232,7 +273,29 @@ export async function startMcp(options = {}) {
232
273
  },
233
274
  }
234
275
  : undefined;
235
- mcpLog.info({ enabled: Boolean(hostEvents) }, "GitHub host events ingress");
276
+ mcpLog.info({ enabled: Boolean(githubEvents) }, "GitHub host events ingress");
277
+ const jiraConfig = loadJiraConfig();
278
+ const jiraEvents = jiraConfig && providers.issueTrackers?.jira
279
+ ? {
280
+ db: prisma,
281
+ executor: providers.executor,
282
+ trackers: providers.issueTrackers,
283
+ webhookSecret: jiraConfig.webhookSecret,
284
+ }
285
+ : undefined;
286
+ mcpLog.info({ enabled: Boolean(jiraEvents) }, "Jira host events ingress");
287
+ if (jiraEvents) {
288
+ // Best effort: show which Jira account wardby acts as; never blocks startup.
289
+ void providers.issueTrackers?.jira?.identity().then(({ accountId, displayName, accountType }) => accountType === "atlassian"
290
+ ? mcpLog.error({ accountId, displayName, accountType }, "Jira token belongs to a person's account; wardby refuses to act and the Jira ingress answers 503 until a service account token is configured")
291
+ : mcpLog.info({ accountId, displayName, accountType }, "Jira acting as"), (err) => mcpLog.warn({ err: err instanceof Error ? err.message : String(err) }, "could not read the Jira account identity"));
292
+ }
293
+ if (jiraConfig?.tokenExpiresAt && jiraConfig.tokenExpiresAt.getTime() - Date.now() < 14 * 24 * 60 * 60 * 1000) {
294
+ mcpLog.warn({ expiresAt: jiraConfig.tokenExpiresAt.toISOString() }, "the Jira API token expires within 14 days; rotate WARDBY_JIRA_API_TOKEN");
295
+ }
296
+ const hostEvents = githubEvents || jiraEvents
297
+ ? { ...(githubEvents ? githubEvents : {}), ...(jiraEvents ? { jira: jiraEvents } : {}) }
298
+ : undefined;
236
299
  const githubAuthorizer = providers.hostUserAuthorizers?.github;
237
300
  const hostUserAuth = githubAuthorizer
238
301
  ? {
@@ -244,10 +307,22 @@ export async function startMcp(options = {}) {
244
307
  }
245
308
  : undefined;
246
309
  mcpLog.info({ enabled: Boolean(hostUserAuth) }, "GitHub account linking");
310
+ const canonicalHref = canonicalUrl(mcpConfig.canonicalUri).href;
311
+ const viewerBus = process.env.DATABASE_URL
312
+ ? createViewerEventBus({ connectionString: process.env.DATABASE_URL })
313
+ : undefined;
314
+ const viewer = viewerBus
315
+ ? createViewerApi({
316
+ db: prisma,
317
+ bus: viewerBus,
318
+ authenticate: (authorization) => authenticate({ authorization }, { authProvider, db: prisma, providers, canonicalUri: canonicalHref }),
319
+ canonicalUri: canonicalHref,
320
+ })
321
+ : undefined;
247
322
  const http = await startHttpServer({
248
323
  mcp,
249
324
  config: {
250
- canonicalUri: canonicalUrl(mcpConfig.canonicalUri).href,
325
+ canonicalUri: canonicalHref,
251
326
  httpBind: mcpConfig.httpBind,
252
327
  authProviderKind,
253
328
  authorizationServer: authConfig.issuer,
@@ -257,6 +332,7 @@ export async function startMcp(options = {}) {
257
332
  selfHosted,
258
333
  hostEvents,
259
334
  hostUserAuth,
335
+ viewer,
260
336
  });
261
337
  const cleanupTimer = selfHosted
262
338
  ? setInterval(() => {
@@ -268,10 +344,12 @@ export async function startMcp(options = {}) {
268
344
  if (cleanupTimer)
269
345
  clearInterval(cleanupTimer);
270
346
  await closeQuietly(http.close(), "HTTP transport close");
347
+ await closeQuietly(viewerBus?.close(), "viewer event bus close");
271
348
  // Let the runs this instance is executing finish before the executor
272
349
  // closes under them; a run abandoned here is lost, never resumed.
273
350
  await waitForInFlightRuns(drainSeconds * 1000);
274
351
  await closeQuietly(providers.executor.close?.(), "executor close");
352
+ modelCatalog.close();
275
353
  },
276
354
  };
277
355
  }
@@ -24,12 +24,15 @@
24
24
  */
25
25
  import { McpServer, fromJsonSchema, createRequestStateCodec, CLIENT_CAPABILITIES_META_KEY, } from "@modelcontextprotocol/server";
26
26
  import { randomBytes } from "node:crypto";
27
+ import { readFileSync } from "node:fs";
27
28
  import { requireScope } from "./auth/resource-server.js";
28
29
  import { McpError, mapPrismaError } from "./errors.js";
29
30
  import { TASKS_EXTENSION_ID, clientSupportsTasks } from "./capabilities.js";
30
31
  import { readableAgentsWhere } from "./auth/access.js";
31
32
  import { grantedIds } from "../core/grants.js";
32
33
  import { logger } from "../core/logger.js";
34
+ /** The package version, reported in MCP server info (package.json sits two levels up from src/mcp and dist/mcp). */
35
+ const SERVER_VERSION = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8")).version;
33
36
  const REQUEST_STATE_KEY_BYTES = 32;
34
37
  /**
35
38
  * REQUEST_STATE_KEY (hex, 32 bytes) shares the requestState HMAC key across
@@ -55,7 +58,7 @@ function loadRequestStateKey(env) {
55
58
  * with the per-agent MCP prompts registered below, which are the concrete,
56
59
  * one-call way to actually do it.
57
60
  */
58
- const SERVER_INSTRUCTIONS = 'wardby hosts reusable LLM agents (a system prompt, model, budget, and attached tools) that normally run on a schedule or webhook trigger. Every agent\'s full system prompt is available via list_agents/get_agent ("systemPrompt" field), and agents you can see are also registered as MCP prompts by name — you can adopt an agent\'s instructions and run them yourself, in this session, right now. This is a good way to shift work like code review left: e.g., before committing, run the "systemPrompt" of a code-review agent against your own working tree using your own tools, rather than waiting for a separately triggered run. Where a pulled prompt names a wardby-specific sandboxed tool, use your own equivalent tool for the same purpose instead.';
61
+ const SERVER_INSTRUCTIONS = 'wardby hosts reusable LLM agents (a system prompt, model, budget, and attached tools) that normally run on a schedule or webhook trigger. Every agent\'s full system prompt is available via list_agents/get_agent ("systemPrompt" field), and agents you can see are also registered as MCP prompts by name — you can adopt an agent\'s instructions and run them yourself, in this session, right now. This is a good way to shift work like code review left: e.g., before committing, run the "systemPrompt" of a code-review agent against your own working tree using your own tools, rather than waiting for a separately triggered run. Where a pulled prompt names a wardby-specific sandboxed tool, use your own equivalent tool for the same purpose instead. To set up a first agent — an architecture keeper or a builder — read the help article "agent-recipes" with get_help_article and follow its procedure.';
59
62
  /** The MCP prompt body for one Agent — the message a client injects when a user invokes it (e.g. as a slash command). */
60
63
  function agentPromptText(agent) {
61
64
  return [
@@ -119,7 +122,7 @@ export function buildMcpServer(opts) {
119
122
  requestHandlers.push({ method, scope: Array.isArray(scope) ? scope : [scope], handler });
120
123
  }
121
124
  const factory = async () => {
122
- const mcpServer = new McpServer({ name: "wardby", version: "0.0.0" }, {
125
+ const mcpServer = new McpServer({ name: "wardby", version: SERVER_VERSION }, {
123
126
  requestState: { verify: (state, ctx) => requestStateCodec.verify(state, ctx) },
124
127
  instructions: SERVER_INSTRUCTIONS,
125
128
  });