@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.
- package/.env.example +22 -3
- package/README.md +11 -9
- package/dist/cli-help.d.ts +1 -1
- package/dist/cli-help.js +1 -0
- package/dist/cli.js +68 -10
- package/dist/coding/base-commit.d.ts +6 -0
- package/dist/coding/base-commit.js +12 -0
- package/dist/coding/protocol.d.ts +17 -1
- package/dist/coding/protocol.js +17 -6
- package/dist/coding/provider.d.ts +6 -1
- package/dist/coding/provider.js +16 -9
- package/dist/config/providers.d.ts +25 -0
- package/dist/config/providers.js +71 -0
- package/dist/core/attribution.d.ts +101 -0
- package/dist/core/attribution.js +208 -0
- package/dist/core/budget-groups.d.ts +17 -10
- package/dist/core/budget-groups.js +15 -12
- package/dist/core/coding-queue.d.ts +3 -0
- package/dist/core/coding-queue.js +6 -2
- package/dist/core/coding-service-status.d.ts +11 -0
- package/dist/core/coding-service-status.js +17 -0
- package/dist/core/cost-report.d.ts +88 -0
- package/dist/core/cost-report.js +248 -0
- package/dist/core/dispatch.d.ts +42 -2
- package/dist/core/dispatch.js +144 -26
- package/dist/core/engine-native.js +17 -4
- package/dist/core/glob.d.ts +10 -0
- package/dist/core/glob.js +33 -0
- package/dist/core/host-events.d.ts +24 -1
- package/dist/core/host-events.js +341 -1
- package/dist/core/host-status.d.ts +19 -2
- package/dist/core/host-status.js +47 -30
- package/dist/core/issue-bridge.d.ts +60 -0
- package/dist/core/issue-bridge.js +189 -0
- package/dist/core/issue-dedupe.d.ts +70 -0
- package/dist/core/issue-dedupe.js +255 -0
- package/dist/core/issue-events.d.ts +42 -0
- package/dist/core/issue-events.js +155 -0
- package/dist/core/issue-status.d.ts +29 -0
- package/dist/core/issue-status.js +241 -0
- package/dist/core/issue-tracker-tools.d.ts +64 -0
- package/dist/core/issue-tracker-tools.js +850 -0
- package/dist/core/model-usage.d.ts +10 -0
- package/dist/core/model-usage.js +24 -0
- package/dist/core/reconciler.d.ts +8 -4
- package/dist/core/reconciler.js +15 -4
- package/dist/core/review-host-tools.js +10 -3
- package/dist/core/run-pricing.d.ts +61 -0
- package/dist/core/run-pricing.js +56 -0
- package/dist/core/runner.d.ts +5 -2
- package/dist/core/runner.js +178 -27
- package/dist/core/scheduler.d.ts +4 -1
- package/dist/core/scheduler.js +3 -2
- package/dist/core/self-defects.d.ts +80 -0
- package/dist/core/self-defects.js +180 -0
- package/dist/core/tool-names.js +3 -0
- package/dist/core/webhooks.d.ts +9 -1
- package/dist/core/webhooks.js +19 -1
- package/dist/env.js +6 -1
- package/dist/generated/prisma/browser.d.ts +66 -0
- package/dist/generated/prisma/client.d.ts +66 -0
- package/dist/generated/prisma/commonInputTypes.d.ts +122 -52
- package/dist/generated/prisma/enums.d.ts +7 -0
- package/dist/generated/prisma/enums.js +6 -0
- package/dist/generated/prisma/internal/class.d.ts +99 -0
- package/dist/generated/prisma/internal/class.js +4 -4
- package/dist/generated/prisma/internal/prismaNamespace.d.ts +826 -1
- package/dist/generated/prisma/internal/prismaNamespace.js +135 -2
- package/dist/generated/prisma/internal/prismaNamespaceBrowser.d.ts +142 -0
- package/dist/generated/prisma/internal/prismaNamespaceBrowser.js +135 -2
- package/dist/generated/prisma/models/Agent.d.ts +389 -1
- package/dist/generated/prisma/models/AgentIssueProject.d.ts +1838 -0
- package/dist/generated/prisma/models/AgentIssueProject.js +1 -0
- package/dist/generated/prisma/models/AgentRepository.d.ts +1 -1
- package/dist/generated/prisma/models/AuthUser.d.ts +1 -1
- package/dist/generated/prisma/models/CodingProxySession.d.ts +73 -1
- package/dist/generated/prisma/models/CodingRun.d.ts +130 -1
- package/dist/generated/prisma/models/CodingRunServiceStatus.d.ts +1404 -0
- package/dist/generated/prisma/models/CodingRunServiceStatus.js +1 -0
- package/dist/generated/prisma/models/IssueFingerprint.d.ts +1183 -0
- package/dist/generated/prisma/models/IssueFingerprint.js +1 -0
- package/dist/generated/prisma/models/IssuePullRequest.d.ts +1255 -0
- package/dist/generated/prisma/models/IssuePullRequest.js +1 -0
- package/dist/generated/prisma/models/ModelCatalogEntry.d.ts +1322 -0
- package/dist/generated/prisma/models/ModelCatalogEntry.js +1 -0
- package/dist/generated/prisma/models/Run.d.ts +933 -1
- package/dist/generated/prisma/models/RunAttribution.d.ts +1259 -0
- package/dist/generated/prisma/models/RunAttribution.js +1 -0
- package/dist/generated/prisma/models/RunIssueStatus.d.ts +1199 -0
- package/dist/generated/prisma/models/RunIssueStatus.js +1 -0
- package/dist/generated/prisma/models/RunModelUsage.d.ts +1316 -0
- package/dist/generated/prisma/models/RunModelUsage.js +1 -0
- package/dist/generated/prisma/models/WorkItem.d.ts +1408 -0
- package/dist/generated/prisma/models/WorkItem.js +1 -0
- package/dist/generated/prisma/models.d.ts +9 -0
- package/dist/help-index.json +355 -16
- package/dist/import/neutral-schema.d.ts +16 -16
- package/dist/knowledge/check.d.ts +13 -0
- package/dist/knowledge/check.js +69 -0
- package/dist/knowledge/cli.d.ts +14 -0
- package/dist/knowledge/cli.js +67 -0
- package/dist/knowledge/concept.d.ts +54 -0
- package/dist/knowledge/concept.js +78 -0
- package/dist/knowledge/note.d.ts +11 -0
- package/dist/knowledge/note.js +39 -0
- package/dist/knowledge/relevance.d.ts +11 -0
- package/dist/knowledge/relevance.js +14 -0
- package/dist/knowledge/span-hash.d.ts +3 -0
- package/dist/knowledge/span-hash.js +16 -0
- package/dist/mcp/auth/access.d.ts +4 -2
- package/dist/mcp/auth/ownership.d.ts +9 -9
- package/dist/mcp/auth/resource-server.d.ts +3 -1
- package/dist/mcp/auth/resource-server.js +18 -3
- package/dist/mcp/auth/self-hosted/credentials.d.ts +3 -3
- package/dist/mcp/auth/self-hosted/session.d.ts +5 -5
- package/dist/mcp/context.d.ts +3 -0
- package/dist/mcp/host-events/deliveries.d.ts +9 -0
- package/dist/mcp/host-events/deliveries.js +17 -0
- package/dist/mcp/host-events/github-ingress.d.ts +4 -2
- package/dist/mcp/host-events/github-ingress.js +4 -13
- package/dist/mcp/host-events/jira-ingress.d.ts +29 -0
- package/dist/mcp/host-events/jira-ingress.js +92 -0
- package/dist/mcp/index.d.ts +2 -0
- package/dist/mcp/index.js +87 -9
- package/dist/mcp/server.js +5 -2
- package/dist/mcp/tools/agents.js +74 -3
- package/dist/mcp/tools/cost-report.d.ts +8 -0
- package/dist/mcp/tools/cost-report.js +60 -0
- package/dist/mcp/tools/issue-projects.d.ts +2 -0
- package/dist/mcp/tools/issue-projects.js +238 -0
- package/dist/mcp/tools/model-catalog.d.ts +22 -0
- package/dist/mcp/tools/model-catalog.js +423 -0
- package/dist/mcp/tools/repositories.js +2 -1
- package/dist/mcp/tools/tools.d.ts +2 -2
- package/dist/mcp/tools/trigger.js +33 -5
- package/dist/mcp/transport/streamable-http.d.ts +5 -0
- package/dist/mcp/transport/streamable-http.js +23 -1
- package/dist/mcp/webhooks/ingress.d.ts +2 -1
- package/dist/mcp/webhooks/ingress.js +9 -2
- package/dist/providers/auth/self-hosted.d.ts +8 -1
- package/dist/providers/auth/self-hosted.js +39 -2
- package/dist/providers/coding-proxy/memory-ledger.d.ts +1 -1
- package/dist/providers/coding-proxy/memory-ledger.js +10 -1
- package/dist/providers/coding-proxy/metering.d.ts +2 -1
- package/dist/providers/coding-proxy/metering.js +13 -2
- package/dist/providers/coding-proxy/prisma-ledger.js +59 -6
- package/dist/providers/coding-proxy/proxy.d.ts +12 -2
- package/dist/providers/coding-proxy/proxy.js +92 -30
- package/dist/providers/coding-proxy/types.d.ts +18 -1
- package/dist/providers/coding-proxy/types.js +12 -1
- package/dist/providers/engine/types.d.ts +19 -0
- package/dist/providers/executor/composition.js +9 -1
- package/dist/providers/executor/container.d.ts +30 -2
- package/dist/providers/executor/container.js +98 -17
- package/dist/providers/executor/dbos.d.ts +2 -0
- package/dist/providers/executor/dbos.js +7 -5
- package/dist/providers/executor/routing.d.ts +6 -0
- package/dist/providers/executor/routing.js +5 -0
- package/dist/providers/executor/types.d.ts +12 -0
- package/dist/providers/issue-tracker/adf.d.ts +31 -0
- package/dist/providers/issue-tracker/adf.js +181 -0
- package/dist/providers/issue-tracker/index.d.ts +5 -0
- package/dist/providers/issue-tracker/index.js +12 -0
- package/dist/providers/issue-tracker/jira-client.d.ts +41 -0
- package/dist/providers/issue-tracker/jira-client.js +151 -0
- package/dist/providers/issue-tracker/jira-events.d.ts +3 -0
- package/dist/providers/issue-tracker/jira-events.js +98 -0
- package/dist/providers/issue-tracker/jira.d.ts +116 -0
- package/dist/providers/issue-tracker/jira.js +502 -0
- package/dist/providers/issue-tracker/types.d.ts +269 -0
- package/dist/providers/issue-tracker/types.js +16 -0
- package/dist/providers/jobs/docker.d.ts +5 -1
- package/dist/providers/jobs/docker.js +61 -33
- package/dist/providers/jobs/kubernetes.d.ts +3 -0
- package/dist/providers/jobs/kubernetes.js +44 -4
- package/dist/providers/jobs/service-state.d.ts +22 -0
- package/dist/providers/jobs/service-state.js +17 -0
- package/dist/providers/llm/anthropic.d.ts +3 -3
- package/dist/providers/llm/anthropic.js +3 -9
- package/dist/providers/llm/bedrock.d.ts +3 -3
- package/dist/providers/llm/bedrock.js +3 -9
- package/dist/providers/llm/catalog-lookup.d.ts +10 -0
- package/dist/providers/llm/catalog-lookup.js +15 -0
- package/dist/providers/llm/catalog-shipped.d.ts +18 -0
- package/dist/providers/llm/catalog-shipped.js +197 -0
- package/dist/providers/llm/catalog-store.d.ts +58 -0
- package/dist/providers/llm/catalog-store.js +138 -0
- package/dist/providers/llm/catalog-types.d.ts +66 -0
- package/dist/providers/llm/catalog-types.js +64 -0
- package/dist/providers/llm/catalog.d.ts +61 -0
- package/dist/providers/llm/catalog.js +147 -0
- package/dist/providers/llm/claude-provider.d.ts +10 -14
- package/dist/providers/llm/claude-provider.js +11 -6
- package/dist/providers/llm/index.d.ts +9 -6
- package/dist/providers/llm/index.js +8 -5
- package/dist/providers/llm/openai.d.ts +14 -5
- package/dist/providers/llm/openai.js +24 -14
- package/dist/providers/llm/pricing-core.d.ts +5 -3
- package/dist/providers/llm/registration.js +8 -12
- package/dist/providers/llm/routing.d.ts +18 -17
- package/dist/providers/llm/routing.js +40 -24
- package/dist/providers/review-host/github-events.js +47 -1
- package/dist/providers/review-host/github.js +7 -6
- package/dist/providers/review-host/types.d.ts +25 -0
- package/dist/providers/vcs/git.js +2 -22
- package/dist/providers/vcs/github.d.ts +20 -0
- package/dist/providers/vcs/github.js +28 -2
- package/dist/providers/vcs/types.d.ts +6 -0
- package/dist/quickstart/index.d.ts +8 -0
- package/dist/quickstart/index.js +34 -34
- package/dist/serve.js +8 -2
- package/dist/viewer/api-schema.d.ts +2757 -0
- package/dist/viewer/api-schema.js +165 -0
- package/dist/viewer/build-schemas.d.ts +2 -0
- package/dist/viewer/build-schemas.js +18 -0
- package/dist/viewer/event-bus.d.ts +38 -0
- package/dist/viewer/event-bus.js +232 -0
- package/dist/viewer/graph.d.ts +40 -0
- package/dist/viewer/graph.js +243 -0
- package/dist/viewer/http.d.ts +30 -0
- package/dist/viewer/http.js +133 -0
- package/dist/viewer/run-detail.d.ts +4 -0
- package/dist/viewer/run-detail.js +61 -0
- package/dist/wardby-bin.js +5 -0
- package/docs/README.md +10 -0
- package/docs/agent-recipes.md +383 -0
- package/docs/code-review-agents.md +29 -2
- package/docs/coding-agent-setup.md +3 -0
- package/docs/coding-worker-isolation.md +39 -5
- package/docs/getting-started-gke.md +28 -11
- package/docs/getting-started-identity-provider.md +49 -38
- package/docs/getting-started.md +14 -0
- package/docs/jira-agents.md +649 -0
- package/docs/knowledge.md +387 -0
- package/docs/models.md +221 -0
- package/docs/security-deployment.md +19 -9
- package/docs/viewer-api.md +142 -0
- package/help/admin-viewer.md +39 -0
- package/help/agent-recipes.md +173 -0
- package/help/architecture-agent.md +189 -0
- package/help/builder-agent.md +80 -0
- package/help/code-review-agents.md +6 -0
- package/help/cost-attribution.md +67 -0
- package/help/creating-agents.md +22 -0
- package/help/deploy-gke.md +6 -0
- package/help/errors/model-unavailable.md +63 -0
- package/help/getting-started.md +1 -0
- package/help/github.md +18 -0
- package/help/identity-and-access.md +8 -3
- package/help/jira.md +135 -0
- package/help/knowledge.md +47 -0
- package/help/models.md +90 -0
- package/help/operating-agents.md +7 -1
- package/help/troubleshooting/budgets.md +6 -0
- package/package.json +5 -2
- package/prisma/migrations/20260930000000_jira_issue_projects/migration.sql +34 -0
- package/prisma/migrations/20261001000000_jira_phase2_allowlists/migration.sql +3 -0
- package/prisma/migrations/20261001010000_jira_link_types_allowlist/migration.sql +2 -0
- package/prisma/migrations/20261002000000_jira_coding_bridge/migration.sql +28 -0
- package/prisma/migrations/20261002010000_jira_issue_creation/migration.sql +25 -0
- package/prisma/migrations/20261003000000_issue_cost_attribution/migration.sql +56 -0
- package/prisma/migrations/20261003010000_coding_run_service_status/migration.sql +23 -0
- package/prisma/migrations/20261003020000_viewer_notify/migration.sql +54 -0
- package/prisma/migrations/20261003030000_viewer_notify_fixes/migration.sql +47 -0
- package/prisma/migrations/20261003040000_viewer_indexes/migration.sql +12 -0
- package/prisma/migrations/20261004000000_model_catalog/migration.sql +26 -0
- package/prisma/schema.prisma +258 -2
- package/dist/mcp/tools/models.d.ts +0 -8
- package/dist/mcp/tools/models.js +0 -15
- package/dist/providers/llm/pricing-anthropic.d.ts +0 -14
- package/dist/providers/llm/pricing-anthropic.js +0 -48
- package/dist/providers/llm/pricing-bedrock-claude.d.ts +0 -20
- package/dist/providers/llm/pricing-bedrock-claude.js +0 -46
- package/dist/providers/llm/pricing.d.ts +0 -30
- 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 = [
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/dist/mcp/context.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/dist/mcp/index.d.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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 = {
|
|
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: {
|
|
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
|
|
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(
|
|
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:
|
|
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
|
}
|
package/dist/mcp/server.js
CHANGED
|
@@ -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:
|
|
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
|
});
|