@taiga-ai/mcp-server 0.25.0-rc.464 → 0.25.0-rc.474
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/dist/index.js +159 -92
- package/package.json +1 -1
- package/src/client.ts +20 -13
- package/src/server.integration.test.ts +427 -64
- package/src/tools/backlogs.ts +36 -0
- package/src/tools/builds.ts +62 -12
- package/src/tools/inbox.ts +7 -1
- package/src/tools/maintenance.ts +19 -7
- package/src/tools/repositories.ts +7 -2
- package/src/tools/sections.ts +3 -16
- package/src/tools/setup.ts +56 -64
- package/taiga-ai-mcp-server-0.25.0-rc.474.tgz +0 -0
- package/taiga-ai-mcp-server-0.25.0-rc.464.tgz +0 -0
package/dist/index.js
CHANGED
|
@@ -356,11 +356,11 @@ var TaigaClient = class {
|
|
|
356
356
|
async getPolicyStanding(productId) {
|
|
357
357
|
return this.request("GET", `/products/${seg(productId)}/maintenance/policies`);
|
|
358
358
|
}
|
|
359
|
-
async remediatePolicyControl(productId, controlId) {
|
|
360
|
-
return this.request("POST", `/products/${seg(productId)}/maintenance/policies/controls/${seg(controlId)}/remediate`,
|
|
359
|
+
async remediatePolicyControl(productId, controlId, body = {}) {
|
|
360
|
+
return this.request("POST", `/products/${seg(productId)}/maintenance/policies/controls/${seg(controlId)}/remediate`, body);
|
|
361
361
|
}
|
|
362
|
-
async remediatePolicyViolation(productId, divergenceId) {
|
|
363
|
-
return this.request("POST", `/products/${seg(productId)}/maintenance/policies/divergences/${seg(divergenceId)}/remediate`,
|
|
362
|
+
async remediatePolicyViolation(productId, divergenceId, body = {}) {
|
|
363
|
+
return this.request("POST", `/products/${seg(productId)}/maintenance/policies/divergences/${seg(divergenceId)}/remediate`, body);
|
|
364
364
|
}
|
|
365
365
|
async listInbox(opts) {
|
|
366
366
|
const { productId, ...rest } = opts;
|
|
@@ -670,6 +670,29 @@ const POLICY_TYPE_BY_CONTROL_PREFIX = {
|
|
|
670
670
|
SCS: "policy_supply_chain",
|
|
671
671
|
CLD: "policy_cloud_governance"
|
|
672
672
|
};
|
|
673
|
+
/** The policy document a control id belongs to, or undefined for an unknown prefix. */
|
|
674
|
+
function policyTypeForControl(controlId) {
|
|
675
|
+
return POLICY_TYPE_BY_CONTROL_PREFIX[controlId.split("-")[0] ?? ""];
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* The policy a control id points a reader to (PRO-2809): its rightmost base
|
|
679
|
+
* prefix. An overlay family names its host policy last: TEL-TIC-1.1 is a
|
|
680
|
+
* telecom control in policy_tic, HC-FI-DATA-1.1 a Finnish healthcare control in
|
|
681
|
+
* policy_data, ARC-AWS-5 an AWS preset in policy_architecture. A regulatory
|
|
682
|
+
* family with no base prefix (GDPR, NIS2, AIACT) spans several policies and
|
|
683
|
+
* names none.
|
|
684
|
+
*
|
|
685
|
+
* For saying where to read a control, not for deciding who holds it:
|
|
686
|
+
* {@link policyTypeForControl} keeps an overlay control with the document it is
|
|
687
|
+
* written in, whatever its id says.
|
|
688
|
+
*/
|
|
689
|
+
function policyNamedByControl(controlId) {
|
|
690
|
+
const prefixes = controlId.split("-").filter((part) => /^[A-Z]/.test(part));
|
|
691
|
+
for (let at = prefixes.length - 1; at >= 0; at -= 1) {
|
|
692
|
+
const owner = POLICY_TYPE_BY_CONTROL_PREFIX[prefixes[at]];
|
|
693
|
+
if (owner) return owner;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
673
696
|
/**
|
|
674
697
|
* The shape of a control id in a policy's control table: `SEC-3.4`, and the
|
|
675
698
|
* overlay families too (`TEL-TIC-1.1`, `HC-FI-DATA-1.1`, `GDPR-1.1`). One
|
|
@@ -2663,17 +2686,6 @@ const sectionsInput = z.array(z.string().min(1).max(200)).max(50).optional().des
|
|
|
2663
2686
|
/** The sections input of a company document, where a policy control id also names the sections that state it. */
|
|
2664
2687
|
const policySectionsInput = sectionsInput.describe("Only these sections, by id, or a policy control id (SEC-5.1) for the sections that state it; [] for the outline (under outline: every section id and its size, no text); omit for the whole document");
|
|
2665
2688
|
/**
|
|
2666
|
-
* The policy that states a control: the rightmost policy prefix in its id, so an overlay family's
|
|
2667
|
-
* TEL-TIC-1.1 is policy_tic and HC-FI-DATA-1.1 policy_data, as the overlays file them.
|
|
2668
|
-
*/
|
|
2669
|
-
function policyOfControl(id) {
|
|
2670
|
-
const prefixes = id.split("-").filter((part) => /^[A-Z]/.test(part));
|
|
2671
|
-
for (const prefix of prefixes.reverse()) {
|
|
2672
|
-
const policy = POLICY_TYPE_BY_CONTROL_PREFIX[prefix];
|
|
2673
|
-
if (policy) return policy;
|
|
2674
|
-
}
|
|
2675
|
-
}
|
|
2676
|
-
/**
|
|
2677
2689
|
* The sections that state each control id asked for (PRO-2800): policy
|
|
2678
2690
|
* standing cites SEC-5.1, and the section holding it has an id of its own
|
|
2679
2691
|
* (secretsManagement) an agent could only guess. A control id is found in
|
|
@@ -2699,7 +2711,7 @@ function withControls(outline, sections, policyType) {
|
|
|
2699
2711
|
return outline.map((entry) => {
|
|
2700
2712
|
const text = sectionContent(sections, entry.id) ?? "";
|
|
2701
2713
|
const ids = [...new Set([...text.matchAll(pattern)].filter((match) => !/^_?Reserved_?/i.test(match[2] ?? "")).map((match) => match[1]).filter((id) => {
|
|
2702
|
-
const owner =
|
|
2714
|
+
const owner = policyTypeForControl(id);
|
|
2703
2715
|
return owner === void 0 || owner === policyType;
|
|
2704
2716
|
}))];
|
|
2705
2717
|
return ids.length > 0 ? {
|
|
@@ -2727,7 +2739,7 @@ function sectionReadResult(doc, asked, label, options = {}) {
|
|
|
2727
2739
|
const known = ids.filter((id) => all.includes(id));
|
|
2728
2740
|
const found = Object.keys(controls).length > 0 ? { controls } : {};
|
|
2729
2741
|
const asControl = options.controlIds ? void 0 : unknownSections.find((id) => isControlId(id));
|
|
2730
|
-
const policy = asControl ?
|
|
2742
|
+
const policy = asControl ? policyNamedByControl(asControl) : void 0;
|
|
2731
2743
|
const controlNote = asControl ? { note: policy ? `${asControl} is a control of ${policy}: read it with taiga_get_document (type: ${policy}, sections: ["${asControl}"]).` : `${asControl} looks like a policy control id, which only a policy resolves: taiga_get_document without type lists them.` } : {};
|
|
2732
2744
|
if (known.length === 0) {
|
|
2733
2745
|
const { data: _data, ...meta } = cut.doc;
|
|
@@ -3729,6 +3741,22 @@ function runOutcome(run) {
|
|
|
3729
3741
|
};
|
|
3730
3742
|
}
|
|
3731
3743
|
/**
|
|
3744
|
+
* What hardening left blocking, in a sentence: the findings the run names (PRO-2817), or, for a run reported before
|
|
3745
|
+
* it recorded them, how many.
|
|
3746
|
+
*/
|
|
3747
|
+
function hardeningSaid(blocking, named) {
|
|
3748
|
+
const count = `Hardening ended with ${blocking} blocking finding${blocking === 1 ? "" : "s"} it could not fix`;
|
|
3749
|
+
if (named.length === 0) return `${count}; the run records how many, not which.`;
|
|
3750
|
+
const shown = named.slice(0, 3).map((finding) => {
|
|
3751
|
+
const message = String(finding["message"]);
|
|
3752
|
+
const where = finding["line"] ? `${String(finding["path"])}:${String(finding["line"])}` : String(finding["path"]);
|
|
3753
|
+
return `${message} (${String(finding["tool"])}, ${where})`;
|
|
3754
|
+
});
|
|
3755
|
+
const rest = blocking - shown.length;
|
|
3756
|
+
const recorded = named.length > shown.length ? `, ${named.length} recorded in taiga_get_run` : "";
|
|
3757
|
+
return `${count}: ${shown.join("; ")}${rest > 0 ? `; and ${rest} more${recorded}` : ""}.`;
|
|
3758
|
+
}
|
|
3759
|
+
/**
|
|
3732
3760
|
* What each failedReason means for the next move (PRO-2800): reviewers met dispatch_rejected and agent_reported as
|
|
3733
3761
|
* bare enums. As @taiga/types documents them.
|
|
3734
3762
|
*/
|
|
@@ -3753,10 +3781,11 @@ function readableRun(run, opts = {}) {
|
|
|
3753
3781
|
const means = FAILED_REASON_MEANS[String(run["failedReason"])];
|
|
3754
3782
|
const hardening = run["hardeningResult"];
|
|
3755
3783
|
const blocking = typeof hardening?.blockingFindings === "number" ? hardening.blockingFindings : 0;
|
|
3784
|
+
const named = Array.isArray(hardening?.blocking) ? hardening.blocking : [];
|
|
3756
3785
|
return {
|
|
3757
3786
|
...rest,
|
|
3758
3787
|
...means ? { failedReasonMeans: means } : {},
|
|
3759
|
-
...blocking > 0 ? { hardeningNote:
|
|
3788
|
+
...blocking > 0 ? { hardeningNote: hardeningSaid(blocking, named) } : {},
|
|
3760
3789
|
...opts.row || error === void 0 ? {} : { error },
|
|
3761
3790
|
...opts.row || reviewEscalation === void 0 ? {} : { reviewEscalation },
|
|
3762
3791
|
...typeof rest["prNumber"] === "number" || rest["prUrl"] ? { prDraft } : {},
|
|
@@ -3769,20 +3798,27 @@ const INITIATIVE_CHECKS = 10;
|
|
|
3769
3798
|
* The status of the initiative behind each run still waiting on its pull
|
|
3770
3799
|
* request (PRO-2800). A reviewing agent read a run of a canceled initiative as
|
|
3771
3800
|
* "waiting for a person to review and merge" while Action Required called the
|
|
3772
|
-
* same review stale.
|
|
3773
|
-
*
|
|
3774
|
-
*
|
|
3801
|
+
* same review stale.
|
|
3802
|
+
*
|
|
3803
|
+
* The run says it itself since PRO-2808 (initiativeClosed: done, canceled,
|
|
3804
|
+
* archived, deleted, or null while live), for every key that can read it.
|
|
3805
|
+
* Against an API from before that, each initiative is read: a 404 is
|
|
3806
|
+
* 'deleted', and an initiative past the cap, one whose read failed, and every
|
|
3807
|
+
* one without backlog:read is 'unchecked', which withInitiativeState adds to
|
|
3808
|
+
* the row's outcome.
|
|
3775
3809
|
*/
|
|
3776
3810
|
async function initiativeStates(client, rows) {
|
|
3777
3811
|
const states = /* @__PURE__ */ new Map();
|
|
3778
3812
|
const waiting = rows.filter((row) => ["needs_review", "completed"].includes(String(row["status"])) && row["prMerged"] !== true);
|
|
3779
|
-
const
|
|
3813
|
+
const told = waiting.filter((row) => "initiativeClosed" in row);
|
|
3814
|
+
for (const row of told) if (typeof row["backlogItemId"] === "string") states.set(row["backlogItemId"], typeof row["initiativeClosed"] === "string" ? row["initiativeClosed"] : "live");
|
|
3815
|
+
const all = [...new Set(waiting.filter((row) => !("initiativeClosed" in row)).map((row) => row["backlogItemId"]).filter((id) => typeof id === "string" && !states.has(id)))];
|
|
3780
3816
|
const ids = client.hasCapability("backlog:read") ? all.slice(0, INITIATIVE_CHECKS) : [];
|
|
3781
3817
|
for (const id of all) if (!ids.includes(id)) states.set(id, "unchecked");
|
|
3782
3818
|
await Promise.all(ids.map(async (id) => {
|
|
3783
3819
|
try {
|
|
3784
3820
|
const item = (await client.getBacklogItem(id)).data;
|
|
3785
|
-
states.set(id, String(item["status"] ?? item["state"]));
|
|
3821
|
+
states.set(id, item["archivedAt"] ? "archived" : String(item["status"] ?? item["state"]));
|
|
3786
3822
|
} catch (error) {
|
|
3787
3823
|
if (error instanceof TaigaApiError && error.status === 404) return void states.set(id, "deleted");
|
|
3788
3824
|
if (!(error instanceof TaigaApiError || isNetworkFailure(error))) throw error;
|
|
@@ -3791,7 +3827,18 @@ async function initiativeStates(client, rows) {
|
|
|
3791
3827
|
}));
|
|
3792
3828
|
return states;
|
|
3793
3829
|
}
|
|
3794
|
-
/**
|
|
3830
|
+
/**
|
|
3831
|
+
* How each way an initiative ends reads on a run it left waiting: the same set
|
|
3832
|
+
* Action Required closes a run's review for (PRO-2806). A done one's waiting
|
|
3833
|
+
* run is not the one that landed it: that run is merged and never waiting.
|
|
3834
|
+
*/
|
|
3835
|
+
const INITIATIVE_ENDED = {
|
|
3836
|
+
done: "was marked done",
|
|
3837
|
+
canceled: "was canceled",
|
|
3838
|
+
archived: "was archived",
|
|
3839
|
+
deleted: "was deleted"
|
|
3840
|
+
};
|
|
3841
|
+
/** A readable run row that also says when its initiative ended while its pull request waited. */
|
|
3795
3842
|
function withInitiativeState(row, states) {
|
|
3796
3843
|
const state = states.get(String(row["backlogItemId"]));
|
|
3797
3844
|
if (!["gated", "unmerged"].includes(String(row["disposition"]))) return row;
|
|
@@ -3799,14 +3846,15 @@ function withInitiativeState(row, states) {
|
|
|
3799
3846
|
...row,
|
|
3800
3847
|
outcome: `${String(row["outcome"])} Whether its initiative still wants it was not checked.`
|
|
3801
3848
|
};
|
|
3802
|
-
|
|
3849
|
+
const ended = state ? INITIATIVE_ENDED[state] : void 0;
|
|
3850
|
+
if (!ended) return row;
|
|
3803
3851
|
const pr = typeof row["prNumber"] === "number" ? `PR #${row["prNumber"]}` : "its pull request";
|
|
3804
3852
|
const open = row["prState"] === "open" ? ` ${pr} is still open on the forge.` : "";
|
|
3805
3853
|
return {
|
|
3806
3854
|
...row,
|
|
3807
3855
|
disposition: "abandoned",
|
|
3808
3856
|
actions: [],
|
|
3809
|
-
outcome: `Its initiative
|
|
3857
|
+
outcome: `Its initiative ${ended}, so nobody is waiting on this run.${open}`
|
|
3810
3858
|
};
|
|
3811
3859
|
}
|
|
3812
3860
|
/** A step that changed only prose: no code of its own could have broken a test. */
|
|
@@ -3913,7 +3961,7 @@ function registerBuildTools(server, client) {
|
|
|
3913
3961
|
if (!client.hasCapability("products:read")) return;
|
|
3914
3962
|
server.registerTool("taiga_list_runs", {
|
|
3915
3963
|
title: "List runs",
|
|
3916
|
-
description: "List coding agent runs, newest first: branch, pull request, step progress, outcome (one sentence), actions and disposition: in_flight, gated (a person owes a review and merge), succeeded (merged), failed, stopped, rejected (pull request closed unmerged), unmerged, or abandoned (initiative canceled or deleted). Without productId, every product's runs; active: true keeps running ones. taiga_get_run reads one; carry work on from the initiative (taiga_resume_initiative).",
|
|
3964
|
+
description: "List coding agent runs, newest first: branch, pull request, step progress, outcome (one sentence), actions and disposition: in_flight, gated (a person owes a review and merge), succeeded (merged), failed, stopped, rejected (pull request closed unmerged), unmerged, or abandoned (its initiative ended: done, canceled, archived or deleted). Without productId, every product's runs; active: true keeps running ones. taiga_get_run reads one; carry work on from the initiative (taiga_resume_initiative).",
|
|
3917
3965
|
inputSchema: z.object({
|
|
3918
3966
|
productId: productId.optional(),
|
|
3919
3967
|
initiativeId: idInput("initiative", "taiga_list_initiatives (query finds one by title) gives it").optional().describe("Only this initiative's runs -- get from taiga_list_initiatives; needs productId"),
|
|
@@ -4236,6 +4284,7 @@ function situationOf(item) {
|
|
|
4236
4284
|
const handback = handbackSaid(build.reviewEscalation);
|
|
4237
4285
|
if (handback) stage += `, handed back because ${handback}`;
|
|
4238
4286
|
}
|
|
4287
|
+
if (life.stage === "done" || life.stage === "canceled") stage += endingSaid(item);
|
|
4239
4288
|
parts.push(`${stage}.`);
|
|
4240
4289
|
if (typeof item["needsHumanCause"] === "string") {
|
|
4241
4290
|
const why = [renderHalt({
|
|
@@ -4253,6 +4302,26 @@ function situationOf(item) {
|
|
|
4253
4302
|
else if (move) parts.push(`Next: ${move}.`);
|
|
4254
4303
|
return parts.join(" ");
|
|
4255
4304
|
}
|
|
4305
|
+
/**
|
|
4306
|
+
* How a settled initiative ended, as the rest of its "Done" or "Canceled"
|
|
4307
|
+
* sentence (PRO-2821): the pull request that landed it, the move that settled
|
|
4308
|
+
* it and who made it, and what took over. Only the single read carries
|
|
4309
|
+
* `ending`; a list row says nothing more.
|
|
4310
|
+
*/
|
|
4311
|
+
function endingSaid(item) {
|
|
4312
|
+
const ending = item["ending"];
|
|
4313
|
+
const merged = typeof ending?.landedPr?.number === "number" ? ending.landedPr.number : null;
|
|
4314
|
+
const said = [];
|
|
4315
|
+
if (merged !== null) said.push(`: PR #${merged} merged`);
|
|
4316
|
+
else if (ending && (ending.via === "moved" || ending.via === "off_the_line")) {
|
|
4317
|
+
const who = typeof ending.by?.name === "string" ? ending.by.name : ending.by?.type === "system" ? "Taiga" : "someone";
|
|
4318
|
+
const when = typeof ending.at === "string" ? ` on ${ending.at.slice(0, 10)}` : "";
|
|
4319
|
+
said.push(ending.via === "off_the_line" ? ` by ${who}${when}, who took it off the line` : ` by ${who}${when}`);
|
|
4320
|
+
}
|
|
4321
|
+
const next = ending?.supersededBy;
|
|
4322
|
+
if (next && typeof next.id === "string") said.push(`; superseded by "${String(next.title)}" (${next.id})`);
|
|
4323
|
+
return said.join("");
|
|
4324
|
+
}
|
|
4256
4325
|
/** A row or item with its situation added when it has a lifecycle. */
|
|
4257
4326
|
function withSituation(item, row = item) {
|
|
4258
4327
|
const situation = situationOf(item);
|
|
@@ -4815,7 +4884,7 @@ function registerMaintenanceTools(server, client) {
|
|
|
4815
4884
|
if (!client.hasCapability("products:read")) return;
|
|
4816
4885
|
server.registerTool("taiga_list_findings", {
|
|
4817
4886
|
title: "List maintenance findings",
|
|
4818
|
-
description: "List what maintenance sweeps found in a product's repository: dependencies, secrets, insecure code (sast) and infrastructure. The first page opens with sweep (open counts, latest sweeps), so an old or failed sweep is not read as an all-clear. Rows: severity, tier (act_now: known exploited; urgent: likely and serious; elevated: one of the two; routine), location, remediableBy (upstream_only: no fixed version
|
|
4887
|
+
description: "List what maintenance sweeps found in a product's repository: dependencies, secrets, insecure code (sast) and infrastructure. The first page opens with sweep (open counts, latest sweeps), so an old or failed sweep is not read as an all-clear. Rows: severity, tier (act_now: known exploited; urgent: likely and serious; elevated: one of the two; routine), location, remediableBy (coding_agent: a run here can fix it; upstream_only: the code is in a submodule, so the fix belongs in its own repository), fixable: false when no fixed version is known yet, breached controls and initiativeId of a fix under way. Omit status for open findings. taiga_get_finding reads one.",
|
|
4819
4888
|
inputSchema: z.object({
|
|
4820
4889
|
productId,
|
|
4821
4890
|
status: z.enum(MAINTENANCE_FINDING_STATUSES).optional().describe("Only findings in this status; omit for the ones needing action"),
|
|
@@ -4861,7 +4930,7 @@ function registerMaintenanceTools(server, client) {
|
|
|
4861
4930
|
}));
|
|
4862
4931
|
server.registerTool("taiga_get_policy_standing", {
|
|
4863
4932
|
title: "Get a product's policy standing",
|
|
4864
|
-
description: "How a product stands against the organization's published policies: summary counts, when the documents were assessed, and each violated control with its violations (critical and high only, citing the policy sentence and the contradicting passage) and the work on each. state: breached (open scanner findings map to it, by findingsByTier; routine-only is not urgent) or diverged (the documents contradict it). live: false means the violation's initiative ended without fixing it: it needs a person or a new fix. An unlisted control is not \"passing\" (summary.controlsWithNothingRecorded counts them). full: true adds evidence and resolved ones. Fix with taiga_fix_policy_violation.",
|
|
4933
|
+
description: "How a product stands against the organization's published policies: summary counts, when the documents were assessed, and each violated control with its violations (critical and high only, citing the policy sentence and the contradicting passage) and the work on each. state: breached (open scanner findings map to it, by findingsByTier; routine-only is not urgent) or diverged (the documents contradict it). live: false means the violation's initiative ended without fixing it: it needs a person or a new fix. raisedFor: false means the link is the backlog planner's: it said an initiative a person asked it for would close the violation, which may be a dedicated fix or broader work; true means a fix was raised for it (taiga_fix_policy_violation or the sweep). An unlisted control is not \"passing\" (summary.controlsWithNothingRecorded counts them). full: true adds evidence and resolved ones. Fix with taiga_fix_policy_violation.",
|
|
4865
4934
|
inputSchema: z.object({
|
|
4866
4935
|
productId,
|
|
4867
4936
|
full: z.boolean().optional().describe("Carry each violation's evidence and policy excerpt, cut to about 1,500 characters each, and up to 20 resolved violations (default false)"),
|
|
@@ -4944,7 +5013,7 @@ function registerMaintenanceTools(server, client) {
|
|
|
4944
5013
|
}));
|
|
4945
5014
|
server.registerTool("taiga_fix_finding", {
|
|
4946
5015
|
title: "Fix a finding",
|
|
4947
|
-
description: "Raise an initiative that fixes a maintenance finding, at the end of the product's line; it ends in a pull request a person merges, and the initiative's review gate applies. wholeGroup: true fixes the group in one initiative: every open advisory on that package version in one bump, or every location the same rule flagged. Refused for a secret only in history,
|
|
5016
|
+
description: "Raise an initiative that fixes a maintenance finding, at the end of the product's line; it ends in a pull request a person merges, and the initiative's review gate applies. wholeGroup: true fixes the group in one initiative: every open advisory on that package version in one bump, or every location the same rule flagged. Refused for a secret only in history, remediableBy upstream_only (code in a submodule), and a dependency with no fixed version yet (fixable: false).",
|
|
4948
5017
|
inputSchema: z.object({
|
|
4949
5018
|
productId,
|
|
4950
5019
|
findingId,
|
|
@@ -4970,16 +5039,18 @@ function registerMaintenanceTools(server, client) {
|
|
|
4970
5039
|
}));
|
|
4971
5040
|
server.registerTool("taiga_fix_policy_violation", {
|
|
4972
5041
|
title: "Fix policy violations",
|
|
4973
|
-
description: "Raise one initiative that fixes a product's policy violations and add it to the end of the line. Name a control to cover all its violations with no initiative yet, or one violationId when the control has several. The violation stays listed until the next assessment no longer finds it, even while the fix is under way.",
|
|
5042
|
+
description: "Raise one initiative that fixes a product's policy violations and add it to the end of the line. Name a control to cover all its violations with no initiative yet, or one violationId when the control has several. The violation stays listed until the next assessment no longer finds it, even while the fix is under way. A violation whose last fix was done after it was last assessed is refused: refresh the reality documents to reassess it first, or pass fixAgain: true if the person wants another fix anyway.",
|
|
4974
5043
|
inputSchema: z.object({
|
|
4975
5044
|
productId,
|
|
4976
5045
|
controlId: z.string().min(1).max(40).optional().describe("The control's identifier, e.g. SEC-3.4"),
|
|
4977
|
-
violationId: idInput("violation", "taiga_get_policy_standing gives it").optional()
|
|
5046
|
+
violationId: idInput("violation", "taiga_get_policy_standing gives it").optional(),
|
|
5047
|
+
fixAgain: z.boolean().optional().describe("Raise a fix even though the last one was done after the last assessment (default false)")
|
|
4978
5048
|
}),
|
|
4979
5049
|
annotations: OPERATE
|
|
4980
5050
|
}, (params) => runTool(async () => {
|
|
4981
5051
|
if (params.controlId === void 0 === (params.violationId === void 0)) return toolError("VALIDATION_ERROR", "Name either a control (controlId) or one violation (violationId), not both or neither.");
|
|
4982
|
-
|
|
5052
|
+
const body = { refuseIfFixedSinceAssessment: params.fixAgain !== true };
|
|
5053
|
+
return toolResult((params.controlId ? await client.remediatePolicyControl(params.productId, params.controlId, body) : await client.remediatePolicyViolation(params.productId, params.violationId, body)).data);
|
|
4983
5054
|
}));
|
|
4984
5055
|
}
|
|
4985
5056
|
//#endregion
|
|
@@ -5196,7 +5267,8 @@ const SETUP_TASK_FIELDS = [
|
|
|
5196
5267
|
"providerActionUrl",
|
|
5197
5268
|
"inboxItemId",
|
|
5198
5269
|
"inboxItemStatus",
|
|
5199
|
-
"createdAt"
|
|
5270
|
+
"createdAt",
|
|
5271
|
+
"stale"
|
|
5200
5272
|
];
|
|
5201
5273
|
/** The API's count across every page, on the first page; nothing from an API that does not send one. */
|
|
5202
5274
|
function totalOf(result) {
|
|
@@ -5236,7 +5308,10 @@ function sameKeyOnce(rows) {
|
|
|
5236
5308
|
const { alsoFiledAs: _also, ...was } = kept;
|
|
5237
5309
|
for (const field of Object.keys(kept)) delete kept[field];
|
|
5238
5310
|
Object.assign(kept, row, { alsoFiledAs: [...also, record(was)] });
|
|
5239
|
-
} else
|
|
5311
|
+
} else {
|
|
5312
|
+
kept["alsoFiledAs"] = [...also, record(row)];
|
|
5313
|
+
if (isOpen(row) && row["stale"] === void 0) delete kept["stale"];
|
|
5314
|
+
}
|
|
5240
5315
|
}
|
|
5241
5316
|
return out;
|
|
5242
5317
|
}
|
|
@@ -5311,7 +5386,7 @@ function registerInboxTools(server, client) {
|
|
|
5311
5386
|
}));
|
|
5312
5387
|
server.registerTool("taiga_list_setup_tasks", {
|
|
5313
5388
|
title: "List setup tasks",
|
|
5314
|
-
description: "List what a person must set up for a product (secrets, variables, accounts), with exactly what and where: key (the name to create; Taiga never stores values), the provider page, and phase: prereq (the deploy fails without it), wiring (generated values back to providers) or followup (can wait until after launch). Runs do not wait for them. One product or all, oldest first; includeDone: true adds done and dismissed. A variable filed twice lists the others under alsoFiledAs; totalCount counts records, and a page that folded some says recordsOnPage. Unlike taiga_list_action_required, which lists the same work as items to resolve.",
|
|
5389
|
+
description: "List what a person must set up for a product (secrets, variables, accounts), with exactly what and where: key (the name to create; Taiga never stores values), the provider page, and phase: prereq (the deploy fails without it), wiring (generated values back to providers) or followup (can wait until after launch). Runs do not wait for them. One product or all, oldest first; includeDone: true adds done and dismissed. A variable filed twice lists the others under alsoFiledAs; totalCount counts records, and a page that folded some says recordsOnPage. stale on an open task says no document derived from the code since it was raised mentions its key: it may not apply any more, so check before asking anyone to set it. Unlike taiga_list_action_required, which lists the same work as items to resolve.",
|
|
5315
5390
|
inputSchema: z.object({
|
|
5316
5391
|
productId: productId.optional(),
|
|
5317
5392
|
includeDone: z.boolean().optional().describe("Also the tasks already done or dismissed (default false)"),
|
|
@@ -21059,7 +21134,8 @@ const GITHUB_REPOSITORY_FIELDS = [
|
|
|
21059
21134
|
"defaultBranch",
|
|
21060
21135
|
"description",
|
|
21061
21136
|
"language",
|
|
21062
|
-
"linked"
|
|
21137
|
+
"linked",
|
|
21138
|
+
"linkedProduct"
|
|
21063
21139
|
];
|
|
21064
21140
|
const repositoryId = idInput("repository link", "taiga_list_product_repositories gives it; a linkable repository id is not one");
|
|
21065
21141
|
/** Round-robin over lists: the first of each, then the second of each, and so on. */
|
|
@@ -21075,7 +21151,8 @@ function interleave(lists) {
|
|
|
21075
21151
|
*/
|
|
21076
21152
|
async function holderOf(client, error, productId) {
|
|
21077
21153
|
if (!(error instanceof TaigaApiError) || error.status !== 409) return null;
|
|
21078
|
-
|
|
21154
|
+
if (error.details?.["holder"]) return null;
|
|
21155
|
+
const name = /^(\S+\/\S+) is already linked to another product\./.exec(error.apiMessage ?? "")?.[1];
|
|
21079
21156
|
if (!name) return null;
|
|
21080
21157
|
try {
|
|
21081
21158
|
const holders = (await client.listProducts({
|
|
@@ -21137,7 +21214,7 @@ function registerRepositoryTools(server, client) {
|
|
|
21137
21214
|
}
|
|
21138
21215
|
if (client.hasCapability("vcs:read")) server.registerTool("taiga_list_linkable_repositories", {
|
|
21139
21216
|
title: "Find a repository to link",
|
|
21140
|
-
description: "List the repositories the organization's GitHub connection can reach, to link one with taiga_link_repository (it takes the row's id as linkableRepositoryId). query searches by name; account (login, owner, or installationId) reads one account, else rows come from every account in turn. linked: true means a product already links it
|
|
21217
|
+
description: "List the repositories the organization's GitHub connection can reach, to link one with taiga_link_repository (it takes the row's id as linkableRepositoryId). query searches by name; account (login, owner, or installationId) reads one account, else rows come from every account in turn. linked: true means a product already links it, and linkedProduct names it to a key that sees every product. No connection means GitHub is not connected yet; someone who can install the app does that once, in the web.",
|
|
21141
21218
|
inputSchema: z.object({
|
|
21142
21219
|
query: z.string().trim().min(1).max(256).optional().describe("Part of the repository name"),
|
|
21143
21220
|
account: z.string().trim().min(1).max(140).optional().describe("The GitHub account login (the owner in owner/repo), case-insensitive"),
|
|
@@ -21406,41 +21483,29 @@ const MISSING_DOCUMENTS_HOW = "An imported product gets them from its import (ta
|
|
|
21406
21483
|
/**
|
|
21407
21484
|
* Refuse a name already taken (PRO-2800): the API appends -2 to the slug and
|
|
21408
21485
|
* creates a second one, so a retried call or two agents at once left
|
|
21409
|
-
* duplicates nobody asked for.
|
|
21486
|
+
* duplicates nobody asked for. The API checks it in the create's transaction
|
|
21487
|
+
* (PRO-2808), so names past any page and two creates at once are caught too.
|
|
21488
|
+
* allowDuplicateName says the person means it.
|
|
21410
21489
|
*/
|
|
21411
21490
|
const allowDuplicateName = z.boolean().optional().describe("Create it even when one with the same name exists (default false: refused, naming the existing one)");
|
|
21412
|
-
/** The same name, as a person would read it: case and surrounding space aside. */
|
|
21413
|
-
const sameName = (a, b) => a.trim().toLowerCase() === b.trim().toLowerCase();
|
|
21414
|
-
/** Pages read looking for a taken name; past this the create goes ahead. */
|
|
21415
|
-
const NAME_PAGES = 5;
|
|
21416
|
-
/**
|
|
21417
|
-
* The first row with this name, across pages. Best effort: a list the API
|
|
21418
|
-
* refuses or fails leaves the check out rather than the create.
|
|
21419
|
-
*/
|
|
21420
|
-
async function firstNamed(name, read) {
|
|
21421
|
-
let cursor;
|
|
21422
|
-
try {
|
|
21423
|
-
for (let pageIndex = 0; pageIndex < NAME_PAGES; pageIndex += 1) {
|
|
21424
|
-
const result = await read(cursor);
|
|
21425
|
-
const same = result.data.find((row) => sameName(row.name, name));
|
|
21426
|
-
if (same) return same;
|
|
21427
|
-
if (!result.nextCursor) return null;
|
|
21428
|
-
cursor = result.nextCursor;
|
|
21429
|
-
}
|
|
21430
|
-
} catch (error) {
|
|
21431
|
-
if (!(error instanceof TaigaApiError || isNetworkFailure(error))) throw error;
|
|
21432
|
-
}
|
|
21433
|
-
return null;
|
|
21434
|
-
}
|
|
21435
21491
|
/**
|
|
21436
|
-
* The refusal
|
|
21437
|
-
*
|
|
21438
|
-
|
|
21439
|
-
|
|
21440
|
-
|
|
21441
|
-
|
|
21442
|
-
|
|
21443
|
-
|
|
21492
|
+
* The API's refusal of a taken name, as the tool's: it names the one that
|
|
21493
|
+
* exists when the caller may read it, and leaves the choice to the person.
|
|
21494
|
+
* Another session may have made it, so "use it" would hand the agent someone
|
|
21495
|
+
* else's work (PRO-2800, review 6). Anything else is not this refusal.
|
|
21496
|
+
*/
|
|
21497
|
+
function duplicateName(kind, error, where) {
|
|
21498
|
+
if (!(error instanceof TaigaApiError) || error.status !== 409 || error.details?.["reason"] !== "duplicate_name") return null;
|
|
21499
|
+
const said = (key) => typeof error.details?.[key] === "string" ? error.details[key] : void 0;
|
|
21500
|
+
const [existingId, existingSlug, name, createdAt] = [
|
|
21501
|
+
said("existingId"),
|
|
21502
|
+
said("existingSlug"),
|
|
21503
|
+
said("existingName"),
|
|
21504
|
+
said("existingCreatedAt")
|
|
21505
|
+
];
|
|
21506
|
+
return toolError("DUPLICATE_NAME", `${name ? `A ${kind} named "${name}"` : `A ${kind} with this name`} already exists ${where}${existingId ? ` (${existingId}${createdAt ? `, created ${createdAt}` : ""})` : ""}. ${existingId ? "It may not be this person's: ask whether they meant that one, want another name, or want a second" : "The caller cannot read it: ask whether they want another name or a second"} (allowDuplicateName: true).`, {
|
|
21507
|
+
...existingId ? { existingId } : {},
|
|
21508
|
+
...existingSlug ? { existingSlug } : {}
|
|
21444
21509
|
});
|
|
21445
21510
|
}
|
|
21446
21511
|
/** A product row the model can act on: how it started, and while in setup what it still needs. */
|
|
@@ -21481,14 +21546,16 @@ function registerSetupTools(server, client) {
|
|
|
21481
21546
|
}),
|
|
21482
21547
|
annotations: WRITE
|
|
21483
21548
|
}, (params) => runTool(async () => {
|
|
21484
|
-
|
|
21485
|
-
|
|
21486
|
-
|
|
21487
|
-
|
|
21488
|
-
}));
|
|
21489
|
-
|
|
21549
|
+
try {
|
|
21550
|
+
return toolResult(pickFields([(await client.createFactory({
|
|
21551
|
+
name: params.name,
|
|
21552
|
+
refuseDuplicateName: !params.allowDuplicateName
|
|
21553
|
+
})).data], FACTORY_FIELDS)[0]);
|
|
21554
|
+
} catch (error) {
|
|
21555
|
+
const refused = duplicateName("factory", error, "in this organization");
|
|
21556
|
+
if (refused) return refused;
|
|
21557
|
+
throw error;
|
|
21490
21558
|
}
|
|
21491
|
-
return toolResult(pickFields([(await client.createFactory({ name: params.name })).data], FACTORY_FIELDS)[0]);
|
|
21492
21559
|
}));
|
|
21493
21560
|
server.registerTool("taiga_rename_factory", {
|
|
21494
21561
|
title: "Rename a factory",
|
|
@@ -21553,22 +21620,22 @@ function registerSetupTools(server, client) {
|
|
|
21553
21620
|
}, (params) => runTool(async () => {
|
|
21554
21621
|
const contradiction = notesWithNothingToDescribe(params);
|
|
21555
21622
|
if (contradiction) return contradiction;
|
|
21556
|
-
|
|
21557
|
-
|
|
21623
|
+
let product;
|
|
21624
|
+
try {
|
|
21625
|
+
product = (await client.createProduct({
|
|
21626
|
+
name: params.name,
|
|
21558
21627
|
factoryId: params.factoryId,
|
|
21559
|
-
|
|
21560
|
-
|
|
21561
|
-
|
|
21562
|
-
|
|
21628
|
+
...params.start === "import" ? { projectType: "brownfield" } : {},
|
|
21629
|
+
...params.ownsInfrastructure !== void 0 ? { ownsInfrastructure: params.ownsInfrastructure } : {},
|
|
21630
|
+
...params.ownsDeployment !== void 0 ? { ownsDeployment: params.ownsDeployment } : {},
|
|
21631
|
+
...params.deploymentNotes !== void 0 ? { deploymentNotes: params.deploymentNotes } : {},
|
|
21632
|
+
refuseDuplicateName: !params.allowDuplicateName
|
|
21633
|
+
})).data;
|
|
21634
|
+
} catch (error) {
|
|
21635
|
+
const refused = duplicateName("product", error, "in this factory");
|
|
21636
|
+
if (refused) return refused;
|
|
21637
|
+
throw error;
|
|
21563
21638
|
}
|
|
21564
|
-
const product = (await client.createProduct({
|
|
21565
|
-
name: params.name,
|
|
21566
|
-
factoryId: params.factoryId,
|
|
21567
|
-
...params.start === "import" ? { projectType: "brownfield" } : {},
|
|
21568
|
-
...params.ownsInfrastructure !== void 0 ? { ownsInfrastructure: params.ownsInfrastructure } : {},
|
|
21569
|
-
...params.ownsDeployment !== void 0 ? { ownsDeployment: params.ownsDeployment } : {},
|
|
21570
|
-
...params.deploymentNotes !== void 0 ? { deploymentNotes: params.deploymentNotes } : {}
|
|
21571
|
-
})).data;
|
|
21572
21639
|
return toolResult(await productRow(client, product));
|
|
21573
21640
|
}));
|
|
21574
21641
|
server.registerTool("taiga_update_product", {
|
package/package.json
CHANGED
package/src/client.ts
CHANGED
|
@@ -31,6 +31,7 @@ import type {
|
|
|
31
31
|
MonitoringHistory,
|
|
32
32
|
MoveProductResult,
|
|
33
33
|
PolicyConformance,
|
|
34
|
+
PolicyRemediationInput,
|
|
34
35
|
PolicyRemediationResult,
|
|
35
36
|
Product,
|
|
36
37
|
ProductDocument,
|
|
@@ -44,6 +45,8 @@ import type {
|
|
|
44
45
|
SetupTaskListEntry,
|
|
45
46
|
TenantDocument,
|
|
46
47
|
UptimeReport,
|
|
48
|
+
CreateFactoryInput,
|
|
49
|
+
CreateProductInput,
|
|
47
50
|
} from '@taiga/types';
|
|
48
51
|
import { SERVER_VERSION } from './version.js';
|
|
49
52
|
|
|
@@ -645,19 +648,19 @@ export class TaigaClient {
|
|
|
645
648
|
return this.request<One<PolicyConformance>>('GET', `/products/${seg(productId)}/maintenance/policies`);
|
|
646
649
|
}
|
|
647
650
|
|
|
648
|
-
async remediatePolicyControl(productId: string, controlId: string) {
|
|
651
|
+
async remediatePolicyControl(productId: string, controlId: string, body: PolicyRemediationInput = {}) {
|
|
649
652
|
return this.request<One<PolicyRemediationResult>>(
|
|
650
653
|
'POST',
|
|
651
654
|
`/products/${seg(productId)}/maintenance/policies/controls/${seg(controlId)}/remediate`,
|
|
652
|
-
|
|
655
|
+
body
|
|
653
656
|
);
|
|
654
657
|
}
|
|
655
658
|
|
|
656
|
-
async remediatePolicyViolation(productId: string, divergenceId: string) {
|
|
659
|
+
async remediatePolicyViolation(productId: string, divergenceId: string, body: PolicyRemediationInput = {}) {
|
|
657
660
|
return this.request<One<PolicyRemediationResult>>(
|
|
658
661
|
'POST',
|
|
659
662
|
`/products/${seg(productId)}/maintenance/policies/divergences/${seg(divergenceId)}/remediate`,
|
|
660
|
-
|
|
663
|
+
body
|
|
661
664
|
);
|
|
662
665
|
}
|
|
663
666
|
|
|
@@ -844,7 +847,7 @@ export class TaigaClient {
|
|
|
844
847
|
return this.request<Page<Factory>>('GET', `/factories${qs ? `?${qs}` : ''}`);
|
|
845
848
|
}
|
|
846
849
|
|
|
847
|
-
async createFactory(body:
|
|
850
|
+
async createFactory(body: CreateFactoryInput) {
|
|
848
851
|
return this.request<One<Factory>>('POST', '/factories', body);
|
|
849
852
|
}
|
|
850
853
|
|
|
@@ -856,14 +859,18 @@ export class TaigaClient {
|
|
|
856
859
|
// Setting a product up
|
|
857
860
|
// ---------------------------------------------------------------------------
|
|
858
861
|
|
|
859
|
-
async createProduct(
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
862
|
+
async createProduct(
|
|
863
|
+
body: Pick<
|
|
864
|
+
CreateProductInput,
|
|
865
|
+
| 'name'
|
|
866
|
+
| 'factoryId'
|
|
867
|
+
| 'projectType'
|
|
868
|
+
| 'ownsInfrastructure'
|
|
869
|
+
| 'ownsDeployment'
|
|
870
|
+
| 'deploymentNotes'
|
|
871
|
+
| 'refuseDuplicateName'
|
|
872
|
+
>
|
|
873
|
+
) {
|
|
867
874
|
return this.request<One<Product>>('POST', '/products', body);
|
|
868
875
|
}
|
|
869
876
|
|