@taiga-ai/mcp-server 0.36.0-rc.800 → 0.36.0-rc.892

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +553 -42
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -23899,6 +23899,18 @@ var TaigaClient = class {
23899
23899
  async getLicenseSummary(productId) {
23900
23900
  return this.request("GET", `/products/${seg(productId)}/maintenance/licenses`);
23901
23901
  }
23902
+ /** One licence across a product's inventory (PRO-3251): what it asks, when to allow it, what brings it in. */
23903
+ async getProductLicense(productId, license) {
23904
+ return this.request("GET", `/products/${seg(productId)}/maintenance/licenses/detail?license=${encodeURIComponent(license)}`);
23905
+ }
23906
+ /** Allow a licence in one product: every licence finding in force for it there ignored (PRO-3727). */
23907
+ async allowProductLicense(productId, body) {
23908
+ return this.request("POST", `/products/${seg(productId)}/maintenance/licenses/ignore`, body);
23909
+ }
23910
+ /** Allow a licence in one product no longer: every one of its ignored licence findings there open again. */
23911
+ async reopenProductLicense(productId, license) {
23912
+ return this.request("POST", `/products/${seg(productId)}/maintenance/licenses/reopen`, { license });
23913
+ }
23902
23914
  /** The organization's approved list and every licence in use across its products (PRO-3311). */
23903
23915
  async getOrganizationLicenses() {
23904
23916
  return this.request("GET", "/tenants/current/licenses");
@@ -23926,6 +23938,10 @@ var TaigaClient = class {
23926
23938
  async updateFinding(id, body) {
23927
23939
  return this.request("PATCH", `/maintenance/findings/${seg(id)}`, body);
23928
23940
  }
23941
+ /** Ship notices (PRO-4165): raise the initiative that builds the product's open-source notices, or get the open one. */
23942
+ async shipNotices(productId) {
23943
+ return this.request("POST", `/products/${seg(productId)}/maintenance/notices`, {});
23944
+ }
23929
23945
  async remediateFinding(productId, findingId) {
23930
23946
  return this.request("POST", `/products/${seg(productId)}/maintenance/findings/${seg(findingId)}/remediate`, {});
23931
23947
  }
@@ -24065,7 +24081,7 @@ var TaigaClient = class {
24065
24081
  async refreshRealityDocuments(productId, docTypes) {
24066
24082
  return this.request("POST", `/products/${seg(productId)}/documents/refresh-reality`, docTypes ? { docTypes } : void 0);
24067
24083
  }
24068
- /** What still blocks finishing setup: required documents unpublished, required checks unstamped (PRO-2800). */
24084
+ /** What still blocks finishing setup: the required documents unpublished (PRO-2800). */
24069
24085
  async getProductSetupBlockers(productId) {
24070
24086
  return this.request("GET", `/products/${seg(productId)}/setup`);
24071
24087
  }
@@ -24402,18 +24418,47 @@ const EU_COUNTRIES = [
24402
24418
  }
24403
24419
  ];
24404
24420
  /**
24405
- * EEA countries outside the EU. Norway only: Iceland and Liechtenstein have
24406
- * no overlay and no customer asking for one.
24421
+ * EEA countries outside the EU, all three, so {@link COUNTRIES} covers the
24422
+ * whole EU and EEA (PRO-3791). Only Norway has a country layer; Iceland and
24423
+ * Liechtenstein get the `eea` region layer alone, and neither is offered as a
24424
+ * Taiga tenant country ({@link offeredCountries}).
24407
24425
  */
24408
- const EEA_COUNTRIES = [{
24409
- code: "NO",
24410
- name: "Norway"
24426
+ const EEA_COUNTRIES = [
24427
+ {
24428
+ code: "NO",
24429
+ name: "Norway"
24430
+ },
24431
+ {
24432
+ code: "IS",
24433
+ name: "Iceland"
24434
+ },
24435
+ {
24436
+ code: "LI",
24437
+ name: "Liechtenstein"
24438
+ }
24439
+ ];
24440
+ /**
24441
+ * The United Kingdom (PRO-3420): ISO code `GB`, neither EU nor EEA, in the
24442
+ * `uk` region. Offered only while the `regulations` flag is on.
24443
+ */
24444
+ const UK_COUNTRIES = [{
24445
+ code: "GB",
24446
+ name: "United Kingdom"
24411
24447
  }];
24412
- [...EU_COUNTRIES, ...EEA_COUNTRIES];
24448
+ [
24449
+ ...EU_COUNTRIES,
24450
+ ...EEA_COUNTRIES,
24451
+ ...UK_COUNTRIES
24452
+ ];
24413
24453
  /** Countries available during closed beta. Expand as we open new markets. */
24414
24454
  const BETA_COUNTRIES = ["FI"];
24455
+ [...EU_COUNTRIES.map((c) => c.code).filter((code) => !BETA_COUNTRIES.includes(code))];
24415
24456
  EU_COUNTRIES.filter((c) => BETA_COUNTRIES.includes(c.code));
24416
- Object.fromEntries([...EU_COUNTRIES.map((c) => [c.code, "eu"]), ...EEA_COUNTRIES.map((c) => [c.code, "eea"])]);
24457
+ Object.fromEntries([
24458
+ ...EU_COUNTRIES.map((c) => [c.code, "eu"]),
24459
+ ...EEA_COUNTRIES.map((c) => [c.code, "eea"]),
24460
+ ...UK_COUNTRIES.map((c) => [c.code, "uk"])
24461
+ ]);
24417
24462
  //#endregion
24418
24463
  //#region ../../packages/types/dist/factories.js
24419
24464
  const FACTORY_DOCUMENT_TYPES = ["factory_instructions"];
@@ -24884,7 +24929,7 @@ const HALT_CAUSES = {
24884
24929
  * reaped on a dead heartbeat, or found at 'planning' with no live job by the
24885
24930
  * reconciler, which fails it with that cause on the tick that notices
24886
24931
  * (`armPlanning` writes the status and creates the job in ONE transaction,
24887
- * so that shape is never a race). One of three halts that mirror
24932
+ * so that shape is never a race). One of four halts that mirror
24888
24933
  * `PLAN_FAILURE_CAUSES` one-to-one; see `PLAN_FAILURE_HALT`.
24889
24934
  */
24890
24935
  planning_stalled: {
@@ -24897,6 +24942,22 @@ const HALT_CAUSES = {
24897
24942
  "remove"
24898
24943
  ]
24899
24944
  },
24945
+ /**
24946
+ * The planner concluded nothing should be built (PRO-3941): a licence's
24947
+ * replacement where only build tools carry the licence and no maintained
24948
+ * package replaces them. Not a fault, so not `planning_failed`; its reason
24949
+ * is the detail, read off the planning job as a failure's is.
24950
+ */
24951
+ planning_declined: {
24952
+ message: "The plan recommends not building this, so the queue stopped here.",
24953
+ params: [],
24954
+ clearedWhen: "planning is retried, or a re-plan replaces it",
24955
+ exits: [
24956
+ "auto",
24957
+ "reorder",
24958
+ "remove"
24959
+ ]
24960
+ },
24900
24961
  /** The run errored out. */
24901
24962
  run_failed: {
24902
24963
  message: "The run failed, so the queue stopped here.",
@@ -25178,7 +25239,8 @@ const READY_BLOCKERS = [
25178
25239
  "bar_lowering",
25179
25240
  "bar_lowering_unchecked",
25180
25241
  "criticals_unresolved",
25181
- "clean_checkout_failed"
25242
+ "clean_checkout_failed",
25243
+ "license_not_approved"
25182
25244
  ];
25183
25245
  /**
25184
25246
  * Why the autonomous loop handed a live pull request to a person (PRO-2409).
@@ -25291,6 +25353,7 @@ const INBOX_ITEM_KINDS = [
25291
25353
  "review_pr",
25292
25354
  "review_maintenance",
25293
25355
  "review_policy",
25356
+ "attest_policy",
25294
25357
  "setup_env_var",
25295
25358
  "setup_secret",
25296
25359
  "setup_third_party",
@@ -26031,6 +26094,11 @@ const MCP_TOOLSETS = [
26031
26094
  "observe",
26032
26095
  "documents"
26033
26096
  ];
26097
+ Object.freeze({
26098
+ required: false,
26099
+ gracePeriodDays: 0,
26100
+ requiredSince: null
26101
+ });
26034
26102
  [...TURN_EVENT_TRIGGERS];
26035
26103
  /** Longest request for changes from Taiga (PRO-3035): about four pages of prose. */
26036
26104
  const CHANGE_REQUEST_MAX_CHARS = 1e4;
@@ -26051,6 +26119,8 @@ const REQUEST_EXITS = [
26051
26119
  "dismiss",
26052
26120
  "reviewed"
26053
26121
  ];
26122
+ /** The most ids an answer's `seen` carries; a longer list is not sent, and the review is not checked. */
26123
+ const REVIEW_SEEN_MAX = 1e4;
26054
26124
  //#endregion
26055
26125
  //#region ../../packages/types/dist/maintenance.js
26056
26126
  /**
@@ -26146,9 +26216,15 @@ function isFixableFinding(finding) {
26146
26216
  if (finding.status !== "open") return false;
26147
26217
  if (finding.location?.historyOnly) return false;
26148
26218
  if (!isAgentRemediable(finding)) return false;
26219
+ if (finding.fingerprint?.startsWith("base-image:")) return typeof finding.baseMove === "boolean" ? finding.baseMove : baseImageMoveRemoves(finding.data);
26149
26220
  if (finding.kind === "vulnerable_dependency" && isMaliciousPackageFinding(finding)) return true;
26150
26221
  return isCodeRemediationKind(finding.kind) || !!finding.recommendedVersion;
26151
26222
  }
26223
+ function baseImageMoveRemoves(data) {
26224
+ const baseImage = typeof data === "object" && data !== null ? data.baseImage : null;
26225
+ const bases = typeof baseImage === "object" && baseImage !== null ? baseImage.bases : null;
26226
+ return Array.isArray(bases) && bases.length > 0 && bases.every((b) => typeof b === "object" && b !== null && typeof b.movesTo === "string" && b.removed === true);
26227
+ }
26152
26228
  /**
26153
26229
  * A malicious package (OSV `MAL-*`): its advisory id or one of its aliases
26154
26230
  * says so, or its tier is Act now with no KEV listing, which only a malware
@@ -26165,6 +26241,17 @@ function isMaliciousPackageFinding(finding) {
26165
26241
  if (Array.isArray(aliases) && aliases.some(mal)) return true;
26166
26242
  return finding.tier === "act_now" && finding.kevListedAt == null;
26167
26243
  }
26244
+ /**
26245
+ * The scanner CATEGORIES a sweep reports, in the order the agent reports them
26246
+ * (PRO-1725). Outcomes are keyed on the category, not the tool, so replacing a
26247
+ * tool in the agents' scanner manifest changes nothing here.
26248
+ */
26249
+ const MAINTENANCE_SCANNERS = [
26250
+ "deps",
26251
+ "secrets",
26252
+ "sast",
26253
+ "iac"
26254
+ ];
26168
26255
  new Map(Object.entries({
26169
26256
  deps: [
26170
26257
  "vulnerable_dependency",
@@ -26370,6 +26457,19 @@ object({
26370
26457
  commit: d.commit
26371
26458
  }));
26372
26459
  /**
26460
+ * The sources whose findings no agent scanner's outcome decides (PRO-4070):
26461
+ * the API's runtime judge (PRO-3659) and base OS judge (PRO-4074), and the
26462
+ * base image reading (PRO-1728), whose findings the agent drafts but only a
26463
+ * sweep that read the images may settle.
26464
+ */
26465
+ const MAINTENANCE_JUDGED_SOURCES = [
26466
+ "runtime",
26467
+ "base_os",
26468
+ "base_image"
26469
+ ];
26470
+ [...MAINTENANCE_SCANNERS, ...MAINTENANCE_JUDGED_SOURCES];
26471
+ new Set(MAINTENANCE_SCANNERS);
26472
+ /**
26373
26473
  * The longest licence string the package list filters by (PRO-2920). The
26374
26474
  * inventory keeps every expression whole whatever its length; this bounds only
26375
26475
  * the query string, far above any expression seen. A longer one is listed in
@@ -26377,6 +26477,200 @@ object({
26377
26477
  */
26378
26478
  const MAINTENANCE_LICENSE_FILTER_MAX = 4096;
26379
26479
  //#endregion
26480
+ //#region ../../packages/types/dist/maintenance-advisory.js
26481
+ /**
26482
+ * Pull the renderable advisory fields out of a finding's raw OSV `data` object.
26483
+ *
26484
+ * `data` is the scanner's raw OSV vulnerability payload (MaintenanceFindingData,
26485
+ * shape varies), so every field is read leniently: a missing or malformed key
26486
+ * yields an empty value rather than throwing. Shared by the web app's finding
26487
+ * detail and the Taiga Audit preview and report (PRO-3791), so both show the
26488
+ * same identifiers for the same finding.
26489
+ */
26490
+ /** A non-empty string, or null: anything else the scanner wrote reads as absent. */
26491
+ const text$3 = string$2().min(1).nullable().catch(null);
26492
+ /** The non-empty strings of a list; a value that is not a list reads as none. */
26493
+ const texts = array(unknown$1()).catch([]).transform((list) => list.filter((v) => typeof v === "string" && v.length > 0));
26494
+ object({
26495
+ type: text$3,
26496
+ score: text$3
26497
+ });
26498
+ object({
26499
+ type: text$3,
26500
+ url: text$3
26501
+ });
26502
+ object({
26503
+ aliases: texts,
26504
+ database_specific: object({
26505
+ cwe_ids: texts,
26506
+ severity: text$3
26507
+ }).nullable().catch(null),
26508
+ severity: array(unknown$1()).catch([]),
26509
+ references: array(unknown$1()).catch([]),
26510
+ published: text$3,
26511
+ modified: text$3
26512
+ });
26513
+ //#endregion
26514
+ //#region ../../packages/types/dist/finding-controls.js
26515
+ /**
26516
+ * A committed credential.
26517
+ *
26518
+ * SEC-3.4 "Secrets must never be stored in source code or logs"
26519
+ * SEC-5.1 "Secrets must be stored in an approved secrets management system"
26520
+ * SEC-5.2 "Secrets must be rotated at least annually" -- the half Taiga cannot
26521
+ * do, and the reason the remediation initiative insists the PR say so.
26522
+ * SDLC-3.5 "Secret detection must run pre-commit and in CI pipeline"
26523
+ */
26524
+ const SECRET_CONTROLS = [
26525
+ "SEC-3.4",
26526
+ "SEC-5.1",
26527
+ "SEC-5.2",
26528
+ "SDLC-3.5"
26529
+ ];
26530
+ /**
26531
+ * A vulnerable dependency.
26532
+ *
26533
+ * SCS-2.6 "critical security patches within 7 days, others within 30 days"
26534
+ * -- breached by a vulnerability whose fix has been out past its
26535
+ * window, and by no other (PRO-3420). One found this morning has missed
26536
+ * nothing; neither has one with no fixed release to move to.
26537
+ * SCS-5.2 "Critical vulnerabilities must block deployment"
26538
+ *
26539
+ * Two controls this map deliberately does NOT claim:
26540
+ * - SCS-2.5 "Transitive dependency vulnerabilities must be monitored
26541
+ * continuously". A finding is that monitoring working, not evidence it is
26542
+ * missing; every vulnerable dependency read as a breach of it. The sweep's
26543
+ * own record shows it met instead (sweep-evidence.ts).
26544
+ * - Anything for an outdated dependency that is no vulnerability: SCS-2.6's
26545
+ * rationale is "timely vulnerability remediation", and an update that fixes
26546
+ * nothing is not a security patch.
26547
+ */
26548
+ const DEPENDENCY_CONTROLS = ["SCS-2.6", "SCS-5.2"];
26549
+ /**
26550
+ * A package whose licence the allowlist does not satisfy (PRO-2862).
26551
+ *
26552
+ * TIC-6.2 "Open source licenses must be reviewed and approved before adoption"
26553
+ * -- the same policy section lists the approved licences and the ones
26554
+ * requiring review, which is where the default allowlist comes from.
26555
+ */
26556
+ const LICENSE_CONTROLS = ["TIC-6.2"];
26557
+ /**
26558
+ * Injection and unsanitized input, whatever the sink.
26559
+ *
26560
+ * SDLC-5.1 "All user input must be validated and sanitized"
26561
+ * SEC-6.1 "All API endpoints must validate and sanitize input"
26562
+ */
26563
+ const INJECTION_CONTROLS = ["SDLC-5.1", "SEC-6.1"];
26564
+ /**
26565
+ * Every opengrep rule Taiga ships, mapped explicitly.
26566
+ *
26567
+ * A closed set, unlike checkov's -- these rules are ours, so an exhaustive map
26568
+ * is possible and a keyword heuristic would be a worse answer to an easier
26569
+ * question. `policy-map.test.ts` reads the rule ids straight out of the YAML
26570
+ * so a new rule that lands unmapped fails the build instead of silently
26571
+ * emitting no control.
26572
+ */
26573
+ const SAST_CONTROLS = {
26574
+ "taiga.csharp.sql-string-concat": INJECTION_CONTROLS,
26575
+ "taiga.go.sql-string-concat": INJECTION_CONTROLS,
26576
+ "taiga.javascript.sql-template-concat": INJECTION_CONTROLS,
26577
+ "taiga.python.sqli-fstring": INJECTION_CONTROLS,
26578
+ "taiga.rust.sql-format-concat": INJECTION_CONTROLS,
26579
+ "taiga.csharp.process-start-shell-dynamic": INJECTION_CONTROLS,
26580
+ "taiga.go.exec-command-dynamic": INJECTION_CONTROLS,
26581
+ "taiga.javascript.child-process-exec-dynamic": INJECTION_CONTROLS,
26582
+ "taiga.python.os-system-dynamic": INJECTION_CONTROLS,
26583
+ "taiga.python.subprocess-shell-true": INJECTION_CONTROLS,
26584
+ "taiga.rust.command-shell-dynamic": INJECTION_CONTROLS,
26585
+ "taiga.javascript.eval-dynamic": INJECTION_CONTROLS,
26586
+ "taiga.python.eval-exec-dynamic": INJECTION_CONTROLS,
26587
+ "taiga.javascript.dangerously-set-inner-html": INJECTION_CONTROLS,
26588
+ "taiga.javascript.innerhtml-dynamic": INJECTION_CONTROLS,
26589
+ "taiga.csharp.binaryformatter-deserialize": ["SDLC-5.7"],
26590
+ "taiga.python.pickle-load-untrusted": ["SDLC-5.7"],
26591
+ "taiga.python.yaml-load-unsafe": ["SDLC-5.7"],
26592
+ "taiga.csharp.cert-validation-disabled": ["SEC-3.2"],
26593
+ "taiga.go.tls-insecure-skip-verify": ["SEC-3.2"],
26594
+ "taiga.javascript.tls-reject-unauthorized-false": ["SEC-3.2"],
26595
+ "taiga.python.requests-verify-false": ["SEC-3.2"],
26596
+ "taiga.rust.tls-verification-disabled": ["SEC-3.2"],
26597
+ "taiga.javascript.jwt-verify-none": ["SDLC-5.4", "SEC-9.1"],
26598
+ "taiga.python.flask-debug-true": ["SDLC-5.5"],
26599
+ "taiga.go.pprof-import": ["SDLC-5.5"],
26600
+ "taiga.csharp.weak-hash": [],
26601
+ "taiga.go.weak-hash": [],
26602
+ "taiga.javascript.weak-hash": [],
26603
+ "taiga.python.weak-hash": [],
26604
+ "taiga.rust.weak-hash": [],
26605
+ "taiga.go.math-rand-for-secrets": [],
26606
+ "taiga.rust.non-crypto-rng-for-secrets": []
26607
+ };
26608
+ /**
26609
+ * Checkov, matched on the check's own description.
26610
+ *
26611
+ * Checkov's rule set is open and numbers in the thousands, so an exhaustive
26612
+ * id map is not available the way it is for our own rules. These phrases are
26613
+ * matched against the check title checkov itself writes ("Ensure all data
26614
+ * stored in the RDS is securely encrypted at rest"), and they are deliberately
26615
+ * narrow: a title matching nothing yields no control, which is the safe
26616
+ * direction to be wrong in.
26617
+ *
26618
+ * Exported so `policy-map.test.ts` validates THESE ids against the registry
26619
+ * too. They are the half most likely to drift, and an id-map that only
26620
+ * validates the closed half is a claim the tests do not support.
26621
+ */
26622
+ const IAC_PHRASES = [
26623
+ {
26624
+ pattern: /encrypt\w*\s+at\s+rest|at\s+rest.*encrypt|securely encrypted/i,
26625
+ controls: ["SEC-3.1"],
26626
+ why: "at-rest encryption"
26627
+ },
26628
+ {
26629
+ pattern: /\bKMS\b|customer[- ]managed key|\bCMK\b/i,
26630
+ controls: ["SEC-3.3"],
26631
+ why: "key management"
26632
+ },
26633
+ {
26634
+ pattern: /in\s+transit|\bTLS\b|\bSSL\b|\bHTTPS\b/i,
26635
+ controls: ["SEC-3.2"],
26636
+ why: "transport encryption"
26637
+ },
26638
+ {
26639
+ pattern: /0\.0\.0\.0\/0|ingress from|security group/i,
26640
+ controls: ["CLD-7.2"],
26641
+ why: "network default-deny"
26642
+ },
26643
+ {
26644
+ pattern: /public(ly)?\s+(access|read|write|expos)|public access/i,
26645
+ controls: ["CLD-1.3"],
26646
+ why: "public exposure"
26647
+ },
26648
+ {
26649
+ pattern: /least privilege|wildcard|admin(istrator)? (policy|access)/i,
26650
+ controls: ["SEC-2.1"],
26651
+ why: "least privilege"
26652
+ },
26653
+ {
26654
+ pattern: /backup|point[- ]in[- ]time recovery/i,
26655
+ controls: ["OPS-4.1"],
26656
+ why: "backup"
26657
+ },
26658
+ {
26659
+ pattern: /audit log|cloudtrail|access logging|logging (is )?enabled/i,
26660
+ controls: ["SDLC-5.8"],
26661
+ why: "security logging"
26662
+ }
26663
+ ];
26664
+ [.../* @__PURE__ */ new Set([
26665
+ ...SECRET_CONTROLS,
26666
+ ...DEPENDENCY_CONTROLS,
26667
+ ...LICENSE_CONTROLS,
26668
+ ...Object.values(SAST_CONTROLS).flat(),
26669
+ ...IAC_PHRASES.flatMap((p) => p.controls)
26670
+ ])];
26671
+ new RegExp(`^\\|\\s*(${CONTROL_ID_SOURCE})\\s*\\|\\s*(.*?)\\s*(?:\\||$)`);
26672
+ REALITY_DERIVED_DOCUMENT_TYPES.filter((t) => t !== "codebase_snapshot");
26673
+ //#endregion
26380
26674
  //#region ../../packages/types/dist/custom-documents.js
26381
26675
  /**
26382
26676
  * Shared types for the custom-documents feature: customer-uploaded reference
@@ -26685,6 +26979,7 @@ const uploadGrant = object({
26685
26979
  expiresAt: string$2()
26686
26980
  });
26687
26981
  object({ version: number$1().int().min(1) });
26982
+ object({ documentId: string$2().uuid() });
26688
26983
  /** The uploads a product's move could not re-file, named while they fit the answer (PRO-3497). */
26689
26984
  const failedDocuments = array(object({
26690
26985
  id: string$2(),
@@ -26811,18 +27106,30 @@ strictObject({
26811
27106
  "replanning",
26812
27107
  "after_step",
26813
27108
  "after_round",
26814
- "held"
26815
- ]).optional() });
27109
+ "held",
27110
+ "sent"
27111
+ ]).optional() }), object({ files: array(object({
27112
+ id: string$2(),
27113
+ url: string$2().url()
27114
+ })) });
27115
+ object({ images: array(object({
27116
+ name: string$2(),
27117
+ url: string$2().url().nullable()
27118
+ })) });
26816
27119
  strictObject({
26817
27120
  exit: _enum(REQUEST_EXITS),
26818
27121
  comment: string$2().trim().min(1).max(PLAN_REJECTION_COMMENT_MAX_CHARS).optional(),
26819
- force: boolean().optional()
27122
+ force: boolean().optional(),
27123
+ seen: array(string$2().uuid()).max(REVIEW_SEEN_MAX).optional()
26820
27124
  }).refine((answer) => answer.exit === "reject" === (answer.comment !== void 0), {
26821
27125
  message: "A rejection takes a comment for the planner, and no other answer takes one.",
26822
27126
  path: ["comment"]
26823
27127
  }).refine((answer) => answer.exit === "replan" || answer.force === void 0, {
26824
27128
  message: "Only Plan again can plan anyway.",
26825
27129
  path: ["force"]
27130
+ }).refine((answer) => answer.exit === "reviewed" || answer.seen === void 0, {
27131
+ message: "Only Mark reviewed says what was shown.",
27132
+ path: ["seen"]
26826
27133
  });
26827
27134
  strictObject({ type: productDocumentType }), object({
26828
27135
  version: number$1().int().min(1),
@@ -26830,7 +27137,7 @@ strictObject({ type: productDocumentType }), object({
26830
27137
  shown: array(string$2()),
26831
27138
  count: number$1().int().min(0)
26832
27139
  })
26833
- }), strictObject({ type: productDocumentType }), strictObject({ type: productDocumentType }), object({ documentId: string$2().uuid() }), strictObject({
27140
+ }), strictObject({ type: productDocumentType }), strictObject({ type: productDocumentType }), strictObject({}), object({ started: number$1().int().min(0) }), strictObject({
26834
27141
  githubRepositoryId: uuid.optional(),
26835
27142
  /** A GitLab project, by the connection whose bot reaches it and GitLab's numeric id (P-PRO-150). */
26836
27143
  gitlabConnectionId: uuid.optional(),
@@ -26852,13 +27159,19 @@ strictObject({ type: productDocumentType }), object({
26852
27159
  enforcementLevel: string$2().nullable()
26853
27160
  }).nullable()
26854
27161
  }), strictObject({
26855
- license: string$2().trim().min(1).max(200),
27162
+ license: string$2().trim().min(1).max(MAINTENANCE_LICENSE_FILTER_MAX),
26856
27163
  ignoreReason: _enum(MAINTENANCE_IGNORE_REASONS),
26857
27164
  ignoreNote,
26858
27165
  ignoreExpiresAt
26859
27166
  }).refine((args) => expiresInFuture(args.ignoreExpiresAt), EXPIRY_IN_FUTURE), strictObject({
26860
- license: string$2().trim().min(1).max(200),
27167
+ license: string$2().trim().min(1).max(MAINTENANCE_LICENSE_FILTER_MAX),
26861
27168
  findingIds: array(uuid).min(1).optional()
27169
+ }), strictObject({ license: string$2().trim().min(1).max(MAINTENANCE_LICENSE_FILTER_MAX) }), object({
27170
+ backlogItemIds: array(string$2()),
27171
+ findingCount: number$1()
27172
+ }), strictObject({}), object({
27173
+ backlogItemId: string$2(),
27174
+ created: boolean()
26862
27175
  }), strictObject({ host: string$2().trim().toLowerCase().refine(isProductNetworkHost, { message: "A host is one exact DNS name, such as api.example.com: no wildcard, scheme, port or path" }).refine((host) => !isSharedHostingDomain(host), { message: "Anyone can host content on this domain, so it cannot be listed whole; list the one host you need under it" }) }), strictObject({
26863
27176
  type: _enum(AGENT_JOB_TYPES),
26864
27177
  documentType: string$2().max(30).optional(),
@@ -26902,6 +27215,14 @@ strictObject({ type: productDocumentType }), object({
26902
27215
  }), strictObject({
26903
27216
  ...policyFixOptions,
26904
27217
  controlId: string$2().min(1).max(40)
27218
+ }), strictObject({
27219
+ controlId: string$2().min(1).max(40),
27220
+ validMonths: union([
27221
+ literal(3),
27222
+ literal(6),
27223
+ literal(12)
27224
+ ]),
27225
+ note: string$2().trim().max(1e3).optional()
26905
27226
  }), strictObject({ environments: array(string$2()).default(["development"]) }), strictObject({
26906
27227
  connection: object({
26907
27228
  name: string$2().min(1).max(150),
@@ -26919,13 +27240,28 @@ strictObject({ type: productDocumentType }), object({
26919
27240
  isProtected: boolean().default(false)
26920
27241
  }).optional()
26921
27242
  });
27243
+ object({
27244
+ url: string$2().url(),
27245
+ expiresAt: string$2()
27246
+ });
27247
+ object({
27248
+ opens: number$1().int().nonnegative(),
27249
+ closes: number$1().int().nonnegative(),
27250
+ products: array(object({
27251
+ productId: string$2().uuid(),
27252
+ opens: number$1().int().nonnegative(),
27253
+ closes: number$1().int().nonnegative()
27254
+ }))
27255
+ });
26922
27256
  object({
26923
27257
  status: customDocumentStatus.nullable(),
26924
27258
  /** The retry itself threw; absent when it went through. */
26925
27259
  error: string$2().optional()
26926
27260
  });
26927
- strictObject({ type: _enum([...FACTORY_DOCUMENT_TYPES, LEGACY_FACTORY_INSTRUCTIONS_TYPE]).transform((type) => type === "team_instructions" ? FACTORY_INSTRUCTIONS_TYPE : type) });
26928
- strictObject({ type: _enum(TENANT_DOCUMENT_TYPES) }), strictObject({
27261
+ /** A factory document type, its legacy name read as the current one. */
27262
+ const factoryDocumentType = _enum([...FACTORY_DOCUMENT_TYPES, LEGACY_FACTORY_INSTRUCTIONS_TYPE]).transform((type) => type === "team_instructions" ? FACTORY_INSTRUCTIONS_TYPE : type);
27263
+ strictObject({ type: factoryDocumentType }), strictObject({ type: factoryDocumentType });
27264
+ strictObject({ type: _enum(TENANT_DOCUMENT_TYPES) }), strictObject({ type: _enum(TENANT_DOCUMENT_TYPES).refine((type) => type !== "design_standards", { message: "The design standards draft is not made through sync" }) }), strictObject({
26929
27265
  name: string$2().min(1).max(50),
26930
27266
  action: _enum(["accept", "decline"])
26931
27267
  }), object({
@@ -26964,7 +27300,17 @@ strictObject({ type: _enum(TENANT_DOCUMENT_TYPES) }), strictObject({
26964
27300
  }), strictObject({ ids: array(uuid).min(1).max(25) }), object({ referenced: array(string$2()) }), strictObject({
26965
27301
  approve: array(string$2().min(1).max(100)).max(200).default([]),
26966
27302
  remove: array(string$2().min(1).max(100)).max(200).default([])
26967
- }).refine((b) => b.approve.length + b.remove.length > 0, { message: "Name a license to approve or remove" }), object({ version: number$1().nullable() });
27303
+ }).refine((b) => b.approve.length + b.remove.length > 0, { message: "Name a license to approve or remove" }), object({ version: number$1().nullable() }), strictObject({
27304
+ approve: string$2().trim().min(1).max(100).optional(),
27305
+ remove: string$2().trim().min(1).max(100).optional()
27306
+ }).refine((a) => Boolean(a.approve) !== Boolean(a.remove), { message: "Name one license to approve or remove" }), object({
27307
+ enrolled: number$1().int().nonnegative(),
27308
+ total: number$1().int().nonnegative()
27309
+ });
27310
+ strictObject({ kind: _enum(["sbom", "notices"]) }), object({
27311
+ url: string$2().url().nullable(),
27312
+ expiresAt: string$2().nullable()
27313
+ });
26968
27314
  strictObject({
26969
27315
  ignoreReason: _enum(MAINTENANCE_IGNORE_REASONS),
26970
27316
  ignoreNote,
@@ -27167,6 +27513,7 @@ const TOOLSET_OF = {
27167
27513
  taiga_fix_finding: "operate",
27168
27514
  taiga_fix_policy_violation: "operate",
27169
27515
  taiga_ignore_finding: "operate",
27516
+ taiga_allow_license: "operate",
27170
27517
  taiga_resolve_action: "operate",
27171
27518
  taiga_create_factory: "setup",
27172
27519
  taiga_rename_factory: "setup",
@@ -27191,6 +27538,7 @@ const TOOLSET_OF = {
27191
27538
  taiga_pause_line: "setup",
27192
27539
  taiga_create_environment: "setup",
27193
27540
  taiga_update_environment: "setup",
27541
+ taiga_ship_notices: "setup",
27194
27542
  taiga_get_deployments: "observe",
27195
27543
  taiga_get_monitoring: "observe",
27196
27544
  taiga_get_delivery_metrics: "observe",
@@ -27242,6 +27590,7 @@ const DELIVERY_TOOLS = /* @__PURE__ */ new Set([
27242
27590
  "taiga_request_initiatives",
27243
27591
  "taiga_fix_finding",
27244
27592
  "taiga_fix_policy_violation",
27593
+ "taiga_ship_notices",
27245
27594
  "taiga_set_auto_build",
27246
27595
  "taiga_set_auto_merge",
27247
27596
  "taiga_pause_line",
@@ -27491,7 +27840,6 @@ function setupNextStep(product) {
27491
27840
  if (product.discoveryStep === void 0) return null;
27492
27841
  switch (product.discoveryStep) {
27493
27842
  case "documents": return `The specification is done; the other documents are generated from it (taiga_generate_product_documents starts the ones still missing). Once every required one is published, ${FINISH_DISCOVERY}`;
27494
- case "checks": return `The documents are published; the checks are stamped in the web, then ${FINISH_DISCOVERY}`;
27495
27843
  default: return `Discovery is a conversation held in the web: the person describes the product there and the specification comes out of it. The other documents follow, then ${FINISH_DISCOVERY}`;
27496
27844
  }
27497
27845
  }
@@ -27520,8 +27868,16 @@ const BY_ARGUMENT = {
27520
27868
  knowledgeId: "taiga_list_knowledge lists the knowledge files.",
27521
27869
  productId: "taiga_list_products lists the products the key can see; a name is not an ID (taiga_get_product takes a slug)."
27522
27870
  };
27871
+ /**
27872
+ * A licence a product does not hold, as against a product the key cannot see
27873
+ * (PRO-3879). Any other 404 (the routes do not exist where the licenses
27874
+ * feature is off) blames neither.
27875
+ */
27876
+ const licenseMiss = (hint) => (said) => /\blicen[cs]e\b/i.test(said) ? hint : /\b(product|project) not found/i.test(said) ? BY_ARGUMENT["productId"] : void 0;
27523
27877
  /** Where an argument means something else on one tool, or the tool's 404 needs its own word. */
27524
27878
  const BY_TOOL = {
27879
+ taiga_allow_license: licenseMiss("taiga_list_packages (productId) summarizes the product's licenses as its findings name them; a license with no finding there has nothing to allow or reopen."),
27880
+ taiga_list_licenses: licenseMiss("taiga_list_licenses without license lists the licenses in use, and taiga_list_packages (productId) one product's, spelled as their findings spell them."),
27525
27881
  taiga_resolve_action: () => "taiga_list_action_required lists the items with their IDs.",
27526
27882
  taiga_get_sbom: () => "An SBOM exists once a sweep has inventoried that repository, where the licenses feature is enabled; taiga_list_packages (linkId) says whether it has packages, and taiga_list_product_repositories lists the links.",
27527
27883
  taiga_get_notices: () => "Notices exist once a sweep has inventoried that repository, where the licenses feature is enabled; taiga_list_product_repositories lists the links.",
@@ -27998,6 +28354,8 @@ const PACKAGE_ROW_FIELDS = [
27998
28354
  "scopeReason",
27999
28355
  "parentReason",
28000
28356
  "sourcePath",
28357
+ "baseImage",
28358
+ "imageLayer",
28001
28359
  "productRepositoryId",
28002
28360
  "detectedAt"
28003
28361
  ];
@@ -28019,7 +28377,10 @@ const FINDING_ROW_FIELDS = [
28019
28377
  "tier",
28020
28378
  "kevListedAt",
28021
28379
  "malicious",
28380
+ "baseMove",
28381
+ "baseImageMove",
28022
28382
  "removal",
28383
+ "replaceable",
28023
28384
  "status",
28024
28385
  "title",
28025
28386
  "ecosystem",
@@ -28148,11 +28509,19 @@ function withFixability(row) {
28148
28509
  fixNote: reason === "not_shipped" ? "No longer applies: the package is only a build for platforms the repository does not deploy to. Nothing to do." : reason === "license_approved" ? "No longer applies: its license is on the organization's approved list. Nothing to do." : reason === "license_undecided" ? "No longer applies: its license can no longer be judged against the list (some of what it can stand for is approved). Nothing to do." : reason === "superseded" ? "Replaced by the repository's finding of its license, which lists every package under it. Nothing to do here." : "Resolved: the sweep no longer reports it. Nothing to do."
28149
28510
  };
28150
28511
  }
28151
- if (row["kind"] === "license_violation") return {
28512
+ if (row["kind"] === "license_violation" && row["replaceable"] === true) return {
28152
28513
  ...row,
28153
- fixable: false,
28154
- fixNote: "A license violation has no fix version: approve the license (taiga_change_license), ignore the finding, or replace the packages under it; taiga_fix_finding refuses it."
28514
+ fixable: true,
28515
+ fixNote: "taiga_fix_finding raises an initiative that replaces the packages under this license with ones whose license, and their own dependencies' licenses, are on the approved list. It never approves the license: that is taiga_change_license, or ignoring the finding."
28155
28516
  };
28517
+ if (row["kind"] === "license_violation") {
28518
+ const status = String(row["status"]);
28519
+ return {
28520
+ ...row,
28521
+ fixable: false,
28522
+ fixNote: status === "remediating" ? "Being fixed: an initiative replaces the packages under this license, and its plan may be waiting for a person's review. taiga_list_initiatives with query \"Replace the\" finds it; follow it there." : status === "open" ? "This Taiga does not replace the packages under a license yet, so taiga_fix_finding refuses it: approve the license (taiga_change_license) or ignore the finding." : `taiga_fix_finding takes only an open license finding, and this one is ${status}.`
28523
+ };
28524
+ }
28156
28525
  const dependency = row["kind"] === "vulnerable_dependency" || row["kind"] === "outdated_dependency";
28157
28526
  const signals = {
28158
28527
  malicious: typeof row["malicious"] === "boolean" ? row["malicious"] : null,
@@ -28176,6 +28545,15 @@ function withFixability(row) {
28176
28545
  fixable: true,
28177
28546
  fixNote: "A malicious package: taiga_fix_finding removes it (or replaces it), with every advisory on that version."
28178
28547
  } : row;
28548
+ if (typeof row["baseMove"] === "boolean" && isAgentRemediable({ remediableBy: typeof row["remediableBy"] === "string" ? row["remediableBy"] : null })) return row["baseMove"] === true && row["status"] === "open" ? {
28549
+ ...row,
28550
+ fixable: true,
28551
+ fixNote: "taiga_fix_finding moves the base images it is in to the builds their tags name now, the FROM line alone, with every other advisory that move removes."
28552
+ } : {
28553
+ ...row,
28554
+ fixable: false,
28555
+ fixNote: "Inside a base image no newer build of which removes it yet: it is fixed by a base move, not a package bump, and is held until one does."
28556
+ };
28179
28557
  if (dependency && !row["recommendedVersion"] && row["remediableBy"] === "coding_agent") return {
28180
28558
  ...row,
28181
28559
  fixable: false,
@@ -28253,7 +28631,10 @@ const ADVISORY_KEPT = [
28253
28631
  "summary",
28254
28632
  "details",
28255
28633
  "severity",
28256
- "published"
28634
+ "severitySource",
28635
+ "cvssScore",
28636
+ "published",
28637
+ "baseImage"
28257
28638
  ];
28258
28639
  const ADVISORY_TEXT_LIMIT = 1500;
28259
28640
  /** The packages of a licence finding shown without full: true (PRO-3394); the rest are counted. */
@@ -28294,6 +28675,36 @@ const SWEEP_FIELDS = [
28294
28675
  "createdAt",
28295
28676
  "completedAt"
28296
28677
  ];
28678
+ /**
28679
+ * The images a sweep's shipped images build on that it could not read, and
28680
+ * why (PRO-4073): a base nobody read is no clean base. Only where the API
28681
+ * returns the list (null with the containerImages flag off, or from a sweep
28682
+ * that listed none).
28683
+ */
28684
+ const BASE_IMAGES_SHOWN = 10;
28685
+ function notReadBases(sweep) {
28686
+ const bases = sweep.baseImages ?? [];
28687
+ const notRead = bases.filter((b) => b.state === "not_read" || b.packages?.state === "not_read");
28688
+ const read = bases.flatMap((b) => b.packages?.state === "read" ? [{
28689
+ image: b.image,
28690
+ path: b.path,
28691
+ os: b.os ?? null,
28692
+ vulnerabilities: b.packages.vulnerabilities,
28693
+ removedByMoving: b.packages.candidate?.removes ?? 0,
28694
+ noDistroFix: b.packages.noDistroFix ?? null,
28695
+ movesTo: b.packages.candidate?.image ?? null
28696
+ }] : []);
28697
+ return {
28698
+ ...read.length > 0 ? {
28699
+ baseImages: read.slice(0, BASE_IMAGES_SHOWN),
28700
+ ...read.length > BASE_IMAGES_SHOWN ? { baseImagesCount: read.length } : {}
28701
+ } : {},
28702
+ ...notRead.length > 0 ? {
28703
+ baseImagesNotRead: notRead.slice(0, BASE_IMAGES_SHOWN),
28704
+ ...notRead.length > BASE_IMAGES_SHOWN ? { baseImagesNotReadCount: notRead.length } : {}
28705
+ } : {}
28706
+ };
28707
+ }
28297
28708
  /** A policy violation in a list: what it is, how bad, and what is being done about it. */
28298
28709
  const VIOLATION_FIELDS = [
28299
28710
  "id",
@@ -28350,11 +28761,51 @@ function standingSummary(standing) {
28350
28761
  resolvedViolations: resolved
28351
28762
  };
28352
28763
  }
28764
+ /**
28765
+ * How the backlog covers the controls that need a feature (PRO-3860), one row
28766
+ * each: the live initiatives that build it, or why it is deferred, and whether
28767
+ * nothing covers it. Empty before a backlog answered for any. It says what is
28768
+ * planned, never that a control is met.
28769
+ */
28770
+ function controlCoverageList(controls) {
28771
+ return (Array.isArray(controls) ? controls : []).flatMap((control) => {
28772
+ const coverage = control["coverage"];
28773
+ if (!coverage || typeof coverage !== "object") return [];
28774
+ const initiatives = Array.isArray(coverage["initiatives"]) ? coverage["initiatives"] : [];
28775
+ return [{
28776
+ controlId: control["controlId"],
28777
+ state: control["state"],
28778
+ planned: plannedFor(coverage),
28779
+ uncovered: coverage["uncovered"] === true,
28780
+ ...initiatives.length > 0 ? { initiatives: initiatives.map((item) => ({
28781
+ id: item["id"],
28782
+ title: item["title"]
28783
+ })) } : {},
28784
+ ...coverage["deferredReason"] ? { deferredReason: coverage["deferredReason"] } : {}
28785
+ }];
28786
+ });
28787
+ }
28788
+ /**
28789
+ * Whether the backlog plans a control: yes when a live initiative builds it, later when it deferred it with a
28790
+ * reason, no otherwise. A deferred control reads "planned later" rather than only "not assessed" (PRO-3860).
28791
+ */
28792
+ function plannedFor(coverage) {
28793
+ if (Array.isArray(coverage["initiatives"]) && coverage["initiatives"].length > 0) return "yes";
28794
+ return coverage["deferredReason"] ? "later" : "no";
28795
+ }
28796
+ /** The coverage, when the backlog answered for any control, for the default and full answers. */
28797
+ function controlCoveragePart(controls) {
28798
+ const coverage = controlCoverageList(controls);
28799
+ return coverage.length > 0 ? { controlCoverage: coverage } : {};
28800
+ }
28353
28801
  const findingId = idInput("finding", "taiga_list_findings gives it");
28354
28802
  /** Open findings by severity and kind, and the latest sweeps, as the first page of findings carries them. */
28355
28803
  async function sweepOverview(client, productId) {
28356
28804
  const [summary, sweeps] = await Promise.all([client.getMaintenanceSummary(productId), client.listMaintenanceSweeps(productId, { limit: 3 })]);
28357
- const latest = pickFields(sweeps.data, SWEEP_FIELDS);
28805
+ const latest = sweeps.data.map((sweep) => ({
28806
+ ...pickFields([sweep], SWEEP_FIELDS)[0],
28807
+ ...notReadBases(sweep)
28808
+ }));
28358
28809
  const { lastRun: _lastRun, lastRuns: _lastRuns, topFindings: _top, violatedControls: _controls, ...counts } = summary.data;
28359
28810
  return {
28360
28811
  ...counts,
@@ -28362,6 +28813,20 @@ async function sweepOverview(client, productId) {
28362
28813
  ...latest.length === 0 ? { note: "This product has never been swept: no findings is not an all-clear." } : {}
28363
28814
  };
28364
28815
  }
28816
+ /** The organization's licence read takes an id this long at most, as `organizationLicenseQuery` does. */
28817
+ const ORGANIZATION_LICENSE_MAX = 200;
28818
+ /**
28819
+ * Why a license is allowed in a product, in a license's words, as the web's
28820
+ * Allow dialog offers them (PRO-3727); each is stored as the ignore reason
28821
+ * beside it.
28822
+ */
28823
+ const LICENSE_ALLOW_REASON_TO_IGNORE = {
28824
+ accepted: "accepted_risk",
28825
+ not_handed_out: "not_reachable",
28826
+ no_alternative: "wont_fix",
28827
+ read_wrong: "false_positive"
28828
+ };
28829
+ const LICENSE_ALLOW_REASONS = Object.keys(LICENSE_ALLOW_REASON_TO_IGNORE);
28365
28830
  function registerMaintenanceTools(server, client) {
28366
28831
  if (client.hasCapability("tenant:settings")) server.registerTool("taiga_change_license", {
28367
28832
  title: "Approve or remove a license",
@@ -28449,10 +28914,18 @@ function registerMaintenanceTools(server, client) {
28449
28914
  }));
28450
28915
  server.registerTool("taiga_list_licenses", {
28451
28916
  title: "List the organization's licenses",
28452
- description: "List the organization's approved license list and every license in use across its products, approved or not, with packages, products and repositories. With license, where that one is used: products, repositories and what brings its packages in. NOT_FOUND where the licenses feature is off.",
28453
- inputSchema: object({ license: string$2().trim().min(1).max(200).optional().describe("An SPDX license id: where it is used, instead of the list") }),
28917
+ description: "List the organization's approved license list and every license in use across its products, approved or not, with packages, products and repositories. With license, where that one is used: products, repositories, what brings its packages in, and its approved-list history. With license and productId, that license in one product: its standing against the list, what it asks, when to allow it there, how many packages ship, and what brings them in. Decide it there with taiga_allow_license. NOT_FOUND where the licenses feature is off.",
28918
+ inputSchema: object({
28919
+ license: string$2().trim().min(1).max(MAINTENANCE_LICENSE_FILTER_MAX).optional().describe("An SPDX license id or expression: where it is used, instead of the list"),
28920
+ productId: productId.optional().describe("With license: that license in this product only (taiga_list_products gives it)")
28921
+ }),
28454
28922
  annotations: READ_ONLY
28455
- }, (params) => runTool(async () => toolResult(params.license ? (await client.getOrganizationLicense(params.license)).data : (await client.getOrganizationLicenses()).data)));
28923
+ }, (params) => runTool(async () => {
28924
+ if (params.productId !== void 0 && params.license === void 0) return toolError("VALIDATION_ERROR", "Name the license to read in this product (license).");
28925
+ if (params.license === void 0) return toolResult((await client.getOrganizationLicenses()).data);
28926
+ if (params.productId === void 0 && params.license.length > ORGANIZATION_LICENSE_MAX) return toolError("VALIDATION_ERROR", `A license id is at most ${ORGANIZATION_LICENSE_MAX} characters; name a product (productId) to read a longer expression.`);
28927
+ return toolResult(params.productId !== void 0 ? (await client.getProductLicense(params.productId, params.license)).data : (await client.getOrganizationLicense(params.license)).data);
28928
+ }));
28456
28929
  server.registerTool("taiga_get_sbom", {
28457
28930
  title: "Download a repository SBOM",
28458
28931
  description: "Download one repository's software bill of materials (CycloneDX 1.6). Large; taiga_list_packages summarizes. NOT_FOUND where the licenses feature is off, or before a sweep has read the repository.",
@@ -28519,7 +28992,8 @@ function registerMaintenanceTools(server, client) {
28519
28992
  const own = violations.filter((violation) => String(violation["controlId"]).toUpperCase() === wanted);
28520
28993
  return toolResult({
28521
28994
  ...control,
28522
- ...control.state === "no_evidence" ? { meaning: "Not assessed: no finding or violation is recorded against it, and the documents were not found to show it met. That is no evidence either way, not a pass." } : {},
28995
+ ...control.state === "no_evidence" ? { meaning: (control.coverage && plannedFor(control.coverage) === "later" ? `Planned later: the backlog deferred it (${String(control.coverage.deferredReason).replace(/\.$/, "")}). ` : "") + "Not assessed: no finding or violation is recorded against it, and the documents were not found to show it met. That is no evidence either way, not a pass." } : {},
28996
+ ...control.coverage ? { coverageNote: "coverage is how the backlog plans for it: the live initiatives that build it, or why it is deferred. It says what is planned, not that the control is met." } : {},
28523
28997
  ...control.assessment == null && (control.state === "breached" || control.state === "diverged") ? { assessmentNote: "No met or not-applicable verdict is shown: an open finding or violation against the control outranks one." } : {},
28524
28998
  ...Number(control.findingCount ?? 0) > 0 ? { findingsNote: `${String(control.findingCount)} open scanner finding${control.findingCount === 1 ? "" : "s"} breach it; taiga_list_findings (productId) lists them with the controls each breaches. violations below are the documents' only.` } : {},
28525
28999
  violations: pickFields(own, VIOLATION_FIELDS),
@@ -28542,6 +29016,7 @@ function registerMaintenanceTools(server, client) {
28542
29016
  ...wording,
28543
29017
  ...standing,
28544
29018
  controls: listed,
29019
+ ...controlCoveragePart(controls),
28545
29020
  divergences: cutLongText(violations),
28546
29021
  resolvedDivergences: cutLongText(resolvedAll.slice(0, FULL_RESOLVED)),
28547
29022
  ...resolvedAll.length > FULL_RESOLVED ? { resolvedNote: `The first ${FULL_RESOLVED} of ${resolvedAll.length} resolved violations.` } : {}
@@ -28565,7 +29040,8 @@ function registerMaintenanceTools(server, client) {
28565
29040
  "findingsByTier",
28566
29041
  "plannedCount"
28567
29042
  ]),
28568
- violations: pickFields(violations, VIOLATION_FIELDS)
29043
+ violations: pickFields(violations, VIOLATION_FIELDS),
29044
+ ...controlCoveragePart(controls)
28569
29045
  });
28570
29046
  }));
28571
29047
  if (!client.hasCapability("products:write")) return;
@@ -28591,9 +29067,31 @@ function registerMaintenanceTools(server, client) {
28591
29067
  const finding = (await client.updateFinding(params.findingId, body)).data;
28592
29068
  return toolResult(findingRows([finding], ["ignoreNote"])[0]);
28593
29069
  }));
29070
+ server.registerTool("taiga_allow_license", {
29071
+ title: "Allow or reopen a license in a product",
29072
+ description: "Allow a license in one product without approving it for the organization: every license finding of it there that is open, or whose ignore lapsed, is ignored with one reason, as the web's Allow in this product does; one already ignored keeps its reason and expiry, so reopen first to change them. allowed: false reopens them. Another product using the license still raises its finding; to approve it everywhere, use taiga_change_license. Allowing a license is the person's decision. NOT_FOUND when the product has no license finding for it.",
29073
+ inputSchema: object({
29074
+ productId,
29075
+ license: string$2().trim().min(1).max(MAINTENANCE_LICENSE_FILTER_MAX).describe("The license as the product's license findings or taiga_list_packages name it, such as MPL-2.0"),
29076
+ allowed: boolean().describe("true allows the license in this product, false reopens its findings"),
29077
+ reason: _enum(LICENSE_ALLOW_REASONS).optional().describe("Why it is allowed; required when allowing. accepted: reviewed and allowed here, usually for a limited time; not_handed_out: the product runs only internally or on your own servers, its code is never given to anyone; no_alternative: no package under an approved license does the job yet; read_wrong: the packages are not actually under this license"),
29078
+ note: string$2().trim().max(280).optional().describe("A short justification, kept for audit"),
29079
+ expiresAt: string$2().datetime({ offset: true }).meta({ pattern: void 0 }).optional().describe("When the allowance lapses and the findings come back if still there (ISO 8601, in the future)")
29080
+ }),
29081
+ annotations: OPERATE_SETTING
29082
+ }, (params) => runTool(async () => {
29083
+ if (!params.allowed) return toolResult((await client.reopenProductLicense(params.productId, params.license)).data);
29084
+ if (params.reason === void 0) return toolError("VALIDATION_ERROR", "Say why the license is allowed (reason).");
29085
+ return toolResult((await client.allowProductLicense(params.productId, {
29086
+ license: params.license,
29087
+ ignoreReason: LICENSE_ALLOW_REASON_TO_IGNORE[params.reason],
29088
+ ...params.note !== void 0 ? { ignoreNote: params.note } : {},
29089
+ ...params.expiresAt !== void 0 ? { ignoreExpiresAt: params.expiresAt } : {}
29090
+ })).data);
29091
+ }));
28594
29092
  server.registerTool("taiga_fix_finding", {
28595
29093
  title: "Fix a finding",
28596
- description: "Raise an initiative that fixes a finding; it ends in a pull request, which merges on its own only where the product's auto-merge allows it. wholeGroup: true fixes, in that repository, every open advisory on that package version in one bump, or every place the rule flagged. A malicious package is removed, with every advisory on that version, whatever wholeGroup says. A language runtime moves every file declaring it together, in a container image only to a tag the image has published. Refused when there is no version to move to (fixable: false; for a runtime, also while its image has not published the fix), the code is in a submodule, or a secret is only in history (rotate it outside Taiga).",
29094
+ description: "Raise an initiative that fixes a finding; it ends in a pull request, which merges on its own only where the product's auto-merge allows it. wholeGroup: true fixes, in that repository, every open advisory on that package version in one bump, or every place the rule flagged. A malicious package is removed, with every advisory on that version, whatever wholeGroup says. A license finding the row marks fixable has its packages replaced by ones on the approved list. A language runtime moves every file declaring it together, in a container image only to a tag the image has published. Refused when there is no version to move to (fixable: false; for a runtime, also while its image has not published the fix), the code is in a submodule, or a secret is only in history (rotate it outside Taiga).",
28597
29095
  inputSchema: object({
28598
29096
  productId,
28599
29097
  findingId,
@@ -28632,6 +29130,13 @@ function registerMaintenanceTools(server, client) {
28632
29130
  const body = { refuseIfFixedSinceAssessment: params.fixAgain !== true };
28633
29131
  return toolResult((params.controlId ? await client.remediatePolicyControl(params.productId, params.controlId, body) : await client.remediatePolicyViolation(params.productId, params.violationId, body)).data);
28634
29132
  }));
29133
+ if (!client.hasCapability("backlog:write")) return;
29134
+ server.registerTool("taiga_ship_notices", {
29135
+ title: "Ship open-source notices",
29136
+ description: "Raise the initiative that builds open-source notices into a product's Vite web app: every production build writes the license texts and NOTICE files of the packages in its browser bundle, and the app shows them at /legal/open-source. Taiga writes the brief from the inventory; the plan waits for review before it builds. One that is already open is handed back (created: false) rather than raised again. Refused where no repository of the product declares Vite.",
29137
+ inputSchema: object({ productId }),
29138
+ annotations: OPERATE
29139
+ }, (params) => runTool(async () => toolResult((await client.shipNotices(params.productId)).data)));
28635
29140
  }
28636
29141
  //#endregion
28637
29142
  //#region src/tools/sections.ts
@@ -28957,20 +29462,17 @@ async function setupReadiness(client, product, links) {
28957
29462
  const settings = product["settings"] ?? {};
28958
29463
  const importState = imported ? IMPORT_STATE[String(settings.importStatus)] ?? "not started" : void 0;
28959
29464
  const missing = blockers ? blockers.missingDocuments : null;
28960
- const openChecks = blockers ? blockers.openChecks.map((check) => check.criterion) : null;
28961
29465
  return {
28962
- ready: blockers ? missing.length === 0 && openChecks.length === 0 : null,
29466
+ ready: blockers ? missing.length === 0 : null,
28963
29467
  repository,
28964
29468
  ...imported ? { import: typeof settings.importStatus === "string" ? settings.importStatus : "pending" } : {},
28965
29469
  ...imported ? {} : { discoveryStep: full["discoveryStep"] ?? null },
28966
29470
  missingDocuments: missing,
28967
- openChecks,
28968
29471
  nextStep: nextStepFor({
28969
29472
  imported,
28970
29473
  importState,
28971
29474
  linked: repository?.linked ?? null,
28972
29475
  missing,
28973
- openChecks,
28974
29476
  product: full
28975
29477
  })
28976
29478
  };
@@ -28992,7 +29494,6 @@ function nextStepFor(state) {
28992
29494
  if (!state.missing) return setupNextStep(state.product);
28993
29495
  if (state.missing.includes("specification")) return "The specification comes from the Discovery conversation, held in the web: the person describes the product there. The other documents follow from it." + (state.linked === false ? " Linking a repository (taiga_link_repository) can wait until before the first build." : "");
28994
29496
  if (state.missing.length > 0) return `Still to publish: ${named(state.missing)}. taiga_generate_product_documents generates the missing ones (paid agent work: ask first); a draft is published with taiga_publish_product_document.`;
28995
- if (state.openChecks && state.openChecks.length > 0) return `The documents are published; the checks are stamped in the web, then ${finish}`;
28996
29497
  return `Every required document is published: ${finish}`;
28997
29498
  }
28998
29499
  //#endregion
@@ -30076,7 +30577,8 @@ const READY_BLOCKER_SAID = {
30076
30577
  bar_lowering: "a fix lowered a check instead of fixing the code",
30077
30578
  bar_lowering_unchecked: "Taiga could not check whether a fix lowered a check",
30078
30579
  criticals_unresolved: "Taiga's own review left a critical issue unresolved",
30079
- clean_checkout_failed: "the branch does not install or pass its tests from a clean checkout"
30580
+ clean_checkout_failed: "the branch does not install or pass its tests from a clean checkout",
30581
+ license_not_approved: "a package it adds has a licence that is not on your approved list"
30080
30582
  };
30081
30583
  /** The first sentence of a run's error, short enough for a row. */
30082
30584
  function errorSaid(error) {
@@ -30834,6 +31336,10 @@ function standing(item) {
30834
31336
  const life = item["lifecycle"];
30835
31337
  const run = life?.latestBuild;
30836
31338
  const situation = situationOf(item);
31339
+ const open = (life?.openPullRequests ?? []).flatMap((pull) => typeof pull.repositoryFullName === "string" && typeof pull.prNumber === "number" ? [{
31340
+ repository: pull.repositoryFullName,
31341
+ prNumber: pull.prNumber
31342
+ }] : []);
30837
31343
  return {
30838
31344
  column: typeof item["state"] === "string" ? item["state"] : null,
30839
31345
  turn: typeof life?.turn === "string" ? life.turn : null,
@@ -30841,7 +31347,8 @@ function standing(item) {
30841
31347
  latestRun: run && typeof run.id === "string" ? {
30842
31348
  id: run.id,
30843
31349
  ...typeof run.prNumber === "number" ? { prNumber: run.prNumber } : {}
30844
- } : null
31350
+ } : null,
31351
+ ...open.length > 1 ? { pullRequests: open } : {}
30845
31352
  };
30846
31353
  }
30847
31354
  /** The move each call to action is, in the tools' words. */
@@ -31024,7 +31531,8 @@ const PLAN_SUMMARY_FIELDS = [
31024
31531
  "backlogItemId",
31025
31532
  "version",
31026
31533
  "status",
31027
- "createdAt"
31534
+ "createdAt",
31535
+ "controls"
31028
31536
  ];
31029
31537
  const PLAN_STEP_SUMMARY_FIELDS = [
31030
31538
  "id",
@@ -31171,10 +31679,12 @@ function registerBacklogReads(server, client) {
31171
31679
  ...revised
31172
31680
  });
31173
31681
  const steps = Array.isArray(plan.steps) ? plan.steps : [];
31682
+ const several = (plan.repositories?.length ?? 0) > 1;
31174
31683
  return toolResult({
31175
31684
  ...pickFields([plan], PLAN_SUMMARY_FIELDS)[0],
31685
+ ...several ? { repositories: plan.repositories } : {},
31176
31686
  ...revised,
31177
- steps: pickFields(steps, PLAN_STEP_SUMMARY_FIELDS)
31687
+ steps: several ? pickFields(steps, [...PLAN_STEP_SUMMARY_FIELDS, "repositoryName"]) : pickFields(steps, PLAN_STEP_SUMMARY_FIELDS)
31178
31688
  });
31179
31689
  }));
31180
31690
  }
@@ -32130,7 +32640,7 @@ async function licenseReviews(client, productId, now) {
32130
32640
  packages: data["packages"],
32131
32641
  ...Number.isNaN(created) ? {} : { waitingDays: Math.floor((now - created) / DAY_MS) },
32132
32642
  readWith: "taiga_list_licenses (license)",
32133
- decideWith: "taiga_change_license (license, action: 'approve'); dismissing it is only in the web"
32643
+ decideWith: "taiga_change_license (license, action: 'approve') for every product" + (client.hasCapability("products:write") ? ", or taiga_allow_license (productId, license) for this one" : "") + "; dismissing it is only in the web"
32134
32644
  };
32135
32645
  }).filter((r) => productId === void 0 || r.productId === productId),
32136
32646
  truncated: result.truncated === true
@@ -48897,7 +49407,7 @@ function registerSetupTools(server, client) {
48897
49407
  }, (params) => runTool(async () => toolResult((await client.refreshRealityDocuments(params.productId, params.docTypes)).data)));
48898
49408
  server.registerTool("taiga_finish_discovery", {
48899
49409
  title: "Finish Discovery",
48900
- description: "Finish a product's setup: lock its documents and start planning its first initiatives. Refused while documents are missing (the refusal lists them) or a check is not stamped in the web.",
49410
+ description: "Finish a product's setup: lock its documents and start planning its first initiatives. Refused while documents are missing (the refusal lists them).",
48901
49411
  inputSchema: object({ productId }),
48902
49412
  annotations: OPERATE
48903
49413
  }, (params) => runTool(async () => {
@@ -49162,6 +49672,7 @@ const PAID_WORK = /* @__PURE__ */ new Set([
49162
49672
  "taiga_request_changes",
49163
49673
  "taiga_request_initiatives",
49164
49674
  "taiga_resume_initiative",
49675
+ "taiga_ship_notices",
49165
49676
  "taiga_retry_knowledge",
49166
49677
  "taiga_review_plan",
49167
49678
  "taiga_set_auto_build",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taiga-ai/mcp-server",
3
- "version": "0.36.0-rc.800",
3
+ "version": "0.36.0-rc.892",
4
4
  "description": "Taiga for AI coding assistants: read products, policies, initiatives and runs, and operate the line, over MCP",
5
5
  "mcpName": "ga.tai/mcp-server",
6
6
  "license": "UNLICENSED",