@nuvlore/extension-bds 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +3 -0
  3. package/agents/bds-airflow-job-status-readonly.yaml +123 -0
  4. package/agents/bds-disk-investigator.yaml +112 -0
  5. package/agents/bds-inventory-readonly.yaml +60 -0
  6. package/agents/bds-oncall-reporter.yaml +113 -0
  7. package/agents/bds-prism-service-proxy-timeout-readonly.yaml +105 -0
  8. package/agents/bds-scylla-tenant-bucket-readonly.yaml +104 -0
  9. package/agents/bds-tenant-data-operation-log-readonly.yaml +98 -0
  10. package/agents/bds-yarn-app-health-readonly.yaml +114 -0
  11. package/agents/pharos-alert-investigator.yaml +81 -0
  12. package/package.json +60 -0
  13. package/skills/bds-airflow-job-status-investigation/SKILL.md +104 -0
  14. package/skills/bds-airflow-job-status-investigation/references/airflow-access-runbook.md +35 -0
  15. package/skills/bds-airflow-job-status-investigation/references/airflow-bas-dr-runbook.md +42 -0
  16. package/skills/bds-airflow-job-status-investigation/references/airflow-copy-and-migration-runbook.md +63 -0
  17. package/skills/bds-airflow-job-status-investigation/references/airflow-job-status-runbook.md +15 -0
  18. package/skills/bds-airflow-job-status-investigation/references/airflow-output-runbook.md +28 -0
  19. package/skills/bds-airflow-job-status-investigation/references/airflow-snapshot-runbook.md +38 -0
  20. package/skills/bds-disk-io-alert-investigation/SKILL.md +116 -0
  21. package/skills/bds-hadoop-review/SKILL.md +66 -0
  22. package/skills/bds-machine-inventory/SKILL.md +48 -0
  23. package/skills/bds-oncall-followup/SKILL.md +71 -0
  24. package/skills/bds-oncall-followup/references/oncall-followup-runbook.md +58 -0
  25. package/skills/bds-prism-service-proxy-timeout-investigation/SKILL.md +73 -0
  26. package/skills/bds-prism-service-proxy-timeout-investigation/references/service-proxy-timeout-runbook.md +48 -0
  27. package/skills/bds-scylla-tenant-bucket-investigation/SKILL.md +73 -0
  28. package/skills/bds-scylla-tenant-bucket-investigation/references/tenant-bucket-runbook.md +91 -0
  29. package/skills/bds-tenant-data-operation-log-investigation/SKILL.md +89 -0
  30. package/skills/bds-yarn-app-health-investigation/SKILL.md +77 -0
  31. package/skills/bds-yarn-app-health-investigation/references/yarn-app-health-runbook.md +53 -0
  32. package/skills/pharos-alert-investigation/SKILL.md +80 -0
  33. package/src/bds-atomic-log-tools.mjs +300 -0
  34. package/src/bds-team-membership-claude-output.mjs +36 -0
  35. package/src/bds-team-membership-quality.mjs +90 -0
  36. package/tools/bds_airflow_dag_log_read.mjs +24 -0
  37. package/tools/bds_airflow_job_status_command_pack.mjs +587 -0
  38. package/tools/bds_bas_log_read.mjs +23 -0
  39. package/tools/bds_bdm_httpd_log_read.mjs +24 -0
  40. package/tools/bds_chef_client_log_read.mjs +23 -0
  41. package/tools/bds_dashboard_links.mjs +178 -0
  42. package/tools/bds_dashboard_query_knowledge.mjs +258 -0
  43. package/tools/bds_dashboard_query_read.mjs +120 -0
  44. package/tools/bds_disk_io_alert_command_pack.mjs +287 -0
  45. package/tools/bds_environment_host_lookup.mjs +741 -0
  46. package/tools/bds_hdfs_datanode_log_read.mjs +23 -0
  47. package/tools/bds_hdfs_journalnode_log_read.mjs +23 -0
  48. package/tools/bds_hdfs_namenode_log_read.mjs +23 -0
  49. package/tools/bds_hdfs_path_knowledge.mjs +324 -0
  50. package/tools/bds_information_source_knowledge.mjs +356 -0
  51. package/tools/bds_kms_log_read.mjs +23 -0
  52. package/tools/bds_log_path_knowledge.mjs +267 -0
  53. package/tools/bds_machine_inventory.mjs +434 -0
  54. package/tools/bds_mapreduce_historyserver_log_read.mjs +23 -0
  55. package/tools/bds_migration_service_operation_command_pack.mjs +233 -0
  56. package/tools/bds_oncall_followup_payload_draft.mjs +285 -0
  57. package/tools/bds_pharos_query_pack.mjs +218 -0
  58. package/tools/bds_portal_operation_knowledge.mjs +212 -0
  59. package/tools/bds_prism_clone_status_links.mjs +129 -0
  60. package/tools/bds_prism_service_proxy_timeout_query_pack.mjs +224 -0
  61. package/tools/bds_public_cloud_pod_log_read.mjs +18 -0
  62. package/tools/bds_scylla_tenant_bucket_command_pack.mjs +323 -0
  63. package/tools/bds_service_proxy_log_read.mjs +23 -0
  64. package/tools/bds_spark_historyserver_log_read.mjs +32 -0
  65. package/tools/bds_spas_mt_log_read.mjs +23 -0
  66. package/tools/bds_tenant_data_operation_log_command_pack.mjs +321 -0
  67. package/tools/bds_yarn_app_health_command_pack.mjs +529 -0
  68. package/tools/bds_yarn_nodemanager_log_read.mjs +23 -0
  69. package/tools/bds_yarn_resourcemanager_log_read.mjs +23 -0
  70. package/tools/bds_zookeeper_log_read.mjs +23 -0
  71. package/tools/confluence_handoff_page_draft.mjs +173 -0
  72. package/vendor/dashboard/src/grafana-dashboard-query.mjs +61 -0
  73. package/vendor/extension-read-only-toolkit/LICENSE +202 -0
  74. package/vendor/extension-read-only-toolkit/README.md +29 -0
  75. package/vendor/extension-read-only-toolkit/package.json +46 -0
  76. package/vendor/extension-read-only-toolkit/src/devops-data-paths.mjs +222 -0
  77. package/vendor/extension-read-only-toolkit/src/devops-diagnostics.mjs +440 -0
  78. package/vendor/extension-read-only-toolkit/src/enterprise-api-read.mjs +180 -0
  79. package/vendor/extension-read-only-toolkit/src/read-only-command-pack.mjs +1 -0
  80. package/vendor/extension-read-only-toolkit/src/read-only-plan.mjs +45 -0
  81. package/vendor/extension-read-only-toolkit/src/read-only-shell-policy.d.mts +26 -0
  82. package/vendor/extension-read-only-toolkit/src/read-only-shell-policy.mjs +170 -0
  83. package/vendor/extension-workday-infra-access/LICENSE +202 -0
  84. package/vendor/extension-workday-infra-access/README.md +3 -0
  85. package/vendor/extension-workday-infra-access/agents/workday-infra-access-readonly.yaml +183 -0
  86. package/vendor/extension-workday-infra-access/package.json +49 -0
  87. package/vendor/extension-workday-infra-access/skills/workday-infra-access/SKILL.md +102 -0
  88. package/vendor/extension-workday-infra-access/skills/workday-infra-access/references/access-boundary-runbook.md +43 -0
  89. package/vendor/extension-workday-infra-access/skills/workday-infra-access/references/access-runbook.md +15 -0
  90. package/vendor/extension-workday-infra-access/skills/workday-infra-access/references/access-scylla-cloud-runbook.md +39 -0
  91. package/vendor/extension-workday-infra-access/skills/workday-infra-access/references/access-spc-argo-runbook.md +99 -0
  92. package/vendor/extension-workday-infra-access/src/workday-access-profiles.mjs +194 -0
  93. package/vendor/extension-workday-infra-access/tools/aws_cust_federated_readonly_command_pack.mjs +234 -0
  94. package/vendor/extension-workday-infra-access/tools/gcp_readonly_command_pack.mjs +167 -0
  95. package/vendor/extension-workday-infra-access/tools/public_cloud_access_status.mjs +48 -0
  96. package/vendor/extension-workday-infra-access/tools/workday_boundary_login.mjs +460 -0
  97. package/vendor/extension-workday-infra-access/tools/workday_private_cloud_sudo_readonly_command.mjs +386 -0
  98. package/vendor/extension-workday-infra-access/tools/workday_public_cloud_environment_map.mjs +189 -0
  99. package/vendor/extension-workday-infra-access/tools/workday_public_cloud_kubernetes_read.mjs +297 -0
  100. package/vendor/extension-workday-infra-access/tools/workday_public_cloud_spc_login.mjs +505 -0
  101. package/vendor/extension-workday-infra-access/tools/workday_spc_argo_readonly_command_pack.mjs +215 -0
  102. package/vendor/extension-workday-infra-access/workflows/public-cloud-readonly-investigation.js +99 -0
  103. package/workflows/bds-airflow-job-status-investigation.js +168 -0
  104. package/workflows/bds-disk-io-alert-investigation.js +173 -0
  105. package/workflows/bds-oncall-followup.js +112 -0
  106. package/workflows/bds-prism-service-proxy-timeout-investigation.js +69 -0
  107. package/workflows/bds-scylla-tenant-bucket-investigation.js +139 -0
  108. package/workflows/bds-tenant-data-operation-log-investigation.js +189 -0
  109. package/workflows/bds-yarn-app-health-investigation.js +217 -0
