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

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 +886 -56
  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}).
24425
+ */
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.
24407
24443
  */
24408
- const EEA_COUNTRIES = [{
24409
- code: "NO",
24410
- name: "Norway"
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"];
@@ -24642,6 +24687,19 @@ const PRODUCT_DOCUMENT_DISPLAY_NAMES = {
24642
24687
  const PRODUCT_DOCUMENT_ORIGINS = ["plan", "reality"];
24643
24688
  /** The forges a repository link can name: `FORGE_PROVIDERS`, under the name links have always used. */
24644
24689
  const VCS_PROVIDERS = FORGE_PROVIDERS;
24690
+ Object.fromEntries(Object.entries({
24691
+ specification: "specification",
24692
+ "user-flows": "user_flow",
24693
+ architecture: "architecture",
24694
+ "tech-decisions": "technology_decisions",
24695
+ "data-flow": "data_flow",
24696
+ "threat-model": "threat_model",
24697
+ dpia: "privacy_assessment",
24698
+ "risk-register": "risk_register",
24699
+ "service-blueprint": "service_blueprint",
24700
+ "ui-direction": "ui_direction",
24701
+ "codebase-snapshot": "codebase_snapshot"
24702
+ }).map(([slug, apiType]) => [apiType, slug]));
24645
24703
  //#endregion
24646
24704
  //#region ../../packages/types/dist/route-key.js
24647
24705
  /**
@@ -24884,7 +24942,7 @@ const HALT_CAUSES = {
24884
24942
  * reaped on a dead heartbeat, or found at 'planning' with no live job by the
24885
24943
  * reconciler, which fails it with that cause on the tick that notices
24886
24944
  * (`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
24945
+ * so that shape is never a race). One of four halts that mirror
24888
24946
  * `PLAN_FAILURE_CAUSES` one-to-one; see `PLAN_FAILURE_HALT`.
24889
24947
  */
24890
24948
  planning_stalled: {
@@ -24897,6 +24955,22 @@ const HALT_CAUSES = {
24897
24955
  "remove"
24898
24956
  ]
24899
24957
  },
24958
+ /**
24959
+ * The planner concluded nothing should be built (PRO-3941): a licence's
24960
+ * replacement where only build tools carry the licence and no maintained
24961
+ * package replaces them. Not a fault, so not `planning_failed`; its reason
24962
+ * is the detail, read off the planning job as a failure's is.
24963
+ */
24964
+ planning_declined: {
24965
+ message: "The plan recommends not building this, so the queue stopped here.",
24966
+ params: [],
24967
+ clearedWhen: "planning is retried, or a re-plan replaces it",
24968
+ exits: [
24969
+ "auto",
24970
+ "reorder",
24971
+ "remove"
24972
+ ]
24973
+ },
24900
24974
  /** The run errored out. */
24901
24975
  run_failed: {
24902
24976
  message: "The run failed, so the queue stopped here.",
@@ -25178,7 +25252,8 @@ const READY_BLOCKERS = [
25178
25252
  "bar_lowering",
25179
25253
  "bar_lowering_unchecked",
25180
25254
  "criticals_unresolved",
25181
- "clean_checkout_failed"
25255
+ "clean_checkout_failed",
25256
+ "license_not_approved"
25182
25257
  ];
25183
25258
  /**
25184
25259
  * Why the autonomous loop handed a live pull request to a person (PRO-2409).
@@ -25291,6 +25366,7 @@ const INBOX_ITEM_KINDS = [
25291
25366
  "review_pr",
25292
25367
  "review_maintenance",
25293
25368
  "review_policy",
25369
+ "attest_policy",
25294
25370
  "setup_env_var",
25295
25371
  "setup_secret",
25296
25372
  "setup_third_party",
@@ -25415,6 +25491,7 @@ discriminatedUnion("kind", [
25415
25491
  })).max(20),
25416
25492
  moreFailed: number$1().int().min(0).optional(),
25417
25493
  observedAt: optionalMoment,
25494
+ repository: string$2().min(1).max(255).optional(),
25418
25495
  stepId: optionalUuid
25419
25496
  }),
25420
25497
  object({
@@ -26007,6 +26084,92 @@ object({
26007
26084
  ...outcomes === void 0 ? {} : { outcomes }
26008
26085
  }));
26009
26086
  //#endregion
26087
+ //#region ../../packages/types/dist/registries.js
26088
+ /**
26089
+ * The package ecosystems a registry can serve: every ecosystem the coding
26090
+ * agent builds (its skills and its image), keyed by the install tooling that
26091
+ * reads the credential. `maven` is a Maven repository, and Maven builds get
26092
+ * its credential (PRO-2916); Gradle builds name theirs in their own build
26093
+ * scripts and get none yet (PRO-2928).
26094
+ */
26095
+ const REGISTRY_ECOSYSTEMS = [
26096
+ "npm",
26097
+ "terraform",
26098
+ "pypi",
26099
+ "nuget",
26100
+ "go",
26101
+ "cargo",
26102
+ "maven",
26103
+ "rubygems",
26104
+ "composer"
26105
+ ];
26106
+ const REGISTRY_VENDORS = [
26107
+ "npmjs",
26108
+ "github",
26109
+ "gcp_artifact",
26110
+ "generic",
26111
+ "hcp_terraform",
26112
+ "scalr",
26113
+ "azure_artifacts",
26114
+ "jfrog",
26115
+ "git_host",
26116
+ "aws_codeartifact"
26117
+ ];
26118
+ /** An IAM role ARN, as the CodeArtifact credential takes it. */
26119
+ const AWS_ROLE_ARN_PATTERN = /^arn:aws:iam::\d{12}:role\/[\w+=,.@/-]{1,512}$/;
26120
+ //#endregion
26121
+ //#region ../../packages/types/dist/gitlab.js
26122
+ /**
26123
+ * GitLab connections (PRO-3096, P-PRO-150): the shared request/response
26124
+ * contract of `/v1/gitlab/connections`. A connection is one GitLab host
26125
+ * (GitLab.com, a self-managed instance or GitLab Dedicated) with a bot token
26126
+ * the tenant pasted. The token is write-only and appears in no response.
26127
+ */
26128
+ const GITLAB_CONNECTION_STATUSES = [
26129
+ "valid",
26130
+ "auth_failed",
26131
+ "unreachable"
26132
+ ];
26133
+ //#endregion
26134
+ //#region ../../packages/types/dist/cloud-connections.js
26135
+ const CLOUD_CONNECTION_STATUSES = [
26136
+ "pending_setup",
26137
+ "validating",
26138
+ "active",
26139
+ "error",
26140
+ "suspended",
26141
+ "disconnected"
26142
+ ];
26143
+ /**
26144
+ * Keys in `providerConfig` that only Taiga's provisioning path may write.
26145
+ *
26146
+ * PRO-858. `accountName` is the correlation key the AFT account-ready callback
26147
+ * arrives with, and the webhook resolves it under service context with no
26148
+ * tenant predicate -- it has to, because the callback carries no tenant. It is
26149
+ * also guessable: `generateAccountName` derives it from tenant, team and
26150
+ * project slugs. So a tenant that could write this key could plant a row
26151
+ * claiming another tenant's account name, wait for that callback, and have the
26152
+ * victim's `deployConfig` (accountId, deployRoleArn, stateBucket) delivered to
26153
+ * their own environment.
26154
+ *
26155
+ * `accountKey` is the same identity in unguessable form
26156
+ * (`sha256(tenantId:productId:envType)`), and is what the AFT module is named
26157
+ * for -- equally not the caller's to assert.
26158
+ *
26159
+ * Neither is tenant-facing: no provider schema models them, and
26160
+ * `providerConfigProblem` documents them as the shape a MANAGED row has. The
26161
+ * update path merges and persists the RAW config precisely so Zod does not
26162
+ * strip them off managed rows, which is what made a tenant-supplied value
26163
+ * durable. Rejecting rather than stripping, so a client sending one is told,
26164
+ * instead of silently having it dropped.
26165
+ */
26166
+ const TAIGA_MANAGED_PROVIDER_CONFIG_KEYS = ["accountName", "accountKey"];
26167
+ /** Whether `config` leaves alone the keys only Taiga's provisioning writes. */
26168
+ function isTenantWritableProviderConfig(config) {
26169
+ return !TAIGA_MANAGED_PROVIDER_CONFIG_KEYS.some((key) => key in config);
26170
+ }
26171
+ const TAIGA_MANAGED_PROVIDER_CONFIG_MESSAGE = `providerConfig may not set ${TAIGA_MANAGED_PROVIDER_CONFIG_KEYS.join(" or ")}: Taiga's provisioning owns those keys`;
26172
+ //#endregion
26010
26173
  //#region ../../packages/types/dist/environments.js
26011
26174
  const ENVIRONMENT_TYPES = [
26012
26175
  "development",
@@ -26020,6 +26183,31 @@ const ENVIRONMENT_TYPES = [
26020
26183
  //#endregion
26021
26184
  //#region ../../packages/types/dist/api-keys.js
26022
26185
  /**
26186
+ * API key scopes control what operations a key can perform.
26187
+ * Scopes act as a ceiling -- the effective permissions are the intersection
26188
+ * of the key's scopes and the user's role-based capabilities.
26189
+ *
26190
+ * Convention: "resource:action" matching the existing Capability type.
26191
+ */
26192
+ const API_KEY_SCOPES = [
26193
+ "products:read",
26194
+ "products:write",
26195
+ "backlog:read",
26196
+ "backlog:write",
26197
+ "factories:read",
26198
+ "factories:documents:write",
26199
+ "users:read",
26200
+ "tenant:documents:read",
26201
+ "tenant:documents:write",
26202
+ "vcs:read",
26203
+ "customDocuments:read",
26204
+ "customDocuments:write",
26205
+ "customDocuments:delete",
26206
+ "factories:write",
26207
+ "vcs:manage"
26208
+ ];
26209
+ const API_KEY_PREFIX = "taiga_sk_live_";
26210
+ /**
26023
26211
  * The groups the MCP server's tools come in (PRO-2838). A key may name the ones
26024
26212
  * its AI tool sees; null is every one. This is focus, not security: a tool the
26025
26213
  * key's role or scopes do not allow is never offered, whatever is named here.
@@ -26031,6 +26219,11 @@ const MCP_TOOLSETS = [
26031
26219
  "observe",
26032
26220
  "documents"
26033
26221
  ];
26222
+ Object.freeze({
26223
+ required: false,
26224
+ gracePeriodDays: 0,
26225
+ requiredSince: null
26226
+ });
26034
26227
  [...TURN_EVENT_TRIGGERS];
26035
26228
  /** Longest request for changes from Taiga (PRO-3035): about four pages of prose. */
26036
26229
  const CHANGE_REQUEST_MAX_CHARS = 1e4;
@@ -26051,6 +26244,8 @@ const REQUEST_EXITS = [
26051
26244
  "dismiss",
26052
26245
  "reviewed"
26053
26246
  ];
26247
+ /** The most ids an answer's `seen` carries; a longer list is not sent, and the review is not checked. */
26248
+ const REVIEW_SEEN_MAX = 1e4;
26054
26249
  //#endregion
26055
26250
  //#region ../../packages/types/dist/maintenance.js
26056
26251
  /**
@@ -26146,10 +26341,30 @@ function isFixableFinding(finding) {
26146
26341
  if (finding.status !== "open") return false;
26147
26342
  if (finding.location?.historyOnly) return false;
26148
26343
  if (!isAgentRemediable(finding)) return false;
26344
+ if (finding.fingerprint?.startsWith("base-image:") || finding.baseImageFix) {
26345
+ if (finding.baseImageFix) return finding.baseImageFix.way === "move" || finding.baseImageFix.way === "upgrade";
26346
+ return typeof finding.baseMove === "boolean" ? finding.baseMove : baseImageMoveRemoves(finding.data);
26347
+ }
26149
26348
  if (finding.kind === "vulnerable_dependency" && isMaliciousPackageFinding(finding)) return true;
26349
+ if (isPinBlocked(finding)) return false;
26150
26350
  return isCodeRemediationKind(finding.kind) || !!finding.recommendedVersion;
26151
26351
  }
26152
26352
  /**
26353
+ * Whether a Fix cannot work: another package's pin caps this finding's fix, and
26354
+ * the last fix ended still vulnerable against it (`opentelemetry-api` under
26355
+ * `aws-opentelemetry-distro`'s `==1.44.0`). Another would spend a run to meet
26356
+ * it again. Blocked with no such fix, a person's Fix still runs (AC-17): they
26357
+ * may know a way round. The gate lifts the hold when the pin moves.
26358
+ */
26359
+ function isPinBlocked(finding) {
26360
+ return finding.autoFixHeld?.reason === "blocked_by_dependency" && finding.autoFixHeld.fixFailed === true;
26361
+ }
26362
+ function baseImageMoveRemoves(data) {
26363
+ const baseImage = typeof data === "object" && data !== null ? data.baseImage : null;
26364
+ const bases = typeof baseImage === "object" && baseImage !== null ? baseImage.bases : null;
26365
+ return Array.isArray(bases) && bases.length > 0 && bases.every((b) => typeof b === "object" && b !== null && typeof b.movesTo === "string" && b.removed === true);
26366
+ }
26367
+ /**
26153
26368
  * A malicious package (OSV `MAL-*`): its advisory id or one of its aliases
26154
26369
  * says so, or its tier is Act now with no KEV listing, which only a malware
26155
26370
  * advisory earns (`findingTierReason`). The id and aliases come first: a
@@ -26165,6 +26380,17 @@ function isMaliciousPackageFinding(finding) {
26165
26380
  if (Array.isArray(aliases) && aliases.some(mal)) return true;
26166
26381
  return finding.tier === "act_now" && finding.kevListedAt == null;
26167
26382
  }
26383
+ /**
26384
+ * The scanner CATEGORIES a sweep reports, in the order the agent reports them
26385
+ * (PRO-1725). Outcomes are keyed on the category, not the tool, so replacing a
26386
+ * tool in the agents' scanner manifest changes nothing here.
26387
+ */
26388
+ const MAINTENANCE_SCANNERS = [
26389
+ "deps",
26390
+ "secrets",
26391
+ "sast",
26392
+ "iac"
26393
+ ];
26168
26394
  new Map(Object.entries({
26169
26395
  deps: [
26170
26396
  "vulnerable_dependency",
@@ -26370,6 +26596,19 @@ object({
26370
26596
  commit: d.commit
26371
26597
  }));
26372
26598
  /**
26599
+ * The sources whose findings no agent scanner's outcome decides (PRO-4070):
26600
+ * the API's runtime judge (PRO-3659) and base OS judge (PRO-4074), and the
26601
+ * base image reading (PRO-1728), whose findings the agent drafts but only a
26602
+ * sweep that read the images may settle.
26603
+ */
26604
+ const MAINTENANCE_JUDGED_SOURCES = [
26605
+ "runtime",
26606
+ "base_os",
26607
+ "base_image"
26608
+ ];
26609
+ [...MAINTENANCE_SCANNERS, ...MAINTENANCE_JUDGED_SOURCES];
26610
+ new Set(MAINTENANCE_SCANNERS);
26611
+ /**
26373
26612
  * The longest licence string the package list filters by (PRO-2920). The
26374
26613
  * inventory keeps every expression whole whatever its length; this bounds only
26375
26614
  * the query string, far above any expression seen. A longer one is listed in
@@ -26377,6 +26616,200 @@ object({
26377
26616
  */
26378
26617
  const MAINTENANCE_LICENSE_FILTER_MAX = 4096;
26379
26618
  //#endregion
26619
+ //#region ../../packages/types/dist/maintenance-advisory.js
26620
+ /**
26621
+ * Pull the renderable advisory fields out of a finding's raw OSV `data` object.
26622
+ *
26623
+ * `data` is the scanner's raw OSV vulnerability payload (MaintenanceFindingData,
26624
+ * shape varies), so every field is read leniently: a missing or malformed key
26625
+ * yields an empty value rather than throwing. Shared by the web app's finding
26626
+ * detail and the Taiga Audit preview and report (PRO-3791), so both show the
26627
+ * same identifiers for the same finding.
26628
+ */
26629
+ /** A non-empty string, or null: anything else the scanner wrote reads as absent. */
26630
+ const text$3 = string$2().min(1).nullable().catch(null);
26631
+ /** The non-empty strings of a list; a value that is not a list reads as none. */
26632
+ const texts = array(unknown$1()).catch([]).transform((list) => list.filter((v) => typeof v === "string" && v.length > 0));
26633
+ object({
26634
+ type: text$3,
26635
+ score: text$3
26636
+ });
26637
+ object({
26638
+ type: text$3,
26639
+ url: text$3
26640
+ });
26641
+ object({
26642
+ aliases: texts,
26643
+ database_specific: object({
26644
+ cwe_ids: texts,
26645
+ severity: text$3
26646
+ }).nullable().catch(null),
26647
+ severity: array(unknown$1()).catch([]),
26648
+ references: array(unknown$1()).catch([]),
26649
+ published: text$3,
26650
+ modified: text$3
26651
+ });
26652
+ //#endregion
26653
+ //#region ../../packages/types/dist/finding-controls.js
26654
+ /**
26655
+ * A committed credential.
26656
+ *
26657
+ * SEC-3.4 "Secrets must never be stored in source code or logs"
26658
+ * SEC-5.1 "Secrets must be stored in an approved secrets management system"
26659
+ * SEC-5.2 "Secrets must be rotated at least annually" -- the half Taiga cannot
26660
+ * do, and the reason the remediation initiative insists the PR say so.
26661
+ * SDLC-3.5 "Secret detection must run pre-commit and in CI pipeline"
26662
+ */
26663
+ const SECRET_CONTROLS = [
26664
+ "SEC-3.4",
26665
+ "SEC-5.1",
26666
+ "SEC-5.2",
26667
+ "SDLC-3.5"
26668
+ ];
26669
+ /**
26670
+ * A vulnerable dependency.
26671
+ *
26672
+ * SCS-2.6 "critical security patches within 7 days, others within 30 days"
26673
+ * -- breached by a vulnerability whose fix has been out past its
26674
+ * window, and by no other (PRO-3420). One found this morning has missed
26675
+ * nothing; neither has one with no fixed release to move to.
26676
+ * SCS-5.2 "Critical vulnerabilities must block deployment"
26677
+ *
26678
+ * Two controls this map deliberately does NOT claim:
26679
+ * - SCS-2.5 "Transitive dependency vulnerabilities must be monitored
26680
+ * continuously". A finding is that monitoring working, not evidence it is
26681
+ * missing; every vulnerable dependency read as a breach of it. The sweep's
26682
+ * own record shows it met instead (sweep-evidence.ts).
26683
+ * - Anything for an outdated dependency that is no vulnerability: SCS-2.6's
26684
+ * rationale is "timely vulnerability remediation", and an update that fixes
26685
+ * nothing is not a security patch.
26686
+ */
26687
+ const DEPENDENCY_CONTROLS = ["SCS-2.6", "SCS-5.2"];
26688
+ /**
26689
+ * A package whose licence the allowlist does not satisfy (PRO-2862).
26690
+ *
26691
+ * TIC-6.2 "Open source licenses must be reviewed and approved before adoption"
26692
+ * -- the same policy section lists the approved licences and the ones
26693
+ * requiring review, which is where the default allowlist comes from.
26694
+ */
26695
+ const LICENSE_CONTROLS = ["TIC-6.2"];
26696
+ /**
26697
+ * Injection and unsanitized input, whatever the sink.
26698
+ *
26699
+ * SDLC-5.1 "All user input must be validated and sanitized"
26700
+ * SEC-6.1 "All API endpoints must validate and sanitize input"
26701
+ */
26702
+ const INJECTION_CONTROLS = ["SDLC-5.1", "SEC-6.1"];
26703
+ /**
26704
+ * Every opengrep rule Taiga ships, mapped explicitly.
26705
+ *
26706
+ * A closed set, unlike checkov's -- these rules are ours, so an exhaustive map
26707
+ * is possible and a keyword heuristic would be a worse answer to an easier
26708
+ * question. `policy-map.test.ts` reads the rule ids straight out of the YAML
26709
+ * so a new rule that lands unmapped fails the build instead of silently
26710
+ * emitting no control.
26711
+ */
26712
+ const SAST_CONTROLS = {
26713
+ "taiga.csharp.sql-string-concat": INJECTION_CONTROLS,
26714
+ "taiga.go.sql-string-concat": INJECTION_CONTROLS,
26715
+ "taiga.javascript.sql-template-concat": INJECTION_CONTROLS,
26716
+ "taiga.python.sqli-fstring": INJECTION_CONTROLS,
26717
+ "taiga.rust.sql-format-concat": INJECTION_CONTROLS,
26718
+ "taiga.csharp.process-start-shell-dynamic": INJECTION_CONTROLS,
26719
+ "taiga.go.exec-command-dynamic": INJECTION_CONTROLS,
26720
+ "taiga.javascript.child-process-exec-dynamic": INJECTION_CONTROLS,
26721
+ "taiga.python.os-system-dynamic": INJECTION_CONTROLS,
26722
+ "taiga.python.subprocess-shell-true": INJECTION_CONTROLS,
26723
+ "taiga.rust.command-shell-dynamic": INJECTION_CONTROLS,
26724
+ "taiga.javascript.eval-dynamic": INJECTION_CONTROLS,
26725
+ "taiga.python.eval-exec-dynamic": INJECTION_CONTROLS,
26726
+ "taiga.javascript.dangerously-set-inner-html": INJECTION_CONTROLS,
26727
+ "taiga.javascript.innerhtml-dynamic": INJECTION_CONTROLS,
26728
+ "taiga.csharp.binaryformatter-deserialize": ["SDLC-5.7"],
26729
+ "taiga.python.pickle-load-untrusted": ["SDLC-5.7"],
26730
+ "taiga.python.yaml-load-unsafe": ["SDLC-5.7"],
26731
+ "taiga.csharp.cert-validation-disabled": ["SEC-3.2"],
26732
+ "taiga.go.tls-insecure-skip-verify": ["SEC-3.2"],
26733
+ "taiga.javascript.tls-reject-unauthorized-false": ["SEC-3.2"],
26734
+ "taiga.python.requests-verify-false": ["SEC-3.2"],
26735
+ "taiga.rust.tls-verification-disabled": ["SEC-3.2"],
26736
+ "taiga.javascript.jwt-verify-none": ["SDLC-5.4", "SEC-9.1"],
26737
+ "taiga.python.flask-debug-true": ["SDLC-5.5"],
26738
+ "taiga.go.pprof-import": ["SDLC-5.5"],
26739
+ "taiga.csharp.weak-hash": [],
26740
+ "taiga.go.weak-hash": [],
26741
+ "taiga.javascript.weak-hash": [],
26742
+ "taiga.python.weak-hash": [],
26743
+ "taiga.rust.weak-hash": [],
26744
+ "taiga.go.math-rand-for-secrets": [],
26745
+ "taiga.rust.non-crypto-rng-for-secrets": []
26746
+ };
26747
+ /**
26748
+ * Checkov, matched on the check's own description.
26749
+ *
26750
+ * Checkov's rule set is open and numbers in the thousands, so an exhaustive
26751
+ * id map is not available the way it is for our own rules. These phrases are
26752
+ * matched against the check title checkov itself writes ("Ensure all data
26753
+ * stored in the RDS is securely encrypted at rest"), and they are deliberately
26754
+ * narrow: a title matching nothing yields no control, which is the safe
26755
+ * direction to be wrong in.
26756
+ *
26757
+ * Exported so `policy-map.test.ts` validates THESE ids against the registry
26758
+ * too. They are the half most likely to drift, and an id-map that only
26759
+ * validates the closed half is a claim the tests do not support.
26760
+ */
26761
+ const IAC_PHRASES = [
26762
+ {
26763
+ pattern: /encrypt\w*\s+at\s+rest|at\s+rest.*encrypt|securely encrypted/i,
26764
+ controls: ["SEC-3.1"],
26765
+ why: "at-rest encryption"
26766
+ },
26767
+ {
26768
+ pattern: /\bKMS\b|customer[- ]managed key|\bCMK\b/i,
26769
+ controls: ["SEC-3.3"],
26770
+ why: "key management"
26771
+ },
26772
+ {
26773
+ pattern: /in\s+transit|\bTLS\b|\bSSL\b|\bHTTPS\b/i,
26774
+ controls: ["SEC-3.2"],
26775
+ why: "transport encryption"
26776
+ },
26777
+ {
26778
+ pattern: /0\.0\.0\.0\/0|ingress from|security group/i,
26779
+ controls: ["CLD-7.2"],
26780
+ why: "network default-deny"
26781
+ },
26782
+ {
26783
+ pattern: /public(ly)?\s+(access|read|write|expos)|public access/i,
26784
+ controls: ["CLD-1.3"],
26785
+ why: "public exposure"
26786
+ },
26787
+ {
26788
+ pattern: /least privilege|wildcard|admin(istrator)? (policy|access)/i,
26789
+ controls: ["SEC-2.1"],
26790
+ why: "least privilege"
26791
+ },
26792
+ {
26793
+ pattern: /backup|point[- ]in[- ]time recovery/i,
26794
+ controls: ["OPS-4.1"],
26795
+ why: "backup"
26796
+ },
26797
+ {
26798
+ pattern: /audit log|cloudtrail|access logging|logging (is )?enabled/i,
26799
+ controls: ["SDLC-5.8"],
26800
+ why: "security logging"
26801
+ }
26802
+ ];
26803
+ [.../* @__PURE__ */ new Set([
26804
+ ...SECRET_CONTROLS,
26805
+ ...DEPENDENCY_CONTROLS,
26806
+ ...LICENSE_CONTROLS,
26807
+ ...Object.values(SAST_CONTROLS).flat(),
26808
+ ...IAC_PHRASES.flatMap((p) => p.controls)
26809
+ ])];
26810
+ new RegExp(`^\\|\\s*(${CONTROL_ID_SOURCE})\\s*\\|\\s*(.*?)\\s*(?:\\||$)`);
26811
+ REALITY_DERIVED_DOCUMENT_TYPES.filter((t) => t !== "codebase_snapshot");
26812
+ //#endregion
26380
26813
  //#region ../../packages/types/dist/custom-documents.js
26381
26814
  /**
26382
26815
  * Shared types for the custom-documents feature: customer-uploaded reference
@@ -26685,6 +27118,7 @@ const uploadGrant = object({
26685
27118
  expiresAt: string$2()
26686
27119
  });
26687
27120
  object({ version: number$1().int().min(1) });
27121
+ object({ documentId: string$2().uuid() });
26688
27122
  /** The uploads a product's move could not re-file, named while they fit the answer (PRO-3497). */
26689
27123
  const failedDocuments = array(object({
26690
27124
  id: string$2(),
@@ -26785,6 +27219,14 @@ const cloudProviderConfig = discriminatedUnion("provider", [
26785
27219
  bestPractices: cloudBestPractices
26786
27220
  })
26787
27221
  ]);
27222
+ const newCloudConnection = strictObject({
27223
+ name: string$2().min(1).max(150),
27224
+ provider: _enum(CLOUD_PROVIDERS),
27225
+ providerConfig: cloudProviderConfig
27226
+ }).refine((connection) => connection.provider === connection.providerConfig.provider, {
27227
+ message: "connection.provider must match connection.providerConfig.provider",
27228
+ path: ["providerConfig", "provider"]
27229
+ });
26788
27230
  const columnIndex = number$1().int().min(1).max(EXCEL_MAX_COLUMN);
26789
27231
  const sheetMap = object({
26790
27232
  sheetName: string$2().min(1).max(120),
@@ -26811,18 +27253,30 @@ strictObject({
26811
27253
  "replanning",
26812
27254
  "after_step",
26813
27255
  "after_round",
26814
- "held"
26815
- ]).optional() });
27256
+ "held",
27257
+ "sent"
27258
+ ]).optional() }), object({ files: array(object({
27259
+ id: string$2(),
27260
+ url: string$2().url()
27261
+ })) });
27262
+ object({ images: array(object({
27263
+ name: string$2(),
27264
+ url: string$2().url().nullable()
27265
+ })) });
26816
27266
  strictObject({
26817
27267
  exit: _enum(REQUEST_EXITS),
26818
27268
  comment: string$2().trim().min(1).max(PLAN_REJECTION_COMMENT_MAX_CHARS).optional(),
26819
- force: boolean().optional()
27269
+ force: boolean().optional(),
27270
+ seen: array(string$2().uuid()).max(REVIEW_SEEN_MAX).optional()
26820
27271
  }).refine((answer) => answer.exit === "reject" === (answer.comment !== void 0), {
26821
27272
  message: "A rejection takes a comment for the planner, and no other answer takes one.",
26822
27273
  path: ["comment"]
26823
27274
  }).refine((answer) => answer.exit === "replan" || answer.force === void 0, {
26824
27275
  message: "Only Plan again can plan anyway.",
26825
27276
  path: ["force"]
27277
+ }).refine((answer) => answer.exit === "reviewed" || answer.seen === void 0, {
27278
+ message: "Only Mark reviewed says what was shown.",
27279
+ path: ["seen"]
26826
27280
  });
26827
27281
  strictObject({ type: productDocumentType }), object({
26828
27282
  version: number$1().int().min(1),
@@ -26830,7 +27284,7 @@ strictObject({ type: productDocumentType }), object({
26830
27284
  shown: array(string$2()),
26831
27285
  count: number$1().int().min(0)
26832
27286
  })
26833
- }), strictObject({ type: productDocumentType }), strictObject({ type: productDocumentType }), object({ documentId: string$2().uuid() }), strictObject({
27287
+ }), strictObject({ type: productDocumentType }), strictObject({ type: productDocumentType }), strictObject({
26834
27288
  githubRepositoryId: uuid.optional(),
26835
27289
  /** A GitLab project, by the connection whose bot reaches it and GitLab's numeric id (P-PRO-150). */
26836
27290
  gitlabConnectionId: uuid.optional(),
@@ -26852,13 +27306,19 @@ strictObject({ type: productDocumentType }), object({
26852
27306
  enforcementLevel: string$2().nullable()
26853
27307
  }).nullable()
26854
27308
  }), strictObject({
26855
- license: string$2().trim().min(1).max(200),
27309
+ license: string$2().trim().min(1).max(MAINTENANCE_LICENSE_FILTER_MAX),
26856
27310
  ignoreReason: _enum(MAINTENANCE_IGNORE_REASONS),
26857
27311
  ignoreNote,
26858
27312
  ignoreExpiresAt
26859
27313
  }).refine((args) => expiresInFuture(args.ignoreExpiresAt), EXPIRY_IN_FUTURE), strictObject({
26860
- license: string$2().trim().min(1).max(200),
27314
+ license: string$2().trim().min(1).max(MAINTENANCE_LICENSE_FILTER_MAX),
26861
27315
  findingIds: array(uuid).min(1).optional()
27316
+ }), strictObject({ license: string$2().trim().min(1).max(MAINTENANCE_LICENSE_FILTER_MAX) }), object({
27317
+ backlogItemIds: array(string$2()),
27318
+ findingCount: number$1()
27319
+ }), strictObject({}), object({
27320
+ backlogItemId: string$2(),
27321
+ created: boolean()
26862
27322
  }), 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
27323
  type: _enum(AGENT_JOB_TYPES),
26864
27324
  documentType: string$2().max(30).optional(),
@@ -26902,15 +27362,16 @@ strictObject({ type: productDocumentType }), object({
26902
27362
  }), strictObject({
26903
27363
  ...policyFixOptions,
26904
27364
  controlId: string$2().min(1).max(40)
26905
- }), strictObject({ environments: array(string$2()).default(["development"]) }), strictObject({
26906
- connection: object({
26907
- name: string$2().min(1).max(150),
26908
- provider: _enum(CLOUD_PROVIDERS),
26909
- providerConfig: cloudProviderConfig
26910
- }).refine((connection) => connection.provider === connection.providerConfig.provider, {
26911
- message: "connection.provider must match connection.providerConfig.provider",
26912
- path: ["providerConfig", "provider"]
26913
- }),
27365
+ }), strictObject({
27366
+ controlId: string$2().min(1).max(40),
27367
+ validMonths: union([
27368
+ literal(3),
27369
+ literal(6),
27370
+ literal(12)
27371
+ ]),
27372
+ note: string$2().trim().max(1e3).optional()
27373
+ }), strictObject({ environments: array(string$2()).default(["development"]) }), object({ id: string$2().uuid() }), strictObject({
27374
+ connection: newCloudConnection,
26914
27375
  environment: object({
26915
27376
  name: string$2().min(1).max(100),
26916
27377
  slug: string$2().min(1).max(63).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "Slug must be lowercase letters, digits, and dashes"),
@@ -26919,13 +27380,159 @@ strictObject({ type: productDocumentType }), object({
26919
27380
  isProtected: boolean().default(false)
26920
27381
  }).optional()
26921
27382
  });
27383
+ /**
27384
+ * The MCP toolsets a key's AI tool sees: at least one, each once, or null for
27385
+ * every one (PRO-2838). An empty list would start a server with no tools at all.
27386
+ */
27387
+ const mcpToolsets = array(_enum(MCP_TOOLSETS)).min(1).refine((sets) => new Set(sets).size === sets.length, "Name each toolset once").nullable();
27388
+ strictObject({
27389
+ key: string$2().regex(new RegExp(`^${API_KEY_PREFIX}[0-9a-f]{64}$`)),
27390
+ name: string$2().min(1).max(255),
27391
+ scopes: array(_enum(API_KEY_SCOPES)).optional(),
27392
+ expiresInDays: number$1().int().min(1).max(365).optional(),
27393
+ mcpToolsets: mcpToolsets.optional()
27394
+ }), object({ keyId: uuid });
27395
+ strictObject({ mcpToolsets });
27396
+ object({
27397
+ url: string$2().url(),
27398
+ expiresAt: string$2()
27399
+ });
27400
+ object({
27401
+ opens: number$1().int().nonnegative(),
27402
+ closes: number$1().int().nonnegative(),
27403
+ products: array(object({
27404
+ productId: string$2().uuid(),
27405
+ opens: number$1().int().nonnegative(),
27406
+ closes: number$1().int().nonnegative()
27407
+ }))
27408
+ });
26922
27409
  object({
26923
27410
  status: customDocumentStatus.nullable(),
26924
27411
  /** The retry itself threw; absent when it went through. */
26925
27412
  error: string$2().optional()
26926
27413
  });
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({
27414
+ /** A factory document type, its legacy name read as the current one. */
27415
+ const factoryDocumentType = _enum([...FACTORY_DOCUMENT_TYPES, LEGACY_FACTORY_INSTRUCTIONS_TYPE]).transform((type) => type === "team_instructions" ? FACTORY_INSTRUCTIONS_TYPE : type);
27416
+ strictObject({ type: factoryDocumentType }), strictObject({ type: factoryDocumentType });
27417
+ /**
27418
+ * A registry scope: an npm scope ("@acme") or a Go module path prefix
27419
+ * ("github.com/acme"), as `POST /registries` takes one (PRO-3670). Which
27420
+ * ecosystem takes which is the service's rule.
27421
+ */
27422
+ const namedSegments = (value) => value.split("/").every((seg) => seg !== "" && seg !== "." && seg !== "..");
27423
+ const registryScope = string$2().regex(/^@?[A-Za-z0-9][A-Za-z0-9._~/-]*$/, "Not a valid scope").max(214).refine(namedSegments, "Not a valid scope");
27424
+ /** The package a registry's check fetches, in the union of the ecosystems' grammars. */
27425
+ const registryTestPackage = string$2().min(1).max(214).regex(/^@?[A-Za-z0-9_][A-Za-z0-9._~/:-]*$/, "Must be a valid package name").refine(namedSegments, "Must be a valid package name");
27426
+ /** KMS direct-encrypt takes 4096 bytes; the raw secret stays well under. */
27427
+ const REGISTRY_CREDENTIAL_MAX_BYTES = 3500;
27428
+ /** A registry's write-only credential: exactly one of a token, a GCP key or an AWS role, as the route takes it. */
27429
+ const registryCredential = strictObject({
27430
+ token: string$2().min(1).max(REGISTRY_CREDENTIAL_MAX_BYTES).optional(),
27431
+ username: string$2().min(1).max(256).regex(/^[^:\s]+$/, "Username must not contain a colon or whitespace").optional(),
27432
+ credentialsJson: string$2().min(2).max(REGISTRY_CREDENTIAL_MAX_BYTES).optional(),
27433
+ awsRoleArn: string$2().max(600).regex(AWS_ROLE_ARN_PATTERN, "Not an IAM role ARN").optional()
27434
+ }).refine((c) => [
27435
+ c.token,
27436
+ c.credentialsJson,
27437
+ c.awsRoleArn
27438
+ ].filter((v) => v != null).length === 1, { message: "Provide exactly one of token, credentialsJson or awsRoleArn" }).refine((c) => c.username == null || c.token != null, { message: "A username goes with a token" }).refine((c) => new TextEncoder().encode(JSON.stringify(c)).length <= 4e3, { message: "Credential is too large to encrypt (must be under 4 KB once JSON-encoded)" });
27439
+ /** An instant as an ISO string, which the service reads as a date. */
27440
+ const isoInstant = string$2().datetime({ offset: true });
27441
+ object({
27442
+ status: _enum([
27443
+ "unvalidated",
27444
+ "valid",
27445
+ "auth_failed",
27446
+ "unreachable"
27447
+ ]),
27448
+ lastValidatedAt: string$2(),
27449
+ detail: string$2().nullable()
27450
+ });
27451
+ strictObject({
27452
+ displayName: string$2().min(1).max(255).optional(),
27453
+ registryUrl: string$2().url().max(512).optional(),
27454
+ scopes: array(registryScope).max(50).optional(),
27455
+ expiresAt: isoInstant.nullable().optional(),
27456
+ credential: registryCredential.optional(),
27457
+ testPackage: registryTestPackage.nullable().optional()
27458
+ });
27459
+ /** A GitLab bot token, as the route takes one. */
27460
+ const gitlabToken = string$2().trim().min(20, "That does not look like a GitLab access token").max(1024).regex(/^[A-Za-z0-9_.-]+$/, "That does not look like a GitLab access token");
27461
+ /** A private CA as PEM certificates; the service parses it. */
27462
+ const gitlabCaBundle = string$2().min(1).max(65536);
27463
+ /** What a connect or a token change learned, beyond the connection's record. */
27464
+ const gitlabValidationReport = object({
27465
+ pushableProjectCount: number$1().int().nullable(),
27466
+ warnings: array(string$2())
27467
+ });
27468
+ strictObject({
27469
+ displayName: string$2().trim().min(1).max(255).optional(),
27470
+ token: gitlabToken.optional(),
27471
+ caBundlePem: gitlabCaBundle.nullable().optional()
27472
+ }), gitlabValidationReport.optional(), object({
27473
+ status: _enum(GITLAB_CONNECTION_STATUSES),
27474
+ lastValidatedAt: string$2(),
27475
+ detail: string$2().nullable()
27476
+ }), object({ finished: boolean() }), strictObject({ search: string$2().trim().max(100).optional() }), object({
27477
+ projects: array(object({
27478
+ id: number$1().int(),
27479
+ pathWithNamespace: string$2(),
27480
+ nameWithNamespace: string$2(),
27481
+ webUrl: string$2(),
27482
+ defaultBranch: string$2().nullable()
27483
+ })).nullable(),
27484
+ truncated: boolean()
27485
+ });
27486
+ object({
27487
+ outcome: _enum([
27488
+ "uninstalled",
27489
+ "left_in_use",
27490
+ "uninstall_failed"
27491
+ ]),
27492
+ manageUrl: string$2().nullable()
27493
+ });
27494
+ /** A value GitHub's redirect carries back: a state token, or a one-time code. */
27495
+ const githubCallbackSecret = string$2().trim().min(1).max(1024);
27496
+ strictObject({
27497
+ displayName: string$2().trim().min(1).max(255),
27498
+ baseUrl: string$2().trim().min(1).max(480),
27499
+ token: gitlabToken,
27500
+ caBundlePem: gitlabCaBundle.nullable().optional()
27501
+ }), object({
27502
+ connectionId: uuid,
27503
+ validation: gitlabValidationReport
27504
+ }), strictObject({
27505
+ displayName: string$2().min(1).max(255),
27506
+ ecosystem: _enum(REGISTRY_ECOSYSTEMS).optional(),
27507
+ vendor: _enum(REGISTRY_VENDORS),
27508
+ registryUrl: string$2().url().max(512),
27509
+ scopes: array(registryScope).max(50).optional(),
27510
+ expiresAt: isoInstant.nullable().optional(),
27511
+ credential: registryCredential,
27512
+ testPackage: registryTestPackage.optional()
27513
+ }), strictObject({ hostId: uuid.optional() }), object({ installUrl: string$2().url() }), strictObject({ state: githubCallbackSecret }), object({
27514
+ clientId: string$2(),
27515
+ authorizeUrl: string$2(),
27516
+ redirectUri: string$2().optional()
27517
+ }), strictObject({
27518
+ installationId: number$1().int().positive(),
27519
+ setupAction: _enum(["install", "update"]),
27520
+ state: githubCallbackSecret.optional(),
27521
+ code: githubCallbackSecret.optional()
27522
+ }), strictObject({
27523
+ subdomain: string$2().min(1).max(39).optional(),
27524
+ serverUrl: string$2().min(1).max(2048).optional(),
27525
+ caBundlePem: string$2().max(65536).optional(),
27526
+ orgLogin: string$2().min(1).max(39),
27527
+ displayName: string$2().max(255).optional()
27528
+ }), object({
27529
+ manifest: record(string$2(), unknown$1()),
27530
+ postUrl: string$2().url(),
27531
+ state: string$2()
27532
+ }), strictObject({
27533
+ state: githubCallbackSecret,
27534
+ code: githubCallbackSecret
27535
+ }), 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
27536
  name: string$2().min(1).max(50),
26930
27537
  action: _enum(["accept", "decline"])
26931
27538
  }), object({
@@ -26964,7 +27571,26 @@ strictObject({ type: _enum(TENANT_DOCUMENT_TYPES) }), strictObject({
26964
27571
  }), strictObject({ ids: array(uuid).min(1).max(25) }), object({ referenced: array(string$2()) }), strictObject({
26965
27572
  approve: array(string$2().min(1).max(100)).max(200).default([]),
26966
27573
  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() });
27574
+ }).refine((b) => b.approve.length + b.remove.length > 0, { message: "Name a license to approve or remove" }), object({ version: number$1().nullable() }), strictObject({
27575
+ approve: string$2().trim().min(1).max(100).optional(),
27576
+ remove: string$2().trim().min(1).max(100).optional()
27577
+ }).refine((a) => Boolean(a.approve) !== Boolean(a.remove), { message: "Name one license to approve or remove" }), object({
27578
+ enrolled: number$1().int().nonnegative(),
27579
+ total: number$1().int().nonnegative()
27580
+ }), object({
27581
+ principalArn: string$2().nullable(),
27582
+ externalId: string$2()
27583
+ });
27584
+ strictObject({ kind: _enum(["sbom", "notices"]) }), object({
27585
+ url: string$2().url().nullable(),
27586
+ expiresAt: string$2().nullable()
27587
+ }), object({
27588
+ branches: array(object({
27589
+ name: string$2(),
27590
+ protected: boolean()
27591
+ })).nullable(),
27592
+ truncated: boolean()
27593
+ });
26968
27594
  strictObject({
26969
27595
  ignoreReason: _enum(MAINTENANCE_IGNORE_REASONS),
26970
27596
  ignoreNote,
@@ -26976,6 +27602,11 @@ strictObject({ tier: _enum([
26976
27602
  "staging",
26977
27603
  "production"
26978
27604
  ]) });
27605
+ strictObject({
27606
+ name: string$2().min(1).max(150).optional(),
27607
+ status: _enum(CLOUD_CONNECTION_STATUSES).optional(),
27608
+ providerConfig: record(string$2(), unknown$1()).refine(isTenantWritableProviderConfig, { message: TAIGA_MANAGED_PROVIDER_CONFIG_MESSAGE }).optional()
27609
+ });
26979
27610
  object({
26980
27611
  url: string$2().url(),
26981
27612
  expiresAt: string$2(),
@@ -27167,6 +27798,7 @@ const TOOLSET_OF = {
27167
27798
  taiga_fix_finding: "operate",
27168
27799
  taiga_fix_policy_violation: "operate",
27169
27800
  taiga_ignore_finding: "operate",
27801
+ taiga_allow_license: "operate",
27170
27802
  taiga_resolve_action: "operate",
27171
27803
  taiga_create_factory: "setup",
27172
27804
  taiga_rename_factory: "setup",
@@ -27191,6 +27823,7 @@ const TOOLSET_OF = {
27191
27823
  taiga_pause_line: "setup",
27192
27824
  taiga_create_environment: "setup",
27193
27825
  taiga_update_environment: "setup",
27826
+ taiga_ship_notices: "setup",
27194
27827
  taiga_get_deployments: "observe",
27195
27828
  taiga_get_monitoring: "observe",
27196
27829
  taiga_get_delivery_metrics: "observe",
@@ -27242,6 +27875,7 @@ const DELIVERY_TOOLS = /* @__PURE__ */ new Set([
27242
27875
  "taiga_request_initiatives",
27243
27876
  "taiga_fix_finding",
27244
27877
  "taiga_fix_policy_violation",
27878
+ "taiga_ship_notices",
27245
27879
  "taiga_set_auto_build",
27246
27880
  "taiga_set_auto_merge",
27247
27881
  "taiga_pause_line",
@@ -27491,7 +28125,6 @@ function setupNextStep(product) {
27491
28125
  if (product.discoveryStep === void 0) return null;
27492
28126
  switch (product.discoveryStep) {
27493
28127
  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
28128
  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
28129
  }
27497
28130
  }
@@ -27520,8 +28153,16 @@ const BY_ARGUMENT = {
27520
28153
  knowledgeId: "taiga_list_knowledge lists the knowledge files.",
27521
28154
  productId: "taiga_list_products lists the products the key can see; a name is not an ID (taiga_get_product takes a slug)."
27522
28155
  };
28156
+ /**
28157
+ * A licence a product does not hold, as against a product the key cannot see
28158
+ * (PRO-3879). Any other 404 (the routes do not exist where the licenses
28159
+ * feature is off) blames neither.
28160
+ */
28161
+ 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
28162
  /** Where an argument means something else on one tool, or the tool's 404 needs its own word. */
27524
28163
  const BY_TOOL = {
28164
+ 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."),
28165
+ 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
28166
  taiga_resolve_action: () => "taiga_list_action_required lists the items with their IDs.",
27526
28167
  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
28168
  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 +28639,8 @@ const PACKAGE_ROW_FIELDS = [
27998
28639
  "scopeReason",
27999
28640
  "parentReason",
28000
28641
  "sourcePath",
28642
+ "baseImage",
28643
+ "imageLayer",
28001
28644
  "productRepositoryId",
28002
28645
  "detectedAt"
28003
28646
  ];
@@ -28019,7 +28662,11 @@ const FINDING_ROW_FIELDS = [
28019
28662
  "tier",
28020
28663
  "kevListedAt",
28021
28664
  "malicious",
28665
+ "baseMove",
28666
+ "baseImageMove",
28667
+ "baseImageFix",
28022
28668
  "removal",
28669
+ "replaceable",
28023
28670
  "status",
28024
28671
  "title",
28025
28672
  "ecosystem",
@@ -28145,14 +28792,22 @@ function withFixability(row) {
28145
28792
  return {
28146
28793
  ...row,
28147
28794
  fixable: false,
28148
- 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."
28795
+ 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." : reason === "license_not_judged" ? "No longer applies: an OS package's license is judged only when it restricts use. Its packages keep it. Nothing to do." : "Resolved: the sweep no longer reports it. Nothing to do."
28149
28796
  };
28150
28797
  }
28151
- if (row["kind"] === "license_violation") return {
28798
+ if (row["kind"] === "license_violation" && row["replaceable"] === true) return {
28152
28799
  ...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."
28800
+ fixable: true,
28801
+ 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
28802
  };
28803
+ if (row["kind"] === "license_violation") {
28804
+ const status = String(row["status"]);
28805
+ return {
28806
+ ...row,
28807
+ fixable: false,
28808
+ 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}.`
28809
+ };
28810
+ }
28156
28811
  const dependency = row["kind"] === "vulnerable_dependency" || row["kind"] === "outdated_dependency";
28157
28812
  const signals = {
28158
28813
  malicious: typeof row["malicious"] === "boolean" ? row["malicious"] : null,
@@ -28176,6 +28831,38 @@ function withFixability(row) {
28176
28831
  fixable: true,
28177
28832
  fixNote: "A malicious package: taiga_fix_finding removes it (or replaces it), with every advisory on that version."
28178
28833
  } : row;
28834
+ const held = row["autoFixHeld"];
28835
+ const inBase = row["baseImageFix"] != null || typeof row["baseMove"] === "boolean";
28836
+ if (row["status"] === "open" && !inBase && held && isPinBlocked({ autoFixHeld: held })) {
28837
+ const what = held.blockingPackage ?? "another dependency";
28838
+ return {
28839
+ ...row,
28840
+ fixable: false,
28841
+ fixNote: `Waiting on ${what}${held.blockingConstraint ? `, which requires ${held.blockingConstraint}` : ""}. taiga_fix_finding refuses it until that moves.`
28842
+ };
28843
+ }
28844
+ const base = row["baseImageFix"];
28845
+ if (base && base.way !== "move" && isAgentRemediable({ remediableBy: remediableByOf(row) })) {
28846
+ if (base.way === "upgrade") return row["status"] === "open" ? {
28847
+ ...row,
28848
+ fixable: true,
28849
+ fixNote: "taiga_fix_finding adds a temporary line to the Dockerfile that upgrades this package over its base, until the base publishes a build with the fix."
28850
+ } : row;
28851
+ return {
28852
+ ...row,
28853
+ fixable: false,
28854
+ fixNote: baseImageWaitNote(base)
28855
+ };
28856
+ }
28857
+ if (typeof row["baseMove"] === "boolean" && isAgentRemediable({ remediableBy: remediableByOf(row) })) return row["baseMove"] === true && row["status"] === "open" ? {
28858
+ ...row,
28859
+ fixable: true,
28860
+ 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."
28861
+ } : {
28862
+ ...row,
28863
+ fixable: false,
28864
+ 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."
28865
+ };
28179
28866
  if (dependency && !row["recommendedVersion"] && row["remediableBy"] === "coding_agent") return {
28180
28867
  ...row,
28181
28868
  fixable: false,
@@ -28237,6 +28924,14 @@ function withResolutionLabel(row) {
28237
28924
  previouslyResolvedReason: resolvedReason
28238
28925
  };
28239
28926
  }
28927
+ /** Why a base image's finding has no Fix, in the page's words (`maintaining_base_wait_*`). */
28928
+ function baseImageWaitNote(fix) {
28929
+ if (fix.way === "wait_for_distro") return `${fix.distro} has no fix for this yet.`;
28930
+ if (fix.way === "end_of_life") return `${fix.distro} has reached its end of life, so upgrading this one package is no fix. It clears when the base moves to a supported release.`;
28931
+ const image = fix.images.join(", ") || "The base image";
28932
+ return `${fix.distro} has fixed this, but ${image} has not published a build with the fix yet. It clears when it does.`;
28933
+ }
28934
+ const remediableByOf = (row) => typeof row["remediableBy"] === "string" ? row["remediableBy"] : null;
28240
28935
  /** A finding as a tool answers it: the row fields, the scope, and whether Fix would take it. */
28241
28936
  function findingRows(findings, extra = []) {
28242
28937
  return pickFields(findings, [
@@ -28253,7 +28948,10 @@ const ADVISORY_KEPT = [
28253
28948
  "summary",
28254
28949
  "details",
28255
28950
  "severity",
28256
- "published"
28951
+ "severitySource",
28952
+ "cvssScore",
28953
+ "published",
28954
+ "baseImage"
28257
28955
  ];
28258
28956
  const ADVISORY_TEXT_LIMIT = 1500;
28259
28957
  /** The packages of a licence finding shown without full: true (PRO-3394); the rest are counted. */
@@ -28294,6 +28992,36 @@ const SWEEP_FIELDS = [
28294
28992
  "createdAt",
28295
28993
  "completedAt"
28296
28994
  ];
28995
+ /**
28996
+ * The images a sweep's shipped images build on that it could not read, and
28997
+ * why (PRO-4073): a base nobody read is no clean base. Only where the API
28998
+ * returns the list (null with the containerImages flag off, or from a sweep
28999
+ * that listed none).
29000
+ */
29001
+ const BASE_IMAGES_SHOWN = 10;
29002
+ function notReadBases(sweep) {
29003
+ const bases = sweep.baseImages ?? [];
29004
+ const notRead = bases.filter((b) => b.state === "not_read" || b.packages?.state === "not_read");
29005
+ const read = bases.flatMap((b) => b.packages?.state === "read" ? [{
29006
+ image: b.image,
29007
+ path: b.path,
29008
+ os: b.os ?? null,
29009
+ vulnerabilities: b.packages.vulnerabilities,
29010
+ removedByMoving: b.packages.candidate?.removes ?? 0,
29011
+ noDistroFix: b.packages.noDistroFix ?? null,
29012
+ movesTo: b.packages.candidate?.image ?? null
29013
+ }] : []);
29014
+ return {
29015
+ ...read.length > 0 ? {
29016
+ baseImages: read.slice(0, BASE_IMAGES_SHOWN),
29017
+ ...read.length > BASE_IMAGES_SHOWN ? { baseImagesCount: read.length } : {}
29018
+ } : {},
29019
+ ...notRead.length > 0 ? {
29020
+ baseImagesNotRead: notRead.slice(0, BASE_IMAGES_SHOWN),
29021
+ ...notRead.length > BASE_IMAGES_SHOWN ? { baseImagesNotReadCount: notRead.length } : {}
29022
+ } : {}
29023
+ };
29024
+ }
28297
29025
  /** A policy violation in a list: what it is, how bad, and what is being done about it. */
28298
29026
  const VIOLATION_FIELDS = [
28299
29027
  "id",
@@ -28350,11 +29078,51 @@ function standingSummary(standing) {
28350
29078
  resolvedViolations: resolved
28351
29079
  };
28352
29080
  }
29081
+ /**
29082
+ * How the backlog covers the controls that need a feature (PRO-3860), one row
29083
+ * each: the live initiatives that build it, or why it is deferred, and whether
29084
+ * nothing covers it. Empty before a backlog answered for any. It says what is
29085
+ * planned, never that a control is met.
29086
+ */
29087
+ function controlCoverageList(controls) {
29088
+ return (Array.isArray(controls) ? controls : []).flatMap((control) => {
29089
+ const coverage = control["coverage"];
29090
+ if (!coverage || typeof coverage !== "object") return [];
29091
+ const initiatives = Array.isArray(coverage["initiatives"]) ? coverage["initiatives"] : [];
29092
+ return [{
29093
+ controlId: control["controlId"],
29094
+ state: control["state"],
29095
+ planned: plannedFor(coverage),
29096
+ uncovered: coverage["uncovered"] === true,
29097
+ ...initiatives.length > 0 ? { initiatives: initiatives.map((item) => ({
29098
+ id: item["id"],
29099
+ title: item["title"]
29100
+ })) } : {},
29101
+ ...coverage["deferredReason"] ? { deferredReason: coverage["deferredReason"] } : {}
29102
+ }];
29103
+ });
29104
+ }
29105
+ /**
29106
+ * Whether the backlog plans a control: yes when a live initiative builds it, later when it deferred it with a
29107
+ * reason, no otherwise. A deferred control reads "planned later" rather than only "not assessed" (PRO-3860).
29108
+ */
29109
+ function plannedFor(coverage) {
29110
+ if (Array.isArray(coverage["initiatives"]) && coverage["initiatives"].length > 0) return "yes";
29111
+ return coverage["deferredReason"] ? "later" : "no";
29112
+ }
29113
+ /** The coverage, when the backlog answered for any control, for the default and full answers. */
29114
+ function controlCoveragePart(controls) {
29115
+ const coverage = controlCoverageList(controls);
29116
+ return coverage.length > 0 ? { controlCoverage: coverage } : {};
29117
+ }
28353
29118
  const findingId = idInput("finding", "taiga_list_findings gives it");
28354
29119
  /** Open findings by severity and kind, and the latest sweeps, as the first page of findings carries them. */
28355
29120
  async function sweepOverview(client, productId) {
28356
29121
  const [summary, sweeps] = await Promise.all([client.getMaintenanceSummary(productId), client.listMaintenanceSweeps(productId, { limit: 3 })]);
28357
- const latest = pickFields(sweeps.data, SWEEP_FIELDS);
29122
+ const latest = sweeps.data.map((sweep) => ({
29123
+ ...pickFields([sweep], SWEEP_FIELDS)[0],
29124
+ ...notReadBases(sweep)
29125
+ }));
28358
29126
  const { lastRun: _lastRun, lastRuns: _lastRuns, topFindings: _top, violatedControls: _controls, ...counts } = summary.data;
28359
29127
  return {
28360
29128
  ...counts,
@@ -28362,6 +29130,20 @@ async function sweepOverview(client, productId) {
28362
29130
  ...latest.length === 0 ? { note: "This product has never been swept: no findings is not an all-clear." } : {}
28363
29131
  };
28364
29132
  }
29133
+ /** The organization's licence read takes an id this long at most, as `organizationLicenseQuery` does. */
29134
+ const ORGANIZATION_LICENSE_MAX = 200;
29135
+ /**
29136
+ * Why a license is allowed in a product, in a license's words, as the web's
29137
+ * Allow dialog offers them (PRO-3727); each is stored as the ignore reason
29138
+ * beside it.
29139
+ */
29140
+ const LICENSE_ALLOW_REASON_TO_IGNORE = {
29141
+ accepted: "accepted_risk",
29142
+ not_handed_out: "not_reachable",
29143
+ no_alternative: "wont_fix",
29144
+ read_wrong: "false_positive"
29145
+ };
29146
+ const LICENSE_ALLOW_REASONS = Object.keys(LICENSE_ALLOW_REASON_TO_IGNORE);
28365
29147
  function registerMaintenanceTools(server, client) {
28366
29148
  if (client.hasCapability("tenant:settings")) server.registerTool("taiga_change_license", {
28367
29149
  title: "Approve or remove a license",
@@ -28449,10 +29231,18 @@ function registerMaintenanceTools(server, client) {
28449
29231
  }));
28450
29232
  server.registerTool("taiga_list_licenses", {
28451
29233
  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") }),
29234
+ 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.",
29235
+ inputSchema: object({
29236
+ 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"),
29237
+ productId: productId.optional().describe("With license: that license in this product only (taiga_list_products gives it)")
29238
+ }),
28454
29239
  annotations: READ_ONLY
28455
- }, (params) => runTool(async () => toolResult(params.license ? (await client.getOrganizationLicense(params.license)).data : (await client.getOrganizationLicenses()).data)));
29240
+ }, (params) => runTool(async () => {
29241
+ if (params.productId !== void 0 && params.license === void 0) return toolError("VALIDATION_ERROR", "Name the license to read in this product (license).");
29242
+ if (params.license === void 0) return toolResult((await client.getOrganizationLicenses()).data);
29243
+ 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.`);
29244
+ return toolResult(params.productId !== void 0 ? (await client.getProductLicense(params.productId, params.license)).data : (await client.getOrganizationLicense(params.license)).data);
29245
+ }));
28456
29246
  server.registerTool("taiga_get_sbom", {
28457
29247
  title: "Download a repository SBOM",
28458
29248
  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 +29309,8 @@ function registerMaintenanceTools(server, client) {
28519
29309
  const own = violations.filter((violation) => String(violation["controlId"]).toUpperCase() === wanted);
28520
29310
  return toolResult({
28521
29311
  ...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." } : {},
29312
+ ...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." } : {},
29313
+ ...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
29314
  ...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
29315
  ...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
29316
  violations: pickFields(own, VIOLATION_FIELDS),
@@ -28542,6 +29333,7 @@ function registerMaintenanceTools(server, client) {
28542
29333
  ...wording,
28543
29334
  ...standing,
28544
29335
  controls: listed,
29336
+ ...controlCoveragePart(controls),
28545
29337
  divergences: cutLongText(violations),
28546
29338
  resolvedDivergences: cutLongText(resolvedAll.slice(0, FULL_RESOLVED)),
28547
29339
  ...resolvedAll.length > FULL_RESOLVED ? { resolvedNote: `The first ${FULL_RESOLVED} of ${resolvedAll.length} resolved violations.` } : {}
@@ -28565,7 +29357,8 @@ function registerMaintenanceTools(server, client) {
28565
29357
  "findingsByTier",
28566
29358
  "plannedCount"
28567
29359
  ]),
28568
- violations: pickFields(violations, VIOLATION_FIELDS)
29360
+ violations: pickFields(violations, VIOLATION_FIELDS),
29361
+ ...controlCoveragePart(controls)
28569
29362
  });
28570
29363
  }));
28571
29364
  if (!client.hasCapability("products:write")) return;
@@ -28591,9 +29384,31 @@ function registerMaintenanceTools(server, client) {
28591
29384
  const finding = (await client.updateFinding(params.findingId, body)).data;
28592
29385
  return toolResult(findingRows([finding], ["ignoreNote"])[0]);
28593
29386
  }));
29387
+ server.registerTool("taiga_allow_license", {
29388
+ title: "Allow or reopen a license in a product",
29389
+ 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.",
29390
+ inputSchema: object({
29391
+ productId,
29392
+ 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"),
29393
+ allowed: boolean().describe("true allows the license in this product, false reopens its findings"),
29394
+ 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"),
29395
+ note: string$2().trim().max(280).optional().describe("A short justification, kept for audit"),
29396
+ 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)")
29397
+ }),
29398
+ annotations: OPERATE_SETTING
29399
+ }, (params) => runTool(async () => {
29400
+ if (!params.allowed) return toolResult((await client.reopenProductLicense(params.productId, params.license)).data);
29401
+ if (params.reason === void 0) return toolError("VALIDATION_ERROR", "Say why the license is allowed (reason).");
29402
+ return toolResult((await client.allowProductLicense(params.productId, {
29403
+ license: params.license,
29404
+ ignoreReason: LICENSE_ALLOW_REASON_TO_IGNORE[params.reason],
29405
+ ...params.note !== void 0 ? { ignoreNote: params.note } : {},
29406
+ ...params.expiresAt !== void 0 ? { ignoreExpiresAt: params.expiresAt } : {}
29407
+ })).data);
29408
+ }));
28594
29409
  server.registerTool("taiga_fix_finding", {
28595
29410
  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).",
29411
+ 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
29412
  inputSchema: object({
28598
29413
  productId,
28599
29414
  findingId,
@@ -28632,6 +29447,13 @@ function registerMaintenanceTools(server, client) {
28632
29447
  const body = { refuseIfFixedSinceAssessment: params.fixAgain !== true };
28633
29448
  return toolResult((params.controlId ? await client.remediatePolicyControl(params.productId, params.controlId, body) : await client.remediatePolicyViolation(params.productId, params.violationId, body)).data);
28634
29449
  }));
29450
+ if (!client.hasCapability("backlog:write")) return;
29451
+ server.registerTool("taiga_ship_notices", {
29452
+ title: "Ship open-source notices",
29453
+ 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.",
29454
+ inputSchema: object({ productId }),
29455
+ annotations: OPERATE
29456
+ }, (params) => runTool(async () => toolResult((await client.shipNotices(params.productId)).data)));
28635
29457
  }
28636
29458
  //#endregion
28637
29459
  //#region src/tools/sections.ts
@@ -28957,20 +29779,17 @@ async function setupReadiness(client, product, links) {
28957
29779
  const settings = product["settings"] ?? {};
28958
29780
  const importState = imported ? IMPORT_STATE[String(settings.importStatus)] ?? "not started" : void 0;
28959
29781
  const missing = blockers ? blockers.missingDocuments : null;
28960
- const openChecks = blockers ? blockers.openChecks.map((check) => check.criterion) : null;
28961
29782
  return {
28962
- ready: blockers ? missing.length === 0 && openChecks.length === 0 : null,
29783
+ ready: blockers ? missing.length === 0 : null,
28963
29784
  repository,
28964
29785
  ...imported ? { import: typeof settings.importStatus === "string" ? settings.importStatus : "pending" } : {},
28965
29786
  ...imported ? {} : { discoveryStep: full["discoveryStep"] ?? null },
28966
29787
  missingDocuments: missing,
28967
- openChecks,
28968
29788
  nextStep: nextStepFor({
28969
29789
  imported,
28970
29790
  importState,
28971
29791
  linked: repository?.linked ?? null,
28972
29792
  missing,
28973
- openChecks,
28974
29793
  product: full
28975
29794
  })
28976
29795
  };
@@ -28992,7 +29811,6 @@ function nextStepFor(state) {
28992
29811
  if (!state.missing) return setupNextStep(state.product);
28993
29812
  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
29813
  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
29814
  return `Every required document is published: ${finish}`;
28997
29815
  }
28998
29816
  //#endregion
@@ -30076,7 +30894,8 @@ const READY_BLOCKER_SAID = {
30076
30894
  bar_lowering: "a fix lowered a check instead of fixing the code",
30077
30895
  bar_lowering_unchecked: "Taiga could not check whether a fix lowered a check",
30078
30896
  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"
30897
+ clean_checkout_failed: "the branch does not install or pass its tests from a clean checkout",
30898
+ license_not_approved: "a package it adds has a licence that is not on your approved list"
30080
30899
  };
30081
30900
  /** The first sentence of a run's error, short enough for a row. */
30082
30901
  function errorSaid(error) {
@@ -30100,10 +30919,11 @@ function runOutcome(run) {
30100
30919
  const why = errorSaid(run["error"]) ?? (typeof run["failedReason"] === "string" ? run["failedReason"] : null);
30101
30920
  const handback = handbackSaid(run["reviewEscalation"]);
30102
30921
  const draft = run["prDraft"] === true ? `, still a draft` : "";
30922
+ const blocked = reasonOnOneLine(run["factoryBlockedBecause"]);
30103
30923
  return {
30104
30924
  disposition,
30105
30925
  outcome: {
30106
- in_flight: status === "pending" ? "Queued: waiting to start." : status === "replanning" ? steerPaused(run["failedReason"], progress) : `Running: ${RUNNING[status] ?? status}${progress}.`,
30926
+ in_flight: blocked ? `Blocked: the factory has stopped working on this run${progress}. ${blocked}` : status === "pending" ? "Queued: waiting to start." : status === "replanning" ? steerPaused(run["failedReason"], progress) : `Running: ${RUNNING[status] ?? status}${progress}.`,
30107
30927
  gated: handback ? `Handed back to a person: ${handback}. ${pr}${draft ? `${draft},` : ""} waits for them to ${handbackNextStep(run["reviewEscalation"], run["provider"])}.` : `Waiting for a person to review and merge ${pr}${draft}.`,
30108
30928
  succeeded: `Merged: ${pr} is in ${typeof run["baseBranch"] === "string" ? run["baseBranch"] : "the base branch"}.`,
30109
30929
  unmerged: typeof run["prNumber"] === "number" || run["prUrl"] ? `Settled without merging: ${pr} was left unmerged.` : "Settled without a merged pull request.",
@@ -30127,12 +30947,12 @@ function steerPaused(reason, progress) {
30127
30947
  */
30128
30948
  function replanHandedBack(reason, error, progress) {
30129
30949
  if (reason !== "replan_refused" && reason !== "replan_failed") return null;
30130
- const why = plannerReasonSaid(error);
30950
+ const why = reasonOnOneLine(error);
30131
30951
  if (reason === "replan_refused") return `Stopped${progress}, handed back: the planner would not plan the request for changes${why ? `: ${why}` : "."} Resume continues the original plan.`;
30132
30952
  return `Stopped${progress}, handed back: ${why ?? "the plan could not be revised."} Resume continues the original plan.`;
30133
30953
  }
30134
30954
  /** A hand-back's reason on one line, ending as a sentence, capped for a row. */
30135
- function plannerReasonSaid(error) {
30955
+ function reasonOnOneLine(error) {
30136
30956
  if (typeof error !== "string") return null;
30137
30957
  const text = error.replace(/\s+/g, " ").trim();
30138
30958
  if (!text) return null;
@@ -30218,7 +31038,7 @@ const FAILED_REASON_MEANS = {
30218
31038
  * text out: its first sentence is in the outcome, and taiga_get_run has it all.
30219
31039
  */
30220
31040
  function readableRun(run, opts = {}) {
30221
- const { prDraft, error, reviewEscalation, runtime: _runtime, origin, ...rest } = run;
31041
+ const { prDraft, error, reviewEscalation, runtime: _runtime, origin, factoryBlockedBecause: _inTheOutcome, ...rest } = run;
30222
31042
  const said = runOutcome(run);
30223
31043
  const means = FAILED_REASON_MEANS[String(run["failedReason"])];
30224
31044
  const hardening = run["hardeningResult"];
@@ -30583,6 +31403,7 @@ function registerBuildTools(server, client) {
30583
31403
  }
30584
31404
  /** What happened to a request that steers a run (PRO-3161), in one sentence. */
30585
31405
  function steerSituation(steer) {
31406
+ if (steer === "sent") return "The run takes it as an instruction while it keeps working.";
30586
31407
  if (steer === "replanning") return "The run has paused and its plan is being revised for the request.";
30587
31408
  if (steer === "after_step") return "The run stops when the step it is building ends, then its plan is revised.";
30588
31409
  if (steer === "after_round") return "The run is answering review comments or fixing CI; the request is planned when that round ends, and no further automatic round starts while it is open.";
@@ -30834,6 +31655,10 @@ function standing(item) {
30834
31655
  const life = item["lifecycle"];
30835
31656
  const run = life?.latestBuild;
30836
31657
  const situation = situationOf(item);
31658
+ const open = (life?.openPullRequests ?? []).flatMap((pull) => typeof pull.repositoryFullName === "string" && typeof pull.prNumber === "number" ? [{
31659
+ repository: pull.repositoryFullName,
31660
+ prNumber: pull.prNumber
31661
+ }] : []);
30837
31662
  return {
30838
31663
  column: typeof item["state"] === "string" ? item["state"] : null,
30839
31664
  turn: typeof life?.turn === "string" ? life.turn : null,
@@ -30841,7 +31666,8 @@ function standing(item) {
30841
31666
  latestRun: run && typeof run.id === "string" ? {
30842
31667
  id: run.id,
30843
31668
  ...typeof run.prNumber === "number" ? { prNumber: run.prNumber } : {}
30844
- } : null
31669
+ } : null,
31670
+ ...open.length > 1 ? { pullRequests: open } : {}
30845
31671
  };
30846
31672
  }
30847
31673
  /** The move each call to action is, in the tools' words. */
@@ -31024,7 +31850,8 @@ const PLAN_SUMMARY_FIELDS = [
31024
31850
  "backlogItemId",
31025
31851
  "version",
31026
31852
  "status",
31027
- "createdAt"
31853
+ "createdAt",
31854
+ "controls"
31028
31855
  ];
31029
31856
  const PLAN_STEP_SUMMARY_FIELDS = [
31030
31857
  "id",
@@ -31171,10 +31998,12 @@ function registerBacklogReads(server, client) {
31171
31998
  ...revised
31172
31999
  });
31173
32000
  const steps = Array.isArray(plan.steps) ? plan.steps : [];
32001
+ const several = (plan.repositories?.length ?? 0) > 1;
31174
32002
  return toolResult({
31175
32003
  ...pickFields([plan], PLAN_SUMMARY_FIELDS)[0],
32004
+ ...several ? { repositories: plan.repositories } : {},
31176
32005
  ...revised,
31177
- steps: pickFields(steps, PLAN_STEP_SUMMARY_FIELDS)
32006
+ steps: several ? pickFields(steps, [...PLAN_STEP_SUMMARY_FIELDS, "repositoryName"]) : pickFields(steps, PLAN_STEP_SUMMARY_FIELDS)
31178
32007
  });
31179
32008
  }));
31180
32009
  }
@@ -32130,7 +32959,7 @@ async function licenseReviews(client, productId, now) {
32130
32959
  packages: data["packages"],
32131
32960
  ...Number.isNaN(created) ? {} : { waitingDays: Math.floor((now - created) / DAY_MS) },
32132
32961
  readWith: "taiga_list_licenses (license)",
32133
- decideWith: "taiga_change_license (license, action: 'approve'); dismissing it is only in the web"
32962
+ 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
32963
  };
32135
32964
  }).filter((r) => productId === void 0 || r.productId === productId),
32136
32965
  truncated: result.truncated === true
@@ -48897,7 +49726,7 @@ function registerSetupTools(server, client) {
48897
49726
  }, (params) => runTool(async () => toolResult((await client.refreshRealityDocuments(params.productId, params.docTypes)).data)));
48898
49727
  server.registerTool("taiga_finish_discovery", {
48899
49728
  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.",
49729
+ 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
49730
  inputSchema: object({ productId }),
48902
49731
  annotations: OPERATE
48903
49732
  }, (params) => runTool(async () => {
@@ -49162,6 +49991,7 @@ const PAID_WORK = /* @__PURE__ */ new Set([
49162
49991
  "taiga_request_changes",
49163
49992
  "taiga_request_initiatives",
49164
49993
  "taiga_resume_initiative",
49994
+ "taiga_ship_notices",
49165
49995
  "taiga_retry_knowledge",
49166
49996
  "taiga_review_plan",
49167
49997
  "taiga_set_auto_build",