@wardby/cli 0.4.1 → 0.5.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 +8 -1
- package/README.md +3 -1
- package/deploy/local/docker-compose.quickstart-coding.yml +27 -0
- package/dist/claude-coding-worker/driver.d.ts +2 -0
- package/dist/claude-coding-worker/driver.js +19 -3
- package/dist/claude-coding-worker/sdk.js +1 -1
- package/dist/cli.js +41 -22
- package/dist/coding/continuation-wording.d.ts +10 -0
- package/dist/coding/continuation-wording.js +13 -0
- package/dist/coding/docker-preflight.d.ts +12 -0
- package/dist/coding/docker-preflight.js +40 -0
- package/dist/coding/local-git.d.ts +22 -0
- package/dist/coding/local-git.js +102 -0
- package/dist/coding/local-repo-status.d.ts +7 -0
- package/dist/coding/local-repo-status.js +56 -0
- package/dist/coding/local-repo.d.ts +17 -0
- package/dist/coding/local-repo.js +65 -0
- package/dist/coding/observability.d.ts +1 -1
- package/dist/coding/observability.js +1 -0
- package/dist/coding/profile.d.ts +6 -0
- package/dist/coding/profile.js +13 -4
- package/dist/coding/protocol.d.ts +32 -7
- package/dist/coding/protocol.js +66 -4
- package/dist/coding-worker/artifact.d.ts +1 -0
- package/dist/coding-worker/errors.js +2 -0
- package/dist/config/providers.d.ts +13 -0
- package/dist/config/providers.js +21 -9
- package/dist/core/attribution.d.ts +13 -0
- package/dist/core/attribution.js +30 -0
- package/dist/core/budget-groups.d.ts +3 -1
- package/dist/core/budget-groups.js +53 -5
- package/dist/core/ci-context.d.ts +8 -0
- package/dist/core/ci-context.js +59 -0
- package/dist/core/delegation-siblings.d.ts +22 -0
- package/dist/core/delegation-siblings.js +40 -0
- package/dist/core/dispatch.d.ts +28 -4
- package/dist/core/dispatch.js +57 -15
- package/dist/core/engine-native.d.ts +16 -0
- package/dist/core/engine-native.js +49 -2
- package/dist/core/host-events.d.ts +25 -2
- package/dist/core/host-events.js +361 -19
- package/dist/core/host-status.d.ts +8 -3
- package/dist/core/host-status.js +38 -7
- package/dist/core/issue-events.d.ts +2 -6
- package/dist/core/issue-events.js +13 -19
- package/dist/core/pull-request-state-sync.d.ts +17 -0
- package/dist/core/pull-request-state-sync.js +53 -0
- package/dist/core/reconciler.d.ts +10 -1
- package/dist/core/reconciler.js +15 -2
- package/dist/core/related-pull-requests.d.ts +95 -0
- package/dist/core/related-pull-requests.js +338 -0
- package/dist/core/repo-access.d.ts +5 -1
- package/dist/core/repo-access.js +17 -1
- package/dist/core/review-fix-ledger.d.ts +20 -0
- package/dist/core/review-fix-ledger.js +41 -0
- package/dist/core/review-fix.d.ts +60 -0
- package/dist/core/review-fix.js +155 -0
- package/dist/core/review-host-tools.d.ts +13 -2
- package/dist/core/review-host-tools.js +72 -11
- package/dist/core/runner.d.ts +2 -2
- package/dist/core/runner.js +379 -184
- package/dist/core/serial-gate.d.ts +11 -0
- package/dist/core/serial-gate.js +19 -0
- package/dist/core/timing.d.ts +7 -0
- package/dist/core/timing.js +7 -0
- package/dist/generated/prisma/browser.d.ts +23 -0
- package/dist/generated/prisma/client.d.ts +23 -0
- package/dist/generated/prisma/commonInputTypes.d.ts +22 -0
- package/dist/generated/prisma/internal/class.d.ts +33 -0
- package/dist/generated/prisma/internal/class.js +4 -4
- package/dist/generated/prisma/internal/prismaNamespace.d.ts +274 -1
- package/dist/generated/prisma/internal/prismaNamespace.js +47 -2
- package/dist/generated/prisma/internal/prismaNamespaceBrowser.d.ts +48 -0
- package/dist/generated/prisma/internal/prismaNamespaceBrowser.js +47 -2
- package/dist/generated/prisma/models/Agent.d.ts +422 -1
- package/dist/generated/prisma/models/AgentRepository.d.ts +120 -2
- package/dist/generated/prisma/models/CodingAgentProfile.d.ts +42 -1
- package/dist/generated/prisma/models/CodingRun.d.ts +205 -1
- package/dist/generated/prisma/models/DeferredReview.d.ts +1336 -0
- package/dist/generated/prisma/models/DeferredReview.js +1 -0
- package/dist/generated/prisma/models/LocalPullRequest.d.ts +1384 -0
- package/dist/generated/prisma/models/LocalPullRequest.js +1 -0
- package/dist/generated/prisma/models/LocalReview.d.ts +1315 -0
- package/dist/generated/prisma/models/LocalReview.js +1 -0
- package/dist/generated/prisma/models/Run.d.ts +223 -0
- package/dist/generated/prisma/models/RunHostCheck.d.ts +148 -1
- package/dist/generated/prisma/models.d.ts +3 -0
- package/dist/help-index.json +486 -37
- package/dist/import/neutral-schema.d.ts +2 -2
- package/dist/mcp/auth/access.d.ts +3 -1
- package/dist/mcp/auth/host-account-cli.js +2 -2
- package/dist/mcp/auth/repo-authorization.d.ts +6 -7
- package/dist/mcp/auth/repo-authorization.js +21 -0
- package/dist/mcp/index.js +4 -1
- package/dist/mcp/tools/agents.js +46 -4
- package/dist/mcp/tools/host-accounts.js +2 -2
- package/dist/mcp/tools/repositories.js +71 -12
- package/dist/mcp/tools/runs.js +23 -0
- package/dist/mcp/tools/trigger.js +153 -24
- package/dist/providers/engine/types.d.ts +11 -0
- package/dist/providers/executor/composition.js +12 -7
- package/dist/providers/executor/container.d.ts +40 -3
- package/dist/providers/executor/container.js +140 -9
- package/dist/providers/executor/types.d.ts +1 -1
- package/dist/providers/jobs/fake-kubernetes-api.d.ts +3 -2
- package/dist/providers/jobs/fake-kubernetes-api.js +3 -0
- package/dist/providers/jobs/kubernetes-api.d.ts +3 -1
- package/dist/providers/jobs/kubernetes-client.d.ts +2 -1
- package/dist/providers/jobs/kubernetes-client.js +3 -0
- package/dist/providers/jobs/kubernetes-isolation.d.ts +7 -0
- package/dist/providers/jobs/kubernetes-isolation.js +8 -3
- package/dist/providers/jobs/kubernetes-preflight.d.ts +8 -0
- package/dist/providers/jobs/kubernetes-preflight.js +7 -0
- package/dist/providers/jobs/kubernetes-quota.d.ts +31 -0
- package/dist/providers/jobs/kubernetes-quota.js +110 -0
- package/dist/providers/jobs/kubernetes.d.ts +9 -0
- package/dist/providers/jobs/kubernetes.js +50 -0
- package/dist/providers/jobs/types.d.ts +7 -0
- package/dist/providers/review-host/ci.d.ts +7 -0
- package/dist/providers/review-host/ci.js +20 -0
- package/dist/providers/review-host/github-events.d.ts +7 -0
- package/dist/providers/review-host/github-events.js +31 -7
- package/dist/providers/review-host/github.d.ts +19 -2
- package/dist/providers/review-host/github.js +145 -2
- package/dist/providers/review-host/index.d.ts +8 -2
- package/dist/providers/review-host/index.js +14 -3
- package/dist/providers/review-host/local.d.ts +72 -0
- package/dist/providers/review-host/local.js +434 -0
- package/dist/providers/review-host/types.d.ts +65 -3
- package/dist/providers/review-host/types.js +5 -3
- package/dist/providers/vcs/git.d.ts +10 -20
- package/dist/providers/vcs/git.js +71 -127
- package/dist/providers/vcs/github-remote.d.ts +67 -0
- package/dist/providers/vcs/github-remote.js +201 -0
- package/dist/providers/vcs/github.d.ts +81 -0
- package/dist/providers/vcs/github.js +203 -6
- package/dist/providers/vcs/index.d.ts +16 -1
- package/dist/providers/vcs/index.js +43 -15
- package/dist/providers/vcs/local-remote.d.ts +44 -0
- package/dist/providers/vcs/local-remote.js +108 -0
- package/dist/providers/vcs/remote.d.ts +58 -0
- package/dist/providers/vcs/remote.js +1 -0
- package/dist/providers/vcs/routing.d.ts +33 -0
- package/dist/providers/vcs/routing.js +52 -0
- package/dist/providers/vcs/types.d.ts +12 -1
- package/dist/quickstart/coding-db.d.ts +11 -0
- package/dist/quickstart/coding-db.js +75 -0
- package/dist/quickstart/coding-doctor.d.ts +13 -0
- package/dist/quickstart/coding-doctor.js +88 -0
- package/dist/quickstart/coding-images.d.ts +15 -0
- package/dist/quickstart/coding-images.js +79 -0
- package/dist/quickstart/coding-seed.d.ts +63 -0
- package/dist/quickstart/coding-seed.js +85 -0
- package/dist/quickstart/coding.d.ts +66 -0
- package/dist/quickstart/coding.js +430 -0
- package/dist/quickstart/images.d.ts +19 -0
- package/dist/quickstart/images.js +63 -0
- package/dist/quickstart/index.js +61 -12
- package/dist/quickstart/starter-services.d.ts +45 -0
- package/dist/quickstart/starter-services.js +186 -0
- package/dist/quickstart-images.json +1 -0
- package/dist/serve.js +14 -1
- package/dist/viewer/api-schema.d.ts +176 -0
- package/dist/viewer/api-schema.js +27 -2
- package/dist/viewer/graph.d.ts +7 -2
- package/dist/viewer/graph.js +25 -11
- package/dist/viewer/http.d.ts +7 -1
- package/dist/viewer/http.js +8 -3
- package/dist/viewer/infra.d.ts +2 -0
- package/dist/viewer/infra.js +23 -0
- package/dist/viewer/run-detail.d.ts +2 -1
- package/dist/viewer/run-detail.js +2 -2
- package/docs/agent-recipes.md +127 -5
- package/docs/code-review-agents.md +293 -38
- package/docs/coding-agent-setup.md +141 -2
- package/docs/coding-packages.md +21 -0
- package/docs/coding-services.md +3 -0
- package/docs/coding-worker-isolation.md +56 -20
- package/docs/getting-started-gke.md +24 -0
- package/docs/getting-started.md +196 -10
- package/docs/jira-agents.md +26 -5
- package/docs/security-deployment.md +9 -0
- package/docs/viewer-api.md +21 -2
- package/help/admin-viewer.md +11 -1
- package/help/agent-recipes.md +30 -4
- package/help/builder-agent.md +4 -0
- package/help/code-review-agents.md +82 -2
- package/help/coding-packages.md +12 -1
- package/help/coding-services.md +4 -0
- package/help/cost-attribution.md +3 -1
- package/help/creating-agents.md +3 -0
- package/help/deploy-gke.md +2 -1
- package/help/errors/coding-provider-not-configured.md +60 -0
- package/help/errors/coding-turn-limit.md +25 -0
- package/help/errors/continuation-closed.md +75 -0
- package/help/errors/local-branch-conflict.md +37 -0
- package/help/errors/local-path-invalid.md +30 -0
- package/help/errors/local-ref-invalid.md +36 -0
- package/help/errors/local-ref-not-found.md +41 -0
- package/help/errors/local-repo-not-allowed.md +46 -0
- package/help/errors/local-repo-not-found.md +39 -0
- package/help/errors/model-unavailable.md +7 -6
- package/help/errors/vcs-github-not-configured.md +34 -0
- package/help/getting-started.md +28 -0
- package/help/github.md +22 -5
- package/help/jira.md +9 -2
- package/help/local-repositories.md +182 -0
- package/help/related-pull-requests.md +89 -0
- package/help/review-fix-rounds.md +69 -0
- package/help/troubleshooting/budgets.md +11 -0
- package/help/troubleshooting/coding-workers.md +22 -0
- package/help/troubleshooting/repository-access.md +5 -0
- package/package.json +3 -2
- package/prisma/migrations/20261004100000_delegations_and_coding_turns/migration.sql +12 -0
- package/prisma/migrations/20261005000000_review_fix_rounds/migration.sql +5 -0
- package/prisma/migrations/20261006000000_parallel_delegations/migration.sql +6 -0
- package/prisma/migrations/20261007000000_ci_rereview/migration.sql +4 -0
- package/prisma/migrations/20261008000000_review_after_ci/migration.sql +33 -0
- package/prisma/migrations/20261009000001_coding_run_local_branch/migration.sql +7 -0
- package/prisma/migrations/20261009000002_local_review/migration.sql +50 -0
- package/prisma/schema.prisma +98 -1
|
@@ -110,13 +110,13 @@ export declare const NeutralAgentSchema: z.ZodObject<{
|
|
|
110
110
|
name: string;
|
|
111
111
|
model: string;
|
|
112
112
|
budgetUsd: string;
|
|
113
|
+
maxTurns: number;
|
|
113
114
|
provider: "openai" | "bedrock";
|
|
114
115
|
kind: "native";
|
|
115
116
|
systemPrompt: string;
|
|
116
117
|
schedule: string | null;
|
|
117
118
|
timezone: string;
|
|
118
119
|
scheduleEnabled: boolean;
|
|
119
|
-
maxTurns: number;
|
|
120
120
|
memoryEnabled: boolean;
|
|
121
121
|
region: string | null;
|
|
122
122
|
ownerEmail: string | null;
|
|
@@ -129,10 +129,10 @@ export declare const NeutralAgentSchema: z.ZodObject<{
|
|
|
129
129
|
name: string;
|
|
130
130
|
model: string;
|
|
131
131
|
budgetUsd: string;
|
|
132
|
+
maxTurns: number;
|
|
132
133
|
provider: "openai" | "bedrock";
|
|
133
134
|
systemPrompt: string;
|
|
134
135
|
scheduleEnabled: boolean;
|
|
135
|
-
maxTurns: number;
|
|
136
136
|
kind?: "native" | undefined;
|
|
137
137
|
schedule?: string | null | undefined;
|
|
138
138
|
timezone?: string | undefined;
|
|
@@ -40,6 +40,7 @@ export declare function requireAgentAccess(ctx: McpRequestContext, id: string, r
|
|
|
40
40
|
name: string;
|
|
41
41
|
model: string;
|
|
42
42
|
budgetUsd: import("@prisma/client-runtime-utils").Decimal;
|
|
43
|
+
maxTurns: number;
|
|
43
44
|
createdAt: Date;
|
|
44
45
|
updatedAt: Date;
|
|
45
46
|
kind: import("#prisma").AgentKind;
|
|
@@ -50,7 +51,8 @@ export declare function requireAgentAccess(ctx: McpRequestContext, id: string, r
|
|
|
50
51
|
timezone: string;
|
|
51
52
|
scheduleEnabled: boolean;
|
|
52
53
|
lastScheduledAt: Date | null;
|
|
53
|
-
|
|
54
|
+
maxDelegationsPerRun: number;
|
|
55
|
+
parallelDelegations: boolean;
|
|
54
56
|
defectProjectKey: string | null;
|
|
55
57
|
defectIssueType: string | null;
|
|
56
58
|
budgetGroupId: string | null;
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* principal (link_host_account refuses one already linked elsewhere).
|
|
7
7
|
*/
|
|
8
8
|
import { parseArgs } from "node:util";
|
|
9
|
-
import {
|
|
9
|
+
import { HOST_ACCOUNT_PROVIDERS } from "../../providers/review-host/types.js";
|
|
10
10
|
const USAGE = "Use auth host-account list [--subject <s>] | host-account unlink --subject <s> [--provider github].";
|
|
11
11
|
export async function hostAccountCommand(args, db, output = console.log) {
|
|
12
12
|
const [action, ...rest] = args;
|
|
@@ -19,7 +19,7 @@ export async function hostAccountCommand(args, db, output = console.log) {
|
|
|
19
19
|
options: { subject: { type: "string" }, provider: { type: "string" } },
|
|
20
20
|
});
|
|
21
21
|
const provider = values.provider ?? "github";
|
|
22
|
-
if (!
|
|
22
|
+
if (!HOST_ACCOUNT_PROVIDERS.includes(provider))
|
|
23
23
|
throw new Error(`Unknown provider "${provider}".`);
|
|
24
24
|
if (action === "list") {
|
|
25
25
|
const rows = await db.hostIdentity.findMany({
|
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Set-time repository authorization for the MCP tools that give an agent
|
|
3
|
-
* authority over a repository (link_repository; create_agent/update_agent's
|
|
4
|
-
* codingProfile.repository). Returns the stamp to store on the granting row,
|
|
5
|
-
* or throws an actionable McpError. See core/repo-access.ts for the use-time
|
|
6
|
-
* side and the rules; the host call happens here, before any transaction.
|
|
7
|
-
*/
|
|
8
1
|
import { type AuthorizedVia, type RepoAccessKind } from "../../core/repo-access.js";
|
|
9
2
|
import type { McpRequestContext } from "../context.js";
|
|
10
3
|
export interface RepositoryAuthorization {
|
|
@@ -12,6 +5,12 @@ export interface RepositoryAuthorization {
|
|
|
12
5
|
authorizedById: string;
|
|
13
6
|
authorizedAt: Date;
|
|
14
7
|
}
|
|
8
|
+
/**
|
|
9
|
+
* The canonical (realpath-based) name of a `local:` repository inside the
|
|
10
|
+
* current trusted roots; a 400 whose message carries the error code otherwise.
|
|
11
|
+
* Agents store this, never the caller's spelling.
|
|
12
|
+
*/
|
|
13
|
+
export declare function canonicalLocalRepository(repository: string): Promise<string>;
|
|
15
14
|
export declare function authorizeRepositoryForSet(ctx: McpRequestContext, input: {
|
|
16
15
|
/** The agent's owner. The caller is always the owner here (or creating the agent). */
|
|
17
16
|
ownerId: string | null;
|
|
@@ -5,9 +5,25 @@
|
|
|
5
5
|
* or throws an actionable McpError. See core/repo-access.ts for the use-time
|
|
6
6
|
* side and the rules; the host call happens here, before any transaction.
|
|
7
7
|
*/
|
|
8
|
+
import { LocalRepoError, isLocalRepository, loadLocalRepoRoots, resolveLocalRepository, } from "../../coding/local-repo.js";
|
|
8
9
|
import { requiredLevel } from "../../core/repo-access.js";
|
|
9
10
|
import { McpError } from "../errors.js";
|
|
10
11
|
import { requireScope } from "./resource-server.js";
|
|
12
|
+
/**
|
|
13
|
+
* The canonical (realpath-based) name of a `local:` repository inside the
|
|
14
|
+
* current trusted roots; a 400 whose message carries the error code otherwise.
|
|
15
|
+
* Agents store this, never the caller's spelling.
|
|
16
|
+
*/
|
|
17
|
+
export async function canonicalLocalRepository(repository) {
|
|
18
|
+
try {
|
|
19
|
+
return (await resolveLocalRepository(repository, loadLocalRepoRoots(process.env).roots)).repository;
|
|
20
|
+
}
|
|
21
|
+
catch (err) {
|
|
22
|
+
if (err instanceof LocalRepoError)
|
|
23
|
+
throw new McpError(400, err.message);
|
|
24
|
+
throw new McpError(400, `local_repo_not_found: ${err.message}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
11
27
|
export async function authorizeRepositoryForSet(ctx, input) {
|
|
12
28
|
if (!input.ownerId) {
|
|
13
29
|
throw new McpError(400, "owner_required: agents without an owner can't be given a repository, because an owner-less agent has no owner " +
|
|
@@ -18,6 +34,11 @@ export async function authorizeRepositoryForSet(ctx, input) {
|
|
|
18
34
|
authorizedById: ctx.principal.id,
|
|
19
35
|
authorizedAt: new Date(),
|
|
20
36
|
});
|
|
37
|
+
if (isLocalRepository(input.repository)) {
|
|
38
|
+
// Trusted roots decide, never a host identity or an admin override.
|
|
39
|
+
await canonicalLocalRepository(input.repository);
|
|
40
|
+
return stamp("local_root");
|
|
41
|
+
}
|
|
21
42
|
if (input.adminOverride) {
|
|
22
43
|
// Role-gated: the scope alone never suffices (resource-server.ts).
|
|
23
44
|
requireScope(ctx, ctx.canonicalUri, "agents:admin");
|
package/dist/mcp/index.js
CHANGED
|
@@ -38,6 +38,7 @@ import { ROLE_NAMES, SCOPES_SUPPORTED, authenticate } from "./auth/resource-serv
|
|
|
38
38
|
import { canonicalUrl } from "./transport/http-limits.js";
|
|
39
39
|
import { createViewerApi } from "../viewer/http.js";
|
|
40
40
|
import { createViewerEventBus } from "../viewer/event-bus.js";
|
|
41
|
+
import { buildInfraInfo } from "../viewer/infra.js";
|
|
41
42
|
import { registerAgentTools } from "./tools/agents.js";
|
|
42
43
|
import { registerServiceTools } from "./tools/services.js";
|
|
43
44
|
import { registerBudgetGroupTools } from "./tools/budget-groups.js";
|
|
@@ -135,7 +136,7 @@ export function buildMcpProviders() {
|
|
|
135
136
|
const secrets = buildSecretCipher(providerConfig);
|
|
136
137
|
const datastore = new PostgresDatastore(prisma, secrets);
|
|
137
138
|
const memory = new PostgresAgentMemory(prisma);
|
|
138
|
-
const reviewHosts = buildReviewHosts();
|
|
139
|
+
const reviewHosts = buildReviewHosts(process.env, prisma);
|
|
139
140
|
const issueTrackers = buildIssueTrackers();
|
|
140
141
|
const hostUserAuthorizers = buildHostUserAuthorizers();
|
|
141
142
|
// One gate (and one cache) for the whole process: set-time checks in the
|
|
@@ -317,6 +318,8 @@ export async function startMcp(options = {}) {
|
|
|
317
318
|
bus: viewerBus,
|
|
318
319
|
authenticate: (authorization) => authenticate({ authorization }, { authProvider, db: prisma, providers, canonicalUri: canonicalHref }),
|
|
319
320
|
canonicalUri: canonicalHref,
|
|
321
|
+
infra: buildInfraInfo(),
|
|
322
|
+
issueSites: jiraConfig ? { jira: jiraConfig.siteUrl } : {},
|
|
320
323
|
})
|
|
321
324
|
: undefined;
|
|
322
325
|
const http = await startHttpServer({
|
package/dist/mcp/tools/agents.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { Prisma } from "#prisma";
|
|
9
9
|
import { z } from "zod";
|
|
10
10
|
import { CodingProfilePatchSchema, CodingProfileSchema } from "../../coding/profile.js";
|
|
11
|
+
import { DEFAULT_CLAUDE_MAX_TURNS, MAX_CODING_TURNS, isLocalRepository } from "../../coding/protocol.js";
|
|
11
12
|
import { codingProviderSupportsModel } from "../../coding/provider.js";
|
|
12
13
|
import { assertAgentModelAvailable } from "../../core/run-pricing.js";
|
|
13
14
|
import { ModelUnavailableError } from "../../providers/llm/catalog-types.js";
|
|
@@ -22,7 +23,7 @@ import { EVERYONE_KEY, atLeast, canDelegate, deleteGrantsFor, effectiveAccess }
|
|
|
22
23
|
import { projectTool } from "./tools.js";
|
|
23
24
|
import { requireAnyScope, requireScope } from "../auth/resource-server.js";
|
|
24
25
|
import { logger } from "../../core/logger.js";
|
|
25
|
-
import { authorizeRepositoryForSet } from "../auth/repo-authorization.js";
|
|
26
|
+
import { authorizeRepositoryForSet, canonicalLocalRepository, } from "../auth/repo-authorization.js";
|
|
26
27
|
import { McpError } from "../errors.js";
|
|
27
28
|
import { textResult } from "./text-result.js";
|
|
28
29
|
// workerImageRef is the BYO-arbitrary-image escape hatch (see
|
|
@@ -48,12 +49,19 @@ const MAX_AGENT_NAME_CHARS = 200;
|
|
|
48
49
|
const MAX_SYSTEM_PROMPT_CHARS = 64 * 1024;
|
|
49
50
|
const MAX_MODEL_CHARS = 128;
|
|
50
51
|
const MAX_BUDGET_USD = 1_000_000;
|
|
52
|
+
/** Upper bound on Agent.maxDelegationsPerRun: a lead fanning out to one builder per repository. */
|
|
53
|
+
const MAX_DELEGATIONS_PER_RUN = 20;
|
|
54
|
+
const PARALLEL_DELEGATIONS_DESCRIPTION = "Native agents with sub-agents: when true, the delegate_to_* calls the agent makes in one model turn run at the same time " +
|
|
55
|
+
"instead of one after another (other tool calls still run in order). The run tree still shares one budget, so later " +
|
|
56
|
+
"sub-agents get what earlier ones leave. Coding sub-agents still queue for CODING_MAX_CONCURRENT slots. Default false.";
|
|
51
57
|
const agentFields = {
|
|
52
58
|
name: z.string().trim().min(1).max(MAX_AGENT_NAME_CHARS),
|
|
53
59
|
systemPrompt: z.string().min(1).max(MAX_SYSTEM_PROMPT_CHARS),
|
|
54
60
|
model: z.string().trim().min(1).max(MAX_MODEL_CHARS),
|
|
55
61
|
budgetUsd: z.number().finite().positive().max(MAX_BUDGET_USD),
|
|
56
62
|
maxTurns: z.number().int().min(1).max(100),
|
|
63
|
+
maxDelegationsPerRun: z.number().int().min(1).max(MAX_DELEGATIONS_PER_RUN),
|
|
64
|
+
parallelDelegations: z.boolean(),
|
|
57
65
|
schedule: z.string().trim().min(1).max(256),
|
|
58
66
|
timezone: z.string().trim().min(1).max(128),
|
|
59
67
|
scheduleEnabled: z.boolean(),
|
|
@@ -85,6 +93,8 @@ const CreateAgentSchema = z
|
|
|
85
93
|
model: agentFields.model,
|
|
86
94
|
budgetUsd: agentFields.budgetUsd,
|
|
87
95
|
maxTurns: agentFields.maxTurns.optional(),
|
|
96
|
+
maxDelegationsPerRun: agentFields.maxDelegationsPerRun.optional(),
|
|
97
|
+
parallelDelegations: agentFields.parallelDelegations.optional(),
|
|
88
98
|
schedule: agentFields.schedule.optional(),
|
|
89
99
|
timezone: agentFields.timezone.optional(),
|
|
90
100
|
scheduleEnabled: agentFields.scheduleEnabled.optional(),
|
|
@@ -139,6 +149,8 @@ const UpdateAgentSchema = z
|
|
|
139
149
|
model: agentFields.model.optional(),
|
|
140
150
|
budgetUsd: agentFields.budgetUsd.optional(),
|
|
141
151
|
maxTurns: agentFields.maxTurns.optional(),
|
|
152
|
+
maxDelegationsPerRun: agentFields.maxDelegationsPerRun.optional(),
|
|
153
|
+
parallelDelegations: agentFields.parallelDelegations.optional(),
|
|
142
154
|
schedule: agentFields.schedule.nullable().optional(),
|
|
143
155
|
timezone: agentFields.timezone.optional(),
|
|
144
156
|
scheduleEnabled: agentFields.scheduleEnabled.optional(),
|
|
@@ -160,6 +172,13 @@ const REPOSITORY_ADMIN_OVERRIDE = {
|
|
|
160
172
|
type: "boolean",
|
|
161
173
|
description: "Admins only (agents:admin with the admin role): approve codingProfile.repository without checking GitHub access, recorded as an admin approval. On an agent the admin doesn't own, only codingProfile.repository may change.",
|
|
162
174
|
};
|
|
175
|
+
/** A `local:` repository is stored as its canonical (realpath) name, and refused outside the trusted roots. */
|
|
176
|
+
async function withCanonicalRepository(args) {
|
|
177
|
+
const repository = args.codingProfile?.repository;
|
|
178
|
+
if (repository === undefined || !isLocalRepository(repository))
|
|
179
|
+
return args;
|
|
180
|
+
return { ...args, codingProfile: { ...args.codingProfile, repository: await canonicalLocalRepository(repository) } };
|
|
181
|
+
}
|
|
163
182
|
/** The profile columns a repository authorization is stamped into. */
|
|
164
183
|
function profileStamp(authorization) {
|
|
165
184
|
return {
|
|
@@ -175,7 +194,7 @@ const profileJsonSchema = {
|
|
|
175
194
|
provider: { type: "string", enum: ["codex", "claude-code"] },
|
|
176
195
|
repository: {
|
|
177
196
|
type: "string",
|
|
178
|
-
description: "owner/name on GitHub.
|
|
197
|
+
description: "owner/name on GitHub, or local:/absolute/path for a git folder inside the server's LOCAL_REPO_ROOTS (stored by its real path; no GitHub access needed). For GitHub, your linked GitHub account (link_host_account) must have write access to it, unless a wardby admin approves it (repositoryAdminOverride). Re-checked on every run.",
|
|
179
198
|
},
|
|
180
199
|
baseRef: { type: "string" },
|
|
181
200
|
defaultTask: { type: ["string", "null"] },
|
|
@@ -198,6 +217,12 @@ const profileJsonSchema = {
|
|
|
198
217
|
maximum: 32768,
|
|
199
218
|
description: "Workspace disk size in MiB. Null uses the deployment default (CODING_DISK_MB), capped by the operator's CODING_MAX_DISK_MB.",
|
|
200
219
|
},
|
|
220
|
+
maxTurns: {
|
|
221
|
+
type: ["integer", "null"],
|
|
222
|
+
minimum: 1,
|
|
223
|
+
maximum: MAX_CODING_TURNS,
|
|
224
|
+
description: `Claude Code runs only: the most agent turns (model calls) a run may take before it stops with coding_turn_limit. Null uses the worker default (${DEFAULT_CLAUDE_MAX_TURNS}). The run's budget and timeout still apply. Codex runs have no turn limit.`,
|
|
225
|
+
},
|
|
201
226
|
packageAllowlist: {
|
|
202
227
|
type: "object",
|
|
203
228
|
description: "Approved top-level packages per ecosystem (npm, pypi). Needs packages:approve or agents:admin.",
|
|
@@ -293,6 +318,7 @@ function storedProfile(profile) {
|
|
|
293
318
|
toolchainVersion: profile.toolchainVersion,
|
|
294
319
|
workerImageRef: profile.workerImageRef,
|
|
295
320
|
workspaceDiskMb: profile.workspaceDiskMb,
|
|
321
|
+
maxTurns: profile.maxTurns,
|
|
296
322
|
packageAllowlist: profile.packageAllowlist,
|
|
297
323
|
packagePolicy: profile.packagePolicy,
|
|
298
324
|
services: profile.services,
|
|
@@ -321,6 +347,14 @@ export function registerAgentTools(mcp) {
|
|
|
321
347
|
model: { type: "string" },
|
|
322
348
|
budgetUsd: { type: "number" },
|
|
323
349
|
maxTurns: { type: "integer" },
|
|
350
|
+
maxDelegationsPerRun: {
|
|
351
|
+
type: "integer",
|
|
352
|
+
minimum: 1,
|
|
353
|
+
maximum: MAX_DELEGATIONS_PER_RUN,
|
|
354
|
+
description: "Native agents with sub-agents: how many delegate_to_* calls one run may make, each to a different sub-agent " +
|
|
355
|
+
"(one after another, or together with parallelDelegations). Default 1: route to exactly one.",
|
|
356
|
+
},
|
|
357
|
+
parallelDelegations: { type: "boolean", description: PARALLEL_DELEGATIONS_DESCRIPTION },
|
|
324
358
|
schedule: { type: "string" },
|
|
325
359
|
timezone: { type: "string" },
|
|
326
360
|
scheduleEnabled: { type: "boolean" },
|
|
@@ -340,7 +374,7 @@ export function registerAgentTools(mcp) {
|
|
|
340
374
|
required: ["name", "systemPrompt", "model", "budgetUsd"],
|
|
341
375
|
},
|
|
342
376
|
handler: async (rawArgs, ctx) => {
|
|
343
|
-
const args = parseCreateAgent(rawArgs);
|
|
377
|
+
const args = await withCanonicalRepository(parseCreateAgent(rawArgs));
|
|
344
378
|
if (args.codingProfile?.workerImageRef != null)
|
|
345
379
|
requireWorkerImageRefScope(ctx);
|
|
346
380
|
const packages = args.codingProfile;
|
|
@@ -396,6 +430,14 @@ export function registerAgentTools(mcp) {
|
|
|
396
430
|
model: { type: "string" },
|
|
397
431
|
budgetUsd: { type: "number" },
|
|
398
432
|
maxTurns: { type: "integer" },
|
|
433
|
+
maxDelegationsPerRun: {
|
|
434
|
+
type: "integer",
|
|
435
|
+
minimum: 1,
|
|
436
|
+
maximum: MAX_DELEGATIONS_PER_RUN,
|
|
437
|
+
description: "Native agents with sub-agents: how many delegate_to_* calls one run may make, each to a different sub-agent " +
|
|
438
|
+
"(one after another, or together with parallelDelegations). Default 1: route to exactly one.",
|
|
439
|
+
},
|
|
440
|
+
parallelDelegations: { type: "boolean", description: PARALLEL_DELEGATIONS_DESCRIPTION },
|
|
399
441
|
schedule: { type: ["string", "null"] },
|
|
400
442
|
timezone: { type: "string" },
|
|
401
443
|
scheduleEnabled: { type: "boolean" },
|
|
@@ -435,7 +477,7 @@ export function registerAgentTools(mcp) {
|
|
|
435
477
|
required: ["id"],
|
|
436
478
|
},
|
|
437
479
|
handler: async (rawArgs, ctx) => {
|
|
438
|
-
const args = parseUpdateAgent(rawArgs);
|
|
480
|
+
const args = await withCanonicalRepository(parseUpdateAgent(rawArgs));
|
|
439
481
|
if (args.codingProfile?.workerImageRef !== undefined)
|
|
440
482
|
requireWorkerImageRefScope(ctx);
|
|
441
483
|
const debugTraceMinutes = args.codingProfile?.debugTraceMinutes;
|
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
* the caller; an operator can list and unlink anyone's with
|
|
7
7
|
* `wardby auth host-account`.
|
|
8
8
|
*/
|
|
9
|
-
import {
|
|
9
|
+
import { HOST_ACCOUNT_PROVIDERS } from "../../providers/review-host/types.js";
|
|
10
10
|
import { confirmHostIdentityLink, HostLinkError, startHostIdentityLink, userCallbackPath, } from "../../core/host-identity-links.js";
|
|
11
11
|
import { McpError } from "../errors.js";
|
|
12
12
|
import { canonicalUrl } from "../transport/http-limits.js";
|
|
13
13
|
import { textResult } from "./text-result.js";
|
|
14
|
-
const PROVIDER_PROP = { type: "string", enum: [...
|
|
14
|
+
const PROVIDER_PROP = { type: "string", enum: [...HOST_ACCOUNT_PROVIDERS] };
|
|
15
15
|
function asMcpError(err) {
|
|
16
16
|
return err instanceof HostLinkError ? new McpError(err.status, err.message) : err;
|
|
17
17
|
}
|
|
@@ -12,17 +12,19 @@
|
|
|
12
12
|
* See docs/private/2026-09-26-repo-access-authorization-spec-and-plan.md.
|
|
13
13
|
*/
|
|
14
14
|
import { Prisma } from "#prisma";
|
|
15
|
-
import { normalizeGitHubRepository } from "../../coding/protocol.js";
|
|
15
|
+
import { isLocalRepository, normalizeGitHubRepository, normalizeLocalRepository } from "../../coding/protocol.js";
|
|
16
16
|
import { requireAgentAccess, requireBindingOwner } from "../auth/access.js";
|
|
17
|
-
import { authorizeRepositoryForSet } from "../auth/repo-authorization.js";
|
|
17
|
+
import { authorizeRepositoryForSet, canonicalLocalRepository } from "../auth/repo-authorization.js";
|
|
18
18
|
import { requireScope } from "../auth/resource-server.js";
|
|
19
19
|
import { McpError } from "../errors.js";
|
|
20
20
|
import { textResult } from "./text-result.js";
|
|
21
|
-
const PROVIDERS = ["github"];
|
|
22
|
-
const TRIGGERS = ["pull_request", "mention", "push"];
|
|
21
|
+
const PROVIDERS = ["github", "local"];
|
|
22
|
+
const TRIGGERS = ["pull_request", "mention", "push", "review_fix"];
|
|
23
23
|
const CHECK_NAME = /^[A-Za-z0-9][A-Za-z0-9 ._/()-]{0,99}$/;
|
|
24
24
|
function normalizeRepository(provider, repository) {
|
|
25
25
|
try {
|
|
26
|
+
if (provider === "local")
|
|
27
|
+
return normalizeLocalRepository(repository);
|
|
26
28
|
if (provider === "github")
|
|
27
29
|
return normalizeGitHubRepository(repository);
|
|
28
30
|
}
|
|
@@ -40,6 +42,9 @@ async function assertNoConflicts(tx, args) {
|
|
|
40
42
|
if (args.triggers.includes("mention") && others.some((o) => o.triggers.includes("mention"))) {
|
|
41
43
|
throw new McpError(409, `Another agent already handles @-mentions on ${args.repository}.`);
|
|
42
44
|
}
|
|
45
|
+
if (args.triggers.includes("review_fix") && others.some((o) => o.triggers.includes("review_fix"))) {
|
|
46
|
+
throw new McpError(409, `Another agent already handles automatic review fixes on ${args.repository}.`);
|
|
47
|
+
}
|
|
43
48
|
// Against every other link, whatever its triggers: a check name identifies
|
|
44
49
|
// one agent's verdict on the repository (also a unique index).
|
|
45
50
|
if (args.checkName !== null && others.some((o) => o.checkName === args.checkName)) {
|
|
@@ -54,11 +59,18 @@ export function registerRepositoryTools(mcp) {
|
|
|
54
59
|
name: "link_repository",
|
|
55
60
|
scope: "agents:write",
|
|
56
61
|
description: "Links a native agent you own to a repository on a code-review host, with its access, event triggers, and checkName. " +
|
|
57
|
-
"Triggers: pull_request (review each PR push), mention (answer @mentions), push (start on merges to the default branch)
|
|
62
|
+
"Triggers: pull_request (review each PR push), mention (answer @mentions), push (start on merges to the default branch), " +
|
|
63
|
+
"review_fix (automatic review fix rounds). The review_fix trigger makes this agent the one wardby starts, automatically " +
|
|
64
|
+
"and without a human comment, when wardby's own review check requests changes on a pull request a wardby coding run " +
|
|
65
|
+
"opened (capped by reviewFixMaxRounds, default 2). " +
|
|
58
66
|
"Your linked GitHub account (link_host_account) must have write access to the repository for a write link, or read for a " +
|
|
59
67
|
"read link; a wardby admin may instead approve it with adminOverride. checkName is only allowed (and required) with the " +
|
|
60
|
-
"pull_request trigger, and is unique per repository.
|
|
61
|
-
"
|
|
68
|
+
"pull_request trigger, and is unique per repository. waitForCi (only with the pull_request trigger) holds this agent's " +
|
|
69
|
+
"review on a pushed head until that head's own CI finishes, instead of racing it; APPROVE is refused while CI on the " +
|
|
70
|
+
"head is failing or still running. Re-linking an already-linked repository replaces its access, triggers, checkName, " +
|
|
71
|
+
"reviewFixMaxRounds, and waitForCi (an omitted field is cleared, not kept) and re-checks access — always send the full " +
|
|
72
|
+
"desired state. A local repository (local:/abs/path, inside the server's trusted roots) takes no triggers or checkName: " +
|
|
73
|
+
"it supports manual reviews only (trigger_agent with review).",
|
|
62
74
|
inputSchema: {
|
|
63
75
|
type: "object",
|
|
64
76
|
additionalProperties: false,
|
|
@@ -69,6 +81,17 @@ export function registerRepositoryTools(mcp) {
|
|
|
69
81
|
access: { type: "string", enum: ["read", "write"] },
|
|
70
82
|
triggers: { type: "array", items: { type: "string", enum: [...TRIGGERS] }, uniqueItems: true },
|
|
71
83
|
checkName: { type: "string", minLength: 1, maxLength: 100 },
|
|
84
|
+
reviewFixMaxRounds: {
|
|
85
|
+
type: "integer",
|
|
86
|
+
minimum: 1,
|
|
87
|
+
maximum: 10,
|
|
88
|
+
description: "Only with the review_fix trigger: automatic fix rounds per pull request before wardby stops (default 2).",
|
|
89
|
+
},
|
|
90
|
+
waitForCi: {
|
|
91
|
+
type: "boolean",
|
|
92
|
+
description: "Only with the pull_request trigger: hold this agent's review on a pushed head until that head's own CI " +
|
|
93
|
+
"finishes, instead of racing it (default false).",
|
|
94
|
+
},
|
|
72
95
|
adminOverride: {
|
|
73
96
|
type: "boolean",
|
|
74
97
|
description: "Admins only (agents:admin with the admin role): approve this link without checking GitHub access, recorded as an admin approval. Allowed on any agent that has an owner, not only the admin's own.",
|
|
@@ -94,9 +117,20 @@ export function registerRepositoryTools(mcp) {
|
|
|
94
117
|
if (agent.kind !== "native") {
|
|
95
118
|
throw new McpError(400, "Repository links are for native agents only; coding agents use codingProfile.repository.");
|
|
96
119
|
}
|
|
97
|
-
const provider = args.provider ?? "github";
|
|
98
|
-
|
|
120
|
+
const provider = args.provider ?? (isLocalRepository(args.repository) ? "local" : "github");
|
|
121
|
+
if ((provider === "local") !== isLocalRepository(args.repository)) {
|
|
122
|
+
throw new McpError(400, `invalid repository "${args.repository}" for provider ${provider}`);
|
|
123
|
+
}
|
|
124
|
+
const repository = provider === "local"
|
|
125
|
+
? await canonicalLocalRepository(args.repository)
|
|
126
|
+
: normalizeRepository(provider, args.repository);
|
|
99
127
|
const triggers = [...new Set(args.triggers ?? [])];
|
|
128
|
+
if (provider === "local") {
|
|
129
|
+
if (triggers.length > 0)
|
|
130
|
+
throw new McpError(400, "local repositories support manual review only");
|
|
131
|
+
// A check is only published for a dispatched host PR; a local link has none.
|
|
132
|
+
args = { ...args, checkName: undefined };
|
|
133
|
+
}
|
|
100
134
|
if (triggers.length > 0 && args.access !== "write")
|
|
101
135
|
throw new McpError(400, "Event triggers need write access.");
|
|
102
136
|
const checkName = args.checkName ?? null;
|
|
@@ -108,6 +142,18 @@ export function registerRepositoryTools(mcp) {
|
|
|
108
142
|
}
|
|
109
143
|
if (checkName !== null && !CHECK_NAME.test(checkName))
|
|
110
144
|
throw new McpError(400, "invalid checkName");
|
|
145
|
+
const reviewFixMaxRounds = args.reviewFixMaxRounds ?? null;
|
|
146
|
+
if (reviewFixMaxRounds !== null && !triggers.includes("review_fix")) {
|
|
147
|
+
throw new McpError(400, "reviewFixMaxRounds needs the review_fix trigger.");
|
|
148
|
+
}
|
|
149
|
+
if (reviewFixMaxRounds !== null &&
|
|
150
|
+
(!Number.isInteger(reviewFixMaxRounds) || reviewFixMaxRounds < 1 || reviewFixMaxRounds > 10)) {
|
|
151
|
+
throw new McpError(400, "reviewFixMaxRounds must be an integer from 1 to 10.");
|
|
152
|
+
}
|
|
153
|
+
const waitForCi = args.waitForCi ?? false;
|
|
154
|
+
if (waitForCi && !triggers.includes("pull_request")) {
|
|
155
|
+
throw new McpError(400, "waitForCi needs the pull_request trigger.");
|
|
156
|
+
}
|
|
111
157
|
// The host call happens before, and outside, the serializable transaction.
|
|
112
158
|
const authorization = await authorizeRepositoryForSet(ctx, {
|
|
113
159
|
ownerId: agent.ownerId,
|
|
@@ -120,7 +166,14 @@ export function registerRepositoryTools(mcp) {
|
|
|
120
166
|
try {
|
|
121
167
|
link = await ctx.db.$transaction(async (tx) => {
|
|
122
168
|
await assertNoConflicts(tx, { agentId: args.agentId, provider, repository, triggers, checkName });
|
|
123
|
-
const fields = {
|
|
169
|
+
const fields = {
|
|
170
|
+
access: args.access,
|
|
171
|
+
triggers,
|
|
172
|
+
checkName,
|
|
173
|
+
reviewFixMaxRounds,
|
|
174
|
+
waitForCi,
|
|
175
|
+
...authorization,
|
|
176
|
+
};
|
|
124
177
|
return tx.agentRepository.upsert({
|
|
125
178
|
where: { agentId_provider_repository: { agentId: args.agentId, provider, repository } },
|
|
126
179
|
create: { agentId: args.agentId, provider, repository, ...fields },
|
|
@@ -153,8 +206,14 @@ export function registerRepositoryTools(mcp) {
|
|
|
153
206
|
handler: async (args, ctx) => {
|
|
154
207
|
const { agent } = await requireAgentAccess(ctx, args.agentId, "read");
|
|
155
208
|
requireBindingOwner(ctx, agent);
|
|
156
|
-
const provider = args.provider ?? "github";
|
|
157
|
-
|
|
209
|
+
const provider = args.provider ?? (isLocalRepository(args.repository) ? "local" : "github");
|
|
210
|
+
let repository;
|
|
211
|
+
if (provider === "local") {
|
|
212
|
+
// The roots may have narrowed since linking; a link must stay removable.
|
|
213
|
+
repository = await canonicalLocalRepository(args.repository).catch(() => normalizeRepository(provider, args.repository));
|
|
214
|
+
}
|
|
215
|
+
else
|
|
216
|
+
repository = normalizeRepository(provider, args.repository);
|
|
158
217
|
const { count } = await ctx.db.agentRepository.deleteMany({
|
|
159
218
|
where: { agentId: args.agentId, provider, repository },
|
|
160
219
|
});
|
package/dist/mcp/tools/runs.js
CHANGED
|
@@ -71,8 +71,14 @@ export function registerRunTools(mcp) {
|
|
|
71
71
|
diagnosticId: true,
|
|
72
72
|
debugTrace: true,
|
|
73
73
|
services: true,
|
|
74
|
+
resultBranch: true,
|
|
75
|
+
baseSha: true,
|
|
74
76
|
},
|
|
75
77
|
});
|
|
78
|
+
const localPr = await ctx.db.localPullRequest.findUnique({
|
|
79
|
+
where: { runId: run.id },
|
|
80
|
+
include: { reviews: { orderBy: { createdAt: "asc" } } },
|
|
81
|
+
});
|
|
76
82
|
const codingResult = publicCodingRunResult(codingRun?.result);
|
|
77
83
|
// A pending coding run with queuedAt is waiting for a concurrency slot
|
|
78
84
|
// (CODING_MAX_CONCURRENT), not stuck.
|
|
@@ -108,6 +114,23 @@ export function registerRunTools(mcp) {
|
|
|
108
114
|
// An admin turned on the debug trace: the worker's full trace is in its pod log.
|
|
109
115
|
...(codingRun?.debugTrace ? { debugTrace: true } : {}),
|
|
110
116
|
...(services.length > 0 ? { services } : {}),
|
|
117
|
+
...(codingRun?.resultBranch ? { resultBranch: codingRun.resultBranch } : {}),
|
|
118
|
+
...(codingRun?.baseSha ? { baseSha: codingRun.baseSha } : {}),
|
|
119
|
+
...(localPr
|
|
120
|
+
? {
|
|
121
|
+
review: {
|
|
122
|
+
number: localPr.number,
|
|
123
|
+
branch: localPr.branch,
|
|
124
|
+
base: localPr.base,
|
|
125
|
+
reviews: localPr.reviews.map((r) => ({
|
|
126
|
+
verdict: r.verdict,
|
|
127
|
+
summary: r.summary,
|
|
128
|
+
body: r.body,
|
|
129
|
+
comments: r.comments,
|
|
130
|
+
})),
|
|
131
|
+
},
|
|
132
|
+
}
|
|
133
|
+
: {}),
|
|
111
134
|
...(codingRun ? { packages, packageRefusals, packagePlan } : {}),
|
|
112
135
|
});
|
|
113
136
|
},
|