@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,587 @@
1
+ export const description = "Generate a atomic-helper, read-only BDS Airflow command pack from environment Kubernetes control-plane JSON, airflow namespace checks, and S3 VPCE probes; does not SSH, run kubectl, or inspect Airflow.";
2
+
3
+ import { readFile } from "node:fs/promises";
4
+ import { z } from "zod";
5
+ import { defineReadOnlyTool, fail, ok } from "@nuvlore/extension-read-only-toolkit/devops-diagnostics";
6
+ import { ReadOnlyStep, remoteCommandInput, shellQuote } from "@nuvlore/extension-read-only-toolkit/read-only-command-pack";
7
+ import {
8
+ DEFAULT_BOUNDARY_BINARY_DIR,
9
+ DEFAULT_BOUNDARY_PROXY_JUMP_HOST,
10
+ DEFAULT_DC_BOUNDARY_ADDR,
11
+ DcLoginProfile,
12
+ SudoEnvironmentProfile
13
+ } from "@nuvlore/extension-workday-infra-access/src/workday-access-profiles.mjs";
14
+
15
+ const DEFAULT_NAMESPACE = "airflow";
16
+ const DEFAULT_BOUNDARY_ADDR = DEFAULT_DC_BOUNDARY_ADDR;
17
+ const DEFAULT_BAS_DR_SLA_SOURCE_HOST = "s-11rdzb3.sys.az1.cust.ash.wd";
18
+ const DEFAULT_BAS_DR_SLA_CLUSTER = "BDS_ASH_CUST_AZ1";
19
+
20
+ const SAFETY_RULES = [
21
+ "read-only only: kubectl get, describe, logs, events, config current-context, and bounded airflow CLI list/state commands",
22
+ "before parsing local BDS config or Airflow code, refresh the relevant local repos with git pull --ff-only; if a repo is dirty, report the pull gap and do not stash or overwrite local changes",
23
+ "S3 VPCE connectivity checks must stay read-only: DNS lookup, route lookup, and curl connect probes with no local file output and no aws s3 cp/copy/write operations",
24
+ "use a Kubernetes control-plane host from the environment repo JSON kube_control_planes list",
25
+ "for ATL, prefer the latest BDS_ATL_CUST_AZ1 config host list; old control-plane hostnames can disappear after config refresh",
26
+ "use Boundary with BOUNDARY_SKIP_CACHE_DAEMON=true and PATH including /opt/homebrew/bin when SSH config proxy commands cannot find or reach boundary",
27
+ "if default SSH config routes ATL through a stale Atl-Nprd-Mgmt bastion or banner exchange times out, explicitly use the Boundary proxy jump host proxy-jump.atl.bnd with -J for read-only SSH",
28
+ "kubectl as a normal SSH user may fall back to localhost:8080 and fail; use the root kubeconfig on the control-plane host for cluster reads",
29
+ "when sudo requires a password, use mcp__devops-tool__workday_private_cloud_sudo_readonly_command; command packs and agents must not read Keychain directly",
30
+ "default namespace is usually empty for BDS Airflow; query the airflow namespace explicitly",
31
+ "for bas_dr_sla_miss or DR copy SLA alerts, collect pod/node/runtime evidence first; do not delete running pods or stop host processes from this command pack",
32
+ "force-delete is only a post-approval remediation candidate for stale completed or terminating DAG task pods after logs prove task success and the pod has no useful live progress",
33
+ "for snapshot-cleanup latency investigations, read the schedule from cookbook_bitbucket bds_kube/recipes/helm_deploy.rb and keep all Airflow, HDFS, and NameNode checks read-only",
34
+ "for snapshot-cleanup lock investigations, bound log scans by cleaner run time and hourly NameNode/audit files; avoid unbounded all-day zgrep over multi-GB audit directories",
35
+ "never run kubectl delete/apply/patch/scale/rollout restart/exec with mutation, helm writes, airflow clear, airflow backfill, or DAG/task state mutation",
36
+ "never print LDAP passwords, Keychain JSON, tokens, kubeconfig, or sudo stdin"
37
+ ];
38
+
39
+ const INTERPRETATION_RULES = [
40
+ "Ready Airflow scheduler/web pods with zero restarts and no pod events support core Airflow component health.",
41
+ "Repeated normal namespace events for short-lived DAG pods can indicate DAG activity rather than failure when reason is Scheduled/Pulled/Created/Started only.",
42
+ "HTTP 200 responses on /airflow/health support webserver liveness/readiness.",
43
+ "Airflow DagBag Failed to import plus FileNotFoundError for /etc/hadoop/conf/bds_config.json or /data/workday-bds-airflow/dr_to_dr_config/dr_to_dr_config.json is a DAG/config risk even when scheduler/web pods are Running.",
44
+ "A long-running tenant-data namespace migration pod can be healthy if Ready with zero restarts and its DistCp/YARN progress is still advancing; report the tenant, process, src/dst clusters, source snapshot, and HDFS source/destination paths separately from Airflow component health.",
45
+ "For bas_dr_sla_miss on tenant-data DR copy, treat mirror snapshot lag as the alert symptom and distinguish DistCp/copy failure from Kubernetes node runtime cleanup failure using pod logs, pod events, deletionTimestamp/finalizers, and node process/runtime evidence.",
46
+ "A DAG task pod stuck in Terminating or ContainerCreating on one worker with no finalizers and no pod events, while task logs already show Command exited with return code 0 and Marking task as SUCCESS, supports stale kubelet/container runtime cleanup failure rather than Airflow DAG or tenant configuration failure.",
47
+ "A Kubernetes worker with very high load, many D/Z state or zombie processes, kubelet/containerd systemctl timeouts, and stuck runc/mount/cgroup cleanup activity should stay cordoned pending DC/node recovery; do not uncordon until node runtime health is confirmed.",
48
+ "After stale pod cleanup by an approved operator, verify recovery by confirming new DAG task pods schedule on healthy workers, affected tenants create newer DR mirror snapshots, and BAS lag drops below the 3600 second threshold.",
49
+ "For BDS_ASH_CUST_AZ1 bas_dr_sla_miss, the live BAS lag source is the Pharos script exporter check on s-11rdzb3.sys.az1.cust.ash.wd: sudo /data/pharos_script_exporter/scripts/bas_dr_sla_miss_metric.rb BDS_ASH_CUST_AZ1.",
50
+ "For BDS migration S3 copy failures, a curl result with http=000 and a TCP connect timeout to the VPCE hostname indicates the request did not reach HTTP/S3 authorization; a reachable alternate VPCE returning HTTP 307 or another HTTP status proves the same worker can reach the S3 endpoint path.",
51
+ "When switching from a failed direct-connect VPCE to an alternate S3 VPCE, verify old and new endpoint hostnames from the same ATL worker/source IP before rerunning the migration copy.",
52
+ "For snapshot cleanup incidents, deleteSnapshot is expected to hold the NameNode write lock; HDFS metadata reads such as getFileInfo/getfileinfo, listStatus, open, and getBlockLocations require the NameNode read lock and wait behind the write lock.",
53
+ "A cleaner run with many repeated snapshot deletions for the same tenant, elevated delete durations, and matching NameNode deleteSnapshot long write-locks supports snapshot backlog or large diff cleanup as the cause; direct diff size remains unproven unless logs or an approved bounded diff/count command show it.",
54
+ "For PRISM/Spark CreateDataFrame latency during snapshot cleanup, use fast-case NameNode audit logs such as target _delta_log getfileinfo before or after the lock window to verify the path issues read-lock metadata RPCs.",
55
+ "For BDS_ASH_CUST_AZ1, the snapshot cleanup schedule is configured in cookbook_bitbucket bds_kube/recipes/helm_deploy.rb as SCHEDULE_ASH_CUST = '0 2,3,5 * * 3-5' with a 3600 second timeout.",
56
+ "A BDS control-panel DAG identifier like dag_tenant_data_copy_on_demand--1780713387370568 can encode epoch microseconds; convert the suffix to UTC and inspect the matching Airflow log directory manual__YYYY-MM-DDTHH:MM:SS.micros+00:00 before concluding the run is missing.",
57
+ "For tenant on-demand copy status, the scheduler container log path /data/workday-bds-airflow/logs/dag_tenant_data_copy_on_demand/<manual_run>/run_tenant_data_ondemand_restore_copy/1.log gives the source/target tenant, snapshots, YARN apps, DistCp progress, and final task success or failure.",
58
+ "Prometheus statsd-exporter superfluous response.WriteHeader lines are noisy unless paired with pod restarts or scrape failures.",
59
+ "sensu-client chown read-only filesystem messages are sidecar noise unless the Airflow containers are unhealthy."
60
+ ];
61
+
62
+ const hostSchema = z
63
+ .string()
64
+ .min(1)
65
+ .max(255)
66
+ .regex(/^(?!-)[a-zA-Z0-9._:@%-]+$/);
67
+
68
+ const namespaceSchema = z
69
+ .string()
70
+ .min(1)
71
+ .max(63)
72
+ .regex(/^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/)
73
+ .default(DEFAULT_NAMESPACE);
74
+
75
+ const envNameSchema = z
76
+ .string()
77
+ .min(1)
78
+ .max(160)
79
+ .regex(/^[A-Za-z0-9_.:-]+$/)
80
+ .optional();
81
+
82
+ const pathSchema = z.string().min(1).max(1024).optional();
83
+ const urlSchema = z.string().trim().url().max(2048).default(DEFAULT_BOUNDARY_ADDR);
84
+ const localPathSchema = z.string().trim().min(1).max(2048);
85
+ const s3EndpointUrlSchema = z.string().trim().url().max(2048).regex(/^https:\/\/[^/]+\.s3\.[A-Za-z0-9-]+\.vpce\.amazonaws\.com\/?$/);
86
+
87
+ const dagSchema = z
88
+ .string()
89
+ .min(1)
90
+ .max(255)
91
+ .regex(/^[A-Za-z0-9_.:@/-]+$/)
92
+ .optional();
93
+
94
+ const runSchema = z
95
+ .string()
96
+ .min(1)
97
+ .max(255)
98
+ .regex(/^[A-Za-z0-9_.:@+=,~%/-]+$/)
99
+ .optional();
100
+
101
+ function urlHostname(value) {
102
+ return new URL(value).hostname;
103
+ }
104
+
105
+ function uniq(values) {
106
+ return Array.from(new Set(values.filter(Boolean)));
107
+ }
108
+
109
+ function collectStringArrays(value, path = [], output = []) {
110
+ if (!value || typeof value !== "object") return output;
111
+ if (Array.isArray(value)) {
112
+ if (value.every((item) => typeof item === "string")) {
113
+ output.push({ path: path.join("."), values: value });
114
+ }
115
+ for (let index = 0; index < value.length; index += 1) {
116
+ collectStringArrays(value[index], [...path, String(index)], output);
117
+ }
118
+ return output;
119
+ }
120
+ for (const [key, nested] of Object.entries(value)) {
121
+ collectStringArrays(nested, [...path, key], output);
122
+ }
123
+ return output;
124
+ }
125
+
126
+ function discoverKubernetesHosts(doc) {
127
+ const arrays = collectStringArrays(doc);
128
+ const byPath = new Map(arrays.map((entry) => [entry.path, entry.values]));
129
+ const controlPlane =
130
+ byPath.get("kubernetes.kube_control_planes") ??
131
+ arrays.find((entry) => /kubernetes.*kube_control_planes$/i.test(entry.path))?.values ??
132
+ [];
133
+ const workers =
134
+ byPath.get("kubernetes.kube_workers") ??
135
+ arrays.find((entry) => /kubernetes.*kube_workers$/i.test(entry.path))?.values ??
136
+ [];
137
+ const etcd =
138
+ byPath.get("kubernetes.etcd") ??
139
+ arrays.find((entry) => /kubernetes.*etcd$/i.test(entry.path))?.values ??
140
+ [];
141
+ return {
142
+ controlPlaneHosts: uniq(controlPlane),
143
+ workerHosts: uniq(workers),
144
+ etcdHosts: uniq(etcd)
145
+ };
146
+ }
147
+
148
+ async function loadEnvironmentJson(configJsonPath) {
149
+ if (!configJsonPath) return {};
150
+ const parsed = JSON.parse(await readFile(configJsonPath, "utf8"));
151
+ return discoverKubernetesHosts(parsed);
152
+ }
153
+
154
+ class BdsAirflowJobStatusCommandPack {
155
+ constructor(input) {
156
+ this.environmentName = input.environmentName;
157
+ this.configJsonPath = input.configJsonPath;
158
+ this.controlPlaneHosts = input.controlPlaneHosts ?? [];
159
+ this.workerHosts = input.workerHosts ?? [];
160
+ this.etcdHosts = input.etcdHosts ?? [];
161
+ this.namespace = input.namespace ?? DEFAULT_NAMESPACE;
162
+ this.dagId = input.dagId;
163
+ this.runId = input.runId;
164
+ this.s3VpceEndpointUrls = input.s3VpceEndpointUrls ?? [];
165
+ this.alternateS3VpceEndpointUrls = input.alternateS3VpceEndpointUrls ?? [];
166
+ this.boundaryAddr = input.boundaryAddr ?? DEFAULT_BOUNDARY_ADDR;
167
+ this.boundaryBinaryDir = input.boundaryBinaryDir ?? DEFAULT_BOUNDARY_BINARY_DIR;
168
+ this.boundaryProxyJumpHost = input.boundaryProxyJumpHost ?? DEFAULT_BOUNDARY_PROXY_JUMP_HOST;
169
+ this.useBoundaryBnd = input.useBoundaryBnd ?? false;
170
+ this.dcProfile = new DcLoginProfile({
171
+ boundaryAddr: this.boundaryAddr,
172
+ boundaryBinaryDir: this.boundaryBinaryDir,
173
+ boundaryProxyJumpHost: this.boundaryProxyJumpHost,
174
+ useBoundaryBnd: this.useBoundaryBnd
175
+ });
176
+ this.sudoProfile = new SudoEnvironmentProfile();
177
+ this.basDrSlaSourceHost = input.basDrSlaSourceHost ?? DEFAULT_BAS_DR_SLA_SOURCE_HOST;
178
+ this.basDrSlaCluster = input.basDrSlaCluster ?? DEFAULT_BAS_DR_SLA_CLUSTER;
179
+ }
180
+
181
+ preferredControlPlane() {
182
+ return this.controlPlaneHosts[0] ?? "<kube_control_plane_host_from_environment_json>";
183
+ }
184
+
185
+ boundaryHost() {
186
+ return this.boundaryHostFor(this.preferredControlPlane());
187
+ }
188
+
189
+ boundaryHostFor(host) {
190
+ return this.dcProfile.boundaryHostFor(host);
191
+ }
192
+
193
+ sshEnvironmentPrefix() {
194
+ return this.dcProfile.environmentPrefix();
195
+ }
196
+
197
+ sshCommand(remoteCommand) {
198
+ return this.sshCommandFor(this.preferredControlPlane(), remoteCommand);
199
+ }
200
+
201
+ sshCommandFor(host, remoteCommand) {
202
+ return this.dcProfile.sshCommandFor(host, remoteCommand);
203
+ }
204
+
205
+ sshProxyJumpCommandFor(host, remoteCommand) {
206
+ return this.dcProfile.proxyJumpCommandFor(host, remoteCommand);
207
+ }
208
+
209
+ rootSsh(command) {
210
+ return this.sshCommand(`sudo -n bash -lc ${JSON.stringify(command)}`);
211
+ }
212
+
213
+ sudoWrapperExample(command) {
214
+ return this.sudoWrapperExampleFor(this.preferredControlPlane(), command);
215
+ }
216
+
217
+ keychainCandidates() {
218
+ return this.sudoProfile.keychainCandidates();
219
+ }
220
+
221
+ keychainReadCommand() {
222
+ return this.sudoProfile.keychainReadCommand();
223
+ }
224
+
225
+ sudoWrapperExampleFor(host, command) {
226
+ return this.sudoProfile.sudoWrapperFor({
227
+ dcProfile: this.dcProfile,
228
+ host,
229
+ command
230
+ });
231
+ }
232
+
233
+ airflowSelector() {
234
+ const items = [];
235
+ if (this.dagId) items.push(`dag_id=${this.dagId}`);
236
+ if (this.runId) items.push(`run_id=${this.runId}`);
237
+ return items.length ? items.join(" ") : "<dag_id/run_id if provided by requester>";
238
+ }
239
+
240
+ s3EndpointUrls() {
241
+ return Array.from(new Set([
242
+ ...this.s3VpceEndpointUrls,
243
+ ...this.alternateS3VpceEndpointUrls
244
+ ]));
245
+ }
246
+
247
+ s3VpceConnectivityScript() {
248
+ const endpoints = this.s3EndpointUrls();
249
+ if (!endpoints.length) {
250
+ return [
251
+ "set -eu",
252
+ "endpoint='<s3_vpce_https_endpoint_from_failure_log>'",
253
+ "host=${endpoint#https://}",
254
+ "host=${host%%/*}",
255
+ "echo ENDPOINT $endpoint",
256
+ "getent hosts \"$host\"",
257
+ "getent hosts \"$host\" | awk '{print $1}' | while read -r ip; do ip route get \"$ip\"; done",
258
+ "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' \"$endpoint\""
259
+ ].join("\n");
260
+ }
261
+
262
+ return [
263
+ "set -eu",
264
+ ...endpoints.flatMap((endpoint) => {
265
+ const label = this.s3VpceEndpointUrls.includes(endpoint) ? "current-or-failing" : "alternate";
266
+ const host = urlHostname(endpoint);
267
+ return [
268
+ `echo ENDPOINT ${label} ${shellQuote(endpoint)}`,
269
+ `getent hosts ${shellQuote(host)}`,
270
+ `getent hosts ${shellQuote(host)} | awk '{print $1}' | while read -r ip; do ip route get "$ip"; done`,
271
+ `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)}`
272
+ ];
273
+ })
274
+ ].join("\n");
275
+ }
276
+
277
+ mcpSafeCommandInputs() {
278
+ const host = this.preferredControlPlane();
279
+ const ns = this.namespace;
280
+ return [
281
+ { label: "control-plane-hostname", input: remoteCommandInput({ host, command: "hostname -f" }) },
282
+ { label: "control-plane-user", input: remoteCommandInput({ host, command: "id -un" }) },
283
+ { label: "normal-user-kubectl-context", input: remoteCommandInput({ host, command: "kubectl config current-context" }) },
284
+ { label: "normal-user-default-pods", input: remoteCommandInput({ host, command: "kubectl get pod" }) },
285
+ { label: "root-kubectl-context", input: remoteCommandInput({ host, command: "sudo -n kubectl config current-context" }) },
286
+ { label: "airflow-pods", input: remoteCommandInput({ host, command: `sudo -n kubectl get pod -n ${ns} -o wide` }) },
287
+ { label: "airflow-events", input: remoteCommandInput({ host, command: `sudo -n kubectl get events -n ${ns} --sort-by=.lastTimestamp | tail -80` }) },
288
+ { label: "airflow-deployments", input: remoteCommandInput({ host, command: `sudo -n kubectl get deploy,statefulset,job,cronjob -n ${ns} -o wide` }) }
289
+ ];
290
+ }
291
+
292
+ commands() {
293
+ const ns = this.namespace;
294
+ return [
295
+ new ReadOnlyStep({
296
+ id: "01-environment-json",
297
+ purpose: "Use the environment repo JSON as the authoritative source for Kubernetes control-plane hosts.",
298
+ command: [
299
+ "# Refresh relevant local repos first. Use --ff-only and report dirty repos instead of stashing or overwriting local changes.",
300
+ "git -C /Users/boqiang.liang/500-work/bds-config-nprd pull --ff-only",
301
+ "git -C /Users/boqiang.liang/500-work/bds-k8-dc-airflow pull --ff-only",
302
+ `jq -r '(.bds.kubernetes.kube_control_planes // .kubernetes.kube_control_planes // [])[]' ${this.configJsonPath ? shellQuote(this.configJsonPath) : "<environment-json-file>"}`,
303
+ `# control planes discovered: ${this.controlPlaneHosts.length ? this.controlPlaneHosts.join(", ") : "<none provided yet>"}`,
304
+ `# workers discovered: ${this.workerHosts.length ? this.workerHosts.join(", ") : "<none provided yet>"}`
305
+ ].join("\n"),
306
+ expectedEvidence: "kubernetes.kube_control_planes, kube_workers, and etcd host lists for the selected environment"
307
+ }),
308
+ new ReadOnlyStep({
309
+ id: "02-control-plane-login",
310
+ purpose: "Run bauth init, then ordinary SSH to one control-plane host and confirm host identity before running cluster reads.",
311
+ command: [
312
+ this.sshCommand("hostname -f; date -u; id -un"),
313
+ "",
314
+ "# If the local SSH config uses a stale ATL bastion or times out during banner exchange, explicitly use the Boundary proxy jump host:",
315
+ this.sshProxyJumpCommandFor(this.preferredControlPlane(), "hostname -f; date -u; id -un")
316
+ ].join("\n"),
317
+ expectedEvidence: "control-plane hostname, UTC timestamp, and current user; for ATL, proxy-jump.atl.bnd can bypass a stale Atl-Nprd-Mgmt bastion path"
318
+ }),
319
+ new ReadOnlyStep({
320
+ id: "03-normal-user-kubectl-baseline",
321
+ purpose: "Document whether the normal SSH user lacks kubeconfig; localhost:8080 refusal is expected evidence, not the cluster state.",
322
+ command: this.sshCommand("kubectl get pod 2>&1 | head -40"),
323
+ expectedEvidence: "either normal-user context output or localhost:8080 refused evidence"
324
+ }),
325
+ new ReadOnlyStep({
326
+ id: "04-root-context-with-sudo-stdin",
327
+ purpose: "When sudo -n requires a password, use the approved PROD LDAP keychain item through stdin only and verify the root kubeconfig context.",
328
+ command: this.sudoWrapperExample("kubectl config current-context"),
329
+ expectedEvidence: "root kubeconfig context such as kubernetes-admin@kubernetes; no password or token output"
330
+ }),
331
+ new ReadOnlyStep({
332
+ id: "05-root-kubectl-airflow-pods",
333
+ purpose: "Use the root kubeconfig on the control-plane host and query the airflow namespace explicitly.",
334
+ command: this.sudoWrapperExample(`kubectl get pod -n ${ns} -o wide`),
335
+ expectedEvidence: "airflow scheduler and web pods, readiness, status, restarts, age, node placement, and pod IP"
336
+ }),
337
+ new ReadOnlyStep({
338
+ id: "06-airflow-workload-and-events",
339
+ purpose: "Read workload objects and namespace events to identify restarts, pending pods, failed jobs, or recent scheduling issues.",
340
+ command: this.sudoWrapperExample(`kubectl get deploy,statefulset,job,cronjob -n ${ns} -o wide; kubectl get events -n ${ns} --sort-by=.lastTimestamp | tail -80`),
341
+ expectedEvidence: "deployment/job shape and recent Airflow namespace warnings or normal scheduling events"
342
+ }),
343
+ new ReadOnlyStep({
344
+ id: "07-airflow-component-logs",
345
+ purpose: "Read bounded scheduler and web logs for DAG parsing, executor, KubernetesPodOperator, or task-state errors.",
346
+ command: [
347
+ this.sudoWrapperExample(`kubectl describe pod -n ${ns} -l app=airflow-scheduler | egrep -A4 '^Name:|^Containers:|^Conditions:|^Events:'`),
348
+ "",
349
+ this.sudoWrapperExample(`kubectl describe pod -n ${ns} -l app=airflow-web | egrep -A4 '^Name:|^Containers:|^Conditions:|^Events:'`),
350
+ "",
351
+ this.sudoWrapperExample(`kubectl logs -n ${ns} deploy/airflow-scheduler --all-containers --tail=160 2>&1`),
352
+ "",
353
+ this.sudoWrapperExample(`kubectl logs -n ${ns} deploy/airflow-web --all-containers --tail=80 2>&1`)
354
+ ].join("\n"),
355
+ expectedEvidence: "pod conditions/events, recent Airflow scheduler/web errors, DAG parse failures, health probe responses, or clean logs"
356
+ }),
357
+ new ReadOnlyStep({
358
+ id: "08-target-dag-run-if-known",
359
+ purpose: "If DAG or run ID is known, inspect the Airflow metadata from the scheduler pod without changing task state.",
360
+ command: this.sudoWrapperExample(`pod=$(kubectl get pod -n ${ns} -o name | grep airflow-scheduler | head -1); kubectl exec -n ${ns} "$pod" -- airflow dags list-runs${this.dagId ? ` -d ${shellQuote(this.dagId)}` : ""} --no-backfill --output table | head -80`),
361
+ expectedEvidence: `DAG run rows for ${this.airflowSelector()}, including run id, state, start/end dates, and execution date`
362
+ }),
363
+ new ReadOnlyStep({
364
+ id: "09-running-task-copy-source-target",
365
+ purpose: "For a long-running DAG task pod, extract the tenant data copy process, src/dst clusters, source snapshot, and HDFS source/destination paths from pod metadata and logs; do not delete pods that are actively progressing.",
366
+ command: [
367
+ this.sudoWrapperExample(`kubectl get pod -n ${ns} -o wide | grep -E 'dag-.*tenant|namespace-migration|tenant-data-copy' || true`),
368
+ "",
369
+ this.sudoWrapperExample(`task_pod=<running_dag_task_pod>; kubectl describe pod -n ${ns} "$task_pod" | egrep -A8 'Labels:|Args:|State:|Started:|Restart Count:|Events:'`),
370
+ "",
371
+ this.sudoWrapperExample(`task_pod=<running_dag_task_pod>; kubectl logs -n ${ns} "$task_pod" --tail=-1 | grep -n -E 'tenant-data-copy|Copy tenant data from cluster|Obtained Source Snapshot|Starting: Building listing|hdfs://|DistCp|Submitted application|map [0-9]+%' | head -220`)
372
+ ].join("\n"),
373
+ expectedEvidence: "tenant/env, task/process name, src-cluster, dst-cluster, source snapshot id, HDFS source path, HDFS destination path, YARN app id, and latest progress"
374
+ }),
375
+ new ReadOnlyStep({
376
+ id: "09b-on-demand-copy-control-panel-log",
377
+ purpose: "When the control panel supplies a DAG identifier with an epoch-microsecond suffix, convert it to the Airflow manual run log directory and read the exact task log.",
378
+ command: this.sudoWrapperExample([
379
+ `pod=$(kubectl get pod -n ${ns} -o name | grep airflow-scheduler | head -1)`,
380
+ "control_panel_id='<dag_tenant_data_copy_on_demand--epoch_microseconds_from_request>'",
381
+ "micros=${control_panel_id##*--}",
382
+ "manual_run=$(MICROS=\"$micros\" /usr/bin/python3 - <<'PY'",
383
+ "import datetime, os",
384
+ "micros = int(os.environ['MICROS'])",
385
+ "dt = datetime.datetime.fromtimestamp(micros / 1_000_000, datetime.timezone.utc)",
386
+ "print('manual__' + dt.isoformat())",
387
+ "PY",
388
+ ")",
389
+ "kubectl exec -n airflow \"$pod\" -- sh -lc \"log=/data/workday-bds-airflow/logs/dag_tenant_data_copy_on_demand/$manual_run/run_tenant_data_ondemand_restore_copy/1.log; echo __TARGET_LOG__ \\\"$log\\\"; grep -n -E 'Running command:|--src-tenant|--dst-tenant|Copy tenant data|Obtained Source Snapshot|Latest remote matching Snapshot|Starting: Building listing|Submitted application|map [0-9]+%|ERROR|Exception|Traceback|Command exited|Marking task|Task exited|return code' \\\"$log\\\" | head -320\""
390
+ ].join("\n")),
391
+ expectedEvidence: "exact on-demand copy log path, src/dst tenant arguments, source snapshot, latest remote matching snapshot, YARN applications, DistCp progress, and final Command exited / Marking task state"
392
+ }),
393
+ new ReadOnlyStep({
394
+ id: "10-bas-dr-sla-miss-node-runtime-evidence",
395
+ purpose: "For bas_dr_sla_miss or DR copy SLA alerts, prove whether the lag is caused by stuck Airflow task pods on a degraded Kubernetes worker rather than DistCp, DAG, or tenant config failure.",
396
+ command: [
397
+ this.sudoWrapperExample(`kubectl get node -o wide`),
398
+ "",
399
+ this.sudoWrapperExample(`kubectl get pod -n ${ns} -o wide | grep -E 'dag-tenant-data-dr-pull|tenant-data-dr-restore-copy|DR|dr' || true`),
400
+ "",
401
+ this.sudoWrapperExample(`task_pod=<affected_dr_task_pod>; kubectl get pod -n ${ns} "$task_pod" -o jsonpath='{.metadata.name}{\" node=\"}{.spec.nodeName}{\" phase=\"}{.status.phase}{\" deletionTimestamp=\"}{.metadata.deletionTimestamp}{\" finalizers=\"}{.metadata.finalizers}{\"\\n\"}'`),
402
+ "",
403
+ this.sudoWrapperExample(`task_pod=<affected_dr_task_pod>; kubectl describe pod -n ${ns} "$task_pod" | egrep -A12 '^Name:|^Node:|^Status:|^Containers:|^Conditions:|^Events:'`),
404
+ "",
405
+ this.sudoWrapperExample(`task_pod=<affected_dr_task_pod>; kubectl logs -n ${ns} "$task_pod" --tail=-1 2>&1 | grep -n -E 'Command exited with return code 0|Marking task as SUCCESS|Task exited with return code 0|Submitted application|ERROR|Traceback|DistCp|Creating Source Snapshot|Creating Destination Snapshot|s[0-9]{12}r' | tail -160`),
406
+ "",
407
+ this.sshCommand("bad_node=<worker_with_stuck_dr_pods>; ssh -o BatchMode=yes -o ConnectTimeout=20 -- \"$bad_node\" \"hostname -f; date -u; uptime; ps -eo stat= | awk '{c[$1]++} END {for (s in c) print s,c[s]}' | sort; ps -eo pid,ppid,stat,comm,args | egrep '(^| )[DZ]|defunct|runc|containerd|kubelet|mount|cgroup' | head -120; timeout 8 systemctl is-active kubelet containerd 2>&1\"")
408
+ ].join("\n"),
409
+ expectedEvidence: "node scheduling status, affected DR pod placement/status/deletionTimestamp/finalizers/events, task success or failure markers, worker load, D/Z or zombie process counts, kubelet/containerd timeout evidence, and stuck runc/mount/cgroup cleanup lines"
410
+ }),
411
+ new ReadOnlyStep({
412
+ id: "11-bas-lag-recovery-verification",
413
+ purpose: "After approved operator remediation, verify that DR copy has caught up without relying only on Slack alert state.",
414
+ command: [
415
+ this.sudoWrapperExample(`kubectl get pod -n ${ns} -o wide | grep -E 'dag-tenant-data-dr-pull|tenant-data-dr-restore-copy' || true`),
416
+ "",
417
+ this.sudoWrapperExample(`kubectl get pod -n ${ns} -o wide | grep '<bad_worker_host>' || true`),
418
+ "",
419
+ this.sshCommand("grep -RInE 's[0-9]{12}r|Marking task as SUCCESS|Command exited with return code 0|tenant WD1-PROD\\.(hss|kla|mawc)' /hadoop/disk*/workday-bds-airflow/logs/dag_tenant_data_dr_pull_* 2>/dev/null | tail -200"),
420
+ "",
421
+ this.sudoWrapperExampleFor(this.basDrSlaSourceHost, `/data/pharos_script_exporter/scripts/bas_dr_sla_miss_metric.rb ${this.basDrSlaCluster} 2>/dev/null | awk '/^bds_bas_dr_sla_miss/ { tenant=$0; sub(/^.*tenant_name="/, "", tenant); sub(/".*$/, "", tenant); val=$NF+0; total++; if (val > 3600) {miss++; missed[tenant]=val} if (val > max) {max=val; maxtenant=tenant} rows[tenant]=val } END { print "total_metrics", total+0; print "sla_missed_gt_3600", miss+0; print "max_lag_seconds", max+0; print "max_lag_tenant", maxtenant; print "__MISSED__"; for (t in missed) print missed[t], t; print "__TOP10__"; for (t in rows) print rows[t], t | "sort -nr | head -10" }'`)
422
+ ].join("\n"),
423
+ expectedEvidence: `new DR task pods on healthy nodes, no affected task pods remaining on the bad worker, newer DR snapshot ids such as sYYYYMMDDHHMMr, task success markers, and BAS metric output from ${this.basDrSlaSourceHost} showing sla_missed_gt_3600 and max lag below the 3600 second alert threshold`
424
+ }),
425
+ new ReadOnlyStep({
426
+ id: "12-s3-vpce-connectivity",
427
+ purpose: "For BDS migration S3 copy failures, compare the failing S3 VPCE hostname with any alternate VPCE from the same worker/source IP before rerunning the copy.",
428
+ command: [
429
+ this.sshCommand(this.s3VpceConnectivityScript()),
430
+ "",
431
+ "# If sudo/root is needed for a worker-host shell, reuse the stdin sudo wrapper and keep the same read-only script body."
432
+ ].join("\n"),
433
+ expectedEvidence: "endpoint DNS answers, source IP, route gateway/device, curl time_connect/http_code/errormsg for failing and alternate S3 VPCE paths"
434
+ }),
435
+ new ReadOnlyStep({
436
+ id: "13-snapshot-cleanup-schedule-and-lock-evidence",
437
+ purpose: "For snapshot-cleanup or PRISM CreateDataFrame latency investigations, confirm the Airflow cleanup schedule and correlate Airflow delete durations with bounded NameNode lock/audit evidence.",
438
+ command: [
439
+ "rg -n \"SCHEDULE_ASH_CUST|LOW_RISK_TIME_WINDOW|dag_bulk_snapshot_cleaner|snapshot_cleaner_timeout\" /Users/boqiang.liang/500-work/cookbook_bitbucket/bds_kube/recipes/helm_deploy.rb",
440
+ "",
441
+ this.sshCommand("for f in /hadoop/disk*/workday-bds-airflow/logs/dag_bulk_snapshot_cleaner/scheduled__<run_time>/run_bulk_snapshot_cleaner/*.log; do echo __FILE__ $f; awk '/Deleting snapshot/ {last=$0} /Successfully deleted, duration\\(ms\\):/ {dur=$NF+0; if (dur>=3000) print dur \" | \" last \" | \" $0}' \"$f\" | sort -nr | head -40; done"),
442
+ "",
443
+ this.sshCommand("for f in /var/log/hadoop-hdfs/hadoop-hdfs-namenode-<active_nn>.log.<yyyy-mm-dd>.*.gz /var/log/hadoop-hdfs/hadoop-hdfs-namenode-<active_nn>.log; do [ -e \"$f\" ] && zgrep -n -A25 -B8 -E '<lock_window_timestamp>|Longest write-lock held' \"$f\" | head -260; done"),
444
+ "",
445
+ this.sshCommand("zgrep -h 'cmd=deleteSnapshot' /var/log/hadoop-hdfs/hdfs-audit.log.<yyyy-mm-dd>/hdfs-audit.log.<yyyy-mm-dd>-<hour>.*.gz 2>/dev/null | grep -E '<tenant>|<snapshot>|allowed=true' | head -120"),
446
+ "",
447
+ this.sshCommand("zgrep -h -E 'cmd=(getfileinfo|getFileInfo|listStatus|open|getBlockLocations)|_delta_log|WPA_|prism_table_wid' /var/log/hadoop-hdfs/hdfs-audit.log.<yyyy-mm-dd>/hdfs-audit.log.<yyyy-mm-dd>-<hour>.*.gz 2>/dev/null | head -160")
448
+ ].join("\n"),
449
+ expectedEvidence: "configured cleanup cron/timeout, top Airflow deleteSnapshot durations by run, repeated tenant snapshot backlog if present, NameNode deleteSnapshot long write-lock stack, hourly deleteSnapshot audit paths, and fast-case metadata audit RPCs that require read lock"
450
+ })
451
+ ];
452
+ }
453
+
454
+ toJson() {
455
+ return {
456
+ environmentName: this.environmentName,
457
+ configJsonPath: this.configJsonPath,
458
+ namespace: this.namespace,
459
+ controlPlaneHosts: this.controlPlaneHosts,
460
+ workerHosts: this.workerHosts,
461
+ etcdHosts: this.etcdHosts,
462
+ dagId: this.dagId,
463
+ runId: this.runId,
464
+ s3VpceEndpointUrls: this.s3VpceEndpointUrls,
465
+ alternateS3VpceEndpointUrls: this.alternateS3VpceEndpointUrls,
466
+ boundaryAddr: this.boundaryAddr,
467
+ boundaryHost: this.boundaryHost(),
468
+ boundaryProxyJumpHost: this.boundaryProxyJumpHost,
469
+ dcLogin: this.dcProfile.toJson(),
470
+ sudoEnvironment: {
471
+ tool: "mcp__devops-tool__workday_private_cloud_sudo_readonly_command",
472
+ credentialBoundary: "Only the dedicated sudo tool may read LDAP PROD password."
473
+ },
474
+ basDrSlaSourceHost: this.basDrSlaSourceHost,
475
+ basDrSlaCluster: this.basDrSlaCluster,
476
+ safetyRules: SAFETY_RULES,
477
+ commands: this.commands().map((command) => command.toJson()),
478
+ mcpSafeCommandInputs: this.mcpSafeCommandInputs()
479
+ };
480
+ }
481
+
482
+ toMarkdown() {
483
+ return [
484
+ "# BDS Airflow Job Status Read-Only Command Pack",
485
+ "",
486
+ this.environmentName ? `Environment: ${this.environmentName}` : "",
487
+ this.configJsonPath ? `Environment JSON: ${this.configJsonPath}` : "",
488
+ `Namespace: ${this.namespace}`,
489
+ `Control-plane hosts: ${this.controlPlaneHosts.length ? this.controlPlaneHosts.join(", ") : "<read kubernetes.kube_control_planes from env JSON>"}`,
490
+ "",
491
+ "## Safety Boundary",
492
+ "",
493
+ ...SAFETY_RULES.map((rule) => `- ${rule}`),
494
+ "",
495
+ "## Root Kubeconfig Note",
496
+ "",
497
+ "If `kubectl get pod` as the normal SSH user returns `The connection to the server localhost:8080 was refused`, treat that as missing user kubeconfig. Become root on the control-plane host for read-only cluster checks, then always query `-n airflow`.",
498
+ "",
499
+ "Use the explicit private-cloud host unchanged after `bauth init`. If direct SSH routes through a stale bastion or times out during banner exchange, retry with the explicit Boundary proxy jump host `proxy-jump.atl.bnd`; do not invent `.bnd` aliases for service hosts.",
500
+ "",
501
+ "```bash",
502
+ this.sudoWrapperExample(`kubectl get pod -n ${this.namespace} -o wide`),
503
+ "```",
504
+ "",
505
+ "## Read-Only Commands",
506
+ "",
507
+ ...this.commands().map((command) => command.toMarkdown()),
508
+ "",
509
+ "## MCP-Safe Remote Execution",
510
+ "",
511
+ "Run one concrete JSON input at a time. If sudo without a password is unavailable, use `workday_private_cloud_sudo_readonly_command`; do not read Keychain directly.",
512
+ "",
513
+ "```json",
514
+ JSON.stringify(this.mcpSafeCommandInputs(), null, 2),
515
+ "```",
516
+ "",
517
+ "Procedure, interpretation, and response shape live in the `bds-airflow-job-status-investigation` skill and runbook."
518
+ ].join("\n");
519
+ }
520
+ }
521
+
522
+ const inputSchema = {
523
+ environmentName: envNameSchema,
524
+ configJsonPath: pathSchema.describe("Optional local environment repo JSON file containing kubernetes.kube_control_planes."),
525
+ controlPlaneHosts: z.array(hostSchema).max(20).default([]),
526
+ workerHosts: z.array(hostSchema).max(100).default([]),
527
+ etcdHosts: z.array(hostSchema).max(20).default([]),
528
+ namespace: namespaceSchema,
529
+ dagId: dagSchema,
530
+ runId: runSchema,
531
+ s3VpceEndpointUrls: z.array(s3EndpointUrlSchema).max(8).default([]),
532
+ alternateS3VpceEndpointUrls: z.array(s3EndpointUrlSchema).max(8).default([]),
533
+ basDrSlaSourceHost: hostSchema.default(DEFAULT_BAS_DR_SLA_SOURCE_HOST),
534
+ basDrSlaCluster: envNameSchema.default(DEFAULT_BAS_DR_SLA_CLUSTER),
535
+ boundaryAddr: urlSchema,
536
+ boundaryBinaryDir: localPathSchema.default(DEFAULT_BOUNDARY_BINARY_DIR),
537
+ boundaryProxyJumpHost: hostSchema.default(DEFAULT_BOUNDARY_PROXY_JUMP_HOST),
538
+ useBoundaryBnd: z.boolean().default(false),
539
+ outputFormat: z.enum(["markdown", "json"]).default("markdown")
540
+ };
541
+
542
+ const inputObjectSchema = z.object(inputSchema);
543
+
544
+ export async function bdsAirflowJobStatusCommandPack(rawInput) {
545
+ const input = inputObjectSchema.parse(rawInput ?? {});
546
+ const discovered = await loadEnvironmentJson(input.configJsonPath);
547
+ const commandPack = new BdsAirflowJobStatusCommandPack({
548
+ ...input,
549
+ controlPlaneHosts: input.controlPlaneHosts.length ? input.controlPlaneHosts : discovered.controlPlaneHosts,
550
+ workerHosts: input.workerHosts.length ? input.workerHosts : discovered.workerHosts,
551
+ etcdHosts: input.etcdHosts.length ? input.etcdHosts : discovered.etcdHosts
552
+ });
553
+ return input.outputFormat === "json" ? JSON.stringify(commandPack.toJson(), null, 2) : commandPack.toMarkdown();
554
+ }
555
+
556
+ export const __test = {
557
+ BdsAirflowJobStatusCommandPack,
558
+ bdsAirflowJobStatusCommandPack,
559
+ discoverKubernetesHosts
560
+ };
561
+
562
+ const sharedTool = defineReadOnlyTool({
563
+ name: "bds_airflow_job_status_command_pack",
564
+ description: "Generate a atomic-helper, read-only BDS Airflow command pack from environment Kubernetes control-plane JSON, airflow namespace checks, and S3 VPCE probes; does not SSH, run kubectl, or inspect Airflow.",
565
+ meta: {
566
+ permissions: { rule: "ask" },
567
+ secret_provider: "keychain"
568
+ },
569
+ inputSchema,
570
+ async handler(input) {
571
+ try {
572
+ return ok(await bdsAirflowJobStatusCommandPack(input));
573
+ } catch (error) {
574
+ return fail(error);
575
+ }
576
+ }
577
+ });
578
+
579
+ export default {
580
+ name: sharedTool.name,
581
+ description,
582
+ inputSchema: sharedTool.inputSchema,
583
+ meta: sharedTool.meta,
584
+ annotations: sharedTool.annotations,
585
+ invoke: sharedTool.invoke,
586
+ handler: sharedTool.handler
587
+ };
@@ -0,0 +1,23 @@
1
+ export const description = "Read BAS logs from /data/workday-bas/logs on a private-cloud BAS/admin host through the approved Boundary sudo read tool.";
2
+
3
+ import { createPrivateCloudLogReadTool, fixedPrivateLogSpec } from "../src/bds-atomic-log-tools.mjs";
4
+
5
+ const spec = fixedPrivateLogSpec({
6
+ toolName: "bds_bas_log_read",
7
+ description: "Read BAS logs from /data/workday-bas/logs on a private-cloud BAS/admin host through the approved Boundary sudo read tool.",
8
+ path: "/data/workday-bas/logs/bas.log"
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,24 @@
1
+ export const description = "Read BDM Apache httpd access or error logs from a private-cloud BDM host through the approved Boundary sudo read tool.";
2
+
3
+ import { BDM_HTTPD_EXTRA_SCHEMA, bdmHttpdCommand, createPrivateCloudLogReadTool } from "../src/bds-atomic-log-tools.mjs";
4
+
5
+ export const __test = {
6
+ buildCommand: bdmHttpdCommand
7
+ };
8
+
9
+ const sharedTool = createPrivateCloudLogReadTool({
10
+ toolName: "bds_bdm_httpd_log_read",
11
+ description: "Read BDM Apache httpd access or error logs from a private-cloud BDM host through the approved Boundary sudo read tool.",
12
+ extraSchema: BDM_HTTPD_EXTRA_SCHEMA,
13
+ buildCommand: bdmHttpdCommand
14
+ });
15
+
16
+ export default {
17
+ name: sharedTool.name,
18
+ description,
19
+ inputSchema: sharedTool.inputSchema,
20
+ meta: sharedTool.meta,
21
+ annotations: sharedTool.annotations,
22
+ invoke: sharedTool.invoke,
23
+ handler: sharedTool.handler
24
+ };