@@ -0,0 +1,434 @@
1
+ export const description = "List BDS machine types or hostnames from the authoritative BDS config repositories for dev, production, and non-production environments.";
2
+
3
+ import { execFile } from "node:child_process";
4
+ import { promises as fs } from "node:fs";
5
+ import { basename, join } from "node:path";
6
+ import { promisify } from "node:util";
7
+ import { z } from "zod";
8
+ import { defineReadOnlyTool, fail, ok } from "@nuvlore/extension-read-only-toolkit/devops-diagnostics";
9
+ import { configuredDataPath, devopsCachePath } from "@nuvlore/extension-read-only-toolkit/devops-data-paths";
10
+
11
+ const execFileAsync = promisify(execFile);
12
+
13
+ const BDS_REPOS = {
14
+ dev: "ssh://git@bitbucket.workday.com:7999/bds/bds-config-dev.git",
15
+ production: "ssh://git@bitbucket.workday.com:7999/bds/bds-config-cust.git",
16
+ "non-production": "ssh://git@bitbucket.workday.com:7999/bds/bds-config-nprd.git"
17
+ };
18
+
19
+ const ENV_ALIASES = new Map([
20
+ ["dev", "dev"],
21
+ ["development", "dev"],
22
+ ["prod", "production"],
23
+ ["production", "production"],
24
+ ["cust", "production"],
25
+ ["customer", "production"],
26
+ ["nprd", "non-production"],
27
+ ["nonprod", "non-production"],
28
+ ["non-prod", "non-production"],
29
+ ["non-production", "non-production"],
30
+ ["implementation", "non-production"],
31
+ ["impl", "non-production"]
32
+ ]);
33
+
34
+ const SKIP_CLUSTER_BASENAMES = new Set(["bdm_bds_mapping", "bds_master_ldap"]);
35
+ const DEFAULT_MAX_HOSTS = 500;
36
+ const HOSTISH_PATTERN = /(^|[._-])(nodes?|hosts?|cnames?|servers?|masters?|workers?|managers?)($|[._-])/i;
37
+
38
+ function bdsConfigCacheRoot() {
39
+ return configuredDataPath(
40
+ "DEVOPS_BDS_CONFIG_CACHE_ROOT",
41
+ "cache.bdsConfig",
42
+ devopsCachePath("bds-config"),
43
+ );
44
+ }
45
+
46
+ function normalizeText(value) {
47
+ return typeof value === "string" ? value.trim() : "";
48
+ }
49
+
50
+ function assertStructuredHitlApproval(input, context) {
51
+ const approval = input.hitlApproval;
52
+ if (!approval || typeof approval !== "object") {
53
+ throw new Error(`${context} requires structured HITL approval with approved, approvedBy, action, target, reason, expectedImpact, and risk.`);
54
+ }
55
+
56
+ const missing = ["approvedBy", "action", "target", "reason", "expectedImpact", "risk"]
57
+ .filter((key) => !normalizeText(approval[key]));
58
+ if (approval.approved !== true || missing.length > 0) {
59
+ throw new Error(`${context} requires approved=true and complete HITL details: ${missing.join(", ") || "approved"}.`);
60
+ }
61
+ }
62
+
63
+ function normalizeEnvironment(env) {
64
+ const normalized = normalizeText(env).toLowerCase();
65
+ const resolved = ENV_ALIASES.get(normalized);
66
+ if (!resolved) {
67
+ throw new Error(`Unsupported BDS environment: ${env}. Use dev, production, or non-production.`);
68
+ }
69
+ return resolved;
70
+ }
71
+
72
+ function repoUrlForEnvironment(env) {
73
+ return BDS_REPOS[normalizeEnvironment(env)];
74
+ }
75
+
76
+ function repoDirForEnvironment(cacheRoot, env) {
77
+ return join(cacheRoot, basename(repoUrlForEnvironment(env), ".git"));
78
+ }
79
+
80
+ async function pathExists(path) {
81
+ try {
82
+ await fs.access(path);
83
+ return true;
84
+ } catch (_error) {
85
+ return false;
86
+ }
87
+ }
88
+
89
+ async function ensureRepo(env, input, execFileImpl = execFileAsync) {
90
+ const explicitRepoPath = normalizeText(input.repoPath);
91
+ if (explicitRepoPath) {
92
+ if (!(await pathExists(join(explicitRepoPath, ".git")))) {
93
+ throw new Error(`repoPath must be a git checkout so bds_machine_inventory can pull latest code before lookup: ${explicitRepoPath}`);
94
+ }
95
+ await pullLatest(explicitRepoPath, input, execFileImpl);
96
+ return explicitRepoPath;
97
+ }
98
+
99
+ const environment = normalizeEnvironment(env);
100
+ const cacheRoot = normalizeText(input.cacheRoot) || bdsConfigCacheRoot();
101
+ const repoPath = repoDirForEnvironment(cacheRoot, environment);
102
+ const repoUrl = repoUrlForEnvironment(environment);
103
+
104
+ if (await pathExists(join(repoPath, ".git"))) {
105
+ await pullLatest(repoPath, input, execFileImpl);
106
+ return repoPath;
107
+ }
108
+
109
+ if (!input.allowCacheUpdate) {
110
+ throw new Error(`Local BDS config cache is missing at ${repoPath}. Provide repoPath for an existing checkout or set allowCacheUpdate=true to explicitly clone the read-only config repo.`);
111
+ }
112
+ assertStructuredHitlApproval(input, "BDS config cache clone");
113
+ await fs.mkdir(cacheRoot, { recursive: true });
114
+ await execFileImpl("git", ["clone", "--depth", "1", repoUrl, repoPath], {
115
+ timeout: input.timeoutMs,
116
+ maxBuffer: 1024 * 1024 * 10
117
+ });
118
+ return repoPath;
119
+ }
120
+
121
+ async function pullLatest(repoPath, input, execFileImpl = execFileAsync) {
122
+ await execFileImpl("git", ["-C", repoPath, "pull", "--ff-only"], {
123
+ timeout: input.timeoutMs,
124
+ maxBuffer: 1024 * 1024 * 10
125
+ });
126
+ }
127
+
128
+ async function listJsonFiles(root) {
129
+ const files = [];
130
+ if (!(await pathExists(root))) {
131
+ return files;
132
+ }
133
+ const entries = await fs.readdir(root, { withFileTypes: true });
134
+ for (const entry of entries) {
135
+ const path = join(root, entry.name);
136
+ if (entry.isDirectory()) {
137
+ files.push(...await listJsonFiles(path));
138
+ } else if (entry.isFile() && entry.name.endsWith(".json")) {
139
+ files.push(path);
140
+ }
141
+ }
142
+ return files;
143
+ }
144
+
145
+ async function loadClusterDocuments(repoPath) {
146
+ const files = [
147
+ ...await listJsonFiles(join(repoPath, "data_bags", "bds")),
148
+ ...await listJsonFiles(join(repoPath, "config", "bds"))
149
+ ];
150
+ const docs = [];
151
+ for (const filePath of files.sort()) {
152
+ const cluster = basename(filePath, ".json");
153
+ if (SKIP_CLUSTER_BASENAMES.has(cluster)) {
154
+ continue;
155
+ }
156
+ try {
157
+ const parsed = JSON.parse(await fs.readFile(filePath, "utf8"));
158
+ docs.push({ cluster, filePath, document: parsed });
159
+ } catch (error) {
160
+ docs.push({ cluster, filePath, parseError: error instanceof Error ? error.message : String(error) });
161
+ }
162
+ }
163
+ return docs;
164
+ }
165
+
166
+ function walk(value, onLeaf, path = []) {
167
+ if (Array.isArray(value)) {
168
+ onLeaf(path, value);
169
+ return;
170
+ }
171
+ if (value && typeof value === "object") {
172
+ for (const [key, child] of Object.entries(value)) {
173
+ walk(child, onLeaf, [...path, key]);
174
+ }
175
+ return;
176
+ }
177
+ onLeaf(path, value);
178
+ }
179
+
180
+ function getPathValue(value, dotPath) {
181
+ return dotPath.split(".").filter(Boolean).reduce((current, part) => {
182
+ if (current === undefined || current === null) {
183
+ return undefined;
184
+ }
185
+ return current[part];
186
+ }, value);
187
+ }
188
+
189
+ function asStringList(value) {
190
+ if (Array.isArray(value)) {
191
+ return value.flatMap((item) => typeof item === "string" ? [item.trim()] : []).filter(Boolean);
192
+ }
193
+ if (typeof value === "string") {
194
+ return value.split(/[,\s]+/).map((item) => item.trim()).filter(Boolean);
195
+ }
196
+ return [];
197
+ }
198
+
199
+ function pathLooksHostLike(dotPath, value) {
200
+ if (!dotPath.startsWith("bds.")) {
201
+ return false;
202
+ }
203
+ if (dotPath.includes(".cluster_spec.")) {
204
+ return false;
205
+ }
206
+ if (!HOSTISH_PATTERN.test(dotPath)) {
207
+ return false;
208
+ }
209
+ return asStringList(value).length > 0;
210
+ }
211
+
212
+ function typeFromPath(dotPath) {
213
+ const last = dotPath.split(".").at(-1) || dotPath;
214
+ return last
215
+ .replace(/_(nodes?|hosts?|servers?)_cnames?$/i, "")
216
+ .replace(/-(nodes?|hosts?|servers?)-cnames?$/i, "")
217
+ .replace(/_(nodes?|hosts?|cnames?|servers?)$/i, "")
218
+ .replace(/-(nodes?|hosts?|cnames?|servers?)$/i, "")
219
+ .replace(/(Nodes?|Hosts?|Cnames?|Servers?)$/g, "")
220
+ .replace(/[^a-zA-Z0-9]+/g, "_")
221
+ .replace(/^_+|_+$/g, "")
222
+ .toLowerCase() || last.toLowerCase();
223
+ }
224
+
225
+ function candidatePathsForMachineType(machineType) {
226
+ const raw = normalizeText(machineType);
227
+ if (!raw) return [];
228
+ const normalized = raw.replace(/[^a-zA-Z0-9]+/g, "_").replace(/^_+|_+$/g, "").toLowerCase();
229
+ return Array.from(new Set([
230
+ raw,
231
+ `bds.${raw}`,
232
+ `bds.${normalized}`,
233
+ `bds.${normalized}_nodes`,
234
+ `bds.${normalized}_hosts`,
235
+ `bds.${normalized}_cnames`,
236
+ `bds.${normalized}_servers`
237
+ ]));
238
+ }
239
+
240
+ function matchesCluster(cluster, filter) {
241
+ const raw = normalizeText(filter);
242
+ return !raw || cluster.toLowerCase().includes(raw.toLowerCase());
243
+ }
244
+
245
+ function collectMachineTypes(docs, clusterFilter = "") {
246
+ const types = new Map();
247
+ const parseErrors = [];
248
+ for (const doc of docs) {
249
+ if (doc.parseError) {
250
+ parseErrors.push({ cluster: doc.cluster, filePath: doc.filePath, error: doc.parseError });
251
+ continue;
252
+ }
253
+ if (!matchesCluster(doc.cluster, clusterFilter)) {
254
+ continue;
255
+ }
256
+ walk(doc.document, (path, value) => {
257
+ const dotPath = path.join(".");
258
+ if (!pathLooksHostLike(dotPath, value)) {
259
+ return;
260
+ }
261
+ const type = typeFromPath(dotPath);
262
+ const entry = types.get(type) ?? { type, paths: new Set(), clusters: new Set(), hostCount: 0 };
263
+ entry.paths.add(dotPath);
264
+ entry.clusters.add(doc.cluster);
265
+ entry.hostCount += asStringList(value).length;
266
+ types.set(type, entry);
267
+ });
268
+ }
269
+ return {
270
+ machineTypes: [...types.values()].map((entry) => ({
271
+ type: entry.type,
272
+ paths: [...entry.paths].sort(),
273
+ clusterCount: entry.clusters.size,
274
+ hostCount: entry.hostCount
275
+ })).sort((left, right) => left.type.localeCompare(right.type)),
276
+ parseErrors
277
+ };
278
+ }
279
+
280
+ function collectHosts(docs, input) {
281
+ const hostField = normalizeText(input.hostField);
282
+ const machineType = normalizeText(input.machineType);
283
+ const candidatePaths = hostField ? [hostField, `bds.${hostField}`] : candidatePathsForMachineType(machineType);
284
+ const maxHosts = input.maxHosts;
285
+ const hosts = [];
286
+ const matchedPaths = new Set();
287
+ const parseErrors = [];
288
+ let truncated = false;
289
+
290
+ for (const doc of docs) {
291
+ if (doc.parseError) {
292
+ parseErrors.push({ cluster: doc.cluster, filePath: doc.filePath, error: doc.parseError });
293
+ continue;
294
+ }
295
+ if (!matchesCluster(doc.cluster, input.cluster)) {
296
+ continue;
297
+ }
298
+
299
+ const addHosts = (dotPath, values) => {
300
+ if (values.length === 0) return;
301
+ matchedPaths.add(dotPath);
302
+ for (const host of values) {
303
+ if (hosts.length >= maxHosts) {
304
+ truncated = true;
305
+ return;
306
+ }
307
+ hosts.push({ cluster: doc.cluster, type: typeFromPath(dotPath), path: dotPath, host });
308
+ }
309
+ };
310
+
311
+ if (candidatePaths.length > 0) {
312
+ for (const dotPath of candidatePaths) {
313
+ addHosts(dotPath, asStringList(getPathValue(doc.document, dotPath)));
314
+ }
315
+ continue;
316
+ }
317
+
318
+ walk(doc.document, (path, value) => {
319
+ if (truncated) return;
320
+ const dotPath = path.join(".");
321
+ if (pathLooksHostLike(dotPath, value)) {
322
+ addHosts(dotPath, asStringList(value));
323
+ }
324
+ });
325
+ }
326
+
327
+ return {
328
+ hosts,
329
+ matchedPaths: [...matchedPaths].sort(),
330
+ truncated,
331
+ parseErrors
332
+ };
333
+ }
334
+
335
+ export class BdsMachineInventoryService {
336
+ constructor(options = {}) {
337
+ this.execFileImpl = options.execFileImpl || execFileAsync;
338
+ this.loadClusterDocuments = options.loadClusterDocuments || loadClusterDocuments;
339
+ }
340
+
341
+ async invoke(input) {
342
+ const environment = normalizeEnvironment(input.environment);
343
+ const repoPath = await ensureRepo(environment, input, this.execFileImpl);
344
+ const docs = await this.loadClusterDocuments(repoPath);
345
+ const base = {
346
+ action: input.action,
347
+ environment,
348
+ repoPath,
349
+ clustersScanned: docs.filter((doc) => !doc.parseError && matchesCluster(doc.cluster, input.cluster)).length
350
+ };
351
+
352
+ if (input.action === "list_types") {
353
+ return JSON.stringify({
354
+ ...base,
355
+ ...collectMachineTypes(docs, input.cluster)
356
+ }, null, 2);
357
+ }
358
+
359
+ return JSON.stringify({
360
+ ...base,
361
+ machineType: normalizeText(input.machineType),
362
+ hostField: normalizeText(input.hostField),
363
+ ...collectHosts(docs, input)
364
+ }, null, 2);
365
+ }
366
+ }
367
+
368
+ async function bdsMachineInventory(input, options = {}) {
369
+ return new BdsMachineInventoryService(options).invoke(input);
370
+ }
371
+
372
+ export const __test = {
373
+ normalizeEnvironment,
374
+ repoUrlForEnvironment,
375
+ repoDirForEnvironment,
376
+ asStringList,
377
+ typeFromPath,
378
+ candidatePathsForMachineType,
379
+ collectMachineTypes,
380
+ collectHosts,
381
+ pullLatest,
382
+ loadClusterDocuments,
383
+ bdsMachineInventory,
384
+ BdsMachineInventoryService
385
+ };
386
+
387
+ const defaultService = new BdsMachineInventoryService();
388
+
389
+ const sharedTool = defineReadOnlyTool({
390
+ name: "bds_machine_inventory",
391
+ description: "List BDS machine types or hostnames from the authoritative BDS config repositories for dev, production, and non-production environments.",
392
+ meta: {
393
+ permissions: { rule: "ask" }
394
+ },
395
+ inputSchema: {
396
+ action: z.enum(["list_types", "list_hosts"]).default("list_types"),
397
+ environment: z.string().trim().min(1).max(64).default("non-production"),
398
+ cluster: z.string().trim().max(256).default(""),
399
+ machineType: z.string().trim().max(256).default(""),
400
+ hostField: z.string().trim().max(512).default(""),
401
+ repoPath: z.string().trim().max(4096).default(""),
402
+ cacheRoot: z.string().trim().max(4096).default(bdsConfigCacheRoot()),
403
+ refresh: z.boolean().default(true).describe("Deprecated: bds_machine_inventory always pulls latest code before lookup."),
404
+ allowCacheUpdate: z.boolean().default(false).describe("Set true only after human-in-the-loop approval to create the local read-only BDS config cache when it is missing."),
405
+ hitlApproval: z.object({
406
+ approved: z.literal(true),
407
+ approvedBy: z.string().trim().min(1).max(256),
408
+ action: z.string().trim().min(1).max(256),
409
+ target: z.string().trim().min(1).max(4096),
410
+ reason: z.string().trim().min(1).max(2000),
411
+ expectedImpact: z.string().trim().min(1).max(2000),
412
+ risk: z.string().trim().min(1).max(2000)
413
+ }).optional().describe("Required when allowCacheUpdate=true because that creates a local cache."),
414
+ maxHosts: z.number().int().positive().max(5000).default(DEFAULT_MAX_HOSTS),
415
+ timeoutMs: z.number().int().positive().max(600_000).default(120_000)
416
+ },
417
+ async handler(input) {
418
+ try {
419
+ return ok(await defaultService.invoke(input));
420
+ } catch (error) {
421
+ return fail(error);
422
+ }
423
+ }
424
+ });
425
+
426
+ export default {
427
+ name: sharedTool.name,
428
+ description,
429
+ inputSchema: sharedTool.inputSchema,
430
+ meta: sharedTool.meta,
431
+ annotations: sharedTool.annotations,
432
+ invoke: sharedTool.invoke,
433
+ handler: sharedTool.handler
434
+ };
@@ -0,0 +1,23 @@
1
+ export const description = "Read MapReduce history server logs from a private-cloud history server host through the approved Boundary sudo read tool.";
2
+
3
+ import { createPrivateCloudLogReadTool, globPrivateLogSpec } from "../src/bds-atomic-log-tools.mjs";
4
+
5
+ const spec = globPrivateLogSpec({
6
+ toolName: "bds_mapreduce_historyserver_log_read",
7
+ description: "Read MapReduce history server logs from a private-cloud history server host through the approved Boundary sudo read tool.",
8
+ glob: "/var/log/hadoop-mapreduce/mapred-mapred-historyserver-*.log /var/log/hadoop-mapreduce/mapred-mapred-historyserver-*.out"
9
+ });
10
+
11
+ export const __test = spec;
12
+
13
+ const sharedTool = createPrivateCloudLogReadTool(spec);
14
+
15
+ export default {
16
+ name: sharedTool.name,
17
+ description,
18
+ inputSchema: sharedTool.inputSchema,
19
+ meta: sharedTool.meta,
20
+ annotations: sharedTool.annotations,
21
+ invoke: sharedTool.invoke,
22
+ handler: sharedTool.handler
23
+ };
@@ -0,0 +1,233 @@
1
+ export const description = "Generate a atomic-helper, read-only BDS migration-service command/API pack from Slack event facts, Jira links, HDFS tenant records, cutover state, and optional S3 VPCE probes; does not call Jira, SSH, or inspect HDFS.";
2
+
3
+ import { z } from "zod";
4
+ import { defineReadOnlyTool, fail, ok } from "@nuvlore/extension-read-only-toolkit/devops-diagnostics";
5
+
6
+ const identifierSchema = z.string().trim().min(1).max(255).regex(/^[A-Za-z0-9_.:@+=,~%/-]+$/).optional();
7
+ const tenantSchema = z.string().trim().min(1).max(255).regex(/^[A-Za-z0-9_.:-]+$/).optional();
8
+ const hostSchema = z.string().trim().min(1).max(255).regex(/^(?!-)[a-zA-Z0-9._:@%-]+$/);
9
+ const hdfsPathSchema = z.string().trim().min(1).max(2048).regex(/^hdfs:\/\/[A-Za-z0-9_.:-]+\/.+$/).optional();
10
+ const urlSchema = z.string().trim().url().max(2048).optional();
11
+ const migrationRunTypeSchema = z.enum(["FIRST_MIGRATION", "DIFF_MIGRATION", "CUTOVER_MIGRATION", "BUCKET_SYNC"]).optional();
12
+
13
+ const SAFETY_RULES = [
14
+ "read-only only: use Slack facts, Jira reads, Confluence reads, Airflow callback evidence, HDFS tenant record reads, Kubernetes metadata, DNS/route lookup, and bounded curl connect probes",
15
+ "do not cancel, retry, trigger, or mutate migration-service runs from this command pack",
16
+ "do not run Airflow clear/backfill/trigger/mark_success or any DAG/task state mutation",
17
+ "do not write HDFS, S3, Kubernetes, or local files; in particular do not use aws s3 cp as a connectivity test",
18
+ "derive HDFS authority from evidence instead of guessing or shortening cluster names",
19
+ "treat Slack thread as event source and Jira as tracking/follow-up evidence"
20
+ ];
21
+
22
+ const INTERPRETATION_RULES = [
23
+ "FIRST_MIGRATION, DIFF_MIGRATION, CUTOVER_MIGRATION, bucket sync, PRISM_READY, and event COMPLETED are migration-service state-chain evidence.",
24
+ "Only cutover run allowed ... Discarding message for DIFF_MIGRATION during cutover is state-machine evidence, not an Airflow trigger failure.",
25
+ "A recovery is not complete from copy success alone; verify source/target file counts, bucket sync, PRISM_READY, and event COMPLETED when applicable.",
26
+ "If a main Jira exists, read issuelinks and use a linked monitoring/product follow-up Jira for on-call report follow-up when present.",
27
+ "For S3 endpoint failures, http=000 plus connect timeout means the request did not reach HTTP/S3 authorization; HTTP 307 or another HTTP status from an alternate endpoint proves that endpoint path is reachable."
28
+ ];
29
+
30
+ function shellQuote(value) {
31
+ return `'${String(value).replaceAll("'", "'\\''")}'`;
32
+ }
33
+
34
+ function summaryLines(input) {
35
+ return [
36
+ input.slackThreadUrl ? `slackThreadUrl=${input.slackThreadUrl}` : "",
37
+ input.migrationEventId ? `migrationEventId=${input.migrationEventId}` : "",
38
+ input.sourceCluster ? `sourceCluster=${input.sourceCluster}` : "",
39
+ input.sourceEnvironment ? `sourceEnvironment=${input.sourceEnvironment}` : "",
40
+ input.targetEnvironment ? `targetEnvironment=${input.targetEnvironment}` : "",
41
+ input.sourceTenant ? `sourceTenant=${input.sourceTenant}` : "",
42
+ input.targetTenant ? `targetTenant=${input.targetTenant}` : "",
43
+ input.migrationRunType ? `migrationRunType=${input.migrationRunType}` : "",
44
+ input.snapshotId ? `snapshotId=${input.snapshotId}` : "",
45
+ input.fromSnapshotId ? `fromSnapshotId=${input.fromSnapshotId}` : "",
46
+ input.dagRunId ? `dagRunId=${input.dagRunId}` : "",
47
+ input.cutoverDateUtc ? `cutoverDateUtc=${input.cutoverDateUtc}` : "",
48
+ input.tenantHdfsPath ? `tenantHdfsPath=${input.tenantHdfsPath}` : "",
49
+ input.mainJira ? `mainJira=${input.mainJira}` : "",
50
+ input.followUpJira ? `followUpJira=${input.followUpJira}` : ""
51
+ ].filter(Boolean);
52
+ }
53
+
54
+ function tenantRecordScript(input) {
55
+ const path = input.tenantHdfsPath || "hdfs://<cluster-authority>/data/tenants/<ENV>.<tenant>";
56
+ return [
57
+ "set -eu",
58
+ `tenant_path=${shellQuote(path)}`,
59
+ "echo TENANT_PATH \"$tenant_path\"",
60
+ "hdfs dfs -ls \"$tenant_path\"",
61
+ "hdfs dfs -du -h \"$tenant_path\"",
62
+ "for f in operationsLog.csv s3MigrationCopyRecords.txt s3MigrationDagRunRecords.txt s3MigrationVerificationRecords.txt; do",
63
+ " echo FILE \"$f\"",
64
+ " hdfs dfs -test -e \"$tenant_path/$f\" && hdfs dfs -tail \"$tenant_path/$f\" || true",
65
+ "done"
66
+ ].join("\n");
67
+ }
68
+
69
+ function endpointScript(input) {
70
+ const endpoints = [...new Set([...(input.s3VpceEndpointUrls || []), ...(input.alternateS3VpceEndpointUrls || [])])];
71
+ if (!endpoints.length) return "";
72
+ return [
73
+ "set -eu",
74
+ ...endpoints.flatMap((endpoint) => {
75
+ const host = new URL(endpoint).hostname;
76
+ return [
77
+ `echo ENDPOINT ${shellQuote(endpoint)}`,
78
+ `getent hosts ${shellQuote(host)}`,
79
+ `getent hosts ${shellQuote(host)} | awk '{print $1}' | while read -r ip; do ip route get "$ip"; done`,
80
+ `curl -k -sS -o /dev/null --connect-timeout 10 --max-time 15 -w 'host=%{local_ip} remote=%{remote_ip} connect=%{time_connect} http=%{http_code} err=%{errormsg}\\n' ${shellQuote(endpoint)}`
81
+ ];
82
+ })
83
+ ].join("\n");
84
+ }
85
+
86
+ function jiraReadPath(jira) {
87
+ const key = String(jira || "").match(/[A-Z][A-Z0-9]+-\d+/u)?.[0] || "<main-jira-key>";
88
+ return `/rest/api/2/issue/${key}?fields=summary,status,issuelinks,subtasks,labels,components`;
89
+ }
90
+
91
+ function buildCommandPack(input = {}) {
92
+ const commands = [
93
+ {
94
+ id: "01-slack-event-facts",
95
+ purpose: "Extract the migration-service incident source of truth from the Slack thread.",
96
+ command: "Read the Slack thread and capture migrationEventId, source cluster, source/target environments, tenant, run type, snapshot id, dagRunId, failure timestamp, cutover window, main Jira, and follow-up Jira candidates.",
97
+ expectedEvidence: "Slack thread URL and concrete migration identifiers"
98
+ },
99
+ {
100
+ id: "02-main-jira-links",
101
+ purpose: "Read main Jira relationship fields before choosing the follow-up Jira.",
102
+ command: jiraReadPath(input.mainJira),
103
+ expectedEvidence: "summary, status, issuelinks, subtasks, labels, components; linked monitoring/product follow-up ticket when present"
104
+ },
105
+ {
106
+ id: "03-tenant-migration-records",
107
+ purpose: "Read tenant record files to establish migration state, copy history, dag run history, and verification counts.",
108
+ command: tenantRecordScript(input),
109
+ expectedEvidence: "last successful/failed copy records, dag run records, verification source/target file counts, cutover success, bucket sync, PRISM_READY, event COMPLETED"
110
+ },
111
+ {
112
+ id: "04-cutover-state-machine",
113
+ purpose: "Classify cutover-window retry behavior without mutating migration-service state.",
114
+ command: "Look for messages such as 'Only cutover run allowed ... Discarding message', CUTOVER_MIGRATION success, bucket sync completion, PRISM_READY, and event COMPLETED.",
115
+ expectedEvidence: "whether retry behavior matches migration-service state machine and whether recovery completed"
116
+ }
117
+ ];
118
+ const endpoint = endpointScript(input);
119
+ if (endpoint) {
120
+ commands.push({
121
+ id: "05-s3-vpce-readonly-comparison",
122
+ purpose: "Compare failing and alternate S3 VPCE endpoint paths from the same worker/source IP.",
123
+ command: endpoint,
124
+ expectedEvidence: "DNS answers, route gateway/device, source IP, remote IP, time_connect, http_code, errormsg"
125
+ });
126
+ }
127
+ return {
128
+ mode: "atomic-helper",
129
+ remoteWrite: "never",
130
+ summary: summaryLines(input),
131
+ workerHosts: input.workerHosts || [],
132
+ safetyRules: SAFETY_RULES,
133
+ commands
134
+ };
135
+ }
136
+
137
+ function renderMarkdown(result) {
138
+ return [
139
+ "# BDS Migration-Service Operation Read-Only Command Pack",
140
+ "",
141
+ "## Summary",
142
+ "",
143
+ ...(result.summary.length ? result.summary.map((line) => `- ${line}`) : ["- <fill from Slack thread>"]),
144
+ "",
145
+ "## Safety Boundary",
146
+ "",
147
+ ...result.safetyRules.map((rule) => `- ${rule}`),
148
+ "",
149
+ "## Read-Only Commands",
150
+ "",
151
+ ...result.commands.flatMap((command) => [
152
+ `### ${command.id}`,
153
+ `Purpose: ${command.purpose}`,
154
+ "",
155
+ "```bash",
156
+ command.command,
157
+ "```",
158
+ "",
159
+ `Evidence to capture: ${command.expectedEvidence}`,
160
+ ""
161
+ ]),
162
+ "Procedure and interpretation live in the `bds-airflow-job-status-investigation` skill runbook."
163
+ ].join("\n");
164
+ }
165
+
166
+ const inputSchema = {
167
+ slackThreadUrl: urlSchema,
168
+ migrationEventId: identifierSchema,
169
+ sourceCluster: identifierSchema,
170
+ sourceEnvironment: identifierSchema,
171
+ targetEnvironment: identifierSchema,
172
+ sourceTenant: tenantSchema,
173
+ targetTenant: tenantSchema,
174
+ migrationRunType: migrationRunTypeSchema,
175
+ snapshotId: identifierSchema,
176
+ fromSnapshotId: identifierSchema,
177
+ dagRunId: identifierSchema,
178
+ cutoverDateUtc: z.string().trim().datetime().optional(),
179
+ tenantHdfsPath: hdfsPathSchema,
180
+ mainJira: identifierSchema,
181
+ followUpJira: identifierSchema,
182
+ workerHosts: z.array(hostSchema).max(100).default([]),
183
+ s3VpceEndpointUrls: z.array(z.string().trim().url().regex(/^https:\/\/[^/]+\.s3\.[A-Za-z0-9-]+\.vpce\.amazonaws\.com\/?$/)).max(8).default([]),
184
+ alternateS3VpceEndpointUrls: z.array(z.string().trim().url().regex(/^https:\/\/[^/]+\.s3\.[A-Za-z0-9-]+\.vpce\.amazonaws\.com\/?$/)).max(8).default([]),
185
+ outputFormat: z.enum(["markdown", "json"]).default("markdown")
186
+ };
187
+
188
+ const schema = z.object(inputSchema);
189
+
190
+ export function bdsMigrationServiceOperationPlan(rawInput = {}) {
191
+ const input = schema.parse(rawInput);
192
+ const result = buildCommandPack(input);
193
+ return input.outputFormat === "json" ? JSON.stringify(result, null, 2) : renderMarkdown(result);
194
+ }
195
+
196
+ export class BdsMigrationServiceOperationCommandPackService {
197
+ invoke(rawInput = {}) {
198
+ return bdsMigrationServiceOperationPlan(rawInput);
199
+ }
200
+ }
201
+
202
+ const defaultService = new BdsMigrationServiceOperationCommandPackService();
203
+
204
+ export const __test = {
205
+ BdsMigrationServiceOperationCommandPackService,
206
+ bdsMigrationServiceOperationPlan,
207
+ endpointScript,
208
+ jiraReadPath,
209
+ tenantRecordScript
210
+ };
211
+
212
+ const sharedTool = defineReadOnlyTool({
213
+ name: "bds_migration_service_operation_command_pack",
214
+ description: "Generate a atomic-helper, read-only BDS migration-service command/API pack from Slack event facts, Jira links, HDFS tenant records, cutover state, and optional S3 VPCE probes; does not call Jira, SSH, or inspect HDFS.",
215
+ inputSchema,
216
+ async handler(input) {
217
+ try {
218
+ return ok(defaultService.invoke(input));
219
+ } catch (error) {
220
+ return fail(error);
221
+ }
222
+ }
223
+ });
224
+
225
+ export default {
226
+ name: sharedTool.name,
227
+ description,
228
+ inputSchema: sharedTool.inputSchema,
229
+ meta: sharedTool.meta,
230
+ annotations: sharedTool.annotations,
231
+ invoke: sharedTool.invoke,
232
+ handler: sharedTool.handler
233
+ };